From ade15b06eb1d6616090e8e6ebd9c77db29efef5d 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/5] 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 61b076dd872cc..f0bf57f9ec98d 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2471,21 +2471,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. @@ -3136,8 +3204,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 d9a51a98af260..3d658e862e9f5 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -782,6 +782,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 f3fe3fc6c3e4e631e22ccc1eedc2c746fdbaddaa 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/5] 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 f0bf57f9ec98d..05dc0d767c77c 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2536,7 +2536,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; } @@ -2547,11 +2547,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 e02802c0db0fe67da02087eefec4711646f75df4 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/5] 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 05dc0d767c77c..ddd8eef8be834 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2531,9 +2531,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 3d658e862e9f5..0cf9981609b8d 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 @@ -835,8 +845,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 0cc2bf5039fa415e8621cbaeac71169ebe776de8 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/5] 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 ddd8eef8be834..e51e209cfa354 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2531,9 +2531,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 { @@ -2547,7 +2547,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; } /** From 40cdf23c61cd8b6cfe24c3cdbecdbb8b401c9dc7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 14 Jul 2026 17:03:55 +1000 Subject: [PATCH 5/5] Apply suggestion from @ramonjd --- src/wp-includes/class-wp-theme-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index e51e209cfa354..4414ed71cb53a 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -2529,7 +2529,7 @@ protected function get_css_variables( $nodes, $origins ) { * object with a `root` key), that selector is returned. Otherwise, * the block's default selector is used. * - * @since 7.0.0 + * @since 7.1.0 * * @param array> $feature_selectors The block's feature selectors map. * @param string $feature_key The feature to look up (e.g. 'dimensions').