From f57d3640ef3ddb9b758f4396124f18bcc43554a0 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:59:39 +1000 Subject: [PATCH 1/4] Theme JSON: Define preset CSS vars for blocks based on feature selectors Backport of WordPress/gutenberg#75226 Updates the global styles engine to generate preset CSS custom properties on feature-specific selectors rather than only the block's root selector. This enables blocks whose root selector targets an inner element to have preset CSS vars output on the appropriate outer wrapper selector. --- src/wp-includes/class-wp-theme-json.php | 87 ++++++++++++++++++++--- tests/phpunit/tests/theme/wpThemeJson.php | 57 +++++++++++++++ 2 files changed, 135 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 3e7f6f3f78475..aa54091e75704 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1862,21 +1862,89 @@ protected function get_css_variables( $nodes, $origins ) { continue; } - $selector = $metadata['selector']; + $selector = $metadata['selector']; + $feature_selectors = $metadata['selectors'] ?? array(); + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $declarations = static::compute_preset_vars( $node, $origins ); - $theme_vars_declarations = static::compute_theme_vars( $node ); - foreach ( $theme_vars_declarations as $theme_vars_declaration ) { - $declarations[] = $theme_vars_declaration; + /* + * Group preset declarations by selector. Blocks that define + * feature-level selectors need their preset CSS variables + * output under that feature selector instead of the block's + * root selector. + */ + $vars_by_selector = array(); + $vars_by_selector[ $selector ] = array(); + + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + if ( empty( $preset_metadata['css_vars'] ) ) { + continue; + } + + $values_by_slug = static::get_settings_values_by_slug( $node, $preset_metadata, $origins ); + if ( empty( $values_by_slug ) ) { + continue; + } + + $target = static::get_feature_selector( $feature_selectors, $preset_metadata['path'][0], $selector ); + + if ( ! isset( $vars_by_selector[ $target ] ) ) { + $vars_by_selector[ $target ] = array(); + } + + foreach ( $values_by_slug as $slug => $value ) { + $vars_by_selector[ $target ][] = array( + 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), + 'value' => $value, + ); + } + } + + // Theme vars always use the block's default selector. + foreach ( static::compute_theme_vars( $node ) as $theme_var ) { + $vars_by_selector[ $selector ][] = $theme_var; } - $stylesheet .= static::to_ruleset( $selector, $declarations ); + foreach ( $vars_by_selector as $rule_selector => $declarations ) { + $stylesheet .= static::to_ruleset( $rule_selector, $declarations ); + } } return $stylesheet; } + /** + * Returns the appropriate selector for a block support feature's + * preset CSS variables. + * + * If the block defines a feature-level selector (as a string or an + * object with a `root` key), that selector is returned. Otherwise, + * the block's default selector is used. + * + * @since 7.0.0 + * + * @param array $feature_selectors The block's feature selectors map. + * @param string $feature_key The feature to look up (e.g. 'dimensions'). + * @param string $default_selector Fallback selector. + * @return string The resolved selector. + */ + private static function get_feature_selector( $feature_selectors, $feature_key, $default_selector ) { + if ( ! isset( $feature_selectors[ $feature_key ] ) ) { + return $default_selector; + } + + $feature = $feature_selectors[ $feature_key ]; + + if ( is_string( $feature ) ) { + return $feature; + } + + if ( isset( $feature['root'] ) ) { + return $feature['root']; + } + + return $default_selector; + } + /** * Given a selector and a declaration list, * creates the corresponding ruleset. @@ -2511,8 +2579,9 @@ protected static function get_setting_nodes( $theme_json, $selectors = array() ) } $nodes[] = array( - 'path' => array( 'settings', 'blocks', $name ), - 'selector' => $selector, + 'path' => array( 'settings', 'blocks', $name ), + 'selector' => $selector, + 'selectors' => $selectors[ $name ]['selectors'] ?? array(), ); } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index b26ff2b9a9c4c..1b075691076b2 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -759,6 +759,63 @@ public function test_get_stylesheet_preset_classes_work_with_compounded_selector ); } + /** + * @ticket 64598 + */ + public function test_get_stylesheet_preset_css_vars_use_feature_selector() { + register_block_type( + 'test/feature-selector', + array( + 'api_version' => 3, + 'selectors' => array( + 'root' => '.wp-block-test-feature-selector .wp-block-test-feature-selector__inner', + 'dimensions' => array( + 'root' => '.wp-block-test-feature-selector', + ), + ), + ) + ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'test/feature-selector' => array( + 'dimensions' => array( + 'dimensionSizes' => array( + array( + 'slug' => '25', + 'size' => '25%', + ), + array( + 'slug' => '50', + 'size' => '50%', + ), + ), + ), + ), + ), + ), + ) + ); + + $variables = $theme_json->get_stylesheet( array( 'variables' ) ); + + // Dimension preset CSS vars should be on the feature selector, + // not the block's root selector. + $this->assertStringContainsString( + '.wp-block-test-feature-selector{--wp--preset--dimension--25: 25%;--wp--preset--dimension--50: 50%;}', + $variables + ); + $this->assertStringNotContainsString( + '.wp-block-test-feature-selector .wp-block-test-feature-selector__inner{--wp--preset--dimension', + $variables + ); + + unregister_block_type( 'test/feature-selector' ); + } + /** * @ticket 53175 * @ticket 54336 From 2daeb7279b6952a8dd411556a5418e4b498d9b29 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:10:34 +0100 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Weston Ruter --- src/wp-includes/class-wp-theme-json.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index aa54091e75704..0360e54dfb6cc 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1927,7 +1927,7 @@ protected function get_css_variables( $nodes, $origins ) { * @param string $default_selector Fallback selector. * @return string The resolved selector. */ - private static function get_feature_selector( $feature_selectors, $feature_key, $default_selector ) { + private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string { if ( ! isset( $feature_selectors[ $feature_key ] ) ) { return $default_selector; } @@ -1938,11 +1938,7 @@ private static function get_feature_selector( $feature_selectors, $feature_key, return $feature; } - if ( isset( $feature['root'] ) ) { - return $feature['root']; - } - - return $default_selector; + return $feature['root'] ?? $default_selector; } /** From 027feba84549c96a81d400b03c94b5739c01c025 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 11 Feb 2026 08:11:32 +0100 Subject: [PATCH 3/4] Additional tweaks from review --- src/wp-includes/class-wp-theme-json.php | 6 +++--- tests/phpunit/tests/theme/wpThemeJson.php | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 0360e54dfb6cc..faeea72b5b790 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1922,9 +1922,9 @@ protected function get_css_variables( $nodes, $origins ) { * * @since 7.0.0 * - * @param array $feature_selectors The block's feature selectors map. - * @param string $feature_key The feature to look up (e.g. 'dimensions'). - * @param string $default_selector Fallback selector. + * @param array $feature_selectors The block's feature selectors map. + * @param string $feature_key The feature to look up (e.g. 'dimensions'). + * @param string $default_selector Fallback selector. * @return string The resolved selector. */ private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string { diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 1b075691076b2..94f3d6ad41a80 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -44,6 +44,16 @@ public static function set_up_before_class() { static::$user_id = self::factory()->user->create(); } + public function tear_down() { + $registry = WP_Block_Type_Registry::get_instance(); + + if ( $registry->is_registered( 'test/feature-selector' ) ) { + unregister_block_type( 'test/feature-selector' ); + } + + parent::tear_down(); + } + /** * @ticket 52991 * @ticket 54336 @@ -812,8 +822,6 @@ public function test_get_stylesheet_preset_css_vars_use_feature_selector() { '.wp-block-test-feature-selector .wp-block-test-feature-selector__inner{--wp--preset--dimension', $variables ); - - unregister_block_type( 'test/feature-selector' ); } /** From 9a2cf7392b2678419194e5fd5241f1f53b2c73fd Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:02:00 +0100 Subject: [PATCH 4/4] Address further feedback on types and selector return values --- src/wp-includes/class-wp-theme-json.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index faeea72b5b790..ba61e8bfb4678 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -1922,9 +1922,9 @@ protected function get_css_variables( $nodes, $origins ) { * * @since 7.0.0 * - * @param array $feature_selectors The block's feature selectors map. - * @param string $feature_key The feature to look up (e.g. 'dimensions'). - * @param string $default_selector Fallback selector. + * @param array> $feature_selectors The block's feature selectors map. + * @param string $feature_key The feature to look up (e.g. 'dimensions'). + * @param string $default_selector Fallback selector. * @return string The resolved selector. */ private static function get_feature_selector( array $feature_selectors, string $feature_key, string $default_selector ): string { @@ -1938,7 +1938,11 @@ private static function get_feature_selector( array $feature_selectors, string $ return $feature; } - return $feature['root'] ?? $default_selector; + if ( isset( $feature['root'] ) && is_string( $feature['root'] ) ) { + return $feature['root']; + } + + return $default_selector; } /**