Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions src/wp-includes/block-supports/states.php
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,41 @@ function wp_get_root_state_style( $state_style, $nested_keys ) {
return $root_style;
}

/**
* Returns the Global Styles a block instance layers over: its block type's,
* then those of the block style variation applied to it.
*
* @since 7.3.0
*
* @param string $block_name Block name.
* @param string|null $class_name Block class name, which names any applied variation.
* @param string[] $state_keys Pseudo-state and breakpoint keys to leave out.
* @return array<string, mixed> Root-only style object.
*/
function wp_get_block_state_global_base_style( $block_name, $class_name, $state_keys ) {
$global_styles = wp_get_global_styles();
$block_styles = $global_styles['blocks'][ $block_name ] ?? array();
if ( ! is_array( $block_styles ) ) {
return array();
}

$nested_keys = array_merge( array( 'elements', 'blocks', 'variations' ), $state_keys );
$base_style = wp_get_root_state_style( $block_styles, $nested_keys );

// Only the first block style variation with data applies, as in the variation support.
foreach ( wp_get_block_style_variation_name_from_class( $class_name ) ?? array() as $variation ) {
$variation_styles = $block_styles['variations'][ $variation ] ?? null;
if ( ! empty( $variation_styles ) && is_array( $variation_styles ) ) {
return wp_merge_state_style_objects(
$base_style,
wp_get_root_state_style( $variation_styles, $nested_keys )
);
}
}

return $base_style;
}

/**
* Generates all element selectors for a block root selector.
*
Expand Down Expand Up @@ -752,6 +787,22 @@ function wp_render_block_states_support( $block_content, $block ) {
);
$viewport_settings = wp_get_global_settings( array( 'viewport' ) );
$responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
$state_keys = array_merge( $supported_pseudo_states, array_keys( $responsive_media_queries ) );

// Global Styles only matter for the clip escapes, so skip the lookup when no state could need one.
if (
array_intersect( $state_keys, array_keys( $style ) ) &&
block_has_support( $block_type, array( 'background', 'backgroundClip' ), false )
) {
$default_root_style = wp_merge_state_style_objects(
wp_get_block_state_global_base_style(
$block_name,
$block['attrs']['className'] ?? null,
$state_keys
),
$default_root_style
);
}

foreach ( $supported_pseudo_states as $pseudo_state ) {
if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) {
Expand Down Expand Up @@ -853,9 +904,13 @@ function wp_render_block_states_support( $block_content, $block ) {
array( $pseudo_state => $style[ $breakpoint ][ $pseudo_state ] ),
$block_type,
$media_query,
// Cascade order: default, breakpoint, then the default pseudo-state.
wp_merge_state_style_objects(
$default_root_style,
$root_state_style
wp_merge_state_style_objects(
$default_root_style,
$root_state_style
),
$style[ $pseudo_state ] ?? array()
)
)
);
Expand Down
30 changes: 27 additions & 3 deletions src/wp-includes/class-wp-theme-json.php
Original file line number Diff line number Diff line change
Expand Up @@ -984,16 +984,18 @@ protected static function schema_in_root_and_per_origin( $schema ) {
* Processes pseudo-selectors for any node (block or variation).
*
* @since 7.0.0
* @since 7.3.0 Added the `$default_node` parameter.
*
* @param array $node The node data (block or variation).
* @param string $base_selector The base selector.
* @param array $settings The theme settings.
* @param string $block_name The block name.
* @param array|null $block_metadata Metadata about the block to get styles for.
* @param array|null $style_variation Style variation metadata.
* @param array|null $default_node For a breakpoint node, the node outside the breakpoint.
* @return array Array of pseudo-selector declarations.
*/
private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null ) {
private function process_pseudo_selectors( $node, $base_selector, $settings, $block_name, $block_metadata = null, $style_variation = null, $default_node = null ) {
$pseudo_declarations = array();
$add_declarations = static function ( $selector, $declarations ) use ( &$pseudo_declarations ) {
if ( empty( $declarations ) ) {
Expand Down Expand Up @@ -1044,7 +1046,11 @@ private function process_pseudo_selectors( $node, $base_selector, $settings, $bl

$combined_selector = static::append_to_selector( $base_selector, $pseudo_selector );
$declarations = static::compute_style_properties( $pseudo_node, $settings, null, null );
$declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $pseudo_node, $base_styles );
// Mirror of `get_state_base_styles()`: the default pseudo-state comes nearest.
$pseudo_base = is_array( $default_node[ $pseudo_selector ] ?? null )
? array_merge( array( $default_node[ $pseudo_selector ] ), $base_styles )
: $base_styles;
$declarations = self::get_state_declarations_with_text_clip_escapes( $declarations, $pseudo_node, $pseudo_base );
$add_declarations( $combined_selector, $declarations );
}
}
Expand Down Expand Up @@ -4046,8 +4052,11 @@ private static function get_style_state_keys( array $settings ): array {
* Returns the style objects a state node layers over, nearest first.
*
* Drops the path's state keys (e.g. `@mobile`) from the innermost outward.
* A breakpoint pseudo-state (e.g. `@mobile` `:hover`) also layers over the
* default pseudo-state.
*
* @since 7.2.0
* @since 7.3.0 Includes the default pseudo-state for a breakpoint pseudo-state.
*
* @param string[] $path Path to the node within the theme.json tree.
* @param array<string, mixed> $tree The theme.json tree to read from.
Expand All @@ -4059,6 +4068,21 @@ private static function get_state_base_styles( array $path, array $tree, array $
$base_path = $path;
$base_styles = array();

// A breakpoint pseudo-state is output after the default pseudo-state, so that comes nearest.
$count = count( $path );
if (
$count >= 2 &&
in_array( $path[ $count - 1 ], array_merge( ...array_values( static::VALID_BLOCK_PSEUDO_SELECTORS ) ), true ) &&
array_key_exists( $path[ $count - 2 ], static::get_viewport_media_queries( $settings['viewport'] ?? null ) )
) {
$default_pseudo_path = $path;
unset( $default_pseudo_path[ $count - 2 ] );
$layer = _wp_array_get( $tree, array_values( $default_pseudo_path ) );
if ( is_array( $layer ) ) {
$base_styles[] = $layer;
}
}

foreach ( array_reverse( array_keys( $path ) ) as $index ) {
if ( ! in_array( $path[ $index ], $state_keys, true ) ) {
continue;
Expand Down Expand Up @@ -4260,7 +4284,7 @@ public function get_styles_for_block( $block_metadata ) {
$variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}';
}

$breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation );
$breakpoint_pseudo_declarations = $this->process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name, $block_metadata, $style_variation, $style_variation_node );
foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) {
if ( empty( $pseudo_declarations ) ) {
continue;
Expand Down
159 changes: 159 additions & 0 deletions tests/phpunit/tests/block-supports/states.php
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,165 @@ public function test_block_state_rule_refills_a_text_color_over_a_base_box_clip(
$this->assertArrayNotHasKey( 'background-image', $declarations, 'Expected no text clip escape.' );
}

/**
* Renders a Paragraph with a mobile box clip over the given Global Styles
* for Paragraph.
*
* @param array $paragraph_styles Global Styles for Paragraph.
* @param string $class_name Optional block class name.
* @return string Generated block-supports stylesheet.
*/
private function get_box_clip_over_global_styles_stylesheet( $paragraph_styles, $class_name = '' ) {
$this->ensure_block_registered(
'core/paragraph',
array(),
array( 'background' => array( 'backgroundClip' => true ) )
);

$filter = static function ( $theme_json ) use ( $paragraph_styles ) {
return $theme_json->update_with(
array(
'version' => WP_Theme_JSON::LATEST_SCHEMA,
'styles' => array(
'blocks' => array( 'core/paragraph' => $paragraph_styles ),
),
)
);
};

add_filter( 'wp_theme_json_data_theme', $filter );
WP_Theme_JSON_Resolver::clean_cached_data();

try {
$block = array(
'blockName' => 'core/paragraph',
'attrs' => array(
'className' => $class_name,
'style' => array(
'@mobile' => array(
'background' => array( 'backgroundClip' => 'border-box' ),
),
),
),
);

wp_render_block_states_support( '<p>Text</p>', $block );

return wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );
} finally {
remove_filter( 'wp_theme_json_data_theme', $filter );
WP_Theme_JSON_Resolver::clean_cached_data();
}
}

/**
* Tests that a block instance state escapes a text clip set for its block
* type in Global Styles.
*
* @ticket 66464
* @covers ::wp_render_block_states_support
*/
public function test_instance_state_escapes_a_global_styles_text_clip() {
$actual_stylesheet = $this->get_box_clip_over_global_styles_stylesheet(
array(
'background' => array(
'gradient' => 'linear-gradient(135deg,#ff0000,#0000ff)',
'backgroundClip' => 'text',
),
)
);

$this->assertStringContainsString( 'background-clip:border-box !important;-webkit-text-fill-color:currentColor !important;background-image:unset !important;', $actual_stylesheet );
}

/**
* Tests that a block instance state escapes a text clip set by the block
* style variation applied to it.
*
* @ticket 66464
* @covers ::wp_render_block_states_support
*/
public function test_instance_state_escapes_a_block_style_variation_text_clip() {
register_block_style(
'core/paragraph',
array(
'name' => 'flashy',
'label' => 'Flashy',
)
);

try {
$actual_stylesheet = $this->get_box_clip_over_global_styles_stylesheet(
array(
'variations' => array(
'flashy' => array(
'background' => array(
'gradient' => 'linear-gradient(135deg,#ff0000,#0000ff)',
'backgroundClip' => 'text',
),
),
),
),
'is-style-flashy'
);
} finally {
unregister_block_style( 'core/paragraph', 'flashy' );
}

$this->assertStringContainsString( 'background-clip:border-box !important;-webkit-text-fill-color:currentColor !important;background-image:unset !important;', $actual_stylesheet );
}

/**
* Tests that a responsive pseudo-state escapes a text clip set by the
* default pseudo-state.
*
* @ticket 66464
* @covers ::wp_render_block_states_support
*/
public function test_responsive_pseudo_state_escapes_the_default_pseudo_state_text_clip() {
$block_type = $this->ensure_block_registered(
'core/button',
array( 'root' => '.wp-block-button .wp-block-button__link' ),
array( 'background' => array( 'backgroundClip' => true ) )
);
$supports = $block_type->supports;

$block_type->supports['background']['backgroundClip'] = true;

try {
$block = array(
'blockName' => 'core/button',
'attrs' => array(
'style' => array(
':hover' => array(
'background' => array(
'gradient' => 'linear-gradient(135deg,#ff0000,#0000ff)',
'backgroundClip' => 'text',
),
),
'@mobile' => array(
':hover' => array(
'background' => array( 'backgroundClip' => 'border-box' ),
),
),
),
),
);

$actual = wp_render_block_states_support( '<div class="wp-block-button"><a class="wp-block-button__link">Click me</a></div>', $block );
} finally {
$block_type->supports = $supports;
}

preg_match( '/wp-states-[a-f0-9]{8}/', $actual, $matches );
$actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );

$this->assertStringContainsString(
'@media (width <= 480px){.' . $matches[0] . ' .wp-block-button__link:hover{background-clip:border-box !important;-webkit-text-fill-color:currentColor !important;background-image:unset !important;}}',
$actual_stylesheet
);
}

/**
* Tests that background-image reset is not added when the state also sets a legacy gradient.
*
Expand Down
Loading
Loading