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
107 changes: 106 additions & 1 deletion src/wp-includes/class-wp-theme-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,16 @@ class WP_Theme_JSON {
'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
);

/**
* The valid pseudo-selectors that can be used for blocks.
*
* @since 7.0
* @var array
*/
const VALID_BLOCK_PSEUDO_SELECTORS = array(
'core/button' => array( ':hover', ':focus', ':focus-visible', ':active' ),
);

/**
* The valid elements that can be found under styles.
*
Expand Down Expand Up @@ -699,6 +709,35 @@ protected static function schema_in_root_and_per_origin( $schema ) {
return $schema_in_root_and_per_origin;
}


/**
* Processes pseudo-selectors for any node (block or variation).
*
* @param array $node The node data (block or variation).
* @param string $base_selector The base selector.
* @param array $settings The theme settings.
* @param string $block_name The block name.
* @return array Array of pseudo-selector declarations.
*/
private static function process_pseudo_selectors( $node, $base_selector, $settings, $block_name ) {
$pseudo_declarations = array();

if ( ! isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) {
return $pseudo_declarations;
}

foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
if ( isset( $node[ $pseudo_selector ] ) ) {
$combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, null );
$pseudo_declarations[ $combined_selector ] = $declarations;
}
}

return $pseudo_declarations;
}


/**
* Returns a class name by an element name.
*
Expand Down Expand Up @@ -1017,6 +1056,13 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n
$schema_settings_blocks[ $block ] = static::VALID_SETTINGS;
$schema_styles_blocks[ $block ] = $styles_non_top_level;
$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;

// Add pseudo-selectors for blocks that support them
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
$schema_styles_blocks[ $block ][ $pseudo_selector ] = $styles_non_top_level;
}
}
}

$block_style_variation_styles = static::VALID_STYLES;
Expand All @@ -1039,7 +1085,18 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n

$schema_styles_variations = array();
if ( ! empty( $style_variation_names ) ) {
$schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles );
foreach ( $style_variation_names as $variation_name ) {
$variation_schema = $block_style_variation_styles;

// Add pseudo-selectors to variations for blocks that support them
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) {
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) {
$variation_schema[ $pseudo_selector ] = $styles_non_top_level;
}
}

$schema_styles_variations[ $variation_name ] = $variation_schema;
}
}

$schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
Expand Down Expand Up @@ -2793,6 +2850,23 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt
);
}

// Handle any pseudo selectors for the block.
if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) {
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) {
if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) {
$nodes[] = array(
'name' => $name,
'path' => array( 'styles', 'blocks', $name, $pseudo_selector ),
'selector' => static::append_to_selector( $selector, $pseudo_selector ),
'selectors' => $feature_selectors,
'duotone' => $duotone_selector,
'variations' => $variation_selectors,
'css' => static::append_to_selector( $selector, $pseudo_selector ),
);
}
}
}

if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
$node_path = array( 'styles', 'blocks', $name, 'elements', $element );
Expand Down Expand Up @@ -2886,6 +2960,12 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) {

// Compute declarations for remaining styles not covered by feature level selectors.
$style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );

// Process pseudo-selectors for this variation (e.g., :hover, :focus)
$block_name = isset( $block_metadata['name'] ) ? $block_metadata['name'] : ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ? $block_metadata['path'][2] : null );
$variation_pseudo_declarations = static::process_pseudo_selectors( $style_variation_node, $style_variation['selector'], $settings, $block_name );
$style_variation_declarations = array_merge( $style_variation_declarations, $variation_pseudo_declarations );

// Store custom CSS for the style variation.
if ( isset( $style_variation_node['css'] ) ) {
$style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
Expand All @@ -2908,6 +2988,23 @@ static function ( $split_selector ) use ( $clean_style_variation_selector ) {
$element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
}

/*
* Check if we're processing a block pseudo-selector.
* $block_metadata['path'] = array( 'styles', 'blocks', 'core/button', ':hover' );
*/
$is_processing_block_pseudo = false;
$block_pseudo_selector = null;
if ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 4 ) {
$block_name = $block_metadata['path'][2]; // 'core/button'
$last_path_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; // ':hover'

if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) &&
in_array( $last_path_element, static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ], true ) ) {
$is_processing_block_pseudo = true;
$block_pseudo_selector = $last_path_element;
}
}

/*
* Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
* This also resets the array keys.
Expand Down Expand Up @@ -2937,6 +3034,14 @@ static function ( $pseudo_selector ) use ( $selector ) {
&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
) {
$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
} elseif ( $is_processing_block_pseudo ) {
// Process block pseudo-selector styles
// For block pseudo-selectors, we need to get the block data first, then access the pseudo-selector
$block_name = $block_metadata['path'][2]; // 'core/button'
$block_data = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $block_name ), array() );
$pseudo_data = isset( $block_data[ $block_pseudo_selector ] ) ? $block_data[ $block_pseudo_selector ] : array();

$declarations = static::compute_style_properties( $pseudo_data, $settings, null, $this->theme_json, $selector, $use_root_padding );
} else {
$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
}
Expand Down
4 changes: 2 additions & 2 deletions src/wp-includes/global-styles-and-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ function wp_add_global_styles_for_blocks() {
foreach ( $block_nodes as $metadata ) {

if ( $can_use_cached ) {
// Use the block name as the key for cached CSS data. Otherwise, use a hash of the metadata.
$cache_node_key = isset( $metadata['name'] ) ? $metadata['name'] : md5( wp_json_encode( $metadata ) );
// Generate a unique cache key based on the full metadata to ensure pseudo-selectors and other variations get unique keys.
$cache_node_key = md5( wp_json_encode( $metadata ) );

if ( isset( $cached['blocks'][ $cache_node_key ] ) ) {
$block_css = $cached['blocks'][ $cache_node_key ];
Expand Down
193 changes: 193 additions & 0 deletions tests/phpunit/tests/theme/wpThemeJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -6623,4 +6623,197 @@ public function test_merge_incoming_data_unique_slugs_always_preserved() {

$this->assertEqualSetsWithIndex( $expected, $actual );
}

/**
* Test that block pseudo selectors are processed correctly.
*/
public function test_block_pseudo_selectors_are_processed() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
'core/button' => array(
'color' => array(
'text' => 'white',
'background' => 'blue',
),
':hover' => array(
'color' => array(
'text' => 'blue',
'background' => 'white',
),
),
':focus' => array(
'color' => array(
'text' => 'red',
'background' => 'yellow',
),
),
),
),
),
)
);

$expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link:focus){background-color: yellow;color: red;}';
$this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}

/**
* Test that block pseudo selectors are processed correctly within variations.
*/
public function test_block_variation_pseudo_selectors_are_processed() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
'core/button' => array(
'color' => array(
'text' => 'white',
'background' => 'blue',
),
'variations' => array(
'outline' => array(
'color' => array(
'text' => 'currentColor',
'background' => 'transparent',
),
'border' => array(
'color' => 'currentColor',
'width' => '1px',
'style' => 'solid',
),
':hover' => array(
'color' => array(
'text' => 'white',
'background' => 'red',
),
),
':focus' => array(
'color' => array(
'text' => 'black',
'background' => 'yellow',
),
),
),
),
),
),
),
)
);

$expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link){background-color: transparent;border-color: currentColor;border-width: 1px;border-style: solid;color: currentColor;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:hover){background-color: red;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:focus){background-color: yellow;color: black;}';
$this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}

/**
* Test that non-whitelisted pseudo selectors are ignored for blocks.
*/
public function test_block_pseudo_selectors_ignores_non_whitelisted() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
'core/button' => array(
'color' => array(
'text' => 'white',
'background' => 'blue',
),
':hover' => array(
'color' => array(
'text' => 'blue',
'background' => 'white',
),
),
':levitate' => array(
'color' => array(
'text' => 'yellow',
'background' => 'black',
),
),
),
),
),
)
);

$expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}';
$this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
$this->assertStringNotContainsString( '.wp-block-button .wp-block-button__link:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) );
}

/**
* Test that blocks without pseudo selector support ignore pseudo selectors.
*/
public function test_blocks_without_pseudo_support_ignore_pseudo_selectors() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
'core/paragraph' => array(
'color' => array(
'text' => 'black',
),
':hover' => array(
'color' => array(
'text' => 'red',
),
),
),
),
),
)
);

$expected = ':root :where(p){color: black;}';
$this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
$this->assertStringNotContainsString( 'p:hover{', $theme_json->get_stylesheet( array( 'styles' ) ) );
}

/**
* Test that block pseudo selectors work with elements within blocks.
*/
public function test_block_pseudo_selectors_with_elements() {
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
'styles' => array(
'blocks' => array(
'core/button' => array(
'color' => array(
'text' => 'white',
'background' => 'blue',
),
':hover' => array(
'color' => array(
'text' => 'blue',
'background' => 'white',
),
),
'elements' => array(
'button' => array(
'color' => array(
'text' => 'green',
),
':hover' => array(
'color' => array(
'text' => 'orange',
),
),
),
),
),
),
),
)
);

$expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button,.wp-block-button .wp-block-button__link .wp-block-button__link){color: green;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button:hover,.wp-block-button .wp-block-button__link .wp-block-button__link:hover){color: orange;}';
$this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) );
}
}
Loading