diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 977e5ccba4bfc6..561f3b5f775a81 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -62,6 +62,7 @@ Settings related to background. | backgroundImage | Allow users to set a background image. | `boolean` | `false` | | backgroundSize | Allow users to set values related to the size of a background image, including size, position, and repeat controls. | `boolean` | `false` | | gradient | Allow users to set a gradient background. | `boolean` | `false` | +| backgroundClip | Allow users to set background clip. Pass true for all methods, or an array of CSS values: ["text", "padding-box", "border-box", "content-box"]. | `boolean`, `[ string ]` | | --- diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index b17a84a31ba63a..f9e52d23e0bba1 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -44,9 +44,10 @@ function gutenberg_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; @@ -55,8 +56,9 @@ function gutenberg_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; } @@ -81,6 +83,10 @@ function gutenberg_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 = gutenberg_style_engine_get_styles( array( 'background' => $background_styles ) ); if ( ! empty( $styles['css'] ) ) { @@ -97,7 +103,13 @@ function gutenberg_render_background_support( $block_content, $block ) { } $tags->set_attribute( 'style', $updated_style ); - $tags->add_class( 'has-background' ); + // Skip has-background when the gradient is used as a text fill via + // background-clip: text, as the visual effect is on the text, not the background. + $is_text_gradient = isset( $block_attributes['style']['background']['backgroundClip'] ) + && 'text' === $block_attributes['style']['background']['backgroundClip']; + if ( ! $is_text_gradient ) { + $tags->add_class( 'has-background' ); + } } return $tags->get_updated_html(); diff --git a/lib/block-supports/colors.php b/lib/block-supports/colors.php index 5ee4cecfcff74d..cce8792a35d578 100644 --- a/lib/block-supports/colors.php +++ b/lib/block-supports/colors.php @@ -123,7 +123,12 @@ function gutenberg_apply_colors_support( $block_type, $block_attributes ) { } $attributes = array(); - $styles = gutenberg_style_engine_get_styles( array( 'color' => $color_block_styles ), array( 'convert_vars_to_classnames' => true ) ); + $styles = gutenberg_style_engine_get_styles( + array( 'color' => $color_block_styles ), + array( + 'convert_vars_to_classnames' => true, + ) + ); if ( ! empty( $styles['classnames'] ) ) { $attributes['class'] = $styles['classnames']; diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index f5b9cf0e50cfb8..73ff89d993e1c5 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -255,6 +255,7 @@ class WP_Theme_JSON_Gutenberg { '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' ), @@ -396,6 +397,7 @@ class WP_Theme_JSON_Gutenberg { 'appearanceTools' => null, 'useRootPaddingAwareAlignments' => null, 'background' => array( + 'backgroundClip' => null, 'backgroundImage' => null, 'backgroundSize' => null, 'gradient' => null, @@ -526,6 +528,7 @@ class WP_Theme_JSON_Gutenberg { */ const VALID_STYLES = array( 'background' => array( + 'backgroundClip' => null, 'backgroundImage' => null, 'backgroundAttachment' => null, 'backgroundPosition' => null, @@ -783,6 +786,7 @@ public static function get_element_class_name( $element ) { * @var array */ const APPEARANCE_TOOLS_OPT_INS = array( + array( 'background', 'backgroundClip' ), array( 'background', 'backgroundImage' ), array( 'background', 'backgroundSize' ), array( 'background', 'gradient' ), @@ -2619,6 +2623,32 @@ protected static function compute_style_properties( $styles, $settings = array() 'name' => $css_property, 'value' => $value, ); + + // When background-clip is set, add vendor-prefixed properties for + // cross-browser support. For 'text', this clips the background to + // the text and makes it visible via transparent fill. For box-model + // values, unset the fill color to cancel any inherited text gradient. + 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-background-clip', + 'value' => 'unset', + ); + $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/lib/compat/wordpress-7.0/kses.php b/lib/compat/wordpress-7.0/kses.php index 3d0a7ab184be9a..4f501f18010c40 100644 --- a/lib/compat/wordpress-7.0/kses.php +++ b/lib/compat/wordpress-7.0/kses.php @@ -20,3 +20,29 @@ function gutenberg_add_display_to_safe_style_css( $attr ) { return $attr; } add_filter( 'safe_style_css', 'gutenberg_add_display_to_safe_style_css' ); + +/** + * Add background-clip and related vendor-prefixed properties to the list of + * safe CSS properties. Without these, `safecss_filter_attr()` strips the + * declarations generated by the background-clip block support, which causes + * the style engine to return an empty `css` string. + * + * @param string[] $attr List of allowed CSS attributes. + * @return string[] Modified list of allowed CSS attributes. + */ +function gutenberg_add_background_clip_to_safe_style_css( $attr ) { + $background_clip_attrs = array( + 'background-clip', + '-webkit-background-clip', + '-webkit-text-fill-color', + ); + + foreach ( $background_clip_attrs as $background_clip_attr ) { + if ( ! in_array( $background_clip_attr, $attr, true ) ) { + $attr[] = $background_clip_attr; + } + } + + return $attr; +} +add_filter( 'safe_style_css', 'gutenberg_add_background_clip_to_safe_style_css' ); diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 5457a9547d271a..dbd54170fc2392 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -192,6 +192,7 @@ export function useSettingsForBlockElement( [ 'backgroundImage', 'backgroundImage' ], [ 'backgroundSize', 'backgroundSize' ], [ 'backgroundGradient', 'gradient' ], + [ 'backgroundClip', 'backgroundClip' ], ].forEach( ( [ styleKey, settingKey ] ) => { if ( ! supportedStyles.includes( styleKey ) ) { updatedSettings.background = { diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index bd5e050286087e..64e093206af48e 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -52,7 +52,8 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { !! support?.backgroundImage || !! support?.backgroundSize || !! support?.backgroundRepeat || - !! support?.gradient + !! support?.gradient || + !! support?.backgroundClip ); } diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 9aae0cc1c62fa9..37b1b87d36b6f4 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -60,7 +60,16 @@ export function getInlineStyles( styles = {} ) { // The goal is to move everything to server side generated engine styles // This is temporary as we absorb more and more styles into the engine. getCSSRules( styles ).forEach( ( rule ) => { - output[ rule.key ] = rule.value; + // Vendor-prefixed CSS properties start with a dash (e.g. `-webkit-background-clip`). + // React style objects require camelCase keys without the leading dash + // (e.g. `WebkitBackgroundClip`), so convert them here. + const key = rule.key.startsWith( '-' ) + ? rule.key + .slice( 1 ) + .replace( /-([a-z])/g, ( _, c ) => c.toUpperCase() ) + .replace( /^[a-z]/, ( c ) => c.toUpperCase() ) + : rule.key; + output[ key ] = rule.value; } ); return output; diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index cb07892bbbf142..d934335734fdcb 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -239,6 +239,7 @@ export function useBlockSettings( name, parentLayout ) { backgroundImage, backgroundSize, gradient, + backgroundClip, customFontFamilies, defaultFontFamilies, themeFontFamilies, @@ -302,6 +303,7 @@ export function useBlockSettings( name, parentLayout ) { 'background.backgroundImage', 'background.backgroundSize', 'background.gradient', + 'background.backgroundClip', 'typography.fontFamilies.custom', 'typography.fontFamilies.default', 'typography.fontFamilies.theme', @@ -369,6 +371,7 @@ export function useBlockSettings( name, parentLayout ) { backgroundImage, backgroundSize, gradient, + backgroundClip, }, color: { palette: { @@ -457,6 +460,7 @@ export function useBlockSettings( name, parentLayout ) { backgroundImage, backgroundSize, gradient, + backgroundClip, customFontFamilies, defaultFontFamilies, themeFontFamilies, diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 3a782085fffc77..c10cdf4bcb3777 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -61,6 +61,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { support: [ 'background', 'gradient' ], useEngine: true, }, + backgroundClip: { + value: [ 'background', 'backgroundClip' ], + support: [ 'background', 'backgroundClip' ], + useEngine: true, + }, borderColor: { value: [ 'border', 'color' ], support: [ '__experimentalBorder', 'color' ], diff --git a/packages/global-styles-engine/src/core/render.tsx b/packages/global-styles-engine/src/core/render.tsx index 1775d4bb833cbf..5f10317d0ea590 100644 --- a/packages/global-styles-engine/src/core/render.tsx +++ b/packages/global-styles-engine/src/core/render.tsx @@ -659,7 +659,7 @@ export function getStylesDeclarations( ) { return; } - const cssProperty = rule.key.startsWith( '--' ) + const cssProperty = rule.key.startsWith( '-' ) ? rule.key : kebabCase( rule.key ); diff --git a/packages/global-styles-engine/src/settings/get-setting.ts b/packages/global-styles-engine/src/settings/get-setting.ts index c9b2d9c6c3b346..76c5f37628ffe6 100644 --- a/packages/global-styles-engine/src/settings/get-setting.ts +++ b/packages/global-styles-engine/src/settings/get-setting.ts @@ -7,6 +7,7 @@ import type { GlobalStylesConfig } from '../types'; const VALID_SETTINGS = [ 'appearanceTools', 'useRootPaddingAwareAlignments', + 'background.backgroundClip', 'background.backgroundImage', 'background.backgroundRepeat', 'background.backgroundSize', diff --git a/packages/style-engine/src/class-wp-style-engine.php b/packages/style-engine/src/class-wp-style-engine.php index 88719124939ba2..befb3d1a78a6b7 100644 --- a/packages/style-engine/src/class-wp-style-engine.php +++ b/packages/style-engine/src/class-wp-style-engine.php @@ -85,6 +85,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( @@ -114,17 +118,14 @@ final class WP_Style_Engine { ), ), 'gradient' => array( - 'property_keys' => array( + 'property_keys' => array( 'default' => 'background', ), - 'css_vars' => array( + 'css_vars' => array( 'gradient' => '--wp--preset--gradient--$slug', ), - 'path' => array( 'color', 'gradient' ), - 'classnames' => array( - 'has-background' => true, - 'has-$slug-gradient-background' => 'gradient', - ), + 'path' => array( 'color', 'gradient' ), + 'classnames_func' => array( self::class, 'get_gradient_classnames' ), ), ), 'border' => array( @@ -460,7 +461,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 ); } @@ -490,11 +491,15 @@ public static function parse_block_styles( $block_styles, $options ) { * * @return array|string[] An array of CSS classnames, or empty array. */ - 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 ) { @@ -676,6 +681,70 @@ protected static function get_url_or_value_css_declaration( $style_value, $style return $css_declarations; } + /** + * Style value parser that returns CSS declarations for background-clip. + * + * When the value is 'text', this also outputs the necessary vendor-prefixed + * properties to clip the background to the text. + * + * @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" ). + */ + // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by value_func callback signature. + 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-background-clip'] = 'unset'; + $css_declarations['-webkit-text-fill-color'] = 'unset'; + } + + return $css_declarations; + } + + /** + * Returns classnames for a gradient color value. + * + * @since 6.8.0 + * + * @param string $style_value The gradient style value. + * @param array $style_definition The style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * @param array $options Optional. An array of options. + * + * @return string[] An array of CSS classnames. + */ + // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required by classnames_func callback signature. + 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/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts index 102d5842e7a0be..8003ca887f74ae 100644 --- a/packages/style-engine/src/index.ts +++ b/packages/style-engine/src/index.ts @@ -31,7 +31,13 @@ export function compileCSS( style: Style, options: StyleOptions = {} ): string { if ( ! options?.selector ) { const inlineRules: string[] = []; rules.forEach( ( rule ) => { - inlineRules.push( `${ kebabCase( rule.key ) }: ${ rule.value };` ); + inlineRules.push( + `${ + rule.key.startsWith( '-' ) + ? rule.key + : kebabCase( rule.key ) + }: ${ rule.value };` + ); } ); return inlineRules.join( ' ' ); } @@ -56,7 +62,11 @@ export function compileCSS( style: Style, options: StyleOptions = {} ): string { `${ subSelector } { ${ groupedRules[ subSelector ] .map( ( rule: GeneratedCSSRule ) => - `${ kebabCase( rule.key ) }: ${ rule.value };` + `${ + rule.key.startsWith( '-' ) + ? rule.key + : kebabCase( rule.key ) + }: ${ rule.value };` ) .join( ' ' ) } }` ); diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts index c77d2be7ce631e..ba72fa8de63004 100644 --- a/packages/style-engine/src/styles/background/index.ts +++ b/packages/style-engine/src/styles/background/index.ts @@ -85,10 +85,70 @@ const backgroundAttachment = { }, }; +const VALID_BACKGROUND_CLIP_VALUES = [ + 'border-box', + 'padding-box', + 'content-box', + 'text', +]; + +const backgroundClip = { + name: 'backgroundClip', + generate: ( + style: Style, + options: StyleOptions + ): ReturnType< typeof generateRule > => { + const value = style?.background?.backgroundClip; + + if ( ! value || ! VALID_BACKGROUND_CLIP_VALUES.includes( value ) ) { + return []; + } + + const rules = [ + { + selector: options.selector, + key: 'backgroundClip', + value, + }, + ]; + + if ( value === 'text' ) { + rules.push( + { + selector: options.selector, + key: '-webkit-background-clip', + value: 'text', + }, + { + selector: options.selector, + key: '-webkit-text-fill-color', + value: 'transparent', + } + ); + } else { + rules.push( + { + selector: options.selector, + key: '-webkit-background-clip', + value: 'unset', + }, + { + selector: options.selector, + key: '-webkit-text-fill-color', + value: 'unset', + } + ); + } + + return rules; + }, +}; + export default [ backgroundImage, backgroundPosition, backgroundRepeat, backgroundSize, backgroundAttachment, + backgroundClip, ]; diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index b00321c14369fb..91f6b2d3c6e0b9 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -435,6 +435,83 @@ describe( 'getCSSRules', () => { ] ); } ); + it( 'should output backgroundClip rule for non-text values', () => { + expect( + getCSSRules( + { + background: { + backgroundClip: 'border-box', + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [ + { + selector: '.some-selector', + key: 'backgroundClip', + value: 'border-box', + }, + { + selector: '.some-selector', + key: '-webkit-background-clip', + value: 'unset', + }, + { + selector: '.some-selector', + key: '-webkit-text-fill-color', + value: 'unset', + }, + ] ); + } ); + + it( 'should output backgroundClip rules with vendor prefixes for text value', () => { + expect( + getCSSRules( + { + background: { + backgroundClip: 'text', + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [ + { + selector: '.some-selector', + key: 'backgroundClip', + value: 'text', + }, + { + selector: '.some-selector', + key: '-webkit-background-clip', + value: 'text', + }, + { + selector: '.some-selector', + key: '-webkit-text-fill-color', + value: 'transparent', + }, + ] ); + } ); + + it( 'should not output backgroundClip rules for invalid values', () => { + expect( + getCSSRules( + { + background: { + backgroundClip: 'invalid-value', + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [] ); + } ); + it( 'should output background image value when that value is a string', () => { expect( getCSSRules( diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 49dd1fc73ba914..53674e770c31f6 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -29,6 +29,7 @@ export interface Style { backgroundRepeat?: CSSProperties[ 'backgroundRepeat' ]; backgroundSize?: CSSProperties[ 'backgroundSize' ]; gradient?: CSSProperties[ 'backgroundImage' ]; + backgroundClip?: CSSProperties[ 'backgroundClip' ]; }; border?: { color?: CSSProperties[ 'borderColor' ]; diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php index a16027edbba15f..b6cb94f4265a33 100644 --- a/phpunit/block-supports/background-test.php +++ b/phpunit/block-supports/background-test.php @@ -132,7 +132,7 @@ public function data_background_block_support() { $apos = $this->get_apostrophe_entity(); 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( @@ -203,7 +203,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( @@ -239,7 +239,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( @@ -285,6 +285,66 @@ 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 block with only backgroundClip 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
', + ), + 'background clip style is appended to existing styles' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-clip-appends-styles', + 'background_settings' => array( + 'backgroundClip' => true, + ), + 'background_style' => array( + 'backgroundClip' => 'content-box', + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), ); } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 4cd8563e3af6dd..899fb985b061a4 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -314,6 +314,7 @@ public function test_get_settings_appearance_true_opts_in() { $actual = $theme_json->get_settings(); $expected = array( 'background' => array( + 'backgroundClip' => true, 'backgroundImage' => true, 'backgroundSize' => true, 'gradient' => true, @@ -357,6 +358,7 @@ public function test_get_settings_appearance_true_opts_in() { ), 'core/group' => array( 'background' => array( + 'backgroundClip' => true, 'backgroundImage' => true, 'backgroundSize' => true, 'gradient' => true, diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index b0cf611c10629a..a2bd95c867d8c8 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -578,6 +578,50 @@ 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-background-clip:unset;-webkit-text-fill-color:unset;', + 'declarations' => array( + 'background-clip' => 'border-box', + '-webkit-background-clip' => 'unset', + '-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(), + ), ); } diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 24733c24f8b2ae..aed76581a6a02a 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -48,6 +48,25 @@ "description": "Allow users to set a gradient background.", "type": "boolean", "default": false + }, + "backgroundClip": { + "description": "Allow users to set background clip. Pass true for all methods, or an array of CSS values: [\"text\", \"padding-box\", \"border-box\", \"content-box\"].", + "oneOf": [ + { "type": "boolean", "default": false }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "text", + "padding-box", + "border-box", + "content-box" + ] + }, + "uniqueItems": true + } + ] } }, "additionalProperties": false