Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions src/wp-includes/class-wp-theme-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,31 @@ protected static function get_pseudo_rule_selector( $base_selector, $pseudo_sele
return ':root :where(' . static::append_to_selector( $base_selector, $pseudo_selector ) . ')';
}

/**
* Appends a sub-selector to every block feature and subfeature selector,
* so styles set inside a block state only apply in that state.
*
* @since 7.3.0
*
* @param array|null $feature_selectors Feature selectors, keyed by feature.
* @param string $to_append Selector to append, e.g. ':hover'.
* @return array The feature selectors with $to_append added.
*/
protected static function append_to_feature_selectors( $feature_selectors, $to_append ) {
$state_feature_selectors = array();
foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
if ( is_array( $feature_selector ) ) {
$state_feature_selectors[ $feature ] = array();
foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
$state_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $to_append );
}
} else {
$state_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $to_append );
}
}
return $state_feature_selectors;
}

/**
* Appends a sub-selector to an existing one.
*
Expand Down Expand Up @@ -3843,17 +3868,7 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
* pseudo-state (e.g. '.wp-block-button:hover') rather than the
* default state (e.g. '.wp-block-button').
*/
$pseudo_feature_selectors = array();
foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) {
if ( is_array( $feature_selector ) ) {
$pseudo_feature_selectors[ $feature ] = array();
foreach ( $feature_selector as $subfeature => $subfeature_selector ) {
$pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector );
}
} else {
$pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector );
}
}
$pseudo_feature_selectors = static::append_to_feature_selectors( $feature_selectors, $pseudo_selector );

if ( $has_pseudo ) {
$nodes[] = array(
Expand Down Expand Up @@ -3895,12 +3910,14 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
isset( $theme_json['styles']['blocks'][ $name ][ $custom_state ] ) &&
isset( $selectors[ $name ]['states'][ $custom_state ] )
) {
$custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
$nodes[] = array(
// Feature selectors must target the element that carries the state class.
$custom_css_selector = $selectors[ $name ]['states'][ $custom_state ];
$state_feature_selectors = static::append_to_feature_selectors( $feature_selectors, ':is(' . $custom_css_selector . ')' );
$nodes[] = array(
'name' => $name,
'path' => array( 'styles', 'blocks', $name, $custom_state ),
'selector' => $custom_css_selector,
'selectors' => $feature_selectors,
'selectors' => $state_feature_selectors,
'elements' => $selectors[ $name ]['elements'] ?? array(),
'duotone' => $duotone_selector,
'variations' => $variation_selectors,
Expand All @@ -3916,7 +3933,7 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
'name' => $name,
'path' => array( 'styles', 'blocks', $name, $custom_state, $pseudo ),
'selector' => $compound_css_selector,
'selectors' => $feature_selectors,
'selectors' => static::append_to_feature_selectors( $state_feature_selectors, $pseudo ),
'elements' => $selectors[ $name ]['elements'] ?? array(),
'duotone' => $duotone_selector,
'variations' => $variation_selectors,
Expand Down
144 changes: 142 additions & 2 deletions tests/phpunit/tests/theme/wpThemeJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ public static function set_up_before_class() {
public function tear_down() {
$registry = WP_Block_Type_Registry::get_instance();

if ( $registry->is_registered( 'test/feature-selector' ) ) {
unregister_block_type( 'test/feature-selector' );
foreach ( array( 'test/feature-selector', 'test/state-feature', 'test/state-subfeature' ) as $block_name ) {
if ( $registry->is_registered( $block_name ) ) {
unregister_block_type( $block_name );
}
}

if ( $registry->is_registered( 'test/deprecated-feature-selector' ) ) {
Expand Down Expand Up @@ -10201,6 +10203,144 @@ public function test_block_custom_states_ignores_non_whitelisted() {
$this->assertStringNotContainsString( 'current-menu-item', $stylesheet_unsupported );
}

/**
* Returns a theme.json object that treats `-current` as a valid custom
* state for the test blocks below.
*
* @param string $block_name Test block name.
* @param array $styles Block styles for the test block.
* @return WP_Theme_JSON
*/
private function get_theme_json_with_custom_state( $block_name, $styles ) {
return new class(
array(
'version' => WP_Theme_JSON::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
$block_name => $styles,
),
),
)
) extends WP_Theme_JSON {
const VALID_BLOCK_PSEUDO_SELECTORS = array(
'test/state-feature' => array( ':hover' ),
'test/state-subfeature' => array( ':hover' ),
);
const VALID_BLOCK_CUSTOM_STATES = array(
'test/state-feature' => array( '-current' ),
'test/state-subfeature' => array( '-current' ),
);
};
}

/**
* Tests that styles in a custom block state use the state-scoped feature selector.
*
* @ticket 66667
*/
public function test_block_custom_states_scope_feature_selectors() {
register_block_type(
'test/state-feature',
array(
'api_version' => 3,
'selectors' => array(
'root' => '.wp-block-test',
'dimensions' => '.wp-block-test:not(.is-open *)',
'states' => array(
'-current' => '.wp-block-list .is-current',
),
),
)
);

$theme_json = $this->get_theme_json_with_custom_state(
'test/state-feature',
array(
'-current' => array(
'color' => array( 'text' => 'red' ),
'dimensions' => array( 'minWidth' => '200px' ),
),
)
);

$expected = ':root :where(.wp-block-list .is-current){color: red;}:root :where(.wp-block-test:not(.is-open *):is(.wp-block-list .is-current)){min-width: 200px;}';
$this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}

/**
* Tests that styles in a custom block state scope each part of a subfeature selector list.
*
* @ticket 66667
*/
public function test_block_custom_states_scope_subfeature_selectors() {
register_block_type(
'test/state-subfeature',
array(
'api_version' => 3,
'selectors' => array(
'root' => '.wp-block-test',
'color' => array(
'text' => '.wp-block-test, .wp-block-test-alias',
),
'states' => array(
'-current' => '.wp-block-test.is-current',
),
),
)
);

$theme_json = $this->get_theme_json_with_custom_state(
'test/state-subfeature',
array(
'-current' => array(
'color' => array(
'text' => 'red',
'background' => 'blue',
),
),
)
);

$expected = ':root :where(.wp-block-test.is-current){background-color: blue;}:root :where(.wp-block-test:is(.wp-block-test.is-current), .wp-block-test-alias:is(.wp-block-test.is-current)){color: red;}';
$this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}

/**
* Tests that pseudo selectors inside a custom block state keep both the state and the pseudo selector on feature selectors.
*
* @ticket 66667
*/
public function test_block_custom_state_pseudo_selectors_scope_feature_selectors() {
register_block_type(
'test/state-feature',
array(
'api_version' => 3,
'selectors' => array(
'root' => '.wp-block-test',
'dimensions' => '.wp-block-test:not(.is-open *)',
'states' => array(
'-current' => '.wp-block-list .is-current',
),
),
)
);

$theme_json = $this->get_theme_json_with_custom_state(
'test/state-feature',
array(
'-current' => array(
':hover' => array(
'color' => array( 'text' => 'green' ),
'dimensions' => array( 'minHeight' => '60px' ),
),
),
)
);

$expected = ':root :where(.wp-block-list .is-current:hover){color: green;}:root :where(.wp-block-test:not(.is-open *):is(.wp-block-list .is-current):hover){min-height: 60px;}';
$this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}

/**
* Tests that append_to_selector correctly appends to single and
* compound (comma-separated) selectors.
Expand Down
Loading