Commit 305097af14 for wordpress.org
commit 305097af142b0e583de68193396fe09b0d942227
Author: mikachan <mikachan@git.wordpress.org>
Date: Tue Oct 20 11:52:00 2026 +0000
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.
Built from https://develop.svn.wordpress.org/trunk@64597
git-svn-id: http://core.svn.wordpress.org/trunk@63747 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php
index f57d473065..4586ff38bb 100644
--- a/wp-includes/class-wp-theme-json.php
+++ b/wp-includes/class-wp-theme-json.php
@@ -657,10 +657,10 @@ class WP_Theme_JSON {
/**
* Defines which pseudo selectors are enabled for which elements.
*
- * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
- * This is to ensure the user action (hover, focus and active) styles have a higher
- * specificity than the visited styles, which in turn have a higher specificity than
- * the unvisited styles.
+ * The selector order controls the cascade when multiple states match. Link and button
+ * selectors follow the LVHA order, with focus-visible immediately after focus. Text input
+ * validation states come before the focus states because an edited field can match both
+ * at once, so user action styles must be output last to win.
*
* See https://core.trac.wordpress.org/ticket/56928.
* Note: this will affect both top-level and block-level elements.
@@ -669,11 +669,14 @@ class WP_Theme_JSON {
* @since 6.2.0 Added support for ':link' and ':any-link'.
* @since 6.8.0 Added support for ':focus-visible'.
* @since 6.9.0 Added `textInput` and `select` elements.
+ * @since 7.2.0 Added ':required', ':user-valid', ':user-invalid', ':focus', ':focus-visible',
+ * and '::placeholder' support for the text input element.
* @var array
*/
const VALID_ELEMENT_PSEUDO_SELECTORS = array(
- 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
- 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
+ 'link' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
+ 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
+ 'textInput' => array( ':required', ':user-valid', ':user-invalid', ':focus', ':focus-visible', '::placeholder' ),
);
/**
@@ -1588,6 +1591,44 @@ class WP_Theme_JSON {
return $output;
}
+ /**
+ * Determines whether a pseudo selector is a pseudo-element, e.g. '::placeholder'.
+ *
+ * Pseudo-elements are not valid inside `:where()`, so rules that target them
+ * cannot use the `:root :where()` specificity wrapper.
+ *
+ * @since 7.2.0
+ *
+ * @param string|null $pseudo_selector Pseudo selector to check, e.g. ':hover' or '::placeholder'.
+ * @return bool True if the pseudo selector is a pseudo-element.
+ */
+ protected static function is_pseudo_element( $pseudo_selector ) {
+ return is_string( $pseudo_selector ) && str_starts_with( $pseudo_selector, '::' );
+ }
+
+ /**
+ * Builds the specificity-capped selector for a pseudo selector rule.
+ *
+ * Pseudo-classes are appended to each part of the base selector inside the
+ * `:root :where()` wrapper, e.g. `:root :where(a:hover)`. Pseudo-elements are
+ * not valid inside `:where()`, so they are appended after the wrapper instead,
+ * e.g. `:root :where(textarea, input)::placeholder`, which keeps the same
+ * specificity regardless of where the element styles are defined.
+ *
+ * @since 7.2.0
+ *
+ * @param string $base_selector Selector without the pseudo selector, e.g. 'textarea, input'.
+ * @param string $pseudo_selector Pseudo selector to apply, e.g. ':hover' or '::placeholder'.
+ * @return string The selector to use for the rule.
+ */
+ protected static function get_pseudo_rule_selector( $base_selector, $pseudo_selector ) {
+ if ( static::is_pseudo_element( $pseudo_selector ) ) {
+ return ':root :where(' . $base_selector . ')' . $pseudo_selector;
+ }
+
+ return ':root :where(' . static::append_to_selector( $base_selector, $pseudo_selector ) . ')';
+ }
+
/**
* Appends a sub-selector to an existing one.
*
@@ -4275,7 +4316,7 @@ class WP_Theme_JSON {
continue;
}
- $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations );
+ $pseudo_selector_ruleset = static::to_ruleset( static::get_pseudo_rule_selector( $variation_element_selector, $pseudo_selector ), $pseudo_declarations );
$variation_responsive_pseudo_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}';
}
}
@@ -4420,6 +4461,9 @@ class WP_Theme_JSON {
* Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
* still need to be wrapped in `:root :where` to cap specificity for nested
* variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
+ *
+ * Pseudo-elements, e.g. ::placeholder, are not valid inside `:where()`, so
+ * they are appended after the wrapper instead.
*/
$element_only_selector = $is_root_selector || (
$current_element &&
@@ -4430,8 +4474,13 @@ class WP_Theme_JSON {
);
// 2. Generate and append the rules that use the general selector.
- $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
- $block_rules .= static::to_ruleset( $general_selector, $declarations );
+ if ( static::is_pseudo_element( $pseudo_selector ) ) {
+ // The node selector already carries the pseudo-element on each part, so strip it to get the base selector.
+ $general_selector = static::get_pseudo_rule_selector( str_replace( $pseudo_selector, '', $selector ), $pseudo_selector );
+ } else {
+ $general_selector = $element_only_selector ? $selector : ":root :where($selector)";
+ }
+ $block_rules .= static::to_ruleset( $general_selector, $declarations );
// 3. Generate and append the rules that use the duotone selector.
if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 5b27ed7f99..f8f182e999 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-64596';
+$wp_version = '7.2-alpha-64597';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.