diff --git a/backport-changelog/7.0/10523.md b/backport-changelog/7.0/10523.md new file mode 100644 index 00000000000000..57922ca2510d43 --- /dev/null +++ b/backport-changelog/7.0/10523.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/10523 + +* https://github.com/WordPress/gutenberg/pull/71418 diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index ffc3f272369938..33049d42274bc9 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -590,6 +590,16 @@ class WP_Theme_JSON_Gutenberg { 'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ), ); + /** + * The valid pseudo-selectors that can be used for blocks. + * + * @since 7.0.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. * @@ -680,6 +690,33 @@ 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. * @@ -1001,6 +1038,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; @@ -1023,7 +1067,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; @@ -2663,7 +2718,11 @@ protected static function get_style_nodes( $theme_json, $selectors = array(), $o return $nodes; } - $block_nodes = static::get_block_nodes( $theme_json, $selectors, $options ); + $block_options = $options; + if ( ! isset( $block_options['include_block_style_variations'] ) ) { + $block_options['include_block_style_variations'] = true; + } + $block_nodes = static::get_block_nodes( $theme_json, $selectors, $block_options ); foreach ( $block_nodes as $block_node ) { $nodes[] = $block_node; } @@ -2815,6 +2874,23 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt 'variations' => $variation_selectors, 'css' => $selector, ); + + // 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 ) { @@ -2913,6 +2989,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 ? static::get_block_name_from_metadata_path( $block_metadata ) : 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'] ); @@ -2936,6 +3018,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 = static::get_block_name_from_metadata_path( $block_metadata ); // '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. @@ -2965,6 +3064,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 = static::get_block_name_from_metadata_path( $block_metadata ); // '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 ); } @@ -4616,4 +4723,18 @@ protected static function get_valid_block_style_variations( $blocks_metadata = a return $valid_variations; } + + /** + * Extracts the block name from the block metadata path. + * + * @since 7.0 + * + * @param array $block_metadata Block metadata. + * @return string|null The block name or null if not found. + */ + private static function get_block_name_from_metadata_path( $block_metadata ) { + if ( isset( $block_metadata['path'] ) ) { + return $block_metadata['path'][2]; + } + } } diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index 3ff5e6cb135e18..8111e8a5449311 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -287,8 +287,8 @@ function gutenberg_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 ]; diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index a6043cb907704a..6dc41194626b3b 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -6192,6 +6192,199 @@ public function test_merge_incoming_data_duotone_presets_with_block_level_defaul $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 ) ) ); + } + public function test_merge_incoming_data_block_level_inherits_global_default_setting() { $defaults = new WP_Theme_JSON_Gutenberg( array( diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 02064dacd9bca9..34f67f1ac00ee3 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -1779,6 +1779,23 @@ } } }, + "stylesBlocksPseudoSelectorsProperties": { + "type": "object", + "properties": { + ":hover": { + "$ref": "#/definitions/stylesPropertiesComplete" + }, + ":focus": { + "$ref": "#/definitions/stylesPropertiesComplete" + }, + ":focus-visible": { + "$ref": "#/definitions/stylesPropertiesComplete" + }, + ":active": { + "$ref": "#/definitions/stylesPropertiesComplete" + } + } + }, "stylesElementsPseudoSelectorsPropertyNames": { "enum": [ ":active", @@ -1905,7 +1922,14 @@ "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, "core/button": { - "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + "allOf": [ + { + "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + }, + { + "$ref": "#/definitions/stylesBlocksPseudoSelectorsProperties" + } + ] }, "core/buttons": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete"