Skip to content

Commit 8ef2465

Browse files
committed
backport changes from 75736
1 parent 6c22d69 commit 8ef2465

File tree

3 files changed

+226
-26
lines changed

3 files changed

+226
-26
lines changed

src/wp-content/themes/twentytwentyfive/theme.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,26 @@
211211
}
212212
},
213213
"core/button": {
214+
"color": {
215+
"background": "blue"
216+
},
217+
":hover": {
218+
"color": {
219+
"background": "green"
220+
}
221+
},
222+
":focus": {
223+
"color": {
224+
"background": "purple"
225+
}
226+
},
214227
"variations": {
215228
"outline": {
229+
":hover": {
230+
"color": {
231+
"background": "red"
232+
}
233+
},
216234
"border": {
217235
"color": "currentColor",
218236
"width": "1px"

src/wp-includes/class-wp-theme-json.php

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,30 @@ class WP_Theme_JSON {
641641
* @var array
642642
*/
643643
const VALID_BLOCK_PSEUDO_SELECTORS = array(
644-
'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
644+
'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
645+
'core/navigation-link' => array( ':hover', ':focus', ':focus-visible', ':active' ),
646+
);
647+
648+
/**
649+
* Custom states for blocks that map to CSS class selectors rather than
650+
* CSS pseudo-selectors. Values use the '@' prefix (e.g. '@current') to
651+
* distinguish them from real CSS pseudo-selectors.
652+
*
653+
* The CSS selector for each state is defined in the block's block.json
654+
* under `selectors.states`, e.g.:
655+
*
656+
* "selectors": { "states": { "@current": ".some-css-selector" } }
657+
*
658+
* This constant controls which states are valid in theme.json for a given
659+
* block. Blocks listed here also inherit their VALID_BLOCK_PSEUDO_SELECTORS
660+
* as valid sub-states, producing compound selectors such as
661+
* `.wp-block-navigation-item.current-menu-item:hover`.
662+
*
663+
* @since 7.1.0
664+
* @var array
665+
*/
666+
const VALID_BLOCK_CUSTOM_STATES = array(
667+
'core/navigation-link' => array( '@current' ),
645668
);
646669

647670
/**
@@ -1094,6 +1117,21 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
10941117
$schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
10951118
}
10961119
}
1120+
1121+
// Add custom states for blocks that support them (e.g. '@current' for navigation).
1122+
if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $block ] ) ) {
1123+
foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $block ] as $custom_state ) {
1124+
$custom_state_schema = $styles_non_top_level;
1125+
// The same pseudo-selectors valid for the block at the top level
1126+
// are also valid within each custom state.
1127+
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
1128+
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo ) {
1129+
$custom_state_schema[ $pseudo ] = $styles_non_top_level;
1130+
}
1131+
}
1132+
$schema_styles_blocks[ $block ][ $custom_state ] = $custom_state_schema;
1133+
}
1134+
}
10971135
}
10981136

10991137
$block_style_variation_styles = static::VALID_STYLES;
@@ -1321,6 +1359,11 @@ protected static function get_blocks_metadata() {
13211359
if ( ! empty( $style_selectors ) ) {
13221360
static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
13231361
}
1362+
1363+
// If the block has custom states defined in block.json, store their selectors.
1364+
if ( ! empty( $block_type->selectors['states'] ) && is_array( $block_type->selectors['states'] ) ) {
1365+
static::$blocks_metadata[ $block_name ]['states'] = $block_type->selectors['states'];
1366+
}
13241367
}
13251368

13261369
return static::$blocks_metadata;
@@ -2897,6 +2940,45 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
28972940
}
28982941
}
28992942
}
2943+
2944+
// Handle custom states (e.g. '@current' for navigation).
2945+
if ( isset( static::VALID_BLOCK_CUSTOM_STATES[ $name ] ) ) {
2946+
foreach ( static::VALID_BLOCK_CUSTOM_STATES[ $name ] as $custom_state ) {
2947+
if (
2948+
isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
2949+
isset( $selectors[ $name ]['states'][ $custom_state ] )
2950+
) {
2951+
$custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
2952+
$nodes[] = array(
2953+
'name' => $name,
2954+
'path' => array( 'styles', 'blocks', $name, $custom_state ),
2955+
'selector' => $custom_css_selector,
2956+
'selectors' => $feature_selectors,
2957+
'duotone' => $duotone_selector,
2958+
'variations' => $variation_selectors,
2959+
'css' => $custom_css_selector,
2960+
);
2961+
2962+
// Sub-pseudo-selectors within the custom state.
2963+
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
2964+
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo ) {
2965+
if ( isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ][ $pseudo ] ) ) {
2966+
$compound_css_selector = static::append_to_selector( $custom_css_selector, $pseudo );
2967+
$nodes[] = array(
2968+
'name' => $name,
2969+
'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
2970+
'selector' => $compound_css_selector,
2971+
'selectors' => $feature_selectors,
2972+
'duotone' => $duotone_selector,
2973+
'variations' => $variation_selectors,
2974+
'css' => $compound_css_selector,
2975+
);
2976+
}
2977+
}
2978+
}
2979+
}
2980+
}
2981+
}
29002982
}
29012983

29022984
if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
@@ -3046,23 +3128,6 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) {
30463128
$element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
30473129
}
30483130

3049-
/*
3050-
* Check if we're processing a block pseudo-selector.
3051-
* $block_metadata['path'] = array( 'styles', 'blocks', 'core/button', ':hover' );
3052-
*/
3053-
$is_processing_block_pseudo = false;
3054-
$block_pseudo_selector = null;
3055-
if ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 4 ) {
3056-
$block_name = $block_metadata['path'][2]; // 'core/button'
3057-
$last_path_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; // ':hover'
3058-
3059-
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) &&
3060-
in_array( $last_path_element, static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ], true ) ) {
3061-
$is_processing_block_pseudo = true;
3062-
$block_pseudo_selector = $last_path_element;
3063-
}
3064-
}
3065-
30663131
/*
30673132
* Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
30683133
* This also resets the array keys.
@@ -3092,14 +3157,6 @@ static function ( $pseudo_selector ) use ( $selector ) {
30923157
&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
30933158
) {
30943159
$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
3095-
} elseif ( $is_processing_block_pseudo ) {
3096-
// Process block pseudo-selector styles
3097-
// For block pseudo-selectors, we need to get the block data first, then access the pseudo-selector
3098-
$block_name = $block_metadata['path'][2]; // 'core/button'
3099-
$block_data = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $block_name ), array() );
3100-
$pseudo_data = $block_data[ $block_pseudo_selector ] ?? array();
3101-
3102-
$declarations = static::compute_style_properties( $pseudo_data, $settings, null, $this->theme_json, $selector, $use_root_padding );
31033160
} else {
31043161
$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
31053162
}

tests/phpunit/tests/theme/wpThemeJson.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7054,4 +7054,129 @@ public function test_sanitize_preserves_null_schema_behavior() {
70547054
$this->assertSame( 'string-value', $settings['appearanceTools'], 'Appearance tools should be string value' );
70557055
$this->assertSame( array( 'nested' => 'value' ), $settings['custom'], 'Custom should be array value' );
70567056
}
7057+
7058+
/**
7059+
* Test that block custom states (e.g. @current) are processed correctly.
7060+
*/
7061+
public function test_block_custom_states_are_processed() {
7062+
// Only @current styles — no base block styles — so we can assert the
7063+
// output uses the current-menu-item selector and not the block selector.
7064+
$theme_json = new WP_Theme_JSON(
7065+
array(
7066+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
7067+
'styles' => array(
7068+
'blocks' => array(
7069+
'core/navigation-link' => array(
7070+
'@current' => array(
7071+
'color' => array(
7072+
'text' => 'red',
7073+
'background' => 'blue',
7074+
),
7075+
),
7076+
),
7077+
),
7078+
),
7079+
)
7080+
);
7081+
7082+
$stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
7083+
$expected = ':root :where(.wp-block-navigation .current-menu-item){background-color: blue;color: red;}';
7084+
$this->assertSame( $expected, $stylesheet );
7085+
}
7086+
7087+
/**
7088+
* Test that block custom states compound correctly with pseudo-selectors (e.g. @current + :hover).
7089+
*/
7090+
public function test_block_custom_states_compound_with_pseudo_selectors() {
7091+
$theme_json = new WP_Theme_JSON(
7092+
array(
7093+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
7094+
'styles' => array(
7095+
'blocks' => array(
7096+
'core/navigation-link' => array(
7097+
'@current' => array(
7098+
'color' => array(
7099+
'text' => 'red',
7100+
'background' => 'blue',
7101+
),
7102+
':hover' => array(
7103+
'color' => array(
7104+
'text' => 'blue',
7105+
'background' => 'white',
7106+
),
7107+
),
7108+
':focus' => array(
7109+
'color' => array(
7110+
'text' => 'green',
7111+
'background' => 'yellow',
7112+
),
7113+
),
7114+
),
7115+
),
7116+
),
7117+
),
7118+
)
7119+
);
7120+
7121+
$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;}';
7122+
$this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
7123+
}
7124+
7125+
/**
7126+
* Test that non-whitelisted custom states are ignored, and that custom states
7127+
* are ignored on blocks that do not declare support for them.
7128+
*/
7129+
public function test_block_custom_states_ignores_non_whitelisted() {
7130+
// A non-whitelisted state key on a block that supports custom states.
7131+
$theme_json_bogus_state = new WP_Theme_JSON(
7132+
array(
7133+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
7134+
'styles' => array(
7135+
'blocks' => array(
7136+
'core/navigation-link' => array(
7137+
'color' => array(
7138+
'text' => 'black',
7139+
),
7140+
'@bogus' => array(
7141+
'color' => array(
7142+
'text' => 'yellow',
7143+
),
7144+
),
7145+
),
7146+
),
7147+
),
7148+
)
7149+
);
7150+
7151+
$stylesheet_bogus = $theme_json_bogus_state->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
7152+
$this->assertStringNotContainsString( '@bogus', $stylesheet_bogus );
7153+
$this->assertStringNotContainsString( 'yellow', $stylesheet_bogus );
7154+
7155+
// A valid custom state key on a block that does not support custom states.
7156+
$theme_json_unsupported_block = new WP_Theme_JSON(
7157+
array(
7158+
'version' => WP_Theme_JSON::LATEST_SCHEMA,
7159+
'styles' => array(
7160+
'blocks' => array(
7161+
'core/paragraph' => array(
7162+
'color' => array(
7163+
'text' => 'black',
7164+
),
7165+
'@current' => array(
7166+
'color' => array(
7167+
'text' => 'red',
7168+
),
7169+
),
7170+
),
7171+
),
7172+
),
7173+
)
7174+
);
7175+
7176+
$stylesheet_unsupported = $theme_json_unsupported_block->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
7177+
$expected = ':root :where(p){color: black;}';
7178+
$this->assertSame( $expected, $stylesheet_unsupported );
7179+
$this->assertStringNotContainsString( '@current', $stylesheet_unsupported );
7180+
$this->assertStringNotContainsString( 'current-menu-item', $stylesheet_unsupported );
7181+
}
70577182
}

0 commit comments

Comments
 (0)