Commit 28d0447918 for wordpress.org
commit 28d0447918dbe0c28e37db5f1f89d575b58c1ff6
Author: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
Date: Tue Oct 20 06:10:51 2026 +0000
Editor: Add a background clip block support.
Adds a `background.backgroundClip` block support, so a block can paint its background into the border box, padding box, content box or the text itself. Clipping to the text gives blocks a gradient text fill.
The style engine gains the `backgroundClip` definition and a `VALID_BACKGROUND_CLIP_VALUES` constant, `WP_Theme_JSON` gains the setting and style, and KSES allows `background-clip` and its vendor prefixed companions. A color clipped to the text no longer adds the `has-background` class.
Pseudo states and breakpoints that change the clip, or set a text color over a text gradient, now escape the inherited text clip, in both block instance styles and global styles.
This brings in changes from https://github.com/WordPress/gutenberg/pull/77141, https://github.com/WordPress/gutenberg/pull/77142 and https://github.com/WordPress/gutenberg/pull/85057.
Developed in: https://github.com/WordPress/wordpress-develop/pull/13491
Props westonruter, isabel_brison, andrewserong, peterwilsoncc, ramonopoly, scruffian, joen, talldanwp.
Fixes #66094.
Built from https://develop.svn.wordpress.org/trunk@64590
git-svn-id: http://core.svn.wordpress.org/trunk@63740 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/block-supports/background.php b/wp-includes/block-supports/background.php
index 3b3e277c37..8d464cc789 100644
--- a/wp-includes/block-supports/background.php
+++ b/wp-includes/block-supports/background.php
@@ -44,21 +44,39 @@ 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
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
+ *
+ * @phpstan-param array{
+ * blockName: string,
+ * attrs: array{
+ * style?: array{
+ * background?: array<string, mixed>,
+ * ...<string, mixed>
+ * },
+ * ...<string, mixed>
+ * },
+ * ...
+ * } $block
*/
function wp_render_background_support( $block_content, $block ) {
- $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+ if ( ! $block_type ) {
+ return $block_content;
+ }
+
$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 +85,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 +114,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 +134,19 @@ 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 glyphs rather than the
+ * block's box, so there is no block background to announce. Read the
+ * clip from the serialized styles so an unsupported or skipped value
+ * left in the block's attributes cannot suppress the class.
+ */
+ $background_clip = $background_styles['backgroundClip'] ?? null;
+ $has_background = ! empty( $background_styles['backgroundImage'] ) || ! empty( $background_styles['gradient'] );
+
+ if ( $has_background && 'text' !== $background_clip ) {
+ $tags->add_class( 'has-background' );
+ }
}
return $tags->get_updated_html();
diff --git a/wp-includes/block-supports/colors.php b/wp-includes/block-supports/colors.php
index 567a542249..3c3396c587 100644
--- a/wp-includes/block-supports/colors.php
+++ b/wp-includes/block-supports/colors.php
@@ -74,13 +74,14 @@ function wp_register_colors_support( $block_type ) {
* @since 5.6.0
* @since 6.1.0 Implemented the style engine to generate CSS and classnames.
* @since 7.1.0 Suppresses color.gradient when background.gradient is supported and set.
+ * @since 7.2.0 Skips `has-background` for a background color clipped to the text.
* @access private
*
- * @param WP_Block_Type $block_type Block type.
- * @param array $block_attributes Block attributes.
- * @return array Colors CSS classes and inline styles.
+ * @param WP_Block_Type $block_type Block type.
+ * @param array<string, mixed> $block_attributes Block attributes.
+ * @return array<string, mixed> Colors CSS classes and inline styles.
*/
-function wp_apply_colors_support( $block_type, $block_attributes ) {
+function wp_apply_colors_support( WP_Block_Type $block_type, array $block_attributes ): array {
$color_support = $block_type->supports['color'] ?? false;
if (
@@ -133,6 +134,18 @@ function wp_apply_colors_support( $block_type, $block_attributes ) {
$attributes = array();
$styles = wp_style_engine_get_styles( array( 'color' => $color_block_styles ), array( 'convert_vars_to_classnames' => true ) );
+ /*
+ * Mirror of the background support: a color clipped to the text paints the
+ * glyphs, not the box, so only a gradient still adds `has-background`.
+ */
+ $clips_to_text = 'text' === ( $block_attributes['style']['background']['backgroundClip'] ?? null ) &&
+ block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) &&
+ ! wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' );
+
+ if ( $clips_to_text && ! empty( $styles['classnames'] ) && empty( $color_block_styles['gradient'] ) ) {
+ $styles['classnames'] = implode( ' ', array_diff( explode( ' ', $styles['classnames'] ), array( 'has-background' ) ) );
+ }
+
if ( ! empty( $styles['classnames'] ) ) {
$attributes['class'] = $styles['classnames'];
}
diff --git a/wp-includes/block-supports/states.php b/wp-includes/block-supports/states.php
index ebd2c0915a..2657558486 100644
--- a/wp-includes/block-supports/states.php
+++ b/wp-includes/block-supports/states.php
@@ -147,6 +147,96 @@ function wp_get_state_declarations_with_background_resets( $declarations ) {
return $declarations;
}
+/**
+ * Merges one state style object over another.
+ *
+ * Mirror of `mergeStyleObjects` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $target Styles underneath.
+ * @param array<string, mixed> $source Styles layered over them.
+ * @return array<string, mixed> Merged style object.
+ */
+function wp_merge_state_style_objects( $target, $source ) {
+ $target = is_array( $target ) ? $target : array();
+ $source = is_array( $source ) ? $source : array();
+ $merged = $target;
+
+ foreach ( $source as $key => $value ) {
+ $merged[ $key ] = is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] )
+ ? wp_merge_state_style_objects( $merged[ $key ], $value )
+ : $value;
+ }
+
+ return $merged;
+}
+
+/**
+ * Checks whether a state paints its own background.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @return bool Whether the state sets a gradient or background image.
+ */
+function wp_state_paints_own_background( array $state_style ): bool {
+ return ! empty( $state_style['background']['gradient'] ) ||
+ ! empty( $state_style['background']['backgroundImage'] ) ||
+ ! empty( $state_style['color']['gradient'] );
+}
+
+/**
+ * Returns the declarations that unclip a text clip when a state sets a different clip.
+ *
+ * Mirror of `getStateTextClipEscapeCSS` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @param string $base_clip Background clip of the styles the state layers over.
+ * @return array<string, string> CSS declarations, empty when no escape is needed.
+ *
+ * @phpstan-return array<non-falsy-string, non-falsy-string>
+ */
+function wp_get_state_text_clip_escape( array $state_style, string $base_clip ): array {
+ $state_clip = $state_style['background']['backgroundClip'] ?? '';
+ if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
+ $state_clip = '';
+ }
+ if ( empty( $state_clip ) || 'text' === $state_clip || 'text' !== $base_clip || wp_state_paints_own_background( $state_style ) ) {
+ return array();
+ }
+ return array( 'background-image' => 'unset' );
+}
+
+/**
+ * Returns the declarations that refill the letters when a state sets a text color but no clip.
+ *
+ * The fill is added even without a known text clip, since a block instance
+ * cannot see a clip set in global styles. It does nothing when no clip applies.
+ *
+ * Mirror of `getStateTextFillResetCSS` in packages/block-editor/src/hooks/style.jsx.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $state_style State style object.
+ * @return array<string, string> CSS declarations, empty when no reset is needed.
+ *
+ * @phpstan-return array<non-falsy-string, non-falsy-string>
+ */
+function wp_get_state_text_fill_reset( array $state_style ): array {
+ $state_clip = $state_style['background']['backgroundClip'] ?? '';
+ if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
+ $state_clip = '';
+ }
+ if ( ! empty( $state_clip ) || empty( $state_style['color']['text'] ) || wp_state_paints_own_background( $state_style ) ) {
+ return array();
+ }
+ return array( '-webkit-text-fill-color' => 'currentColor' );
+}
+
/**
* Adds fallback dimension styles for aspectRatio and height block-support values.
*
@@ -365,14 +455,16 @@ function wp_get_block_state_element_selectors( $root_selector ) {
*
* @since 7.1.0
* @since 7.2.0 Added fallback dimension styles.
- *
- * @param array $css_rules Style rules.
- * @param string $state Pseudo-state selector.
- * @param string|null $selector Block, feature, or element selector.
- * @param array $style Style object.
- * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
+ * Added the `$base_style` parameter.
+ *
+ * @param array[] $css_rules Style rules.
+ * @param string $state Pseudo-state selector.
+ * @param string|null $selector Block, feature, or element selector.
+ * @param array<string, mixed> $style Style object.
+ * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed>|null $base_style Optional styles the state layers over.
*/
-function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) {
+function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null, $base_style = null ) {
if ( empty( $style ) || ! is_array( $style ) ) {
return;
}
@@ -389,6 +481,16 @@ function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style,
$declarations['text-align'] = $text_align;
}
+ // Only block states pass a base; element states match the editor and skip it.
+ if ( null !== $base_style ) {
+ $base_clip = $base_style['background']['backgroundClip'] ?? '';
+ $declarations = array_merge(
+ $declarations,
+ wp_get_state_text_clip_escape( $style, is_string( $base_clip ) ? $base_clip : '' ),
+ wp_get_state_text_fill_reset( $style )
+ );
+ }
+
if ( empty( $declarations ) ) {
return;
}
@@ -407,13 +509,20 @@ function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style,
* Builds compiled state style rules, preserving the selector each rule targets.
*
* @since 7.1.0
+ * @since 7.2.0 Added the `$base_style` parameter.
*
- * @param array $state_styles Map of state to style array.
- * @param WP_Block_Type $block_type Block type.
- * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed> $state_styles Map of state to style array.
+ * @param WP_Block_Type $block_type Block type.
+ * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
+ * @param array<string, mixed>|null $base_style Optional styles the state layers over.
* @return array[] State style rules.
*/
-function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null ) {
+function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null, $base_style = null ) {
+ // Only a block that supports the clip can sit under a text clip to escape.
+ if ( ! block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) ) {
+ $base_style = null;
+ }
+
$css_rules = array();
$block_selectors = is_array( $block_type->selectors )
? $block_type->selectors
@@ -430,7 +539,8 @@ function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_grou
$state,
$group['selector'],
$group['style'],
- $rules_group
+ $rules_group,
+ $base_style
);
}
}
@@ -576,6 +686,10 @@ function wp_render_block_states_support( $block_content, $block ) {
$supported_pseudo_states = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
$css_rules = array();
+ $default_root_style = wp_get_root_state_style(
+ $style,
+ array_merge( array( 'elements' ), $supported_pseudo_states )
+ );
$viewport_settings = wp_get_global_settings( array( 'viewport' ) );
$responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
@@ -588,7 +702,9 @@ function wp_render_block_states_support( $block_content, $block ) {
$css_rules,
wp_get_block_state_style_rules(
array( $pseudo_state => $style[ $pseudo_state ] ),
- $block_type
+ $block_type,
+ null,
+ $default_root_style
)
);
}
@@ -609,7 +725,8 @@ function wp_render_block_states_support( $block_content, $block ) {
wp_get_block_state_style_rules(
array( '' => $root_state_style ),
$block_type,
- $media_query
+ $media_query,
+ $default_root_style
)
);
}
@@ -675,7 +792,11 @@ function wp_render_block_states_support( $block_content, $block ) {
wp_get_block_state_style_rules(
array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ),
$block_type,
- $media_query
+ $media_query,
+ wp_merge_state_style_objects(
+ $default_root_style,
+ $root_state_style
+ )
)
);
}
diff --git a/wp-includes/class-wp-theme-json.php b/wp-includes/class-wp-theme-json.php
index 116a8ad951..f57d473065 100644
--- a/wp-includes/class-wp-theme-json.php
+++ b/wp-includes/class-wp-theme-json.php
@@ -259,6 +259,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(
@@ -270,6 +271,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' ),
@@ -428,6 +430,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`.
* @since 7.2.0 Added support for `typography.textShadow`, `typography.textShadowPresets`,
* and `typography.defaultTextShadowPresets`.
@@ -437,6 +440,7 @@ class WP_Theme_JSON {
'appearanceTools' => null,
'useRootPaddingAwareAlignments' => null,
'background' => array(
+ 'backgroundClip' => null,
'backgroundImage' => null,
'backgroundSize' => null,
'gradient' => null,
@@ -582,10 +586,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,
@@ -1000,6 +1006,15 @@ class WP_Theme_JSON {
return $pseudo_declarations;
}
+ // Nearest first: this node, then its variation's block.
+ $base_styles = array( $node );
+ foreach ( array( $style_variation['path'] ?? null, $block_metadata['path'] ?? null ) as $path ) {
+ $layer = $path ? _wp_array_get( $this->theme_json, $path ) : null;
+ if ( is_array( $layer ) ) {
+ $base_styles[] = $layer;
+ }
+ }
+
foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
if ( isset( $node[ $pseudo_selector ] ) ) {
$pseudo_node = $node[ $pseudo_selector ];
@@ -1021,6 +1036,7 @@ class WP_Theme_JSON {
$combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
$declarations = static::compute_style_properties( $pseudo_node, $settings, null, null );
+ $declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $pseudo_node, $base_styles );
$add_declarations( $combined_selector, $declarations );
}
}
@@ -3224,6 +3240,34 @@ class WP_Theme_JSON {
'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. Values outside the set the style engine accepts
+ * get neither, so a typo cannot reset the fill color.
+ */
+ 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',
+ );
+ } elseif ( in_array( $value, array( 'border-box', 'padding-box', 'content-box' ), true ) ) {
+ $declarations[] = array(
+ 'name' => '-webkit-text-fill-color',
+ 'value' => 'currentColor',
+ );
+ }
+ }
}
// If a variable value is added to the root, the corresponding property should be removed.
@@ -3921,6 +3965,108 @@ class WP_Theme_JSON {
return $nodes;
}
+ /**
+ * Returns the node keys that hold a state's styles: a breakpoint, a pseudo
+ * state or a custom state.
+ *
+ * @since 7.2.0
+ *
+ * @param array<string, mixed> $settings Settings from theme.json.
+ * @return string[] Style-state keys.
+ *
+ * @phpstan-return list<non-falsy-string>
+ */
+ private static function get_style_state_keys( array $settings ): array {
+ $keys = array_keys( static::get_viewport_media_queries( $settings['viewport'] ?? null ) );
+
+ foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS as $pseudo_selectors ) {
+ $keys = array_merge( $keys, $pseudo_selectors );
+ }
+
+ foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS as $pseudo_selectors ) {
+ $keys = array_merge( $keys, $pseudo_selectors );
+ }
+
+ foreach ( static::VALID_BLOCK_CUSTOM_STATES as $custom_states ) {
+ $keys = array_merge( $keys, $custom_states );
+ }
+
+ return array_values( array_unique( $keys ) );
+ }
+
+ /**
+ * Returns the style objects a state node layers over, nearest first.
+ *
+ * Drops the path's state keys (e.g. `@mobile`) from the innermost outward.
+ *
+ * @since 7.2.0
+ *
+ * @param string[] $path Path to the node within the theme.json tree.
+ * @param array<string, mixed> $tree The theme.json tree to read from.
+ * @param array<string, mixed> $settings Settings from theme.json.
+ * @return array[] Style objects, nearest first.
+ */
+ private static function get_state_base_styles( array $path, array $tree, array $settings ): array {
+ $state_keys = self::get_style_state_keys( $settings );
+ $base_path = $path;
+ $base_styles = array();
+
+ foreach ( array_reverse( array_keys( $path ) ) as $index ) {
+ if ( ! in_array( $path[ $index ], $state_keys, true ) ) {
+ continue;
+ }
+
+ unset( $base_path[ $index ] );
+ $layer = _wp_array_get( $tree, array_values( $base_path ) );
+ if ( is_array( $layer ) ) {
+ $base_styles[] = $layer;
+ }
+ }
+
+ return $base_styles;
+ }
+
+ /**
+ * Adds the declarations a state needs to escape a text clip set below it.
+ *
+ * @since 7.2.0
+ *
+ * @param array[] $declarations Declarations generated for the state.
+ * @param array<string, mixed> $state_style State style object.
+ * @param array[] $base_styles Style objects the state layers over, nearest first.
+ * @return array[] Declarations with the escapes applied where needed.
+ *
+ * @phpstan-param list<array{ name: non-falsy-string, value: scalar }> $declarations
+ * @phpstan-return list<array{ name: non-falsy-string, value: scalar }>
+ */
+ private static function get_state_declarations_with_text_clip_escapes( array $declarations, array $state_style, array $base_styles ): array {
+ $base_clip = '';
+ foreach ( $base_styles as $base_style ) {
+ if ( ! empty( $base_style['background']['backgroundClip'] ) ) {
+ $base_clip = is_string( $base_style['background']['backgroundClip'] ) ? $base_style['background']['backgroundClip'] : '';
+ break;
+ }
+ }
+
+ if ( 'text' !== $base_clip ) {
+ return $declarations;
+ }
+
+ $escapes = array_merge(
+ wp_get_state_text_clip_escape( $state_style, $base_clip ),
+ wp_get_state_text_fill_reset( $state_style )
+ );
+
+ foreach ( $escapes as $name => $value ) {
+ $declarations[] = array(
+ 'name' => $name,
+ 'value' => $value,
+ );
+ }
+
+ return $declarations;
+ }
+
/**
* Gets the CSS rules for a particular block from theme.json.
*
@@ -4056,6 +4202,11 @@ class WP_Theme_JSON {
// Process base properties for this breakpoint.
$breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
+ $breakpoint_declarations = self::get_state_declarations_with_text_clip_escapes(
+ $breakpoint_declarations,
+ $breakpoint_node,
+ array( $style_variation_node, $node )
+ );
if ( ! empty( $breakpoint_declarations ) ) {
$base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
$variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
@@ -4180,11 +4331,18 @@ class WP_Theme_JSON {
* 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 );
+
if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
&& 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 );
+ $declarations = self::get_state_declarations_with_text_clip_escapes(
+ $declarations,
+ $node[ $pseudo_selector ],
+ array_merge( array( $node ), $base_styles )
+ );
} else {
/*
* For block pseudo-selector nodes (e.g. ':hover'), $node has already had any
@@ -4192,6 +4350,7 @@ class WP_Theme_JSON {
* so those properties are not output twice.
*/
$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
+ $declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $node, $base_styles );
}
$block_rules = '';
diff --git a/wp-includes/kses.php b/wp-includes/kses.php
index cbe2f4d04c..c68b71fcc1 100644
--- a/wp-includes/kses.php
+++ b/wp-includes/kses.php
@@ -3692,6 +3692,7 @@ function kses_init() {
* Added support for transform functions, `clip-path` basic shapes,
* and URLs in the SVG element reference properties.
* @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`.
+ * Added support for `background-clip`.
*
* @param string $css A string of CSS rules, decoded from an HTML `style` attribute.
* @param string $deprecated Not used.
@@ -3717,6 +3718,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.
*/
@@ -3731,6 +3733,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/wp-includes/style-engine/class-wp-style-engine.php b/wp-includes/style-engine/class-wp-style-engine.php
index b2988c7140..81499d15e5 100644
--- a/wp-includes/style-engine/class-wp-style-engine.php
+++ b/wp-includes/style-engine/class-wp-style-engine.php
@@ -31,6 +31,14 @@
*/
#[AllowDynamicProperties]
final class WP_Style_Engine {
+ /**
+ * The values `background-clip` accepts.
+ *
+ * @since 7.2.0
+ * @var string[]
+ */
+ const VALID_BACKGROUND_CLIP_VALUES = array( 'border-box', 'padding-box', 'content-box', 'text' );
+
/**
* Style definitions that contain the instructions to parse/output valid Gutenberg styles from a block's attributes.
*
@@ -50,6 +58,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(
@@ -97,6 +106,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(
@@ -768,6 +781,46 @@ final class WP_Style_Engine {
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.
+ *
+ * @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 array<string, string> An associative array of CSS definitions, mapping properties to values.
+ *
+ * @phpstan-return array<non-falsy-string, non-falsy-string>
+ */
+ protected static function get_background_clip_css_declarations( $style_value, $style_definition ) {
+ if ( ! in_array( $style_value, static::VALID_BACKGROUND_CLIP_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 {
+ /*
+ * Only the fill color is restored. `-webkit-background-clip` is an
+ * alias of `background-clip` in Chromium, so resetting it here would
+ * discard the value set above. The fill color is inherited, so it
+ * needs its initial value rather than `unset`, which would take a
+ * transparent fill from an ancestor clipping to text.
+ */
+ $css_declarations['-webkit-text-fill-color'] = 'currentColor';
+ }
+
+ return $css_declarations;
+ }
+
/**
* Returns compiled CSS from CSS declarations.
*
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 2a6ea03638..ce61ecff64 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-64589';
+$wp_version = '7.2-alpha-64590';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.