Make WordPress Core


Ignore:
Timestamp:
10/20/2026 11:50:22 AM (17 hours ago)
Author:
mikachan
Message:

Editor: Add pseudo-selector support for text inputs.

Adds :required, :user-valid, :user-invalid, :focus, :focus-visible, and ::placeholder to the pseudo selectors that WP_Theme_JSON allows for the textInput element, so themes can style these states in theme.json.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/13056.
Discussed in ​https://github.com/WordPress/gutenberg/pull/71634.

Props onemaggie, scruffian, sarthaknagoshe2002, wildworks, im3dabasia1, audrasjb, mamaduka.
Fixes #65880.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-theme-json.php

    r64590 r64597  
    658658         * Defines which pseudo selectors are enabled for which elements.
    659659         *
    660          * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
    661          * This is to ensure the user action (hover, focus and active) styles have a higher
    662          * specificity than the visited styles, which in turn have a higher specificity than
    663          * the unvisited styles.
     660         * The selector order controls the cascade when multiple states match. Link and button
     661         * selectors follow the LVHA order, with focus-visible immediately after focus. Text input
     662         * validation states come before the focus states because an edited field can match both
     663         * at once, so user action styles must be output last to win.
    664664         *
    665665         * See https://core.trac.wordpress.org/ticket/56928.
    … …  
    670670         * @since 6.8.0 Added support for ':focus-visible'.
    671671         * @since 6.9.0 Added `textInput` and `select` elements.
     672         * @since 7.2.0 Added ':required', ':user-valid', ':user-invalid', ':focus', ':focus-visible',
     673         *              and '::placeholder' support for the text input element.
    672674         * @var array
    673675         */
    674676        const VALID_ELEMENT_PSEUDO_SELECTORS = array(
    675                 'link'   => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
    676                 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
     677                'link'      => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
     678                'button'    => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
     679                'textInput' => array( ':required', ':user-valid', ':user-invalid', ':focus', ':focus-visible', '::placeholder' ),
    677680        );
    678681
    … …  
    15871590
    15881591                return $output;
     1592        }
     1593
     1594        /**
     1595         * Determines whether a pseudo selector is a pseudo-element, e.g. '::placeholder'.
     1596         *
     1597         * Pseudo-elements are not valid inside `:where()`, so rules that target them
     1598         * cannot use the `:root :where()` specificity wrapper.
     1599         *
     1600         * @since 7.2.0
     1601         *
     1602         * @param string|null $pseudo_selector Pseudo selector to check, e.g. ':hover' or '::placeholder'.
     1603         * @return bool True if the pseudo selector is a pseudo-element.
     1604         */
     1605        protected static function is_pseudo_element( $pseudo_selector ) {
     1606                return is_string( $pseudo_selector ) && str_starts_with( $pseudo_selector, '::' );
     1607        }
     1608
     1609        /**
     1610         * Builds the specificity-capped selector for a pseudo selector rule.
     1611         *
     1612         * Pseudo-classes are appended to each part of the base selector inside the
     1613         * `:root :where()` wrapper, e.g. `:root :where(a:hover)`. Pseudo-elements are
     1614         * not valid inside `:where()`, so they are appended after the wrapper instead,
     1615         * e.g. `:root :where(textarea, input)::placeholder`, which keeps the same
     1616         * specificity regardless of where the element styles are defined.
     1617         *
     1618         * @since 7.2.0
     1619         *
     1620         * @param string $base_selector   Selector without the pseudo selector, e.g. 'textarea, input'.
     1621         * @param string $pseudo_selector Pseudo selector to apply, e.g. ':hover' or '::placeholder'.
     1622         * @return string The selector to use for the rule.
     1623         */
     1624        protected static function get_pseudo_rule_selector( $base_selector, $pseudo_selector ) {
     1625                if ( static::is_pseudo_element( $pseudo_selector ) ) {
     1626                        return ':root :where(' . $base_selector . ')' . $pseudo_selector;
     1627                }
     1628
     1629                return ':root :where(' . static::append_to_selector( $base_selector, $pseudo_selector ) . ')';
    15891630        }
    15901631
    … …  
    42764317                                                                        }
    42774318
    4278                                                                         $pseudo_selector_ruleset          = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
     4319                                                                        $pseudo_selector_ruleset          = static::to_ruleset( static::get_pseudo_rule_selector( $variation_element_selector, $pseudo_selector ), $pseudo_declarations );
    42794320                                                                        $variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
    42804321                                                                }
    … …  
    44214462                 * still need to be wrapped in `:root :where` to cap specificity for nested
    44224463                 * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
     4464                 *
     4465                 * Pseudo-elements, e.g. ::placeholder, are not valid inside `:where()`, so
     4466                 * they are appended after the wrapper instead.
    44234467                 */
    44244468                $element_only_selector = $is_root_selector || (
    … …  
    44314475
    44324476                // 2. Generate and append the rules that use the general selector.
    4433                 $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
    4434                 $block_rules     .= static::to_ruleset( $general_selector, $declarations );
     4477                if ( static::is_pseudo_element( $pseudo_selector ) ) {
     4478                        // The node selector already carries the pseudo-element on each part, so strip it to get the base selector.
     4479                        $general_selector = static::get_pseudo_rule_selector( str_replace( $pseudo_selector, '', $selector ), $pseudo_selector );
     4480                } else {
     4481                        $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
     4482                }
     4483                $block_rules .= static::to_ruleset( $general_selector, $declarations );
    44354484
    44364485                // 3. Generate and append the rules that use the duotone selector.
Note: See TracChangeset for help on using the changeset viewer.