diff --git a/src/wp-includes/block-supports/states.php b/src/wp-includes/block-supports/states.php index d283dc2b4acf3..82f6b43a9b2a6 100644 --- a/src/wp-includes/block-supports/states.php +++ b/src/wp-includes/block-supports/states.php @@ -295,6 +295,91 @@ function wp_get_root_state_style( $state_style, $nested_keys ) { return $root_style; } +/** + * Generates all element selectors for a block root selector. + * + * @since 7.1.0 + * + * @param string $root_selector The block root CSS selector. + * @return string[] Element selectors keyed by element name. + */ +function wp_get_block_state_element_selectors( $root_selector ) { + if ( ! is_string( $root_selector ) || '' === trim( $root_selector ) ) { + return array(); + } + + $block_selectors = wp_split_selector_list( $root_selector ); + $element_selectors = array(); + + foreach ( WP_Theme_JSON::ELEMENTS as $element_name => $element_selector ) { + $selectors = array(); + + foreach ( $block_selectors as $block_selector ) { + $block_selector = trim( $block_selector ); + if ( '' === $block_selector ) { + continue; + } + + if ( $block_selector === $element_selector ) { + $selectors = array( $element_selector ); + break; + } + + $selector_prefix = "$block_selector "; + if ( ! str_contains( $element_selector, ',' ) ) { + $selectors[] = $selector_prefix . $element_selector; + continue; + } + + $prepended_selectors = array(); + foreach ( wp_split_selector_list( $element_selector ) as $selector ) { + $prepended_selectors[] = $selector_prefix . $selector; + } + $selectors[] = implode( ',', $prepended_selectors ); + } + + if ( ! empty( $selectors ) ) { + $element_selectors[ $element_name ] = implode( ',', $selectors ); + } + } + + return $element_selectors; +} + +/** + * Adds a compiled state style rule to a rule list. + * + * @since 7.1.0 + * + * @param array $css_rules Style rules. + * @param string $state Pseudo-state selector. + * @param string|null $selector Block, feature, or element selector. + * @param array $style Style object. + * @param string|null $rules_group Optional CSS grouping rule, e.g. a media query. + */ +function wp_add_block_state_style_rule( &$css_rules, $state, $selector, $style, $rules_group = null ) { + if ( empty( $style ) || ! is_array( $style ) ) { + return; + } + + $compiled = wp_style_engine_get_styles( + wp_normalize_state_style_for_css_output( $style ) + ); + + if ( empty( $compiled['declarations'] ) ) { + return; + } + + $css_rules[] = array( + 'state' => $state, + 'selector' => $selector, + 'declarations' => $compiled['declarations'], + ); + if ( ! empty( $rules_group ) ) { + $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; + } +} + /** * Builds compiled state style rules, preserving the selector each rule targets. * @@ -317,21 +402,13 @@ function wp_get_block_state_style_rules( $state_styles, $block_type, $rules_grou } foreach ( wp_get_state_style_groups( $state_style, $block_selectors ) as $group ) { - $style = wp_get_state_style_with_fallback_dimension_styles( $group['style'] ); - $compiled = wp_style_engine_get_styles( - wp_normalize_state_style_for_css_output( $style ) + wp_add_block_state_style_rule( + $css_rules, + $state, + $group['selector'], + $group['style'], + $rules_group ); - - if ( ! empty( $compiled['declarations'] ) ) { - $css_rules[] = array( - 'state' => $state, - 'selector' => $group['selector'], - 'declarations' => $compiled['declarations'], - ); - if ( ! empty( $rules_group ) ) { - $css_rules[ count( $css_rules ) - 1 ]['rules_group'] = $rules_group; - } - } } } @@ -503,6 +580,57 @@ function wp_render_block_states_support( $block_content, $block ) { ); } + if ( + ! empty( $style[ $breakpoint ]['elements'] ) && + is_array( $style[ $breakpoint ]['elements'] ) + ) { + $element_selectors = wp_get_block_state_element_selectors( + wp_get_block_css_selector( $block_type ) + ); + + foreach ( $style[ $breakpoint ]['elements'] as $element_name => $element_style ) { + if ( + empty( $element_style ) || + ! is_array( $element_style ) || + empty( $element_selectors[ $element_name ] ) + ) { + continue; + } + + $element_pseudo_states = WP_Theme_JSON::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] + ?? array(); + $root_element_style = wp_get_root_state_style( + $element_style, + $element_pseudo_states + ); + + wp_add_block_state_style_rule( + $css_rules, + '', + $element_selectors[ $element_name ], + $root_element_style, + $media_query + ); + + foreach ( $element_pseudo_states as $pseudo_state ) { + if ( + empty( $element_style[ $pseudo_state ] ) || + ! is_array( $element_style[ $pseudo_state ] ) + ) { + continue; + } + + wp_add_block_state_style_rule( + $css_rules, + $pseudo_state, + $element_selectors[ $element_name ], + $element_style[ $pseudo_state ], + $media_query + ); + } + } + } + foreach ( $supported_pseudo_states as $pseudo_state ) { if ( empty( $style[ $breakpoint ][ $pseudo_state ] ) || ! is_array( $style[ $breakpoint ][ $pseudo_state ] ) ) { continue; diff --git a/tests/phpunit/tests/block-supports/states.php b/tests/phpunit/tests/block-supports/states.php index 3c8fa6951d20d..122c1c3ea5fcf 100644 --- a/tests/phpunit/tests/block-supports/states.php +++ b/tests/phpunit/tests/block-supports/states.php @@ -964,6 +964,49 @@ public function test_responsive_root_state_generates_media_query_scoped_css() { ); } + /** + * Tests that a responsive element color generates media-query scoped CSS. + * + * @covers ::wp_render_block_states_support + * + * @ticket 65164 + */ + public function test_responsive_element_color_generates_media_query_scoped_css() { + $this->ensure_block_registered( 'core/group' ); + + $block_content = '

Link

'; + $block = array( + 'blockName' => 'core/group', + 'attrs' => array( + 'style' => array( + 'mobile' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => '#00ff00', + ), + ), + ), + ), + ), + ), + ); + + $actual = wp_render_block_states_support( $block_content, $block ); + + $this->assertMatchesRegularExpression( + '/^

Link<\/a><\/p><\/div>$/', + $actual + ); + 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] . ' a:where(:not(.wp-element-button)){color:#00ff00 !important;}}', + $actual_stylesheet + ); + } + /** * Tests that a responsive pseudo-state generates media-query scoped CSS. *