From a00bd38790ea11e52f4003734f18634f8bc558ca Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 7 Aug 2026 10:37:00 +1000 Subject: [PATCH] Editor: Render theme.json element styles defined only inside a breakpoint. Element styles written inside @mobile or @tablet in theme.json now produce CSS whether or not the same element is also styled outside the breakpoint. Previously, WP_Theme_JSON::get_block_nodes() only looped over elements present under styles.blocks..elements, so an element styled only inside a breakpoint never produced a node and no CSS was output. Backports https://github.com/WordPress/gutenberg/pull/81265. See #65829. --- src/wp-includes/class-wp-theme-json.php | 41 ++++++-- tests/phpunit/tests/theme/wpThemeJson.php | 112 ++++++++++++++++++++++ 2 files changed, 144 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 7e2cb54731b1f..456d6a0b7f8d5 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -3715,22 +3715,45 @@ private static function get_block_nodes( $theme_json, $selectors = array(), $opt } } } - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { + /* + * Elements can be styled outside any breakpoint, inside one, or both, + * so collect the names from all of those places before looping. An + * element styled only inside a breakpoint still needs a node. + */ + $block_node = $theme_json['styles']['blocks'][ $name ] ?? array(); + $element_names = array_keys( $block_node['elements'] ?? array() ); + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) { + $element_names = array_merge( + $element_names, + array_keys( $block_node[ $breakpoint ]['elements'] ?? array() ) + ); + } + $element_names = array_unique( $element_names ); + + if ( ! empty( $element_names ) ) { + foreach ( $element_names as $element ) { $element_path = array( 'styles', 'blocks', $name, 'elements', $element ); if ( $include_node_paths_only ) { - $nodes[] = array( - 'path' => $element_path, - ); + if ( isset( $block_node['elements'][ $element ] ) ) { + $nodes[] = array( + 'path' => $element_path, + ); + } + continue; + } + + if ( ! isset( $selectors[ $name ]['elements'][ $element ] ) ) { continue; } $element_selector = $selectors[ $name ]['elements'][ $element ]; - $nodes[] = array( - 'path' => $element_path, - 'selector' => $element_selector, - ); + if ( isset( $block_node['elements'][ $element ] ) ) { + $nodes[] = array( + 'path' => $element_path, + 'selector' => $element_selector, + ); + } // Responsive element nodes: one node per breakpoint that has // styles for this element. Cascade: a{} → @media{a{}} diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 130bd5ae9d323..6997d54036be6 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -1448,6 +1448,118 @@ public function test_get_styles_for_block_outputs_responsive_block_gap_after_def $this->assertLessThan( strpos( $actual_styles, $mobile_gap ), strpos( $actual_styles, $default_gap ) ); } + /** + * @ticket 65829 + */ + public function test_get_stylesheet_renders_element_styles_defined_only_in_a_breakpoint() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + '@mobile' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $expected = '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: red;}}'; + + $this->assertSame( + $expected, + $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) + ); + } + + /** + * @ticket 65829 + */ + public function test_get_stylesheet_renders_element_pseudo_styles_defined_only_in_a_breakpoint() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + '@mobile' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $expected = '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button)):hover){color: red;}}'; + + $this->assertSame( + $expected, + $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) + ); + } + + /** + * @ticket 65829 + */ + public function test_get_stylesheet_renders_element_styles_defined_only_in_separate_breakpoints() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + '@mobile' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + '@tablet' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $link_selector = ':root :where(.wp-block-group a:where(:not(.wp-element-button)))'; + $expected = '@media (width <= 480px){' . $link_selector . '{color: red;}}' . + '@media (480px < width <= 782px){' . $link_selector . '{color: blue;}}'; + + $this->assertSame( + $expected, + $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) + ); + } + /** * @ticket 65164 */