Make WordPress Core

Changeset 64590


Ignore:
Timestamp:
10/20/2026 06:09:21 AM (36 hours ago)
Author:
aaronrobertshaw
Message:

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.

Location:
trunk
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/block-supports/background.php

    r63529 r64590  
    4545 * @since 6.7.0 Added support for `backgroundAttachment` output.
    4646 * @since 7.1.0 Added support for `background.gradient` output.
     47 * @since 7.2.0 Added support for `background.backgroundClip` output.
    4748 *
    4849 * @access private
    … …  
    5152 * @param array  $block         Block object.
    5253 * @return string Filtered block content.
     54 *
     55 * @phpstan-param array{
     56 *     blockName: string,
     57 *     attrs: array{
     58 *         style?: array{
     59 *             background?: array<string, mixed>,
     60 *             ...<string, mixed>
     61 *         },
     62 *         ...<string, mixed>
     63 *     },
     64 *     ...
     65 * } $block
    5366 */
    5467function wp_render_background_support( $block_content, $block ) {
    55         $block_type                      = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
     68        $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
     69        if ( ! $block_type ) {
     70                return $block_content;
     71        }
     72
    5673        $block_attributes                = ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) ) ? $block['attrs'] : array();
    5774        $has_background_image_support    = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false );
    5875        $has_background_gradient_support = block_has_support( $block_type, array( 'background', 'gradient' ), false );
     76        $has_background_clip_support     = block_has_support( $block_type, array( 'background', 'backgroundClip' ), false );
    5977
    6078        if (
    61                 ( ! $has_background_image_support && ! $has_background_gradient_support ) ||
     79                ( ! $has_background_image_support && ! $has_background_gradient_support && ! $has_background_clip_support ) ||
    6280                ! isset( $block_attributes['style']['background'] )
    6381        ) {
    … …  
    6886        $skip_background_image    = ! $has_background_image_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' );
    6987        $skip_background_gradient = ! $has_background_gradient_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'gradient' );
     88        $skip_background_clip     = ! $has_background_clip_support || wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' );
    7089
    71         if ( $skip_background_image && $skip_background_gradient ) {
     90        if ( $skip_background_image && $skip_background_gradient && $skip_background_clip ) {
    7291                return $block_content;
    7392        }
    … …  
    96115        }
    97116
     117        if ( ! $skip_background_clip ) {
     118                $background_styles['backgroundClip'] = $block_attributes['style']['background']['backgroundClip'] ?? null;
     119        }
     120
    98121        $styles = wp_style_engine_get_styles( array( 'background' => $background_styles ) );
    99122
    … …  
    112135
    113136                        $tags->set_attribute( 'style', $updated_style );
    114                         $tags->add_class( 'has-background' );
     137
     138                        /*
     139                         * A background clipped to the text paints the glyphs rather than the
     140                         * block's box, so there is no block background to announce. Read the
     141                         * clip from the serialized styles so an unsupported or skipped value
     142                         * left in the block's attributes cannot suppress the class.
     143                         */
     144                        $background_clip = $background_styles['backgroundClip'] ?? null;
     145                        $has_background  = ! empty( $background_styles['backgroundImage'] ) || ! empty( $background_styles['gradient'] );
     146
     147                        if ( $has_background && 'text' !== $background_clip ) {
     148                                $tags->add_class( 'has-background' );
     149                        }
    115150                }
    116151
  • trunk/src/wp-includes/block-supports/colors.php

    r63529 r64590  
    7575 * @since 6.1.0 Implemented the style engine to generate CSS and classnames.
    7676 * @since 7.1.0 Suppresses color.gradient when background.gradient is supported and set.
     77 * @since 7.2.0 Skips `has-background` for a background color clipped to the text.
    7778 * @access private
    7879 *
    79  * @param WP_Block_Type $block_type       Block type.
    80  * @param array        $block_attributes Block attributes.
    81  * @return array Colors CSS classes and inline styles.
     80 * @param WP_Block_Type        $block_type       Block type.
     81 * @param array<string, mixed> $block_attributes Block attributes.
     82 * @return array<string, mixed> Colors CSS classes and inline styles.
    8283 */
    83 function wp_apply_colors_support( $block_type, $block_attributes ) {
     84function wp_apply_colors_support( WP_Block_Type $block_type, array $block_attributes ): array {
    8485        $color_support = $block_type->supports['color'] ?? false;
    8586
    … …  
    134135        $styles     = wp_style_engine_get_styles( array( 'color' => $color_block_styles ), array( 'convert_vars_to_classnames' => true ) );
    135136
     137        /*
     138         * Mirror of the background support: a color clipped to the text paints the
     139         * glyphs, not the box, so only a gradient still adds `has-background`.
     140         */
     141        $clips_to_text = 'text' === ( $block_attributes['style']['background']['backgroundClip'] ?? null ) &&
     142                block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) &&
     143                ! wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundClip' );
     144
     145        if ( $clips_to_text && ! empty( $styles['classnames'] ) && empty( $color_block_styles['gradient'] ) ) {
     146                $styles['classnames'] = implode( ' ', array_diff( explode( ' ', $styles['classnames'] ), array( 'has-background' ) ) );
     147        }
     148
    136149        if ( ! empty( $styles['classnames'] ) ) {
    137150                $attributes['class'] = $styles['classnames'];
  • trunk/src/wp-includes/block-supports/states.php

    r64322 r64590  
    146146
    147147        return $declarations;
     148}
     149
     150/**
     151 * Merges one state style object over another.
     152 *
     153 * Mirror of `mergeStyleObjects` in packages/block-editor/src/hooks/style.jsx.
     154 *
     155 * @since 7.2.0
     156 *
     157 * @param array<string, mixed> $target Styles underneath.
     158 * @param array<string, mixed> $source Styles layered over them.
     159 * @return array<string, mixed> Merged style object.
     160 */
     161function wp_merge_state_style_objects( $target, $source ) {
     162        $target = is_array( $target ) ? $target : array();
     163        $source = is_array( $source ) ? $source : array();
     164        $merged = $target;
     165
     166        foreach ( $source as $key => $value ) {
     167                $merged[ $key ] = is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] )
     168                        ? wp_merge_state_style_objects( $merged[ $key ], $value )
     169                        : $value;
     170        }
     171
     172        return $merged;
     173}
     174
     175/**
     176 * Checks whether a state paints its own background.
     177 *
     178 * @since 7.2.0
     179 * @access private
     180 *
     181 * @param array<string, mixed> $state_style State style object.
     182 * @return bool Whether the state sets a gradient or background image.
     183 */
     184function wp_state_paints_own_background( array $state_style ): bool {
     185        return ! empty( $state_style['background']['gradient'] ) ||
     186                ! empty( $state_style['background']['backgroundImage'] ) ||
     187                ! empty( $state_style['color']['gradient'] );
     188}
     189
     190/**
     191 * Returns the declarations that unclip a text clip when a state sets a different clip.
     192 *
     193 * Mirror of `getStateTextClipEscapeCSS` in packages/block-editor/src/hooks/style.jsx.
     194 *
     195 * @since 7.2.0
     196 *
     197 * @param array<string, mixed> $state_style State style object.
     198 * @param string               $base_clip   Background clip of the styles the state layers over.
     199 * @return array<string, string> CSS declarations, empty when no escape is needed.
     200 *
     201 * @phpstan-return array<non-falsy-string, non-falsy-string>
     202 */
     203function wp_get_state_text_clip_escape( array $state_style, string $base_clip ): array {
     204        $state_clip = $state_style['background']['backgroundClip'] ?? '';
     205        if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
     206                $state_clip = '';
     207        }
     208        if ( empty( $state_clip ) || 'text' === $state_clip || 'text' !== $base_clip || wp_state_paints_own_background( $state_style ) ) {
     209                return array();
     210        }
     211        return array( 'background-image' => 'unset' );
     212}
     213
     214/**
     215 * Returns the declarations that refill the letters when a state sets a text color but no clip.
     216 *
     217 * The fill is added even without a known text clip, since a block instance
     218 * cannot see a clip set in global styles. It does nothing when no clip applies.
     219 *
     220 * Mirror of `getStateTextFillResetCSS` in packages/block-editor/src/hooks/style.jsx.
     221 *
     222 * @since 7.2.0
     223 *
     224 * @param array<string, mixed> $state_style State style object.
     225 * @return array<string, string> CSS declarations, empty when no reset is needed.
     226 *
     227 * @phpstan-return array<non-falsy-string, non-falsy-string>
     228 */
     229function wp_get_state_text_fill_reset( array $state_style ): array {
     230        $state_clip = $state_style['background']['backgroundClip'] ?? '';
     231        if ( ! in_array( $state_clip, WP_Style_Engine::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
     232                $state_clip = '';
     233        }
     234        if ( ! empty( $state_clip ) || empty( $state_style['color']['text'] ) || wp_state_paints_own_background( $state_style ) ) {
     235                return array();
     236        }
     237        return array( '-webkit-text-fill-color' => 'currentColor' );
    148238}
    149239
    … …  
    366456 * @since 7.1.0
    367457 * @since 7.2.0 Added fallback dimension styles.
    368  *
    369  * @param array       $css_rules   Style rules.
    370  * @param string      $state       Pseudo-state selector.
    371  * @param string|null $selector    Block, feature, or element selector.
    372  * @param array       $style       Style object.
    373  * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query.
    374  */
    375 function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) {
     458 *              Added the `$base_style` parameter.
     459 *
     460 * @param array[]                   $css_rules   Style rules.
     461 * @param string                    $state       Pseudo-state selector.
     462 * @param string|null               $selector    Block, feature, or element selector.
     463 * @param array<string, mixed>      $style       Style object.
     464 * @param string|null               $rules_group Optional CSS grouping rule, e.g. a media query.
     465 * @param array<string, mixed>|null $base_style  Optional styles the state layers over.
     466 */
     467function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null, $base_style = null ) {
    376468        if ( empty( $style ) || ! is_array( $style ) ) {
    377469                return;
    … …  
    390482        }
    391483
     484        // Only block states pass a base; element states match the editor and skip it.
     485        if ( null !== $base_style ) {
     486                $base_clip    = $base_style['background']['backgroundClip'] ?? '';
     487                $declarations = array_merge(
     488                        $declarations,
     489                        wp_get_state_text_clip_escape( $style, is_string( $base_clip ) ? $base_clip : '' ),
     490                        wp_get_state_text_fill_reset( $style )
     491                );
     492        }
     493
    392494        if ( empty( $declarations ) ) {
    393495                return;
    … …  
    408510 *
    409511 * @since 7.1.0
    410  *
    411  * @param array         $state_styles Map of state to style array.
    412  * @param WP_Block_Type $block_type   Block type.
    413  * @param string|null   $rules_group  Optional CSS grouping rule, e.g. a media query.
     512 * @since 7.2.0 Added the `$base_style` parameter.
     513 *
     514 * @param array<string, mixed>      $state_styles Map of state to style array.
     515 * @param WP_Block_Type             $block_type   Block type.
     516 * @param string|null               $rules_group  Optional CSS grouping rule, e.g. a media query.
     517 * @param array<string, mixed>|null $base_style   Optional styles the state layers over.
    414518 * @return array[] State style rules.
    415519 */
    416 function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null ) {
     520function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_group = null, $base_style = null ) {
     521        // Only a block that supports the clip can sit under a text clip to escape.
     522        if ( ! block_has_support( $block_type, array( 'background', 'backgroundClip' ), false ) ) {
     523                $base_style = null;
     524        }
     525
    417526        $css_rules       = array();
    418527        $block_selectors = is_array( $block_type->selectors )
    … …  
    431540                                $group['selector'],
    432541                                $group['style'],
    433                                 $rules_group
     542                                $rules_group,
     543                                $base_style
    434544                        );
    435545                }
    … …  
    577687        $supported_pseudo_states  = WP_Theme_JSON::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
    578688        $css_rules                = array();
     689        $default_root_style       = wp_get_root_state_style(
     690                $style,
     691                array_merge( array( 'elements' ), $supported_pseudo_states )
     692        );
    579693        $viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
    580694        $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
    … …  
    589703                        wp_get_block_state_style_rules(
    590704                                array( $pseudo_state => $style[ $pseudo_state ] ),
    591                                 $block_type
     705                                $block_type,
     706                                null,
     707                                $default_root_style
    592708                        )
    593709                );
    … …  
    610726                                        array( '' => $root_state_style ),
    611727                                        $block_type,
    612                                         $media_query
     728                                        $media_query,
     729                                        $default_root_style
    613730                                )
    614731                        );
    … …  
    676793                                        array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ),
    677794                                        $block_type,
    678                                         $media_query
     795                                        $media_query,
     796                                        wp_merge_state_style_objects(
     797                                                $default_root_style,
     798                                                $root_state_style
     799                                        )
    679800                                )
    680801                        );
  • trunk/src/wp-includes/class-wp-theme-json.php

    r64491 r64590  
    260260         *              Added `text-indent` property.
    261261         * @since 7.1.0 Added `min-width` and `text-shadow`.
     262         * @since 7.2.0 Added `background-clip`.
    262263         * @var array
    263264         */
    … …  
    271272                'background-size'                   => array( 'background', 'backgroundSize' ),
    272273                'background-attachment'             => array( 'background', 'backgroundAttachment' ),
     274                'background-clip'                   => array( 'background', 'backgroundClip' ),
    273275                'border-radius'                     => array( 'border', 'radius' ),
    274276                'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
    … …  
    429431         *              Added support for `typography.textIndent`.
    430432         * @since 7.1.0 Added `viewport` property.
     433         * @since 7.2.0 Added `background.backgroundClip` property.
    431434         *              Added support for `background.gradient`, `dimensions.minWidth` and `blockVisibility.allowEditing`.
    432435         * @since 7.2.0 Added support for `typography.textShadow`, `typography.textShadowPresets`,
    … …  
    438441                'useRootPaddingAwareAlignments' => null,
    439442                'background'                    => array(
     443                        'backgroundClip'  => null,
    440444                        'backgroundImage' => null,
    441445                        'backgroundSize'  => null,
    … …  
    583587         * @since 7.1.0 Added support for `background.gradient`,`dimensions.minWidth`,
    584588         *              and `typography.textShadow`.
     589         * @since 7.2.0 Added support for `background.backgroundClip`.
    585590         * @var array
    586591         */
    587592        const VALID_STYLES = array(
    588593                'background' => array(
     594                        'backgroundClip'       => null,
    589595                        'backgroundImage'      => null,
    590596                        'backgroundPosition'   => null,
    … …  
    10011007                }
    10021008
     1009                // Nearest first: this node, then its variation's block.
     1010                $base_styles = array( $node );
     1011                foreach ( array( $style_variation['path'] ?? null, $block_metadata['path'] ?? null ) as $path ) {
     1012                        $layer = $path ? _wp_array_get( $this->theme_json, $path ) : null;
     1013                        if ( is_array( $layer ) ) {
     1014                                $base_styles[] = $layer;
     1015                        }
     1016                }
     1017
    10031018                foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) {
    10041019                        if ( isset( $node[ $pseudo_selector ] ) ) {
    … …  
    10221037                                $combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
    10231038                                $declarations      = static::compute_style_properties( $pseudo_node, $settings, null, null );
     1039                                $declarations      = self::get_state_declarations_with_text_clip_escapes( $declarations, $pseudo_node, $base_styles );
    10241040                                $add_declarations( $combined_selector, $declarations );
    10251041                        }
    … …  
    32253241                                'value' => $value,
    32263242                        );
     3243
     3244                        /*
     3245                         * Background clipping needs vendor prefixed properties for
     3246                         * cross-browser support. The `text` value clips the background to
     3247                         * the text and makes it visible through a transparent fill. The box
     3248                         * values only reset the fill color, to cancel any inherited text
     3249                         * clipping. `-webkit-background-clip` is an alias of
     3250                         * `background-clip` in Chromium, so resetting it would discard the
     3251                         * value set above. Values outside the set the style engine accepts
     3252                         * get neither, so a typo cannot reset the fill color.
     3253                         */
     3254                        if ( 'background-clip' === $css_property ) {
     3255                                if ( 'text' === $value ) {
     3256                                        $declarations[] = array(
     3257                                                'name'  => '-webkit-background-clip',
     3258                                                'value' => 'text',
     3259                                        );
     3260                                        $declarations[] = array(
     3261                                                'name'  => '-webkit-text-fill-color',
     3262                                                'value' => 'transparent',
     3263                                        );
     3264                                } elseif ( in_array( $value, array( 'border-box', 'padding-box', 'content-box' ), true ) ) {
     3265                                        $declarations[] = array(
     3266                                                'name'  => '-webkit-text-fill-color',
     3267                                                'value' => 'currentColor',
     3268                                        );
     3269                                }
     3270                        }
    32273271                }
    32283272
    … …  
    39233967
    39243968        /**
     3969         * Returns the node keys that hold a state's styles: a breakpoint, a pseudo
     3970         * state or a custom state.
     3971         *
     3972         * @since 7.2.0
     3973         *
     3974         * @param array<string, mixed> $settings Settings from theme.json.
     3975         * @return string[] Style-state keys.
     3976         *
     3977         * @phpstan-return list<non-falsy-string>
     3978         */
     3979        private static function get_style_state_keys( array $settings ): array {
     3980                $keys = array_keys( static::get_viewport_media_queries( $settings['viewport'] ?? null ) );
     3981
     3982                foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS as $pseudo_selectors ) {
     3983                        $keys = array_merge( $keys, $pseudo_selectors );
     3984                }
     3985
     3986                foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS as $pseudo_selectors ) {
     3987                        $keys = array_merge( $keys, $pseudo_selectors );
     3988                }
     3989
     3990                foreach ( static::VALID_BLOCK_CUSTOM_STATES as $custom_states ) {
     3991                        $keys = array_merge( $keys, $custom_states );
     3992                }
     3993
     3994                return array_values( array_unique( $keys ) );
     3995        }
     3996
     3997        /**
     3998         * Returns the style objects a state node layers over, nearest first.
     3999         *
     4000         * Drops the path's state keys (e.g. `@mobile`) from the innermost outward.
     4001         *
     4002         * @since 7.2.0
     4003         *
     4004         * @param string[]             $path     Path to the node within the theme.json tree.
     4005         * @param array<string, mixed> $tree     The theme.json tree to read from.
     4006         * @param array<string, mixed> $settings Settings from theme.json.
     4007         * @return array[] Style objects, nearest first.
     4008         */
     4009        private static function get_state_base_styles( array $path, array $tree, array $settings ): array {
     4010                $state_keys  = self::get_style_state_keys( $settings );
     4011                $base_path   = $path;
     4012                $base_styles = array();
     4013
     4014                foreach ( array_reverse( array_keys( $path ) ) as $index ) {
     4015                        if ( ! in_array( $path[ $index ], $state_keys, true ) ) {
     4016                                continue;
     4017                        }
     4018
     4019                        unset( $base_path[ $index ] );
     4020                        $layer = _wp_array_get( $tree, array_values( $base_path ) );
     4021                        if ( is_array( $layer ) ) {
     4022                                $base_styles[] = $layer;
     4023                        }
     4024                }
     4025
     4026                return $base_styles;
     4027        }
     4028
     4029        /**
     4030         * Adds the declarations a state needs to escape a text clip set below it.
     4031         *
     4032         * @since 7.2.0
     4033         *
     4034         * @param array[]              $declarations Declarations generated for the state.
     4035         * @param array<string, mixed> $state_style  State style object.
     4036         * @param array[]              $base_styles  Style objects the state layers over, nearest first.
     4037         * @return array[] Declarations with the escapes applied where needed.
     4038         *
     4039         * @phpstan-param list<array{ name: non-falsy-string, value: scalar }> $declarations
     4040         * @phpstan-return list<array{ name: non-falsy-string, value: scalar }>
     4041         */
     4042        private static function get_state_declarations_with_text_clip_escapes( array $declarations, array $state_style, array $base_styles ): array {
     4043                $base_clip = '';
     4044                foreach ( $base_styles as $base_style ) {
     4045                        if ( ! empty( $base_style['background']['backgroundClip'] ) ) {
     4046                                $base_clip = is_string( $base_style['background']['backgroundClip'] ) ? $base_style['background']['backgroundClip'] : '';
     4047                                break;
     4048                        }
     4049                }
     4050
     4051                if ( 'text' !== $base_clip ) {
     4052                        return $declarations;
     4053                }
     4054
     4055                $escapes = array_merge(
     4056                        wp_get_state_text_clip_escape( $state_style, $base_clip ),
     4057                        wp_get_state_text_fill_reset( $state_style )
     4058                );
     4059
     4060                foreach ( $escapes as $name => $value ) {
     4061                        $declarations[] = array(
     4062                                'name'  => $name,
     4063                                'value' => $value,
     4064                        );
     4065                }
     4066
     4067                return $declarations;
     4068        }
     4069
     4070        /**
    39254071         * Gets the CSS rules for a particular block from theme.json.
    39264072         *
    … …  
    40574203                                        // Process base properties for this breakpoint.
    40584204                                        $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json );
     4205                                        $breakpoint_declarations = self::get_state_declarations_with_text_clip_escapes(
     4206                                                $breakpoint_declarations,
     4207                                                $breakpoint_node,
     4208                                                array( $style_variation_node, $node )
     4209                                        );
    40594210                                        if ( ! empty( $breakpoint_declarations ) ) {
    40604211                                                $base_ruleset              = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations );
    … …  
    41814332                 * Otherwise just compute the styles for the default selector as normal.
    41824333                 */
     4334                $base_styles = self::get_state_base_styles( $block_metadata['path'] ?? array(), $this->theme_json, $settings );
     4335
    41834336                if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
    41844337                        isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
    … …  
    41864339                ) {
    41874340                        $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
     4341                        $declarations = self::get_state_declarations_with_text_clip_escapes(
     4342                                $declarations,
     4343                                $node[ $pseudo_selector ],
     4344                                array_merge( array( $node ), $base_styles )
     4345                        );
    41884346                } else {
    41894347                        /*
    … …  
    41934351                         */
    41944352                        $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
     4353                        $declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $node, $base_styles );
    41954354                }
    41964355
  • trunk/src/wp-includes/kses.php

    r64436 r64590  
    36933693 *              and URLs in the SVG element reference properties.
    36943694 * @since 7.2.0 Added support for CSS anchor positioning properties and `text-shadow`.
     3695 *              Added support for `background-clip`.
    36953696 *
    36963697 * @param string $css        A string of CSS rules, decoded from an HTML `style` attribute.
    … …  
    37183719         * @since 2.8.1
    37193720         * @since 7.1.0 Added support for SVG presentation attributes.
     3721         * @since 7.2.0 Added support for `background-clip` and its vendor prefixed companions.
    37203722         *
    37213723         * @param string[] $attr Array of allowed CSS attributes.
    … …  
    37323734                        'background-attachment',
    37333735                        'background-blend-mode',
     3736                        'background-clip',
     3737                        '-webkit-background-clip',
     3738                        '-webkit-text-fill-color',
    37343739
    37353740                        'border',
  • trunk/src/wp-includes/style-engine/class-wp-style-engine.php

    r64093 r64590  
    3333final class WP_Style_Engine {
    3434        /**
     35         * The values `background-clip` accepts.
     36         *
     37         * @since 7.2.0
     38         * @var string[]
     39         */
     40        const VALID_BACKGROUND_CLIP_VALUES = array( 'border-box', 'padding-box', 'content-box', 'text' );
     41
     42        /**
    3543         * Style definitions that contain the instructions to parse/output valid Gutenberg styles from a block's attributes.
    3644         *
    … …  
    5159         * @since 6.1.0
    5260         * @since 7.1.0 Added `background.gradient` property.
     61         * @since 7.2.0 Added `background.backgroundClip` property.
    5362         * @var array
    5463         */
    … …  
    97106                                        'has-background' => true,
    98107                                ),
     108                        ),
     109                        'backgroundClip'       => array(
     110                                'value_func' => array( self::class, 'get_background_clip_css_declarations' ),
     111                                'path'       => array( 'background', 'backgroundClip' ),
    99112                        ),
    100113                ),
    … …  
    770783
    771784        /**
     785         * Style value parser that returns the CSS declarations for background clipping.
     786         *
     787         * For the `text` value the background is clipped to the block's text, which
     788         * requires the vendor prefixed properties and a transparent fill color.
     789         *
     790         * @since 7.2.0
     791         *
     792         * @param string $style_value      A single raw style value from $block_styles array.
     793         * @param array  $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA.
     794         * @return array<string, string> An associative array of CSS definitions, mapping properties to values.
     795         *
     796         * @phpstan-return array<non-falsy-string, non-falsy-string>
     797         */
     798        protected static function get_background_clip_css_declarations( $style_value, $style_definition ) {
     799                if ( ! in_array( $style_value, static::VALID_BACKGROUND_CLIP_VALUES, true ) ) {
     800                        return array();
     801                }
     802
     803                $css_declarations = array(
     804                        'background-clip' => $style_value,
     805                );
     806
     807                if ( 'text' === $style_value ) {
     808                        $css_declarations['-webkit-background-clip'] = 'text';
     809                        $css_declarations['-webkit-text-fill-color'] = 'transparent';
     810                } else {
     811                        /*
     812                         * Only the fill color is restored. `-webkit-background-clip` is an
     813                         * alias of `background-clip` in Chromium, so resetting it here would
     814                         * discard the value set above. The fill color is inherited, so it
     815                         * needs its initial value rather than `unset`, which would take a
     816                         * transparent fill from an ancestor clipping to text.
     817                         */
     818                        $css_declarations['-webkit-text-fill-color'] = 'currentColor';
     819                }
     820
     821                return $css_declarations;
     822        }
     823
     824        /**
    772825         * Returns compiled CSS from CSS declarations.
    773826         *
  • trunk/tests/phpunit/tests/block-supports/colors.php

    r62718 r64590  
    300300                $this->assertSame( $expected, $actual );
    301301        }
     302
     303        /**
     304         * Registers a block with color and background clip support.
     305         *
     306         * @param array|bool  $background_skip Background serialization skip.
     307         * @return WP_Block_Type The registered block type.
     308         */
     309        private function register_background_clip_block( $background_skip = false ) {
     310                $this->test_block_name = 'test/color-background-clip';
     311                register_block_type(
     312                        $this->test_block_name,
     313                        array(
     314                                'api_version' => 3,
     315                                'attributes'  => array(
     316                                        'backgroundColor' => array(
     317                                                'type' => 'string',
     318                                        ),
     319                                        'gradient'        => array(
     320                                                'type' => 'string',
     321                                        ),
     322                                        'style'           => array(
     323                                                'type' => 'object',
     324                                        ),
     325                                ),
     326                                'supports'    => array(
     327                                        'color'      => array(
     328                                                'background' => true,
     329                                                'gradients'  => true,
     330                                        ),
     331                                        'background' => array(
     332                                                'backgroundClip'                  => true,
     333                                                '__experimentalSkipSerialization' => $background_skip,
     334                                        ),
     335                                ),
     336                        )
     337                );
     338
     339                return WP_Block_Type_Registry::get_instance()->get_registered( $this->test_block_name );
     340        }
     341
     342        /**
     343         * @ticket 66094
     344         *
     345         * @dataProvider data_background_color_has_background_with_a_clip
     346         *
     347         * @param array       $block_atts      Block attributes.
     348         * @param array|bool  $background_skip Background serialization skip.
     349         * @param string|null $expected_class  Expected class attribute.
     350         */
     351        public function test_background_color_has_background_with_a_clip( $block_atts, $background_skip, $expected_class ) {
     352                $block_type = $this->register_background_clip_block( $background_skip );
     353
     354                $actual = wp_apply_colors_support( $block_type, $block_atts );
     355
     356                $this->assertSame( $expected_class, $actual['class'] ?? null );
     357        }
     358
     359        /**
     360         * Data provider.
     361         *
     362         * @return array
     363         */
     364        public function data_background_color_has_background_with_a_clip() {
     365                return array(
     366                        'preset color clipped to the text'      => array(
     367                                array(
     368                                        'backgroundColor' => 'base',
     369                                        'style'           => array( 'background' => array( 'backgroundClip' => 'text' ) ),
     370                                ),
     371                                false,
     372                                'has-base-background-color',
     373                        ),
     374                        'custom color clipped to the text'      => array(
     375                                array(
     376                                        'style' => array(
     377                                                'color'      => array( 'background' => '#ff0000' ),
     378                                                'background' => array( 'backgroundClip' => 'text' ),
     379                                        ),
     380                                ),
     381                                false,
     382                                null,
     383                        ),
     384                        'preset color clipped to a box'         => array(
     385                                array(
     386                                        'backgroundColor' => 'base',
     387                                        'style'           => array( 'background' => array( 'backgroundClip' => 'padding-box' ) ),
     388                                ),
     389                                false,
     390                                'has-background has-base-background-color',
     391                        ),
     392                        'preset color with a skipped text clip' => array(
     393                                array(
     394                                        'backgroundColor' => 'base',
     395                                        'style'           => array( 'background' => array( 'backgroundClip' => 'text' ) ),
     396                                ),
     397                                array( 'backgroundClip' ),
     398                                'has-background has-base-background-color',
     399                        ),
     400                        'color gradient over a text clip'       => array(
     401                                array(
     402                                        'gradient' => 'vivid',
     403                                        'style'    => array( 'background' => array( 'backgroundClip' => 'text' ) ),
     404                                ),
     405                                false,
     406                                'has-background has-vivid-gradient-background',
     407                        ),
     408                );
     409        }
    302410}
  • trunk/tests/phpunit/tests/block-supports/states.php

    r64322 r64590  
    162162
    163163        /**
     164         * Tests that a state escaping a text clip stops the inherited gradient painting.
     165         *
     166         * @ticket 66094
     167         * @covers ::wp_get_state_text_clip_escape
     168         */
     169        public function test_text_clip_escape_unsets_the_background_image_when_a_state_escapes_a_text_clip() {
     170                $actual = wp_get_state_text_clip_escape(
     171                        array( 'background' => array( 'backgroundClip' => 'border-box' ) ),
     172                        'text'
     173                );
     174
     175                $this->assertSame( array( 'background-image' => 'unset' ), $actual );
     176        }
     177
     178        /**
     179         * Tests that a background the state paints itself is kept.
     180         *
     181         * @ticket 66094
     182         * @covers ::wp_get_state_text_clip_escape
     183         * @covers ::wp_get_state_text_fill_reset
     184         * @covers ::wp_state_paints_own_background
     185         *
     186         * @dataProvider data_state_styles_that_paint_their_own_background
     187         *
     188         * @param array $state_style State style object.
     189         */
     190        public function test_text_clip_helpers_add_nothing_when_the_state_paints_its_own_background( $state_style ) {
     191                $this->assertSame( array(), wp_get_state_text_clip_escape( $state_style, 'text' ), 'Expected no text clip escape.' );
     192                $this->assertSame( array(), wp_get_state_text_fill_reset( $state_style ), 'Expected no text fill reset.' );
     193        }
     194
     195        /**
     196         * Data provider.
     197         *
     198         * @return array[]
     199         */
     200        public function data_state_styles_that_paint_their_own_background() {
     201                $color = array( 'text' => '#ff0000' );
     202
     203                return array(
     204                        'background gradient' => array(
     205                                array(
     206                                        'color'      => $color,
     207                                        'background' => array(
     208                                                'backgroundClip' => 'padding-box',
     209                                                'gradient'       => 'linear-gradient(135deg, #ff0000, #0000ff)',
     210                                        ),
     211                                ),
     212                        ),
     213                        'background image'    => array(
     214                                array(
     215                                        'color'      => $color,
     216                                        'background' => array(
     217                                                'backgroundClip'  => 'padding-box',
     218                                                'backgroundImage' => array( 'url' => 'image.png' ),
     219                                        ),
     220                                ),
     221                        ),
     222                        'color gradient'      => array(
     223                                array(
     224                                        'color'      => array_merge( $color, array( 'gradient' => 'linear-gradient(135deg, #ff0000, #0000ff)' ) ),
     225                                        'background' => array( 'backgroundClip' => 'padding-box' ),
     226                                ),
     227                        ),
     228                        'gradient, no clip'   => array(
     229                                array(
     230                                        'color'      => $color,
     231                                        'background' => array( 'gradient' => 'linear-gradient(135deg, #ff0000, #0000ff)' ),
     232                                ),
     233                        ),
     234                );
     235        }
     236
     237        /**
     238         * Tests that a box clip is left alone when nothing below it clips to text.
     239         *
     240         * @ticket 66094
     241         * @covers ::wp_get_state_text_clip_escape
     242         *
     243         * @dataProvider data_base_clips_that_are_not_text
     244         *
     245         * @param string $base_clip Background clip the state layers over.
     246         */
     247        public function test_text_clip_escape_adds_nothing_without_a_base_text_clip( $base_clip ) {
     248                $actual = wp_get_state_text_clip_escape(
     249                        array( 'background' => array( 'backgroundClip' => 'padding-box' ) ),
     250                        $base_clip
     251                );
     252
     253                $this->assertSame( array(), $actual );
     254        }
     255
     256        /**
     257         * Data provider.
     258         *
     259         * @return array[]
     260         */
     261        public function data_base_clips_that_are_not_text() {
     262                return array(
     263                        'no base clip' => array( '' ),
     264                        'border-box'   => array( 'border-box' ),
     265                );
     266        }
     267
     268        /**
     269         * Tests that an empty state clip of any type counts as no clip.
     270         *
     271         * @ticket 66094
     272         * @covers ::wp_get_state_text_clip_escape
     273         * @covers ::wp_get_state_text_fill_reset
     274         *
     275         * @dataProvider data_empty_state_clips
     276         *
     277         * @param mixed $state_clip Background clip set by the state.
     278         */
     279        public function test_text_clip_helpers_treat_an_empty_state_clip_as_no_clip( $state_clip ) {
     280                $state_style = array(
     281                        'color'      => array( 'text' => '#ff0000' ),
     282                        'background' => array( 'backgroundClip' => $state_clip ),
     283                );
     284
     285                $this->assertSame( array(), wp_get_state_text_clip_escape( $state_style, 'text' ), 'Expected no text clip escape.' );
     286                $this->assertSame(
     287                        array( '-webkit-text-fill-color' => 'currentColor' ),
     288                        wp_get_state_text_fill_reset( $state_style ),
     289                        'Expected the text fill reset.'
     290                );
     291        }
     292
     293        /**
     294         * Data provider.
     295         *
     296         * @return array[]
     297         */
     298        public function data_empty_state_clips() {
     299                return array(
     300                        'false'        => array( false ),
     301                        'zero'         => array( 0 ),
     302                        'zero string'  => array( '0' ),
     303                        'empty string' => array( '' ),
     304                        'empty array'  => array( array() ),
     305                );
     306        }
     307
     308        /**
     309         * Tests that an invalid state clip over a base text clip does not unset the gradient.
     310         *
     311         * @ticket 66094
     312         * @covers ::wp_get_state_text_clip_escape
     313         */
     314        public function test_text_clip_escape_ignores_an_invalid_state_clip() {
     315                $actual = wp_get_state_text_clip_escape(
     316                        array( 'background' => array( 'backgroundClip' => 'borderbox' ) ),
     317                        'text'
     318                );
     319
     320                $this->assertSame( array(), $actual );
     321        }
     322
     323        /**
     324         * Tests that a text color with an invalid state clip gets its fill back.
     325         *
     326         * @ticket 66094
     327         * @covers ::wp_get_state_text_fill_reset
     328         */
     329        public function test_text_fill_reset_ignores_an_invalid_state_clip() {
     330                $actual = wp_get_state_text_fill_reset(
     331                        array(
     332                                'color'      => array( 'text' => '#ff0000' ),
     333                                'background' => array( 'backgroundClip' => 'borderbox' ),
     334                        )
     335                );
     336
     337                $this->assertSame( array( '-webkit-text-fill-color' => 'currentColor' ), $actual );
     338        }
     339
     340        /**
     341         * Tests that a state without its own clip, or keeping the text clip, needs no escape.
     342         *
     343         * @ticket 66094
     344         * @covers ::wp_get_state_text_clip_escape
     345         */
     346        public function test_text_clip_escape_adds_nothing_when_the_state_keeps_the_text_clip() {
     347                $this->assertSame(
     348                        array(),
     349                        wp_get_state_text_clip_escape( array( 'background' => array( 'backgroundClip' => 'text' ) ), 'text' ),
     350                        'Expected no escape for a state that also clips to text.'
     351                );
     352                $this->assertSame(
     353                        array(),
     354                        wp_get_state_text_clip_escape( array( 'color' => array( 'text' => '#ff0000' ) ), 'text' ),
     355                        'Expected no escape for a state without a clip.'
     356                );
     357        }
     358
     359        /**
     360         * Tests that a text color set without a clip gets its fill back.
     361         *
     362         * @ticket 66094
     363         * @covers ::wp_get_state_text_fill_reset
     364         */
     365        public function test_text_fill_reset_refills_a_text_color() {
     366                $actual = wp_get_state_text_fill_reset( array( 'color' => array( 'text' => '#ff0000' ) ) );
     367
     368                $this->assertSame( array( '-webkit-text-fill-color' => 'currentColor' ), $actual );
     369        }
     370
     371        /**
     372         * Tests that the fill is left to the style engine when the state sets its own clip.
     373         *
     374         * @ticket 66094
     375         * @covers ::wp_get_state_text_fill_reset
     376         */
     377        public function test_text_fill_reset_adds_nothing_when_the_state_sets_its_own_clip() {
     378                $state_style = array(
     379                        'color'      => array( 'text' => '#ff0000' ),
     380                        'background' => array( 'backgroundClip' => 'text' ),
     381                );
     382
     383                $this->assertSame( array(), wp_get_state_text_fill_reset( $state_style ) );
     384        }
     385
     386        /**
     387         * Tests that the fill is left alone when the state sets no text color.
     388         *
     389         * @ticket 66094
     390         * @covers ::wp_get_state_text_fill_reset
     391         */
     392        public function test_text_fill_reset_adds_nothing_without_a_text_color() {
     393                $this->assertSame( array(), wp_get_state_text_fill_reset( array( 'color' => array( 'background' => '#ff0000' ) ) ) );
     394        }
     395
     396        /**
     397         * Tests that a block state escapes its base text clip.
     398         *
     399         * @ticket 66094
     400         * @covers ::wp_add_block_state_style_rule
     401         */
     402        public function test_block_state_rule_escapes_a_base_text_clip() {
     403                $css_rules = array();
     404
     405                wp_add_block_state_style_rule(
     406                        $css_rules,
     407                        ':hover',
     408                        '.wp-block-test',
     409                        array( 'background' => array( 'backgroundClip' => 'border-box' ) ),
     410                        null,
     411                        array( 'background' => array( 'backgroundClip' => 'text' ) )
     412                );
     413
     414                $this->assertSame( 'unset', $css_rules[0]['declarations']['background-image'] ?? null );
     415        }
     416
     417        /**
     418         * Tests that a block state text color gets its fill back over a base box clip,
     419         * since a text clip from global styles cannot be seen from the block.
     420         *
     421         * @ticket 66094
     422         * @covers ::wp_add_block_state_style_rule
     423         */
     424        public function test_block_state_rule_refills_a_text_color_over_a_base_box_clip() {
     425                $css_rules = array();
     426
     427                wp_add_block_state_style_rule(
     428                        $css_rules,
     429                        ':hover',
     430                        '.wp-block-test',
     431                        array( 'color' => array( 'text' => '#ff0000' ) ),
     432                        null,
     433                        array( 'background' => array( 'backgroundClip' => 'border-box' ) )
     434                );
     435
     436                $declarations = $css_rules[0]['declarations'];
     437                $this->assertSame( 'currentColor', $declarations['-webkit-text-fill-color'] ?? null, 'Expected the text fill reset.' );
     438                $this->assertArrayNotHasKey( 'background-image', $declarations, 'Expected no text clip escape.' );
     439        }
     440
     441        /**
    164442         * Tests that background-image reset is not added when the state also sets a legacy gradient.
    165443         *
    … …  
    10151293                $this->assertStringContainsString(
    10161294                        '@media (width <= 480px){.' . $matches[0] . '{color:#ff0000 !important;}}',
     1295                        $actual_stylesheet
     1296                );
     1297        }
     1298
     1299        /**
     1300         * Tests that a responsive text color fills the letters again for a block
     1301         * that supports the background clip, so a text gradient from global styles
     1302         * cannot hide it.
     1303         *
     1304         * @ticket 66094
     1305         */
     1306        public function test_responsive_text_color_fills_text_for_a_block_that_supports_the_clip() {
     1307                $this->ensure_block_registered(
     1308                        'test/responsive-text-clip-state',
     1309                        array(),
     1310                        array( 'background' => array( 'backgroundClip' => true ) )
     1311                );
     1312
     1313                $block = array(
     1314                        'blockName' => 'test/responsive-text-clip-state',
     1315                        'attrs'     => array(
     1316                                'style' => array(
     1317                                        '@mobile' => array(
     1318                                                'color' => array(
     1319                                                        'text' => '#ff0000',
     1320                                                ),
     1321                                        ),
     1322                                ),
     1323                        ),
     1324                );
     1325
     1326                $actual = wp_render_block_states_support( '<div class="wp-block-test">Hello</div>', $block );
     1327                preg_match( '/wp-states-[a-f0-9]{8}/', $actual, $matches );
     1328                $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
     1329
     1330                $this->assertStringContainsString(
     1331                        '@media (width <= 480px){.' . $matches[0] . '{color:#ff0000 !important;-webkit-text-fill-color:currentColor !important;}}',
    10171332                        $actual_stylesheet
    10181333                );
  • trunk/tests/phpunit/tests/block-supports/wpRenderBackgroundSupport.php

    r64449 r64590  
    7171         * @ticket 61858
    7272         * @ticket 64974
     73         * @ticket 66094
    7374         *
    7475         * @covers ::wp_render_background_support
    … …  
    283284                                'wrapper'             => '<div>Content</div>',
    284285                        ),
     286                        'background clip alone does not add has-background' => array(
     287                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     288                                'block_name'          => 'test/background-clip-is-output',
     289                                'background_settings' => array(
     290                                        'backgroundClip' => true,
     291                                ),
     292                                'background_style'    => array(
     293                                        'backgroundClip' => 'border-box',
     294                                ),
     295                                'expected_wrapper'    => '<div style="background-clip:border-box;-webkit-text-fill-color:currentColor;">Content</div>',
     296                                'wrapper'             => '<div>Content</div>',
     297                        ),
     298                        'background clip text style is applied with vendor prefixes' => array(
     299                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     300                                'block_name'          => 'test/background-clip-text-is-output',
     301                                'background_settings' => array(
     302                                        'backgroundClip' => true,
     303                                ),
     304                                'background_style'    => array(
     305                                        'backgroundClip' => 'text',
     306                                ),
     307                                'expected_wrapper'    => '<div style="background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;">Content</div>',
     308                                'wrapper'             => '<div>Content</div>',
     309                        ),
     310                        'background clip alone does not add has-background for block with only backgroundClip support' => array(
     311                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     312                                'block_name'          => 'test/background-clip-only-support',
     313                                'background_settings' => array(
     314                                        'backgroundClip' => true,
     315                                ),
     316                                'background_style'    => array(
     317                                        'backgroundClip' => 'padding-box',
     318                                ),
     319                                'expected_wrapper'    => '<p style="background-clip:padding-box;-webkit-text-fill-color:currentColor;">Content</p>',
     320                                'wrapper'             => '<p>Content</p>',
     321                        ),
     322                        'background clip style is not applied if the block does not support it' => array(
     323                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     324                                'block_name'          => 'test/background-clip-not-supported',
     325                                'background_settings' => array(
     326                                        'backgroundClip' => false,
     327                                ),
     328                                'background_style'    => array(
     329                                        'backgroundClip' => 'text',
     330                                ),
     331                                'expected_wrapper'    => '<div>Content</div>',
     332                                'wrapper'             => '<div>Content</div>',
     333                        ),
     334                        'background clip keeps has-background when a background is painted into a box' => array(
     335                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     336                                'block_name'          => 'test/background-clip-with-gradient',
     337                                'background_settings' => array(
     338                                        'gradient'       => true,
     339                                        'backgroundClip' => true,
     340                                ),
     341                                'background_style'    => array(
     342                                        'gradient'       => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)',
     343                                        'backgroundClip' => 'padding-box',
     344                                ),
     345                                'expected_wrapper'    => '<div class="has-background" style="background-image:linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%);background-clip:padding-box;-webkit-text-fill-color:currentColor;">Content</div>',
     346                                'wrapper'             => '<div>Content</div>',
     347                        ),
     348                        'background clipped to text drops has-background from a gradient' => array(
     349                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     350                                'block_name'          => 'test/background-clip-text-with-gradient',
     351                                'background_settings' => array(
     352                                        'gradient'       => true,
     353                                        'backgroundClip' => true,
     354                                ),
     355                                'background_style'    => array(
     356                                        'gradient'       => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)',
     357                                        'backgroundClip' => 'text',
     358                                ),
     359                                'expected_wrapper'    => '<div style="background-image:linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%);background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;">Content</div>',
     360                                'wrapper'             => '<div>Content</div>',
     361                        ),
     362                        'background clipped to text keeps has-background when the block does not support the clip' => array(
     363                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     364                                'block_name'          => 'test/background-clip-text-without-clip-support',
     365                                'background_settings' => array(
     366                                        'gradient'       => true,
     367                                        'backgroundClip' => false,
     368                                ),
     369                                'background_style'    => array(
     370                                        'gradient'       => 'linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%)',
     371                                        'backgroundClip' => 'text',
     372                                ),
     373                                'expected_wrapper'    => '<div class="has-background" style="background-image:linear-gradient(135deg,rgb(255,0,0) 0%,rgb(0,0,255) 100%);">Content</div>',
     374                                'wrapper'             => '<div>Content</div>',
     375                        ),
     376                        'background clip style is appended to existing styles' => array(
     377                                'theme_name'          => 'block-theme-child-with-fluid-typography',
     378                                'block_name'          => 'test/background-clip-appends-styles',
     379                                'background_settings' => array(
     380                                        'backgroundClip' => true,
     381                                ),
     382                                'background_style'    => array(
     383                                        'backgroundClip' => 'content-box',
     384                                ),
     385                                'expected_wrapper'    => '<div class="wp-block-test" style="color: red;background-clip:content-box;-webkit-text-fill-color:currentColor;">Content</div>',
     386                                'wrapper'             => '<div class="wp-block-test" style="color: red">Content</div>',
     387                        ),
    285388                );
    286389        }
  • trunk/tests/phpunit/tests/style-engine/styleEngine.php

    r62730 r64590  
    2626         * @ticket 64974
    2727         * @ticket 65037
     28         * @ticket 66094
    2829         *
    2930         * @covers ::wp_style_engine_get_styles
    … …  
    668669                                ),
    669670                        ),
     671
     672                        'inline_background_clip_border_box'            => array(
     673                                'block_styles'    => array(
     674                                        'background' => array(
     675                                                'backgroundClip' => 'border-box',
     676                                        ),
     677                                ),
     678                                'options'         => array(),
     679                                'expected_output' => array(
     680                                        'css'          => 'background-clip:border-box;-webkit-text-fill-color:currentColor;',
     681                                        'declarations' => array(
     682                                                'background-clip'         => 'border-box',
     683                                                '-webkit-text-fill-color' => 'currentColor',
     684                                        ),
     685                                ),
     686                        ),
     687
     688                        'inline_background_clip_text_with_vendor_prefixes' => array(
     689                                'block_styles'    => array(
     690                                        'background' => array(
     691                                                'backgroundClip' => 'text',
     692                                        ),
     693                                ),
     694                                'options'         => array(),
     695                                'expected_output' => array(
     696                                        'css'          => 'background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent;',
     697                                        'declarations' => array(
     698                                                'background-clip'         => 'text',
     699                                                '-webkit-background-clip' => 'text',
     700                                                '-webkit-text-fill-color' => 'transparent',
     701                                        ),
     702                                ),
     703                        ),
     704
     705                        'inline_background_clip_invalid_value'         => array(
     706                                'block_styles'    => array(
     707                                        'background' => array(
     708                                                'backgroundClip' => 'invalid-value',
     709                                        ),
     710                                ),
     711                                'options'         => array(),
     712                                'expected_output' => array(),
     713                        ),
    670714                );
    671715        }
  • trunk/tests/phpunit/tests/theme/wpThemeJson.php

    r64491 r64590  
    13691369                $this->assertStringContainsString(
    13701370                        '@media (640px < width <= 960px){:root :where(.wp-block-group){color: blue;}}',
     1371                        $stylesheet
     1372                );
     1373        }
     1374
     1375        /**
     1376         * Builds a Paragraph stylesheet with a text gradient and the given states.
     1377         *
     1378         * @param array $state_styles State styles to merge into the block styles.
     1379         * @return string Generated stylesheet.
     1380         */
     1381        private function get_text_clip_stylesheet( $state_styles ) {
     1382                $theme_json = new WP_Theme_JSON(
     1383                        array(
     1384                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1385                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1386                                'styles'   => array(
     1387                                        'blocks' => array(
     1388                                                'core/paragraph' => array_merge(
     1389                                                        array(
     1390                                                                'background' => array(
     1391                                                                        'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1392                                                                        'backgroundClip' => 'text',
     1393                                                                ),
     1394                                                        ),
     1395                                                        $state_styles
     1396                                                ),
     1397                                        ),
     1398                                ),
     1399                        )
     1400                );
     1401
     1402                return $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
     1403        }
     1404
     1405        /**
     1406         * @ticket 66094
     1407         */
     1408        public function test_breakpoint_escaping_a_text_clip_clears_the_inherited_gradient() {
     1409                $stylesheet = $this->get_text_clip_stylesheet(
     1410                        array(
     1411                                '@mobile' => array(
     1412                                        'background' => array( 'backgroundClip' => 'border-box' ),
     1413                                ),
     1414                        )
     1415                );
     1416
     1417                $this->assertStringContainsString(
     1418                        '@media (width <= 480px){:root :where(p){background-clip: border-box;-webkit-text-fill-color: currentColor;background-image: unset;}}',
     1419                        $stylesheet
     1420                );
     1421        }
     1422
     1423        /**
     1424         * @ticket 66094
     1425         */
     1426        public function test_breakpoint_painting_its_own_background_keeps_it() {
     1427                $stylesheet = $this->get_text_clip_stylesheet(
     1428                        array(
     1429                                '@mobile' => array(
     1430                                        'background' => array(
     1431                                                'gradient'       => 'linear-gradient(135deg,#00ff00,#ffff00)',
     1432                                                'backgroundClip' => 'border-box',
     1433                                        ),
     1434                                ),
     1435                        )
     1436                );
     1437
     1438                $this->assertStringNotContainsString( 'background-image: unset', $stylesheet );
     1439        }
     1440
     1441        /**
     1442         * @ticket 66094
     1443         */
     1444        public function test_breakpoint_text_color_under_a_text_clip_gets_its_fill_back() {
     1445                $stylesheet = $this->get_text_clip_stylesheet(
     1446                        array( '@mobile' => array( 'color' => array( 'text' => '#00ff00' ) ) )
     1447                );
     1448
     1449                $this->assertStringContainsString(
     1450                        '@media (width <= 480px){:root :where(p){color: #00ff00;-webkit-text-fill-color: currentColor;}}',
     1451                        $stylesheet
     1452                );
     1453        }
     1454
     1455        /**
     1456         * @ticket 66094
     1457         */
     1458        public function test_breakpoint_keeping_the_text_clip_needs_no_escape() {
     1459                $stylesheet = $this->get_text_clip_stylesheet(
     1460                        array(
     1461                                '@mobile' => array(
     1462                                        'background' => array(
     1463                                                'gradient'       => 'linear-gradient(135deg,#00ff00,#ffff00)',
     1464                                                'backgroundClip' => 'text',
     1465                                        ),
     1466                                ),
     1467                        )
     1468                );
     1469
     1470                $this->assertStringNotContainsString( 'background-image: unset', $stylesheet );
     1471                $this->assertStringNotContainsString( '-webkit-text-fill-color: currentColor', $stylesheet );
     1472        }
     1473
     1474        /**
     1475         * @ticket 66094
     1476         */
     1477        public function test_breakpoint_element_styles_read_the_element_clip() {
     1478                $theme_json = new WP_Theme_JSON(
     1479                        array(
     1480                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1481                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1482                                'styles'   => array(
     1483                                        'blocks' => array(
     1484                                                'core/group' => array(
     1485                                                        'elements' => array(
     1486                                                                'link' => array(
     1487                                                                        'background' => array(
     1488                                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1489                                                                                'backgroundClip' => 'text',
     1490                                                                        ),
     1491                                                                ),
     1492                                                        ),
     1493                                                        '@mobile'  => array(
     1494                                                                'elements' => array(
     1495                                                                        'link' => array( 'color' => array( 'text' => '#00ff00' ) ),
     1496                                                                ),
     1497                                                        ),
     1498                                                ),
     1499                                        ),
     1500                                ),
     1501                        )
     1502                );
     1503
     1504                $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
     1505
     1506                // The state key sits in the middle of this node's path, not at its end.
     1507                $this->assertStringContainsString(
     1508                        '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: #00ff00;-webkit-text-fill-color: currentColor;}}',
     1509                        $stylesheet
     1510                );
     1511        }
     1512
     1513        /**
     1514         * @ticket 66094
     1515         */
     1516        public function test_breakpoint_variation_styles_read_the_variation_clip() {
     1517                register_block_style(
     1518                        'core/group',
     1519                        array(
     1520                                'name'  => 'flashy',
     1521                                'label' => 'Flashy',
     1522                        )
     1523                );
     1524
     1525                $theme_json = new WP_Theme_JSON(
     1526                        array(
     1527                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1528                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1529                                'styles'   => array(
     1530                                        'blocks' => array(
     1531                                                'core/group' => array(
     1532                                                        'variations' => array(
     1533                                                                'flashy' => array(
     1534                                                                        'background' => array(
     1535                                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1536                                                                                'backgroundClip' => 'text',
     1537                                                                        ),
     1538                                                                        '@mobile'    => array(
     1539                                                                                'background' => array( 'backgroundClip' => 'border-box' ),
     1540                                                                        ),
     1541                                                                ),
     1542                                                        ),
     1543                                                ),
     1544                                        ),
     1545                                ),
     1546                        )
     1547                );
     1548
     1549                $actual_styles = $theme_json->get_styles_for_block(
     1550                        array(
     1551                                'name'       => 'core/group',
     1552                                'path'       => array( 'styles', 'blocks', 'core/group' ),
     1553                                'selector'   => '.wp-block-group',
     1554                                'css'        => '.wp-block-group',
     1555                                'variations' => array(
     1556                                        array(
     1557                                                'path'     => array( 'styles', 'blocks', 'core/group', 'variations', 'flashy' ),
     1558                                                'selector' => '.is-style-flashy.wp-block-group',
     1559                                        ),
     1560                                ),
     1561                        )
     1562                );
     1563
     1564                unregister_block_style( 'core/group', 'flashy' );
     1565
     1566                $this->assertStringContainsString(
     1567                        '@media (width <= 480px){:root :where(.is-style-flashy.wp-block-group){background-clip: border-box;-webkit-text-fill-color: currentColor;background-image: unset;}}',
     1568                        $actual_styles
     1569                );
     1570        }
     1571
     1572        /**
     1573         * @ticket 66094
     1574         */
     1575        public function test_breakpoint_variation_styles_read_the_block_clip() {
     1576                register_block_style(
     1577                        'core/group',
     1578                        array(
     1579                                'name'  => 'flashy',
     1580                                'label' => 'Flashy',
     1581                        )
     1582                );
     1583
     1584                $theme_json = new WP_Theme_JSON(
     1585                        array(
     1586                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1587                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1588                                'styles'   => array(
     1589                                        'blocks' => array(
     1590                                                'core/group' => array(
     1591                                                        'background' => array(
     1592                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1593                                                                'backgroundClip' => 'text',
     1594                                                        ),
     1595                                                        'variations' => array(
     1596                                                                'flashy' => array(
     1597                                                                        '@mobile' => array(
     1598                                                                                'background' => array( 'backgroundClip' => 'border-box' ),
     1599                                                                        ),
     1600                                                                ),
     1601                                                        ),
     1602                                                ),
     1603                                        ),
     1604                                ),
     1605                        )
     1606                );
     1607
     1608                $actual_styles = $theme_json->get_styles_for_block(
     1609                        array(
     1610                                'name'       => 'core/group',
     1611                                'path'       => array( 'styles', 'blocks', 'core/group' ),
     1612                                'selector'   => '.wp-block-group',
     1613                                'css'        => '.wp-block-group',
     1614                                'variations' => array(
     1615                                        array(
     1616                                                'path'     => array( 'styles', 'blocks', 'core/group', 'variations', 'flashy' ),
     1617                                                'selector' => '.is-style-flashy.wp-block-group',
     1618                                        ),
     1619                                ),
     1620                        )
     1621                );
     1622
     1623                unregister_block_style( 'core/group', 'flashy' );
     1624
     1625                $this->assertStringContainsString(
     1626                        '@media (width <= 480px){:root :where(.is-style-flashy.wp-block-group){background-clip: border-box;-webkit-text-fill-color: currentColor;background-image: unset;}}',
     1627                        $actual_styles
     1628                );
     1629        }
     1630
     1631        /**
     1632         * @ticket 66094
     1633         */
     1634        public function test_variation_pseudo_state_reads_the_block_clip() {
     1635                register_block_style(
     1636                        'core/button',
     1637                        array(
     1638                                'name'  => 'flashy',
     1639                                'label' => 'Flashy',
     1640                        )
     1641                );
     1642
     1643                $theme_json = new WP_Theme_JSON(
     1644                        array(
     1645                                'version' => WP_Theme_JSON::LATEST_SCHEMA,
     1646                                'styles'  => array(
     1647                                        'blocks' => array(
     1648                                                'core/button' => array(
     1649                                                        'background' => array(
     1650                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1651                                                                'backgroundClip' => 'text',
     1652                                                        ),
     1653                                                        'variations' => array(
     1654                                                                'flashy' => array(
     1655                                                                        ':hover' => array(
     1656                                                                                'background' => array( 'backgroundClip' => 'border-box' ),
     1657                                                                        ),
     1658                                                                ),
     1659                                                        ),
     1660                                                ),
     1661                                        ),
     1662                                ),
     1663                        )
     1664                );
     1665
     1666                $actual_styles = $theme_json->get_styles_for_block(
     1667                        array(
     1668                                'name'       => 'core/button',
     1669                                'path'       => array( 'styles', 'blocks', 'core/button' ),
     1670                                'selector'   => '.wp-block-button',
     1671                                'css'        => '.wp-block-button',
     1672                                'variations' => array(
     1673                                        array(
     1674                                                'path'     => array( 'styles', 'blocks', 'core/button', 'variations', 'flashy' ),
     1675                                                'selector' => '.is-style-flashy.wp-block-button',
     1676                                        ),
     1677                                ),
     1678                        )
     1679                );
     1680
     1681                unregister_block_style( 'core/button', 'flashy' );
     1682
     1683                $this->assertStringContainsString(
     1684                        ':root :where(.is-style-flashy.wp-block-button:hover){background-clip: border-box;-webkit-text-fill-color: currentColor;background-image: unset;}',
     1685                        $actual_styles
     1686                );
     1687        }
     1688
     1689        /**
     1690         * @ticket 66094
     1691         */
     1692        public function test_variation_breakpoint_pseudo_state_reads_the_block_clip() {
     1693                register_block_style(
     1694                        'core/button',
     1695                        array(
     1696                                'name'  => 'flashy',
     1697                                'label' => 'Flashy',
     1698                        )
     1699                );
     1700
     1701                $theme_json = new WP_Theme_JSON(
     1702                        array(
     1703                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1704                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1705                                'styles'   => array(
     1706                                        'blocks' => array(
     1707                                                'core/button' => array(
     1708                                                        'background' => array(
     1709                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1710                                                                'backgroundClip' => 'text',
     1711                                                        ),
     1712                                                        'variations' => array(
     1713                                                                'flashy' => array(
     1714                                                                        '@mobile' => array(
     1715                                                                                ':hover' => array(
     1716                                                                                        'background' => array( 'backgroundClip' => 'border-box' ),
     1717                                                                                ),
     1718                                                                        ),
     1719                                                                ),
     1720                                                        ),
     1721                                                ),
     1722                                        ),
     1723                                ),
     1724                        )
     1725                );
     1726
     1727                $actual_styles = $theme_json->get_styles_for_block(
     1728                        array(
     1729                                'name'       => 'core/button',
     1730                                'path'       => array( 'styles', 'blocks', 'core/button' ),
     1731                                'selector'   => '.wp-block-button',
     1732                                'css'        => '.wp-block-button',
     1733                                'variations' => array(
     1734                                        array(
     1735                                                'path'     => array( 'styles', 'blocks', 'core/button', 'variations', 'flashy' ),
     1736                                                'selector' => '.is-style-flashy.wp-block-button',
     1737                                        ),
     1738                                ),
     1739                        )
     1740                );
     1741
     1742                unregister_block_style( 'core/button', 'flashy' );
     1743
     1744                $this->assertStringContainsString(
     1745                        '@media (width <= 480px){:root :where(.is-style-flashy.wp-block-button:hover){background-clip: border-box;-webkit-text-fill-color: currentColor;background-image: unset;}}',
     1746                        $actual_styles
     1747                );
     1748        }
     1749
     1750        /**
     1751         * @ticket 66094
     1752         */
     1753        public function test_pseudo_state_reads_the_clip_its_own_breakpoint_sets() {
     1754                $theme_json = new WP_Theme_JSON(
     1755                        array(
     1756                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1757                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1758                                'styles'   => array(
     1759                                        'blocks' => array(
     1760                                                'core/button' => array(
     1761                                                        'background' => array( 'backgroundClip' => 'border-box' ),
     1762                                                        '@mobile'    => array(
     1763                                                                'background' => array(
     1764                                                                        'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1765                                                                        'backgroundClip' => 'text',
     1766                                                                ),
     1767                                                                ':hover'     => array( 'color' => array( 'text' => '#00ff00' ) ),
     1768                                                        ),
     1769                                                ),
     1770                                        ),
     1771                                ),
     1772                        )
     1773                );
     1774
     1775                $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
     1776
     1777                $this->assertStringContainsString(
     1778                        '@media (width <= 480px){:root :where(.wp-block-button .wp-block-button__link:hover){color: #00ff00;-webkit-text-fill-color: currentColor;}}',
     1779                        $stylesheet
     1780                );
     1781        }
     1782
     1783        /**
     1784         * @ticket 66094
     1785         */
     1786        public function test_pseudo_state_inside_a_breakpoint_reads_the_breakpoint_clip() {
     1787                $theme_json = new WP_Theme_JSON(
     1788                        array(
     1789                                'version'  => WP_Theme_JSON::LATEST_SCHEMA,
     1790                                'settings' => array( 'viewport' => array( 'mobile' => '480px' ) ),
     1791                                'styles'   => array(
     1792                                        'blocks' => array(
     1793                                                'core/button' => array(
     1794                                                        'background' => array(
     1795                                                                'gradient'       => 'linear-gradient(135deg,#ff0000,#0000ff)',
     1796                                                                'backgroundClip' => 'text',
     1797                                                        ),
     1798                                                        '@mobile'    => array(
     1799                                                                'background' => array( 'backgroundClip' => 'border-box' ),
     1800                                                                ':hover'     => array( 'color' => array( 'text' => '#00ff00' ) ),
     1801                                                        ),
     1802                                                ),
     1803                                        ),
     1804                                ),
     1805                        )
     1806                );
     1807
     1808                $stylesheet = $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) );
     1809
     1810                // The breakpoint already escaped the clip, so no fill is needed.
     1811                $this->assertStringContainsString(
     1812                        '@media (width <= 480px){:root :where(.wp-block-button .wp-block-button__link:hover){color: #00ff00;}}',
    13711813                        $stylesheet
    13721814                );
Note: See TracChangeset for help on using the changeset viewer.