diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md index b4200496f5c0c..4944a7805b0d4 100644 --- a/docs/reference-guides/block-api/block-variations.md +++ b/docs/reference-guides/block-api/block-variations.md @@ -48,7 +48,7 @@ An object describing a variation defined for the block type can contain the foll - `block` - Used by blocks to filter specific block variations. Mostly used in Placeholder patterns like `Columns` block. - `transform` - Block Variation will be shown in the component for Block Variations transformations. - `keywords` (optional, type `string[]`) - An array of terms (which can be translated) that help users discover the variation while searching. -- `isActive` (optional, type `Function`) - A function that accepts a block's attributes and the variation's attributes and determines if a variation is active. This function doesn't try to find a match dynamically based on all block's attributes, as in many cases some attributes are irrelevant. An example would be for `embed` block where we only care about `providerNameSlug` attribute's value. +- `isActive` (optional, type `Function|string[]`) - This can be a function or an array of block attributes. Function that accepts a block's attributes and the variation's attributes and determines if a variation is active. This function doesn't try to find a match dynamically based on all block's attributes, as in many cases some attributes are irrelevant. An example would be for `embed` block where we only care about `providerNameSlug` attribute's value. We can also use a `string[]` to tell which attributes should be compared as a shorthand. Each attributes will be matched and the variation will be active if all of them are matching. The main difference between style variations and block variations is that a style variation just applies a `css class` to the block, so it can be styled in an alternative way. If we want to apply initial attributes or inner blocks, we fall in block variation territory. diff --git a/packages/block-editor/src/components/use-block-display-information/index.js b/packages/block-editor/src/components/use-block-display-information/index.js index 7ee0d350ffef8..a06a397caac1f 100644 --- a/packages/block-editor/src/components/use-block-display-information/index.js +++ b/packages/block-editor/src/components/use-block-display-information/index.js @@ -43,21 +43,19 @@ export default function useBlockDisplayInformation( clientId ) { const { getBlockName, getBlockAttributes } = select( blockEditorStore ); - const { getBlockType, getBlockVariations } = select( blocksStore ); + const { getBlockType, getActiveBlockVariation } = select( + blocksStore + ); const blockName = getBlockName( clientId ); const blockType = getBlockType( blockName ); if ( ! blockType ) return null; - const variations = getBlockVariations( blockName ); const blockTypeInfo = { title: blockType.title, icon: blockType.icon, description: blockType.description, }; - if ( ! variations?.length ) return blockTypeInfo; const attributes = getBlockAttributes( clientId ); - const match = variations.find( ( variation ) => - variation.isActive?.( attributes, variation.attributes ) - ); + const match = getActiveBlockVariation( blockName, attributes ); if ( ! match ) return blockTypeInfo; return { title: match.title || blockType.title, diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 0930f57bfc5d4..33308fe2da1b9 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -21,8 +21,8 @@ import { /** * WordPress dependencies */ -import { combineReducers } from '@wordpress/data'; -import { getBlockVariations } from '@wordpress/blocks'; +import { combineReducers, select } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies */ @@ -1510,9 +1510,9 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) { case 'REPLACE_BLOCKS': return action.blocks.reduce( ( prevState, block ) => { const { attributes, name: blockName } = block; - const variations = getBlockVariations( blockName ); - const match = variations?.find( ( variation ) => - variation.isActive?.( attributes, variation.attributes ) + const match = select( blocksStore ).getActiveBlockVariation( + blockName, + attributes ); // If a block variation match is found change the name to be the same with the // one that is used for block variations in the Inserter (`getItemFromVariation`). diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index c0153a703785b..81e27faab26f2 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -101,14 +101,16 @@ import { store as blocksStore } from '../store'; * @property {string[]} [keywords] An array of terms (which can be translated) * that help users discover the variation * while searching. - * @property {Function} [isActive] A function that accepts a block's attributes - * and the variation's attributes and determines - * if a variation is active. This function doesn't - * try to find a match dynamically based on all - * block's attributes, as in many cases some - * attributes are irrelevant. An example would - * be for `embed` block where we only care about - * `providerNameSlug` attribute's value. + * @property {Function|string[]} [isActive] This can be a function or an array of block attributes. + * Function that accepts a block's attributes and the + * variation's attributes and determines if a variation is active. + * This function doesn't try to find a match dynamically based + * on all block's attributes, as in many cases some attributes are irrelevant. + * An example would be for `embed` block where we only care + * about `providerNameSlug` attribute's value. + * We can also use a `string[]` to tell which attributes + * should be compared as a shorthand. Each attributes will + * be matched and the variation will be active if all of them are matching. */ /** diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 5513ff4e4c488..6fbbe72d054d9 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -95,6 +95,51 @@ export function getBlockVariations( state, blockName, scope ) { } ); } +/** + * Returns the active block variation for a given block based on its attributes. + * Variations are determined by their `isActive` property. + * Which is either an array of block attribute keys or a function. + * + * In case of an array of block attribute keys, the `attributes` are compared + * to the variation's attributes using strict equality check. + * + * In case of function type, the function should accept a block's attributes + * and the variation's attributes and determines if a variation is active. + * A function that accepts a block's attributes and the variation's attributes and determines if a variation is active. + * + * @param {Object} state Data state. + * @param {string} blockName Name of block (example: “core/columns”). + * @param {Object} attributes Block attributes used to determine active variation. + * @param {WPBlockVariationScope} [scope] Block variation scope name. + * + * @return {(WPBlockVariation|undefined)} Active block variation. + */ +export function getActiveBlockVariation( state, blockName, attributes, scope ) { + const variations = getBlockVariations( state, blockName, scope ); + + const match = variations?.find( ( variation ) => { + if ( Array.isArray( variation.isActive ) ) { + const blockType = getBlockType( state, blockName ); + const attributeKeys = Object.keys( blockType.attributes || {} ); + const definedAttributes = variation.isActive.filter( + ( attribute ) => attributeKeys.includes( attribute ) + ); + if ( definedAttributes.length === 0 ) { + return false; + } + return definedAttributes.every( + ( attribute ) => + attributes[ attribute ] === + variation.attributes[ attribute ] + ); + } + + return variation.isActive?.( attributes, variation.attributes ); + } ); + + return match; +} + /** * Returns the default block variation for the given block type. * When there are multiple variations annotated as the default one, diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js index 328293a898fcd..47b521f05bc30 100644 --- a/packages/blocks/src/store/test/selectors.js +++ b/packages/blocks/src/store/test/selectors.js @@ -16,6 +16,7 @@ import { getGroupingBlockName, isMatchingSearchTerm, getCategories, + getActiveBlockVariation, } from '../selectors'; describe( 'selectors', () => { @@ -277,6 +278,255 @@ describe( 'selectors', () => { ); } ); } ); + describe( 'getActiveBlockVariation', () => { + const blockTypeWithTestAttributes = { + name: 'block/name', + attributes: { + testAttribute: {}, + firstTestAttribute: {}, + secondTestAttribute: {}, + }, + }; + const FIRST_VARIATION_TEST_ATTRIBUTE_VALUE = 1; + const SECOND_VARIATION_TEST_ATTRIBUTE_VALUE = 2; + const UNUSED_TEST_ATTRIBUTE_VALUE = 5555; + const firstActiveBlockVariationFunction = { + ...firstBlockVariation, + attributes: { + testAttribute: FIRST_VARIATION_TEST_ATTRIBUTE_VALUE, + }, + isActive: ( blockAttributes, variationAttributes ) => { + return ( + blockAttributes.testAttribute === + variationAttributes.testAttribute + ); + }, + }; + const secondActiveBlockVariationFunction = { + ...secondBlockVariation, + attributes: { + testAttribute: SECOND_VARIATION_TEST_ATTRIBUTE_VALUE, + }, + isActive: ( blockAttributes, variationAttributes ) => { + return ( + blockAttributes.testAttribute === + variationAttributes.testAttribute + ); + }, + }; + const firstActiveBlockVariationArray = { + ...firstBlockVariation, + attributes: { + testAttribute: FIRST_VARIATION_TEST_ATTRIBUTE_VALUE, + }, + isActive: [ 'testAttribute' ], + }; + const secondActiveBlockVariationArray = { + ...secondBlockVariation, + attributes: { + testAttribute: SECOND_VARIATION_TEST_ATTRIBUTE_VALUE, + }, + isActive: [ 'testAttribute' ], + }; + const createBlockVariationsStateWithTestBlockType = ( + variations + ) => + deepFreeze( { + ...createBlockVariationsState( variations ), + blockTypes: { + [ blockTypeWithTestAttributes.name ]: blockTypeWithTestAttributes, + }, + } ); + const stateFunction = createBlockVariationsStateWithTestBlockType( [ + firstActiveBlockVariationFunction, + secondActiveBlockVariationFunction, + thirdBlockVariation, + ] ); + const stateArray = createBlockVariationsStateWithTestBlockType( [ + firstActiveBlockVariationArray, + secondActiveBlockVariationArray, + thirdBlockVariation, + ] ); + test.each( [ + [ + firstActiveBlockVariationFunction.name, + firstActiveBlockVariationFunction, + ], + [ + secondActiveBlockVariationFunction.name, + secondActiveBlockVariationFunction, + ], + ] )( + 'should return the active variation based on the given isActive function (%s)', + ( _variationName, variation ) => { + const blockAttributes = { + testAttribute: variation.attributes.testAttribute, + }; + + const result = getActiveBlockVariation( + stateFunction, + blockName, + blockAttributes + ); + + expect( result ).toEqual( variation ); + } + ); + it( 'should return undefined if no active variation is found', () => { + const blockAttributes = { + testAttribute: UNUSED_TEST_ATTRIBUTE_VALUE, + }; + + const result = getActiveBlockVariation( + stateFunction, + blockName, + blockAttributes + ); + + expect( result ).toBeUndefined(); + } ); + it( 'should return the active variation based on the given isActive array', () => { + [ + firstActiveBlockVariationArray, + secondActiveBlockVariationArray, + ].forEach( ( variation ) => { + const blockAttributes = { + testAttribute: variation.attributes.testAttribute, + }; + + const result = getActiveBlockVariation( + stateArray, + blockName, + blockAttributes + ); + + expect( result ).toEqual( variation ); + } ); + } ); + it( 'should return the active variation based on the given isActive array (multiple values)', () => { + const variations = [ + { + name: 'variation-1', + attributes: { + firstTestAttribute: 1, + secondTestAttribute: 10, + }, + isActive: [ + 'firstTestAttribute', + 'secondTestAttribute', + ], + }, + { + name: 'variation-2', + attributes: { + firstTestAttribute: 2, + secondTestAttribute: 20, + }, + isActive: [ + 'firstTestAttribute', + 'secondTestAttribute', + ], + }, + { + name: 'variation-3', + attributes: { + firstTestAttribute: 1, + secondTestAttribute: 20, + }, + isActive: [ + 'firstTestAttribute', + 'secondTestAttribute', + ], + }, + ]; + + const state = createBlockVariationsStateWithTestBlockType( + variations + ); + + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 1, + secondTestAttribute: 10, + } ) + ).toEqual( variations[ 0 ] ); + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 2, + secondTestAttribute: 20, + } ) + ).toEqual( variations[ 1 ] ); + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 1, + secondTestAttribute: 20, + } ) + ).toEqual( variations[ 2 ] ); + } ); + it( 'should ignore attributes that are not defined in the block type', () => { + const variations = [ + { + name: 'variation-1', + attributes: { + firstTestAttribute: 1, + secondTestAttribute: 10, + undefinedTestAttribute: 100, + }, + isActive: [ + 'firstTestAttribute', + 'secondTestAttribute', + 'undefinedTestAttribute', + ], + }, + { + name: 'variation-2', + attributes: { + firstTestAttribute: 2, + secondTestAttribute: 20, + undefinedTestAttribute: 200, + }, + isActive: [ + 'firstTestAttribute', + 'secondTestAttribute', + 'undefinedTestAttribute', + ], + }, + ]; + + const state = createBlockVariationsStateWithTestBlockType( + variations + ); + + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 1, + secondTestAttribute: 10, + undefinedTestAttribute: 100, + } ) + ).toEqual( variations[ 0 ] ); + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 1, + secondTestAttribute: 10, + undefinedTestAttribute: 1234, + } ) + ).toEqual( variations[ 0 ] ); + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 2, + secondTestAttribute: 20, + undefinedTestAttribute: 200, + } ) + ).toEqual( variations[ 1 ] ); + expect( + getActiveBlockVariation( state, blockName, { + firstTestAttribute: 2, + secondTestAttribute: 20, + undefinedTestAttribute: 2345, + } ) + ).toEqual( variations[ 1 ] ); + } ); + } ); describe( 'getDefaultBlockVariation', () => { it( 'should return the default variation when set', () => { const defaultBlockVariation = {