-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Block Variations: Have getActiveBlockVariation
return variation with highest specificity
#62031
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -129,9 +129,9 @@ While the `isActive` property is optional, it's recommended. This API is used by | |||||
|
||||||
If `isActive` is not set, the Editor cannot distinguish between an instance of the original block and your variation, so the original block information will be displayed. | ||||||
|
||||||
The property can be set to either a function or an array of strings (`string[]`). | ||||||
The property can be set to either an array of strings (`string[]`), or a function. It is recommended to use the string array version whenever possible. | ||||||
|
||||||
The function version of this property accepts a block instance's `blockAttributes` as the first argument, and the `variationAttributes` declared for a variation as the second argument. These arguments can be used to determine if a variation is active by comparing them and returning a `true` or `false` (indicating whether this variation is inactive for this block instance). | ||||||
The `string[]` version is used to declare which of the block instance's attributes should be compared to the given variation's. Each attribute will be checked and the variation will be active if all of them match. | ||||||
|
||||||
As an example, in the core Embed block, the `providerNameSlug` attribute is used to determine the embed provider (e.g. 'youtube' or 'twitter'). The variations may be declared like this: | ||||||
|
||||||
|
@@ -162,28 +162,32 @@ const variations = [ | |||||
] | ||||||
``` | ||||||
|
||||||
The `isActive` function can compare the block instance value for `providerNameSlug` to the value declared in the variation's declaration (the values in the code snippet above) to determine which embed variation is active: | ||||||
The `isActive` property would then look like this: | ||||||
|
||||||
```js | ||||||
isActive: ( blockAttributes, variationAttributes ) => | ||||||
blockAttributes.providerNameSlug === variationAttributes.providerNameSlug, | ||||||
isActive: [ 'providerNameSlug' ] | ||||||
``` | ||||||
|
||||||
The `string[]` version is used to declare which attributes should be compared as a shorthand. Each attribute will be checked and the variation will be active if all of them match. Using the same example for the embed block, the string version would look like this: | ||||||
This will cause the block instance value for `providerNameSlug` to be compared to the value declared in the variation's declaration (the values in the code snippet above) to determine which embed variation is active. | ||||||
|
||||||
Nested object paths are also supported. For example, consider a block variation that has a `query` object as an attribute. It is possible to determine if the variation is active solely based on that object's `postType` property (while ignoring all its other properties): | ||||||
|
||||||
```js | ||||||
isActive: [ 'providerNameSlug' ] | ||||||
isActive: [ 'query.postType' ] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @justintadlock and @bacoords, I think you will like these change in relation to Block Bindings 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a fantastic PR overall. 🙌 |
||||||
``` | ||||||
|
||||||
Nested object paths are also supported. For example, consider a block variation that has a `query` object as an attribute. It is possible to determine if the variation is active solely based on that object's `postType` property (while ignoring all its other properties): | ||||||
The function version of this property accepts a block instance's `blockAttributes` as the first argument, and the `variationAttributes` declared for a variation as the second argument. These arguments can be used to determine if a variation is active by comparing them and returning a `true` or `false` (indicating whether this variation is inactive for this block instance). | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Using the same example for the embed block, the function version would look like this: | ||||||
|
||||||
```js | ||||||
isActive: [ 'query.postType' ] | ||||||
isActive: ( blockAttributes, variationAttributes ) => | ||||||
blockAttributes.providerNameSlug === variationAttributes.providerNameSlug, | ||||||
``` | ||||||
|
||||||
### Caveats to using `isActive` | ||||||
### Specificity of `isActive` matches | ||||||
|
||||||
The `isActive` property can return false positives if multiple variations exist for a specific block and the `isActive` checks are not specific enough. To demonstrate this, consider the following example: | ||||||
If there are multiple variations whose `isActive` check matches a given block instance, and all of them are string arrays, then the variation with the highest _specificity_ will be chosen. Consider the following example: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe make bold the |
||||||
|
||||||
```js | ||||||
wp.blocks.registerBlockVariation( | ||||||
|
@@ -212,6 +216,6 @@ wp.blocks.registerBlockVariation( | |||||
); | ||||||
``` | ||||||
|
||||||
The `isActive` check on both variations tests the `textColor`, but each variations uses `vivid-red`. Since the `paragraph-red` variation is registered first, once the `paragraph-red-grey` variation is inserted into the Editor, it will have the title `Red Paragraph` instead of `Red/Grey Paragraph`. As soon as the Editor finds a match, it stops checking. | ||||||
If a block instance has attributes `textColor: vivid-red` and `backgroundColor: cyan-bluish-gray`, both variations' `isActive` criterion will match that block instance. In this case, the more _specific_ match will be determined to be the active variation, where specificity is calculated as the length of each `isActive` array. This means that the `Red/Grey Paragraph` will be shown as the active variation. | ||||||
|
||||||
There have been [discussions](https://github.com/WordPress/gutenberg/issues/41303#issuecomment-1526193087) around how the API can be improved, but as of WordPress 6.3, this remains an issue to watch out for. | ||||||
Note that specificity cannot be determined for a matching variation if its `isActive` property is a function rather than a `string[]`. In this case, the first matching variation will be determined to be the active variation. For this reason, it is generally recommended to use a `string[]` rather than a `function` for the `isActive` property. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -237,10 +237,17 @@ export const getBlockVariations = createSelector( | |
export function getActiveBlockVariation( state, blockName, attributes, scope ) { | ||
const variations = getBlockVariations( state, blockName, scope ); | ||
|
||
const match = variations?.find( ( variation ) => { | ||
if ( ! variations ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure about this like but |
||
return variations; | ||
} | ||
|
||
const blockType = getBlockType( state, blockName ); | ||
const attributeKeys = Object.keys( blockType?.attributes || {} ); | ||
let match; | ||
let maxMatchedAttributes = 0; | ||
|
||
for ( const variation of variations ) { | ||
if ( Array.isArray( variation.isActive ) ) { | ||
const blockType = getBlockType( state, blockName ); | ||
const attributeKeys = Object.keys( blockType?.attributes || {} ); | ||
const definedAttributes = variation.isActive.filter( | ||
( attribute ) => { | ||
// We support nested attribute paths, e.g. `layout.type`. | ||
|
@@ -250,10 +257,11 @@ export function getActiveBlockVariation( state, blockName, attributes, scope ) { | |
return attributeKeys.includes( topLevelAttribute ); | ||
} | ||
); | ||
if ( definedAttributes.length === 0 ) { | ||
return false; | ||
const definedAttributesLength = definedAttributes.length; | ||
if ( definedAttributesLength === 0 ) { | ||
continue; | ||
} | ||
return definedAttributes.every( ( attribute ) => { | ||
const isMatch = definedAttributes.every( ( attribute ) => { | ||
const attributeValue = getValueFromObjectPath( | ||
attributes, | ||
attribute | ||
|
@@ -266,11 +274,17 @@ export function getActiveBlockVariation( state, blockName, attributes, scope ) { | |
getValueFromObjectPath( variation.attributes, attribute ) | ||
); | ||
} ); | ||
if ( isMatch && definedAttributesLength > maxMatchedAttributes ) { | ||
match = variation; | ||
maxMatchedAttributes = definedAttributesLength; | ||
} | ||
} else if ( variation.isActive?.( attributes, variation.attributes ) ) { | ||
// If isActive is a function, we cannot know how many attributes it matches. | ||
// This means that we cannot compare the specificity of our matches, | ||
// and simply return the best match we have found. | ||
return match || variation; | ||
} | ||
|
||
return variation.isActive?.( attributes, variation.attributes ); | ||
} ); | ||
|
||
} | ||
return match; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.