diff --git a/backport-changelog/6.6/6989.md b/backport-changelog/6.6/6989.md new file mode 100644 index 0000000000000..3d236938ff74a --- /dev/null +++ b/backport-changelog/6.6/6989.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/6989 + +* https://github.com/WordPress/gutenberg/pull/63172 diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php index 1c049f4a0fee5..d1a63915f1288 100644 --- a/lib/block-supports/block-style-variations.php +++ b/lib/block-supports/block-style-variations.php @@ -38,6 +38,42 @@ function gutenberg_get_block_style_variation_name_from_class( $class_string ) { return $matches[1] ?? null; } +/** + * Recursively resolves any `ref` values within a block style variation's data. + * + * @since 6.6.0 + * + * @param array $variation_data Reference to the variation data being processed. + * @param array $theme_json Theme.json data to retrieve referenced values from. + */ +function gutenberg_resolve_block_style_variation_ref_values( &$variation_data, $theme_json ) { + foreach ( $variation_data as $key => &$value ) { + // Only need to potentially process arrays. + if ( is_array( $value ) ) { + // If ref value is set, attempt to find its matching value and update it. + if ( array_key_exists( 'ref', $value ) ) { + // Clean up any invalid ref value. + if ( empty( $value['ref'] ) || ! is_string( $value['ref'] ) ) { + unset( $variation_data[ $key ] ); + } + + $value_path = explode( '.', $value['ref'] ?? '' ); + $ref_value = _wp_array_get( $theme_json, $value_path ); + + // Only update the current value if the referenced path matched a value. + if ( null === $ref_value ) { + unset( $variation_data[ $key ] ); + } else { + $value = $ref_value; + } + } else { + // Recursively look for ref instances. + gutenberg_resolve_block_style_variation_ref_values( $value, $theme_json ); + } + } + } +} + /** * Render the block style variation's styles. * @@ -79,6 +115,10 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) return $parsed_block; } + // Recursively resolve any ref values with the appropriate value within the + // theme_json data. + gutenberg_resolve_block_style_variation_ref_values( $variation_data, $theme_json ); + $variation_instance = gutenberg_create_block_style_variation_instance_name( $parsed_block, $variation ); $class_name = "is-style-$variation_instance"; $updated_class_name = $parsed_block['attrs']['className'] . " $class_name"; diff --git a/packages/block-editor/src/hooks/block-style-variation.js b/packages/block-editor/src/hooks/block-style-variation.js index 21259966d8a63..f302cf2aa3a2a 100644 --- a/packages/block-editor/src/hooks/block-style-variation.js +++ b/packages/block-editor/src/hooks/block-style-variation.js @@ -14,6 +14,7 @@ import { getBlockSelectors, } from '../components/global-styles'; import { useStyleOverride } from './utils'; +import { getValueFromObjectPath } from '../utils/object'; import { store as blockEditorStore } from '../store'; import { globalStylesDataKey } from '../store/private-keys'; import { unlock } from '../lock-unlock'; @@ -180,6 +181,77 @@ export function __unstableBlockStyleVariationOverridesWithConfig( { config } ) { ); } +/** + * Retrieves any variation styles data and resolves any referenced values. + * + * @param {Object} globalStyles A complete global styles object, containing settings and styles. + * @param {string} name The name of the desired block type. + * @param {variation} variation The of the block style variation to retrieve data for. + * + * @return {Object|undefined} The global styles data for the specified variation. + */ +export function getVariationStylesWithRefValues( + globalStyles, + name, + variation +) { + if ( ! globalStyles?.styles?.blocks?.[ name ]?.variations?.[ variation ] ) { + return; + } + + // Helper to recursively look for `ref` values to resolve. + const replaceRefs = ( variationStyles ) => { + Object.keys( variationStyles ).forEach( ( key ) => { + const value = variationStyles[ key ]; + + // Only process objects. + if ( typeof value === 'object' && value !== null ) { + // Process `ref` value if present. + if ( value.ref !== undefined ) { + if ( + typeof value.ref !== 'string' || + value.ref.trim() === '' + ) { + // Remove invalid ref. + delete variationStyles[ key ]; + } else { + // Resolve `ref` value. + const refValue = getValueFromObjectPath( + globalStyles, + value.ref + ); + + if ( refValue ) { + variationStyles[ key ] = refValue; + } else { + delete variationStyles[ key ]; + } + } + } else { + // Recursively resolve `ref` values in nested objects. + replaceRefs( value ); + + // After recursion, if value is empty due to explicitly + // `undefined` ref value, remove it. + if ( Object.keys( value ).length === 0 ) { + delete variationStyles[ key ]; + } + } + } + } ); + }; + + // Deep clone variation node to avoid mutating it within global styles and losing refs. + const styles = JSON.parse( + JSON.stringify( + globalStyles.styles.blocks[ name ].variations[ variation ] + ) + ); + replaceRefs( styles ); + + return styles; +} + function useBlockStyleVariation( name, variation, clientId ) { // Prefer global styles data in GlobalStylesContext, which are available // if in the site editor. Otherwise fall back to whatever is in the @@ -194,9 +266,14 @@ function useBlockStyleVariation( name, variation, clientId ) { }, [] ); return useMemo( () => { - const styles = mergedConfig?.styles ?? globalStyles; - const variationStyles = - styles?.blocks?.[ name ]?.variations?.[ variation ]; + const variationStyles = getVariationStylesWithRefValues( + { + settings: mergedConfig?.settings ?? globalSettings, + styles: mergedConfig?.styles ?? globalStyles, + }, + name, + variation + ); return { settings: mergedConfig?.settings ?? globalSettings, diff --git a/packages/block-editor/src/hooks/test/get-variation-styles-with-ref-values.js b/packages/block-editor/src/hooks/test/get-variation-styles-with-ref-values.js new file mode 100644 index 0000000000000..e42fcef06122b --- /dev/null +++ b/packages/block-editor/src/hooks/test/get-variation-styles-with-ref-values.js @@ -0,0 +1,91 @@ +/** + * Internal dependencies + */ +import { getVariationStylesWithRefValues } from '../block-style-variation'; + +describe( 'getVariationStylesWithRefValues', () => { + it( 'should resolve ref values correctly', () => { + const globalStyles = { + styles: { + color: { background: 'red' }, + blocks: { + 'core/heading': { + color: { text: 'blue' }, + }, + 'core/group': { + variations: { + custom: { + color: { + text: { ref: 'styles.does-not-exist' }, + background: { + ref: 'styles.color.background', + }, + }, + blocks: { + 'core/heading': { + color: { + text: { + ref: 'styles.blocks.core/heading.color.text', + }, + background: { ref: '' }, + }, + }, + }, + elements: { + link: { + color: { + text: { + ref: 'styles.elements.link.color.text', + }, + background: { ref: undefined }, + }, + ':hover': { + color: { + text: { + ref: 'styles.elements.link.:hover.color.text', + }, + }, + }, + }, + }, + }, + }, + }, + }, + elements: { + link: { + color: { text: 'green' }, + ':hover': { + color: { text: 'yellow' }, + }, + }, + }, + }, + }; + + expect( + getVariationStylesWithRefValues( + globalStyles, + 'core/group', + 'custom' + ) + ).toEqual( { + color: { background: 'red' }, + blocks: { + 'core/heading': { + color: { text: 'blue' }, + }, + }, + elements: { + link: { + color: { + text: 'green', + }, + ':hover': { + color: { text: 'yellow' }, + }, + }, + }, + } ); + } ); +} ); diff --git a/phpunit/block-supports/block-style-variations-test.php b/phpunit/block-supports/block-style-variations-test.php index 0236beff468ee..350efb3473076 100644 --- a/phpunit/block-supports/block-style-variations-test.php +++ b/phpunit/block-supports/block-style-variations-test.php @@ -189,4 +189,76 @@ public function test_add_registered_block_styles_to_theme_data() { $this->assertSameSetsWithIndex( $expected, $group_styles, 'Variation data does not match' ); } + + /** + * Tests that block style variations resolve any `ref` values when generating styles. + */ + public function test_block_style_variation_ref_values() { + switch_theme( 'block-theme' ); + + $variation_data = array( + 'color' => array( + 'text' => array( + 'ref' => 'styles.does-not-exist', + ), + 'background' => array( + 'ref' => 'styles.blocks.core/group.variations.block-style-variation-a.color.text', + ), + ), + 'blocks' => array( + 'core/heading' => array( + 'color' => array( + 'text' => array( + 'ref' => 'styles.blocks.core/group.variations.block-style-variation-a.color.background', + ), + 'background' => array( + 'ref' => '', + ), + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => array( + 'ref' => 'styles.blocks.core/group.variations.block-style-variation-b.color.text', + ), + 'background' => array( + 'ref' => null, + ), + ), + ':hover' => array( + 'color' => array( + 'text' => array( + 'ref' => 'styles.blocks.core/group.variations.block-style-variation-b.color.background', + ), + ), + ), + ), + ), + ); + + $theme_json = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_raw_data(); + + gutenberg_resolve_block_style_variation_ref_values( $variation_data, $theme_json ); + + $expected = array( + 'color' => array( 'background' => 'plum' ), + 'blocks' => array( + 'core/heading' => array( + 'color' => array( 'text' => 'indigo' ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( 'text' => 'lightblue' ), + ':hover' => array( + 'color' => array( 'text' => 'midnightblue' ), + ), + ), + ), + ); + + $this->assertSameSetsWithIndex( $expected, $variation_data, 'Variation data with resolved ref values does not match' ); + } }