From 60d14a07a16f2afbf2555ed54cdc8949469064fe Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 5 Mar 2026 11:30:40 +0100 Subject: [PATCH 1/3] backport changes from 75736 --- src/wp-includes/class-wp-theme-json.php | 84 ++++++++++++++- tests/phpunit/tests/theme/wpThemeJson.php | 125 ++++++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 455aeeb1214c3..727419e0ef086 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -641,7 +641,30 @@ class WP_Theme_JSON { * @var array */ const VALID_BLOCK_PSEUDO_SELECTORS = array( - 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ), + 'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ), + 'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ), + ); + + /** + * Custom states for blocks that map to CSS class selectors rather than + * CSS pseudo-selectors. Values use the '-' prefix (e.g. '-current') to + * distinguish them from real CSS pseudo-selectors and breakpoint states. + * + * The CSS selector for each state is defined in the block's block.json + * under `selectors.states`, e.g.: + * + * "selectors": { "states": { "-current": ".some-css-selector" } } + * + * This constant controls which states are valid in theme.json for a given + * block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS + * as valid sub-states, producing compound selectors such as + * `.wp-block-navigation-item.current-menu-item:hover`. + * + * @since 7.1.0 + * @var array + */ + const VALID_BLOCK_CUSTOM_STATES = array( + 'core/navigation-link' => array( '-current' ), ); /** @@ -1157,6 +1180,21 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level; } } + + // Add custom states for blocks that support them (e.g. '-current' for navigation). + if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) { + foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) { + $custom_state_schema = $styles_non_top_level; + // The same pseudo-selectors valid for the block at the top level + // are also valid within each custom state. + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) { + $custom_state_schema[ $pseudo ] = $styles_non_top_level; + } + } + $schema_styles_blocks[ $block ][ $custom_state ] = $custom_state_schema; + } + } } $block_style_variation_styles = static::VALID_STYLES; @@ -1556,6 +1594,11 @@ protected static function get_blocks_metadata() { if ( ! empty( $style_selectors ) ) { static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } + + // If the block has custom states defined in block.json, store their selectors. + if ( ! empty( $block_type->selectors['states'] ) && is_array( $block_type->selectors['states'] ) ) { + static::$blocks_metadata[ $block_name ]['states'] = $block_type->selectors['states']; + } } return static::$blocks_metadata; @@ -3218,6 +3261,45 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt } } } + + // Handle custom states (e.g. '-current' for navigation). + if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) { + foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) { + if ( + isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) && + isset( $selectors[ $name ]['states'][ $custom_state ] ) + ) { + $custom_css_selector = $selectors[ $name ]['states'][ $custom_state ]; + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name, $custom_state ), + 'selector' => $custom_css_selector, + 'selectors' => $feature_selectors, + 'duotone' => $duotone_selector, + 'variations' => $variation_selectors, + 'css' => $custom_css_selector, + ); + + // Sub-pseudo-selectors within the custom state. + if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) { + foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) { + if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) { + $compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo ); + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ), + 'selector' => $compound_css_selector, + 'selectors' => $feature_selectors, + 'duotone' => $duotone_selector, + 'variations' => $variation_selectors, + 'css' => $compound_css_selector, + ); + } + } + } + } + } + } } if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index dfc09200ec2b4..08ace6ab99a4b 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -7570,4 +7570,129 @@ public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() { $this->assertStringNotContainsString( 'padding', $result, 'Boolean value should be skipped' ); $this->assertStringNotContainsString( 'gap', $result, 'Array value should be skipped' ); } + + /** + * Test that block custom states (e.g. -current) are processed correctly. + */ + public function test_block_custom_states_are_processed() { + // Only -current styles, no base block styles, so we can assert the + // output uses the current-menu-item selector and not the block selector. + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/navigation-link' => array( + '-current' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ), + ), + ), + ), + ) + ); + + $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ); + $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}'; + $this->assertSame( $expected, $stylesheet ); + } + + /** + * Test that block custom states compound correctly with pseudo-selectors (e.g. -current + :hover). + */ + public function test_block_custom_states_compound_with_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/navigation-link' => array( + '-current' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'blue', + 'background' => 'white', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'yellow', + ), + ), + ), + ), + ), + ), + ) + ); + + $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}:root :where(.wp-block-navigation .current-menu-item:hover){background-color: white;color: blue;}:root :where(.wp-block-navigation .current-menu-item:focus){background-color: yellow;color: green;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Test that non-whitelisted custom states are ignored, and that custom states + * are ignored on blocks that do not declare support for them. + */ + public function test_block_custom_states_ignores_non_whitelisted() { + // A non-whitelisted state key on a block that supports custom states. + $theme_json_bogus_state = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/navigation-link' => array( + 'color' => array( + 'text' => 'black', + ), + '-bogus' => array( + 'color' => array( + 'text' => 'yellow', + ), + ), + ), + ), + ), + ) + ); + + $stylesheet_bogus = $theme_json_bogus_state->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ); + $this->assertStringNotContainsString( '-bogus', $stylesheet_bogus ); + $this->assertStringNotContainsString( 'yellow', $stylesheet_bogus ); + + // A valid custom state key on a block that does not support custom states. + $theme_json_unsupported_block = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'black', + ), + '-current' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ), + ) + ); + + $stylesheet_unsupported = $theme_json_unsupported_block->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ); + $expected = ':root :where(p){color: black;}'; + $this->assertSame( $expected, $stylesheet_unsupported ); + $this->assertStringNotContainsString( '-current', $stylesheet_unsupported ); + $this->assertStringNotContainsString( 'current-menu-item', $stylesheet_unsupported ); + } } From 9fc7e2c186f23aa140c596a13570aec11f39a03e Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Mon, 1 Jun 2026 15:46:33 +1000 Subject: [PATCH 2/3] add docs to tests --- tests/phpunit/tests/theme/wpThemeJson.php | 46 ++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 08ace6ab99a4b..724c5ff997abb 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -7573,6 +7573,10 @@ public function test_to_ruleset_skips_non_scalar_values_and_casts_numerics() { /** * Test that block custom states (e.g. -current) are processed correctly. + * + * @covers WP_Theme_JSON::get_styles_for_block + * + * @ticket 64806 */ public function test_block_custom_states_are_processed() { // Only -current styles, no base block styles, so we can assert the @@ -7595,13 +7599,21 @@ public function test_block_custom_states_are_processed() { ) ); - $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ); - $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}'; - $this->assertSame( $expected, $stylesheet ); + $current_node = array( + 'path' => array( 'styles', 'blocks', 'core/navigation-link', '-current' ), + 'selector' => '.wp-block-navigation .current-menu-item', + ); + $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}'; + + $this->assertSame( $expected, $theme_json->get_styles_for_block( $current_node ) ); } /** * Test that block custom states compound correctly with pseudo-selectors (e.g. -current + :hover). + * + * @covers WP_Theme_JSON::get_styles_for_block + * + * @ticket 64806 */ public function test_block_custom_states_compound_with_pseudo_selectors() { $theme_json = new WP_Theme_JSON( @@ -7634,13 +7646,37 @@ public function test_block_custom_states_compound_with_pseudo_selectors() { ) ); - $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}:root :where(.wp-block-navigation .current-menu-item:hover){background-color: white;color: blue;}:root :where(.wp-block-navigation .current-menu-item:focus){background-color: yellow;color: green;}'; - $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + $current_node = array( + 'path' => array( 'styles', 'blocks', 'core/navigation-link', '-current' ), + 'selector' => '.wp-block-navigation .current-menu-item', + ); + $hover_node = array( + 'path' => array( 'styles', 'blocks', 'core/navigation-link', '-current', ':hover' ), + 'selector' => '.wp-block-navigation .current-menu-item:hover', + ); + $focus_node = array( + 'path' => array( 'styles', 'blocks', 'core/navigation-link', '-current', ':focus' ), + 'selector' => '.wp-block-navigation .current-menu-item:focus', + ); + + $expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}'; + $expected .= ':root :where(.wp-block-navigation .current-menu-item:hover){background-color: white;color: blue;}'; + $expected .= ':root :where(.wp-block-navigation .current-menu-item:focus){background-color: yellow;color: green;}'; + + $actual = $theme_json->get_styles_for_block( $current_node ); + $actual .= $theme_json->get_styles_for_block( $hover_node ); + $actual .= $theme_json->get_styles_for_block( $focus_node ); + + $this->assertSame( $expected, $actual ); } /** * Test that non-whitelisted custom states are ignored, and that custom states * are ignored on blocks that do not declare support for them. + * + * @covers WP_Theme_JSON::get_stylesheet + * + * @ticket 64806 */ public function test_block_custom_states_ignores_non_whitelisted() { // A non-whitelisted state key on a block that supports custom states. From 27f0701b80fc54e9f4fad47e5ca2637eb36866df Mon Sep 17 00:00:00 2001 From: MaggieCabrera Date: Thu, 2 Jul 2026 11:03:15 +0200 Subject: [PATCH 3/3] reformatted comments and added missingelements frm array --- src/wp-includes/class-wp-theme-json.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 727419e0ef086..3c11927d6787b 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1185,8 +1185,10 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) { foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) { $custom_state_schema = $styles_non_top_level; - // The same pseudo-selectors valid for the block at the top level - // are also valid within each custom state. + /* + * The same pseudo-selectors valid for the block at the top level + * are also valid within each custom state. + */ if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) { $custom_state_schema[ $pseudo ] = $styles_non_top_level; @@ -3275,6 +3277,7 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt 'path' => array( 'styles', 'blocks', $name, $custom_state ), 'selector' => $custom_css_selector, 'selectors' => $feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), 'duotone' => $duotone_selector, 'variations' => $variation_selectors, 'css' => $custom_css_selector, @@ -3290,6 +3293,7 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt 'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ), 'selector' => $compound_css_selector, 'selectors' => $feature_selectors, + 'elements' => $selectors[ $name ]['elements'] ?? array(), 'duotone' => $duotone_selector, 'variations' => $variation_selectors, 'css' => $compound_css_selector,