From 582fe2b51484b2aa99bb7676eb1df5147cbe2fc7 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 11 Sep 2026 19:06:20 +1000 Subject: [PATCH] Editor: Add a background clip block support Adds a background.backgroundClip block support, so a block can set which box its background is painted into: border-box, padding-box, content-box, or the text itself. Clipping to the text is what gives blocks a gradient text fill. Registers the property in the style engine and theme.json, allows the declarations through KSES, and skips the has-background class when the background is clipped to the text. Props aaronrobertshaw. See #66094. --- src/wp-includes/block-supports/background.php | 23 ++++- src/wp-includes/class-wp-theme-json.php | 33 +++++++ src/wp-includes/kses.php | 5 + .../style-engine/class-wp-style-engine.php | 92 +++++++++++++++++-- .../wpRenderBackgroundSupport.php | 55 ++++++++++- .../tests/style-engine/styleEngine.php | 44 +++++++++ 6 files changed, 237 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/block-supports/background.php b/src/wp-includes/block-supports/background.php index 3b3e277c377f2..4d0b795eaf687 100644 --- a/src/wp-includes/block-supports/background.php +++ b/src/wp-includes/block-supports/background.php @@ -44,6 +44,7 @@ function wp_register_background_support( $block_type ) { * @since 6.6.0 Removed requirement for `backgroundImage.source`. A file/url is the default. * @since 6.7.0 Added support for `backgroundAttachment` output. * @since 7.1.0 Added support for `background.gradient` output. + * @since 7.2.0 Added support for `background.backgroundClip` output. * * @access private * @@ -56,9 +57,10 @@ function wp_render_background_support( $block_content, $block ) { $block_attributes = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array(); $has_background_image_support = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false ); $has_background_gradient_support = block_has_support( $block_type, array( 'background', 'gradient' ), false ); + $has_background_clip_support = block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ); if ( - ( ! $has_background_image_support && ! $has_background_gradient_support ) || + ( ! $has_background_image_support && ! $has_background_gradient_support && ! $has_background_clip_support ) || ! isset( $block_attributes['style']['background'] ) ) { return $block_content; @@ -67,8 +69,9 @@ function wp_render_background_support( $block_content, $block ) { // Check serialization skip for each feature individually. $skip_background_image = ! $has_background_image_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ); $skip_background_gradient = ! $has_background_gradient_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'gradient' ); + $skip_background_clip = ! $has_background_clip_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' ); - if ( $skip_background_image && $skip_background_gradient ) { + if ( $skip_background_image && $skip_background_gradient && $skip_background_clip ) { return $block_content; } @@ -95,6 +98,10 @@ function wp_render_background_support( $block_content, $block ) { $background_styles['gradient'] = $block_attributes['style']['background']['gradient'] ?? null; } + if ( ! $skip_background_clip ) { + $background_styles['backgroundClip'] = $block_attributes['style']['background']['backgroundClip'] ?? null; + } + $styles = wp_style_engine_get_styles( array( 'background' => $background_styles ) ); if ( ! empty( $styles['css'] ) ) { @@ -111,7 +118,17 @@ function wp_render_background_support( $block_content, $block ) { } $tags->set_attribute( 'style', $updated_style ); - $tags->add_class( 'has-background' ); + + /* + * A background clipped to the text paints the text rather than the + * block's background, so the block has no background to announce. + */ + $is_text_clip = isset( $block_attributes['style']['background']['backgroundClip'] ) + && 'text' === $block_attributes['style']['background']['backgroundClip']; + + if ( ! $is_text_clip ) { + $tags->add_class( 'has-background' ); + } } return $tags->get_updated_html(); diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index e91d547eb58f1..dc20633273f48 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -249,6 +249,7 @@ class WP_Theme_JSON { * @since 7.0.0 Added `dimensions.width` and `dimensions.height`. * Added `text-indent` property. * @since 7.1.0 Added `min-width` and `text-shadow`. + * @since 7.2.0 Added `background-clip`. * @var array */ const PROPERTIES_METADATA = array( @@ -260,6 +261,7 @@ class WP_Theme_JSON { 'background-repeat' => array( 'background', 'backgroundRepeat' ), 'background-size' => array( 'background', 'backgroundSize' ), 'background-attachment' => array( 'background', 'backgroundAttachment' ), + 'background-clip' => array( 'background', 'backgroundClip' ), 'border-radius' => array( 'border', 'radius' ), 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), @@ -418,6 +420,7 @@ class WP_Theme_JSON { * Added support for `dimensions.width` and `dimensions.height`. * Added support for `typography.textIndent`. * @since 7.1.0 Added `viewport` property. + * @since 7.2.0 Added `background.backgroundClip` property. * Added support for `background.gradient`, `dimensions.minWidth` and `blockVisibility.allowEditing`. * @var array */ @@ -425,6 +428,7 @@ class WP_Theme_JSON { 'appearanceTools' => null, 'useRootPaddingAwareAlignments' => null, 'background' => array( + 'backgroundClip' => null, 'backgroundImage' => null, 'backgroundSize' => null, 'gradient' => null, @@ -567,10 +571,12 @@ class WP_Theme_JSON { * @since 7.0.0 Added support for `dimensions.width` and `dimensions.height`. * @since 7.1.0 Added support for `background.gradient`,`dimensions.minWidth`, * and `typography.textShadow`. + * @since 7.2.0 Added support for `background.backgroundClip`. * @var array */ const VALID_STYLES = array( 'background' => array( + 'backgroundClip' => null, 'backgroundImage' => null, 'backgroundPosition' => null, 'backgroundRepeat' => null, @@ -3118,6 +3124,33 @@ protected static function compute_style_properties( $styles, $settings = array() 'name' => $css_property, 'value' => $value, ); + + /* + * Background clipping needs vendor prefixed properties for + * cross-browser support. The `text` value clips the background to + * the text and makes it visible through a transparent fill. The box + * values only reset the fill color, to cancel any inherited text + * clipping. `-webkit-background-clip` is an alias of + * `background-clip` in Chromium, so resetting it would discard the + * value set above. + */ + if ( 'background-clip' === $css_property ) { + if ( 'text' === $value ) { + $declarations[] = array( + 'name' => '-webkit-background-clip', + 'value' => 'text', + ); + $declarations[] = array( + 'name' => '-webkit-text-fill-color', + 'value' => 'transparent', + ); + } else { + $declarations[] = array( + 'name' => '-webkit-text-fill-color', + 'value' => 'unset', + ); + } + } } // If a variable value is added to the root, the corresponding property should be removed. diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index 9394b75989912..16c704a2dd194 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -2637,6 +2637,7 @@ function kses_init() { * @since 6.6.0 Added support for `grid-column`, `grid-row`, and `container-type`. * @since 6.9.0 Added support for `white-space`. * @since 7.1.0 Extended gradient support to allow any single-level nested function. + * @since 7.2.0 Added support for `background-clip`. * Added support for transform functions, `clip-path` basic shapes, * and URLs in the SVG element reference properties. * @@ -2662,6 +2663,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) { * * @since 2.8.1 * @since 7.1.0 Added support for SVG presentation attributes. + * @since 7.2.0 Added support for `background-clip` and its vendor prefixed companions. * * @param string[] $attr Array of allowed CSS attributes. */ @@ -2676,6 +2678,9 @@ function safecss_filter_attr( $css, $deprecated = '' ) { 'background-size', 'background-attachment', 'background-blend-mode', + 'background-clip', + '-webkit-background-clip', + '-webkit-text-fill-color', 'border', 'border-radius', diff --git a/src/wp-includes/style-engine/class-wp-style-engine.php b/src/wp-includes/style-engine/class-wp-style-engine.php index 5d393b3782ec9..aff0d82ab20a6 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine.php +++ b/src/wp-includes/style-engine/class-wp-style-engine.php @@ -49,6 +49,7 @@ final class WP_Style_Engine { * * @since 6.1.0 * @since 7.1.0 Added `background.gradient` property. + * @since 7.2.0 Added `background.backgroundClip` property. * @var array */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( @@ -96,6 +97,10 @@ final class WP_Style_Engine { 'has-background' => true, ), ), + 'backgroundClip' => array( + 'value_func' => array( self::class, 'get_background_clip_css_declarations' ), + 'path' => array( 'background', 'backgroundClip' ), + ), ), 'color' => array( 'text' => array( @@ -125,17 +130,14 @@ final class WP_Style_Engine { ), ), 'gradient' => array( - 'property_keys' => array( + 'property_keys' => array( 'default' => 'background', ), - 'path' => array( 'color', 'gradient' ), - 'css_vars' => array( + 'css_vars' => array( 'gradient' => '--wp--preset--gradient--$slug', ), - 'classnames' => array( - 'has-background' => true, - 'has-$slug-gradient-background' => 'gradient', - ), + 'path' => array( 'color', 'gradient' ), + 'classnames_func' => array( self::class, 'get_gradient_classnames' ), ), ), 'border' => array( @@ -513,7 +515,7 @@ public static function parse_block_styles( $block_styles, $options ) { continue; } - $classnames = static::get_classnames( $style_value, $style_definition ); + $classnames = static::get_classnames( $style_value, $style_definition, $options ); if ( ! empty( $classnames ) ) { $parsed_styles['classnames'] = array_merge( $parsed_styles['classnames'], $classnames ); } @@ -540,17 +542,23 @@ public static function parse_block_styles( $block_styles, $options ) { * e.g. `var:preset||`. * * @since 6.1.0 + * @since 7.2.0 Added the `$options` parameter and support for `classnames_func`. * * @param string $style_value A single raw style value or CSS preset property * from the `$block_styles` array. * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $options Optional. An array of options. Default empty array. * @return string[] An array of CSS classnames, or empty array if there are none. */ - protected static function get_classnames( $style_value, $style_definition ) { + protected static function get_classnames( $style_value, $style_definition, $options = array() ) { if ( empty( $style_value ) ) { return array(); } + if ( isset( $style_definition['classnames_func'] ) && is_callable( $style_definition['classnames_func'] ) ) { + return call_user_func( $style_definition['classnames_func'], $style_value, $style_definition, $options ); + } + $classnames = array(); if ( ! empty( $style_definition['classnames'] ) ) { foreach ( $style_definition['classnames'] as $classname => $property_key ) { @@ -755,6 +763,72 @@ protected static function get_url_or_value_css_declaration( $style_value, $style return $css_declarations; } + /** + * Style value parser that returns the CSS declarations for background clipping. + * + * For the `text` value the background is clipped to the block's text, which + * requires the vendor prefixed properties and a transparent fill color. For + * the box values only the fill color is reset, to cancel any inherited text + * clipping. `-webkit-background-clip` is an alias of `background-clip` in + * Chromium, so resetting it there would discard the value set above. + * + * @since 7.2.0 + * + * @param string $style_value A single raw style value from $block_styles array. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @return string[] An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + */ + protected static function get_background_clip_css_declarations( $style_value, $style_definition ) { + if ( empty( $style_value ) || ! is_string( $style_value ) ) { + return array(); + } + + $valid_values = array( 'border-box', 'padding-box', 'content-box', 'text' ); + + if ( ! in_array( $style_value, $valid_values, true ) ) { + return array(); + } + + $css_declarations = array( + 'background-clip' => $style_value, + ); + + if ( 'text' === $style_value ) { + $css_declarations['-webkit-background-clip'] = 'text'; + $css_declarations['-webkit-text-fill-color'] = 'transparent'; + } else { + $css_declarations['-webkit-text-fill-color'] = 'unset'; + } + + return $css_declarations; + } + + /** + * Returns the classnames for a gradient value. + * + * @since 7.2.0 + * + * @param string $style_value The gradient style value. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $options Optional. An array of options. Default empty array. + * @return string[] An array of CSS classnames, or empty array if there are none. + */ + protected static function get_gradient_classnames( $style_value, $style_definition, $options = array() ) { + if ( empty( $style_value ) ) { + return array(); + } + + $classnames = array( 'has-background' ); + + $slug = static::get_slug_from_preset_value( $style_value, 'gradient' ); + + if ( $slug ) { + $classnames[] = "has-{$slug}-gradient-background"; + } + + return $classnames; + } + /** * Returns compiled CSS from CSS declarations. * diff --git a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php index 6f0a6cc023db7..d63b230a68387 100644 --- a/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php @@ -70,6 +70,7 @@ public function filter_set_theme_root() { * @ticket 61720 * @ticket 61858 * @ticket 64974 + * @ticket 66094 * * @covers ::wp_render_background_support * @@ -126,7 +127,7 @@ public function test_background_block_support( $theme_name, $block_name, $backgr */ public function data_background_block_support() { return array( - 'background image style is applied' => array( + 'background image style is applied' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-rules-are-output', 'background_settings' => array( @@ -200,7 +201,7 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), - 'background gradient style is applied' => array( + 'background gradient style is applied' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-gradient-rules-are-output', 'background_settings' => array( @@ -236,7 +237,7 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), - 'background gradient and image combined' => array( + 'background gradient and image combined' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-gradient-and-image-combined', 'background_settings' => array( @@ -282,6 +283,54 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), + 'background clip border-box style is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-clip-is-output', + 'background_settings' => array( + 'backgroundClip' => true, + ), + 'background_style' => array( + 'backgroundClip' => 'border-box', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background clip text style is applied with vendor prefixes' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-clip-text-is-output', + 'background_settings' => array( + 'backgroundClip' => true, + ), + 'background_style' => array( + 'backgroundClip' => 'text', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), + 'background clip style is applied for a block with only clip support' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-clip-only-support', + 'background_settings' => array( + 'backgroundClip' => true, + ), + 'background_style' => array( + 'backgroundClip' => 'padding-box', + ), + 'expected_wrapper' => '

Content

', + 'wrapper' => '

Content

', + ), + 'background clip style is not applied if the block does not support it' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-clip-not-supported', + 'background_settings' => array( + 'backgroundClip' => false, + ), + 'background_style' => array( + 'backgroundClip' => 'text', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), ); } diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php index fe858e07cb6a7..338138431de6a 100644 --- a/tests/phpunit/tests/style-engine/styleEngine.php +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -25,6 +25,7 @@ class Tests_wpStyleEngine extends WP_UnitTestCase { * @ticket 63799 * @ticket 64974 * @ticket 65037 + * @ticket 66094 * * @covers ::wp_style_engine_get_styles * @@ -667,6 +668,49 @@ public function data_wp_style_engine_get_styles() { ), ), ), + + 'inline_background_clip_border_box' => array( + 'block_styles' => array( + 'background' => array( + 'backgroundClip' => 'border-box', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'background-clip:border-box;-webkit-text-fill-color:unset;', + 'declarations' => array( + 'background-clip' => 'border-box', + '-webkit-text-fill-color' => 'unset', + ), + ), + ), + + 'inline_background_clip_text_with_vendor_prefixes' => array( + 'block_styles' => array( + 'background' => array( + 'backgroundClip' => 'text', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;', + 'declarations' => array( + 'background-clip' => 'text', + '-webkit-background-clip' => 'text', + '-webkit-text-fill-color' => 'transparent', + ), + ), + ), + + 'inline_background_clip_invalid_value' => array( + 'block_styles' => array( + 'background' => array( + 'backgroundClip' => 'invalid-value', + ), + ), + 'options' => array(), + 'expected_output' => array(), + ), ); }