@@ -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 and breakpoint states.
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 /**
@@ -1157,6 +1180,23 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
11571180 $ schema_styles_blocks [ $ block ][ $ pseudo_selector ] = $ styles_non_top_level ;
11581181 }
11591182 }
1183+
1184+ // Add custom states for blocks that support them (e.g. '-current' for navigation).
1185+ if ( isset ( static ::VALID_BLOCK_CUSTOM_STATES [ $ block ] ) ) {
1186+ foreach ( static ::VALID_BLOCK_CUSTOM_STATES [ $ block ] as $ custom_state ) {
1187+ $ custom_state_schema = $ styles_non_top_level ;
1188+ /*
1189+ * The same pseudo-selectors valid for the block at the top level
1190+ * are also valid within each custom state.
1191+ */
1192+ if ( isset ( static ::VALID_BLOCK_PSEUDO_SELECTORS [ $ block ] ) ) {
1193+ foreach ( static ::VALID_BLOCK_PSEUDO_SELECTORS [ $ block ] as $ pseudo ) {
1194+ $ custom_state_schema [ $ pseudo ] = $ styles_non_top_level ;
1195+ }
1196+ }
1197+ $ schema_styles_blocks [ $ block ][ $ custom_state ] = $ custom_state_schema ;
1198+ }
1199+ }
11601200 }
11611201
11621202 $ block_style_variation_styles = static ::VALID_STYLES ;
@@ -1556,6 +1596,11 @@ protected static function get_blocks_metadata() {
15561596 if ( ! empty ( $ style_selectors ) ) {
15571597 static ::$ blocks_metadata [ $ block_name ]['styleVariations ' ] = $ style_selectors ;
15581598 }
1599+
1600+ // If the block has custom states defined in block.json, store their selectors.
1601+ if ( ! empty ( $ block_type ->selectors ['states ' ] ) && is_array ( $ block_type ->selectors ['states ' ] ) ) {
1602+ static ::$ blocks_metadata [ $ block_name ]['states ' ] = $ block_type ->selectors ['states ' ];
1603+ }
15591604 }
15601605
15611606 return static ::$ blocks_metadata ;
@@ -3218,6 +3263,47 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
32183263 }
32193264 }
32203265 }
3266+
3267+ // Handle custom states (e.g. '-current' for navigation).
3268+ if ( isset ( static ::VALID_BLOCK_CUSTOM_STATES [ $ name ] ) ) {
3269+ foreach ( static ::VALID_BLOCK_CUSTOM_STATES [ $ name ] as $ custom_state ) {
3270+ if (
3271+ isset ( $ theme_json ['styles ' ]['blocks ' ][ $ name ][ $ custom_state ] ) &&
3272+ isset ( $ selectors [ $ name ]['states ' ][ $ custom_state ] )
3273+ ) {
3274+ $ custom_css_selector = $ selectors [ $ name ]['states ' ][ $ custom_state ];
3275+ $ nodes [] = array (
3276+ 'name ' => $ name ,
3277+ 'path ' => array ( 'styles ' , 'blocks ' , $ name , $ custom_state ),
3278+ 'selector ' => $ custom_css_selector ,
3279+ 'selectors ' => $ feature_selectors ,
3280+ 'elements ' => $ selectors [ $ name ]['elements ' ] ?? array (),
3281+ 'duotone ' => $ duotone_selector ,
3282+ 'variations ' => $ variation_selectors ,
3283+ 'css ' => $ custom_css_selector ,
3284+ );
3285+
3286+ // Sub-pseudo-selectors within the custom state.
3287+ if ( isset ( static ::VALID_BLOCK_PSEUDO_SELECTORS [ $ name ] ) ) {
3288+ foreach ( static ::VALID_BLOCK_PSEUDO_SELECTORS [ $ name ] as $ pseudo ) {
3289+ if ( isset ( $ theme_json ['styles ' ]['blocks ' ][ $ name ][ $ custom_state ][ $ pseudo ] ) ) {
3290+ $ compound_css_selector = static ::append_to_selector ( $ custom_css_selector , $ pseudo );
3291+ $ nodes [] = array (
3292+ 'name ' => $ name ,
3293+ 'path ' => array ( 'styles ' , 'blocks ' , $ name , $ custom_state , $ pseudo ),
3294+ 'selector ' => $ compound_css_selector ,
3295+ 'selectors ' => $ feature_selectors ,
3296+ 'elements ' => $ selectors [ $ name ]['elements ' ] ?? array (),
3297+ 'duotone ' => $ duotone_selector ,
3298+ 'variations ' => $ variation_selectors ,
3299+ 'css ' => $ compound_css_selector ,
3300+ );
3301+ }
3302+ }
3303+ }
3304+ }
3305+ }
3306+ }
32213307 }
32223308 if ( isset ( $ theme_json ['styles ' ]['blocks ' ][ $ name ]['elements ' ] ) ) {
32233309 foreach ( $ theme_json ['styles ' ]['blocks ' ][ $ name ]['elements ' ] as $ element => $ node ) {
0 commit comments