diff --git a/src/wp-includes/block-supports/block-style-variations.php b/src/wp-includes/block-supports/block-style-variations.php index 07b33ed36e978..437a6110bed72 100644 --- a/src/wp-includes/block-supports/block-style-variations.php +++ b/src/wp-includes/block-supports/block-style-variations.php @@ -175,6 +175,8 @@ function wp_render_block_style_variation_support_styles( $parsed_block ) { 'include_block_style_variations' => true, 'skip_root_layout_styles' => true, 'scope' => ".$class_name", + // The merged block type styles, so a text clip set there can be escaped. + 'text_clip_base_blocks' => $theme_json['styles']['blocks'] ?? array(), ) ); diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 9027c779a7eb3..70021a24c8c4c 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1022,6 +1022,9 @@ private function process_pseudo_selectors( $node, $base_selector, $settings, $bl $base_styles[] = $layer; } } + if ( is_array( $block_metadata ) ) { + $base_styles[] = self::get_text_clip_base_block_styles( $block_metadata ); + } foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { if ( isset( $node[ $pseudo_selector ] ) ) { @@ -2110,6 +2113,7 @@ public function get_settings() { * @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options * to control styles output as desired. * @since 7.0.0 Deprecated 'base-layout-styles' type; added `base_layout_styles` option for classic themes. + * @since 7.3.0 Added the private `text_clip_base_blocks` option. * * @param string[] $types Types of styles to load. Will load all by default. It accepts: * - `variables`: only the CSS Custom Properties for presets & custom ones. @@ -2126,6 +2130,8 @@ public function get_settings() { * @type bool $skip_root_layout_styles Omits root layout styles from the generated stylesheet. Default false. * @type bool $base_layout_styles When true generates only base layout styles without alignment rules. Default false. * @type bool $include_block_style_variations Includes styles for block style variations in the generated stylesheet. Default false. + * @type array $text_clip_base_blocks Private. Block type styles the nodes layer over, read only to escape + * a text clip set there. They are never output. * } * @return string The resulting stylesheet. */ @@ -2150,6 +2156,14 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options ); $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); + // `get_block_classes()` passes only node metadata to `get_styles_for_block()`. + if ( ! empty( $options['text_clip_base_blocks'] ) ) { + foreach ( $style_nodes as &$node ) { + $node['text_clip_base_blocks'] = $options['text_clip_base_blocks']; + } + unset( $node ); + } + $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); @@ -4074,6 +4088,29 @@ private static function get_state_base_styles( array $path, array $tree, array $ return $base_styles; } + /** + * Returns the block type styles a block node layers over, from the private + * `text_clip_base_blocks` stylesheet option. + * + * A block style variation instance stylesheet is built from the variation + * alone, so it cannot otherwise see a text clip set for its block type. + * + * @since 7.3.0 + * + * @param array $block_metadata Metadata about the block to get styles for. + * @return array The block type styles, empty when there are none. + */ + private static function get_text_clip_base_block_styles( array $block_metadata ): array { + $path = $block_metadata['path'] ?? array(); + if ( empty( $block_metadata['text_clip_base_blocks'] ) || 'blocks' !== ( $path[1] ?? null ) || ! isset( $path[2] ) || in_array( 'elements', $path, true ) ) { + return array(); + } + + $base_styles = $block_metadata['text_clip_base_blocks'][ $path[2] ] ?? array(); + + return is_array( $base_styles ) ? $base_styles : array(); + } + /** * Adds the declarations a state needs to escape a text clip set below it. * @@ -4136,6 +4173,8 @@ public function get_styles_for_block( $block_metadata ) { $media_query = $block_metadata['media_query'] ?? null; $responsive_media_queries = static::get_viewport_media_queries( $settings['viewport'] ?? null ); + $text_clip_base_block_styles = self::get_text_clip_base_block_styles( $block_metadata ); + $css_feature_selector = $block_metadata['selectors']['css'] ?? null; if ( is_array( $css_feature_selector ) ) { $css_feature_selector = $css_feature_selector['root'] ?? null; @@ -4159,6 +4198,9 @@ public function get_styles_for_block( $block_metadata ) { $style_variation_responsive_pseudo_css = array(); $style_variation_layout_metadata = array(); if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) { + // A block pseudo node lists the variations too, so read the block's resting styles, not `$node`. + $variation_block_node = _wp_array_get( $this->theme_json, array_slice( $block_metadata['path'], 0, 3 ), array() ); + foreach ( $block_metadata['variations'] as $style_variation ) { $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); @@ -4180,7 +4222,11 @@ public function get_styles_for_block( $block_metadata ) { } // 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 ); + $style_variation_declarations[ $style_variation['selector'] ] = self::get_state_declarations_with_text_clip_escapes( + static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ), + $style_variation_node, + array( $variation_block_node, $text_clip_base_block_styles ) + ); // Process pseudo-selectors for this variation (e.g., :hover, :focus) if ( isset( $block_metadata['name'] ) ) { @@ -4253,7 +4299,7 @@ public function get_styles_for_block( $block_metadata ) { $breakpoint_declarations = self::get_state_declarations_with_text_clip_escapes( $breakpoint_declarations, $breakpoint_node, - array( $style_variation_node, $node ) + array( $style_variation_node, $variation_block_node, $text_clip_base_block_styles ) ); if ( ! empty( $breakpoint_declarations ) ) { $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations ); @@ -4379,7 +4425,8 @@ static function ( $pseudo_selector ) use ( $selector ) { * element then compute the style properties for it. * Otherwise just compute the styles for the default selector as normal. */ - $base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings ); + $base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings ); + $base_styles[] = $text_clip_base_block_styles; if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) diff --git a/tests/phpunit/tests/block-supports/block-style-variations.php b/tests/phpunit/tests/block-supports/block-style-variations.php index 3898178099e7e..5830a3baf244b 100644 --- a/tests/phpunit/tests/block-supports/block-style-variations.php +++ b/tests/phpunit/tests/block-supports/block-style-variations.php @@ -662,4 +662,263 @@ public function test_block_style_variation_class_name_with_missing_class_name() wp_render_block_style_variation_class_name( $block_content, $block ) ); } + + /** + * Renders a block's style variation and returns the instance stylesheet, + * with the given block type styles in the theme origin. + * + * @param string $block_name Block name. + * @param string $class_name Block class name. + * @param array $block_types Block type styles to merge into the theme origin. + * @return string The block style variation instance stylesheet. + */ + private function render_variation_styles_with_block_type_styles( $block_name, $class_name, $block_types ) { + $filter = static function ( $theme_json ) use ( $block_types ) { + return $theme_json->update_with( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => $block_types, + ), + ) + ); + }; + + add_filter( 'wp_theme_json_data_theme', $filter ); + WP_Theme_JSON_Resolver::clean_cached_data(); + + try { + wp_render_block_style_variation_support_styles( + array( + 'blockName' => $block_name, + 'attrs' => array( + 'className' => $class_name, + ), + ) + ); + $inline_styles = wp_styles()->get_data( 'block-style-variation-styles', 'after' ); + return is_array( $inline_styles ) ? implode( '', $inline_styles ) : ''; + } finally { + remove_filter( 'wp_theme_json_data_theme', $filter ); + wp_deregister_style( 'block-style-variation-styles' ); + WP_Theme_JSON_Resolver::clean_cached_data(); + } + } + + /** + * Returns block type styles with a text gradient for the given block. + * + * @param string $block_name Block name. + * @return array Block type styles. + */ + private static function get_text_gradient_block_type_styles( $block_name ) { + return array( + $block_name => array( + 'background' => array( + 'gradient' => 'linear-gradient(90deg, #ff0000 0%, #0000ff 100%)', + 'backgroundClip' => 'text', + ), + ), + ); + } + + /** + * Tests that a variation text color refills the letters over a block type text gradient. + * + * @ticket 66438 + * + * @covers ::wp_render_block_style_variation_support_styles + */ + public function test_block_style_variation_text_color_escapes_block_type_text_clip() { + switch_theme( 'block-theme' ); + register_block_style( + 'core/paragraph', + array( + 'name' => 'clip-text-color', + 'style_data' => array( + 'color' => array( + 'text' => '#00aa00', + ), + ), + ) + ); + + try { + $actual = $this->render_variation_styles_with_block_type_styles( + 'core/paragraph', + 'is-style-clip-text-color', + self::get_text_gradient_block_type_styles( 'core/paragraph' ) + ); + } finally { + unregister_block_style( 'core/paragraph', 'clip-text-color' ); + } + + $this->assertMatchesRegularExpression( + '/:root :where\(p\.is-style-clip-text-color--\d+\)\{color: #00aa00;-webkit-text-fill-color: currentColor;\}/', + $actual + ); + } + + /** + * Tests that an inner block color in a variation shared by several blocks + * refills the letters over the inner block type text gradient. + * + * @ticket 66438 + * + * @covers ::wp_render_block_style_variation_support_styles + */ + public function test_block_style_variation_inner_block_text_color_escapes_block_type_text_clip() { + switch_theme( 'block-theme' ); + register_block_style( + array( 'core/group', 'core/columns' ), + array( + 'name' => 'clip-section', + 'style_data' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => '#aa00aa', + ), + ), + ), + ), + ) + ); + + try { + $actual = $this->render_variation_styles_with_block_type_styles( + 'core/group', + 'is-style-clip-section', + self::get_text_gradient_block_type_styles( 'core/paragraph' ) + ); + } finally { + unregister_block_style( 'core/group', 'clip-section' ); + unregister_block_style( 'core/columns', 'clip-section' ); + } + + $this->assertMatchesRegularExpression( + '/:root :where\(\.is-style-clip-section--\d+ p\)\{color: #aa00aa;-webkit-text-fill-color: currentColor;\}/', + $actual + ); + } + + /** + * Tests that a variation breakpoint text color refills the letters over a + * block type text gradient. + * + * @ticket 66438 + * + * @covers ::wp_render_block_style_variation_support_styles + */ + public function test_block_style_variation_breakpoint_text_color_escapes_block_type_text_clip() { + switch_theme( 'block-theme' ); + register_block_style( + 'core/paragraph', + array( + 'name' => 'clip-mobile', + 'style_data' => array( + '@mobile' => array( + 'color' => array( + 'text' => '#0000aa', + ), + ), + ), + ) + ); + + try { + $actual = $this->render_variation_styles_with_block_type_styles( + 'core/paragraph', + 'is-style-clip-mobile', + self::get_text_gradient_block_type_styles( 'core/paragraph' ) + ); + } finally { + unregister_block_style( 'core/paragraph', 'clip-mobile' ); + } + + $this->assertMatchesRegularExpression( + '/@media \(width <= 480px\)\{:root :where\(p\.is-style-clip-mobile--\d+\)\{color: #0000aa;-webkit-text-fill-color: currentColor;\}\}/', + $actual + ); + } + + /** + * Tests that a variation pseudo-state text color refills the letters over + * a block type text gradient. + * + * @ticket 66438 + * + * @covers ::wp_render_block_style_variation_support_styles + */ + public function test_block_style_variation_pseudo_state_text_color_escapes_block_type_text_clip() { + switch_theme( 'block-theme' ); + register_block_style( + 'core/button', + array( + 'name' => 'clip-hover', + 'style_data' => array( + ':hover' => array( + 'color' => array( + 'text' => '#aa0000', + ), + ), + ), + ) + ); + + try { + $actual = $this->render_variation_styles_with_block_type_styles( + 'core/button', + 'is-style-clip-hover', + self::get_text_gradient_block_type_styles( 'core/button' ) + ); + } finally { + unregister_block_style( 'core/button', 'clip-hover' ); + } + + $this->assertMatchesRegularExpression( + '/:root :where\(\.wp-block-button\.is-style-clip-hover--\d+ \.wp-block-button__link:hover\)\{color: #aa0000;-webkit-text-fill-color: currentColor;\}/', + $actual + ); + } + + /** + * Tests that variation styles are unchanged when no block type sets a text clip. + * + * @ticket 66438 + * + * @covers ::wp_render_block_style_variation_support_styles + */ + public function test_block_style_variation_without_block_type_text_clip_is_unchanged() { + switch_theme( 'block-theme' ); + register_block_style( + 'core/paragraph', + array( + 'name' => 'no-clip', + 'style_data' => array( + 'color' => array( + 'text' => '#00aa00', + ), + ), + ) + ); + + try { + $actual = $this->render_variation_styles_with_block_type_styles( + 'core/paragraph', + 'is-style-no-clip', + array( + 'core/paragraph' => array( + 'background' => array( + 'gradient' => 'linear-gradient(90deg, #ff0000 0%, #0000ff 100%)', + ), + ), + ) + ); + } finally { + unregister_block_style( 'core/paragraph', 'no-clip' ); + } + + $this->assertMatchesRegularExpression( '/:root :where\(p\.is-style-no-clip--\d+\)\{color: #00aa00;\}/', $actual ); + } } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 56c448ee1c43a..76f7c7fa72480 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -1686,6 +1686,61 @@ public function test_variation_pseudo_state_reads_the_block_clip() { ); } + /** + * @ticket 66438 + */ + public function test_variation_rule_from_a_pseudo_node_ignores_the_pseudo_clip() { + register_block_style( + 'core/button', + array( + 'name' => 'flashy', + 'label' => 'Flashy', + ) + ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'background' => array( + 'gradient' => 'linear-gradient(135deg,#ff0000,#0000ff)', + ), + ':hover' => array( + 'background' => array( 'backgroundClip' => 'text' ), + ), + 'variations' => array( + 'flashy' => array( + 'background' => array( 'backgroundClip' => 'padding-box' ), + ), + ), + ), + ), + ), + ) + ); + + $actual_styles = $theme_json->get_styles_for_block( + array( + 'name' => 'core/button', + 'path' => array( 'styles', 'blocks', 'core/button', ':hover' ), + 'selector' => '.wp-block-button:hover', + 'css' => '.wp-block-button:hover', + 'variations' => array( + array( + 'path' => array( 'styles', 'blocks', 'core/button', 'variations', 'flashy' ), + 'selector' => '.is-style-flashy.wp-block-button', + ), + ), + ) + ); + + unregister_block_style( 'core/button', 'flashy' ); + + $this->assertStringNotContainsString( 'background-image: unset', $actual_styles ); + } + /** * @ticket 66094 */