diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index a15191c19ffb73..f92eacbda1a531 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -176,8 +176,11 @@ attributes: { ## className -- Type: `boolean` +- Type: `boolean` or `Object` - Default value: `true` +- Subproperties: + - `block`: type `boolean`, default value `true` + - `variation`: type `boolean`, default value `false` By default, the class `.wp-block-your-block-name` is added to the root element of your saved markup. This helps by providing a consistent mechanism for styling blocks that themes and plugins can rely on. If, for whatever reason, a class is not desired on the markup, this functionality can be disabled. @@ -188,6 +191,32 @@ supports: { } ``` +### className.block + +The above is equivalent to the more verbose + +```js +supports: { + // Remove the support for the generated className. + className: { + block: false + } +} +``` + +### className.variation + +In the same vein, it is possible to have a variation-specific class added to a block (if the latter supports variations). E.g. if a block named `your/block-name` has a variation called `your-variation`, the following will add the class `.wp-block-your-block-name__your-variation`: + +```js +supports: { + // Add block variation-specific className. + className: { + variation: true + } +} +``` + ## color - Type: `Object` diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 637cecadf1402b..b1a2372577366b 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -270,7 +270,7 @@ To avoid this validation error, use `render_block` server-side to modify existin ### `blocks.getBlockDefaultClassName` -Generated HTML classes for blocks follow the `wp-block-{name}` nomenclature. This filter allows to provide an alternative class name. +[Generated HTML classes for blocks](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#classname) follow the `wp-block-{name}` nomenclature. This filter allows to provide an alternative class name. ```js // Our filter function. @@ -286,6 +286,8 @@ wp.hooks.addFilter( ); ``` +If a block has opted into [block support for generated block _variation_ specific class names](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/#classname), the filter also affects those. With the above example, if the `core/code` block has a `php` variation, it would get `my-plugin-code-php` as its variation specific class name (instead of the default `wp-block-code-php`). + ### `blocks.switchToBlockType.transformedBlock` Used to filter an individual transform result from block transformation. All of the original blocks are passed since transformations are many-to-many, not one-to-one. diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f829..c04ee34b8aee37 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -9,7 +9,7 @@ import clsx from 'clsx'; import { withFilters } from '@wordpress/components'; import { getBlockDefaultClassName, - hasBlockSupport, + getBlockVariationClassName, getBlockType, } from '@wordpress/blocks'; import { useContext, useMemo } from '@wordpress/element'; @@ -18,6 +18,10 @@ import { useContext, useMemo } from '@wordpress/element'; * Internal dependencies */ import BlockContext from '../block-context'; +import { + hasBlockClassNameSupport, + hasVariationClassNameSupport, +} from '../../hooks/supports'; /** * Default value used for blocks which do not define their own context needs, @@ -71,12 +75,23 @@ const EditWithGeneratedProps = ( props ) => { return ; } - // Generate a class name for the block's editable form. - const generatedClassName = hasBlockSupport( blockType, 'className', true ) - ? getBlockDefaultClassName( name ) - : null; + const generatedClassNames = []; + + if ( hasBlockClassNameSupport( blockType ) ) { + generatedClassNames.push( getBlockDefaultClassName( name ) ); + } + if ( hasVariationClassNameSupport( blockType ) ) { + const variationClassName = getBlockVariationClassName( + blockType.name, + attributes + ); + if ( variationClassName ) { + generatedClassNames.push( variationClassName ); + } + } + const className = clsx( - generatedClassName, + generatedClassNames, attributes.className, props.className ); diff --git a/packages/block-editor/src/components/block-edit/test/edit.js b/packages/block-editor/src/components/block-edit/test/edit.js index 76afbcb852ac19..8747da0f4bf7a9 100644 --- a/packages/block-editor/src/components/block-edit/test/edit.js +++ b/packages/block-editor/src/components/block-edit/test/edit.js @@ -84,6 +84,49 @@ describe( 'Edit', () => { expect( editElement ).toHaveClass( 'my-class' ); } ); + it( 'should combine the default class name with a variation one', () => { + const edit = ( { className } ) => ( +
+ ); + + registerBlockType( 'core/test-block', { + edit, + save: noop, + category: 'text', + title: 'block title', + attributes: { + fruit: { + type: 'string', + default: 'Apples', + }, + }, + supports: { + className: { + block: true, + variation: true, + }, + }, + variations: [ + { + name: 'variation', + title: 'block variation title', + attributes: { + fruit: 'Bananas', + }, + isActive: [ 'fruit' ], + }, + ], + } ); + + render( + + ); + + const editElement = screen.getByTestId( 'foo-bar' ); + expect( editElement ).toHaveClass( 'wp-block-test-block' ); + expect( editElement ).toHaveClass( 'wp-block-test-block__variation' ); + } ); + it( 'should assign context', () => { const edit = ( { context } ) => context.value; registerBlockType( 'core/test-block', { diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index deda4e3b9d0897..75532b49517028 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -23,6 +23,7 @@ import { isUnmodifiedBlock, isReusableBlock, getBlockDefaultClassName, + getBlockVariationClassName, hasBlockSupport, store as blocksStore, } from '@wordpress/blocks'; @@ -43,6 +44,7 @@ import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; import { PrivateBlockContext } from './private-block-context'; +import { hasVariationClassNameSupport } from '../../hooks/supports'; import { unlock } from '../../lock-unlock'; @@ -592,12 +594,27 @@ function BlockListBlockProvider( props ) { hasBlockSupport: _hasBlockSupport, getActiveBlockVariation, } = select( blocksStore ); + const attributes = getBlockAttributes( clientId ); const { name: blockName, isValid } = blockWithoutAttributes; const blockType = getBlockType( blockName ); const { supportsLayout, __unstableIsPreviewMode: isPreviewMode } = getSettings(); const hasLightBlockWrapper = blockType?.apiVersion > 1; + const defaultClassNames = []; + if ( hasLightBlockWrapper ) { + defaultClassNames.push( getBlockDefaultClassName( blockName ) ); + + if ( hasVariationClassNameSupport( blockType ) ) { + const variationClassName = getBlockVariationClassName( + blockType.name, + attributes + ); + if ( variationClassName ) { + defaultClassNames.push( variationClassName ); + } + } + } const previewContext = { isPreviewMode, blockWithoutAttributes, @@ -610,9 +627,10 @@ function BlockListBlockProvider( props ) { className: hasLightBlockWrapper ? attributes.className : undefined, - defaultClassName: hasLightBlockWrapper - ? getBlockDefaultClassName( blockName ) - : undefined, + defaultClassName: + defaultClassNames.length > 0 + ? clsx( defaultClassNames ) + : undefined, blockTitle: blockType?.title, }; @@ -625,7 +643,6 @@ function BlockListBlockProvider( props ) { const _isSelected = isBlockSelected( clientId ); const canRemove = canRemoveBlock( clientId ); const canMove = canMoveBlock( clientId ); - const match = getActiveBlockVariation( blockName, attributes ); const isMultiSelected = isBlockMultiSelected( clientId ); const checkDeep = true; const isAncestorOfSelectedBlock = hasSelectedInnerBlock( @@ -635,6 +652,8 @@ function BlockListBlockProvider( props ) { const movingClientId = hasBlockMovingClientId(); const blockEditingMode = getBlockEditingMode( clientId ); + const match = getActiveBlockVariation( blockName, attributes ); + const multiple = hasBlockSupport( blockName, 'multiple', true ); // For block types with `multiple` support, there is no "original diff --git a/packages/block-editor/src/hooks/generated-class-name.js b/packages/block-editor/src/hooks/generated-class-name.js index 0f4e5f43576fa9..71d8bd9f21359a 100644 --- a/packages/block-editor/src/hooks/generated-class-name.js +++ b/packages/block-editor/src/hooks/generated-class-name.js @@ -2,7 +2,18 @@ * WordPress dependencies */ import { addFilter } from '@wordpress/hooks'; -import { hasBlockSupport, getBlockDefaultClassName } from '@wordpress/blocks'; +import { + getBlockDefaultClassName, + getBlockVariationClassName, +} from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { + hasBlockClassNameSupport, + hasVariationClassNameSupport, +} from '../hooks/supports'; /** * Override props assigned to save component to inject generated className if @@ -11,30 +22,46 @@ import { hasBlockSupport, getBlockDefaultClassName } from '@wordpress/blocks'; * * @param {Object} extraProps Additional props applied to save element. * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. * * @return {Object} Filtered props applied to save element. */ -export function addGeneratedClassName( extraProps, blockType ) { - // Adding the generated className. - if ( hasBlockSupport( blockType, 'className', true ) ) { - if ( typeof extraProps.className === 'string' ) { - // We have some extra classes and want to add the default classname - // We use uniq to prevent duplicate classnames. - - extraProps.className = [ - ...new Set( [ - getBlockDefaultClassName( blockType.name ), - ...extraProps.className.split( ' ' ), - ] ), - ] - .join( ' ' ) - .trim(); - } else { - // There is no string in the className variable, - // so we just dump the default name in there. - extraProps.className = getBlockDefaultClassName( blockType.name ); +export function addGeneratedClassName( extraProps, blockType, attributes ) { + const generatedClassNames = []; + if ( hasBlockClassNameSupport( blockType ) ) { + generatedClassNames.push( getBlockDefaultClassName( blockType.name ) ); + } + if ( hasVariationClassNameSupport( blockType ) ) { + const variationClassName = getBlockVariationClassName( + blockType.name, + attributes + ); + if ( variationClassName ) { + generatedClassNames.push( variationClassName ); } } + + if ( generatedClassNames.length === 0 ) { + return extraProps; + } + + if ( typeof extraProps.className === 'string' ) { + // We have some extra classes and want to add the default classname + // We use a Set to prevent duplicate classnames. + extraProps.className = [ + ...new Set( [ + ...generatedClassNames, + ...extraProps.className.split( ' ' ), + ] ), + ] + .join( ' ' ) + .trim(); + } else { + // There is no string in the className variable, + // so we just dump the default name(s) in there. + extraProps.className = generatedClassNames.join( ' ' ); + } + return extraProps; } diff --git a/packages/block-editor/src/hooks/supports.js b/packages/block-editor/src/hooks/supports.js index 75f2bdf2dc219e..923629f9ffda5d 100644 --- a/packages/block-editor/src/hooks/supports.js +++ b/packages/block-editor/src/hooks/supports.js @@ -339,3 +339,33 @@ export const getLayoutSupport = ( nameOrType ) => */ export const hasStyleSupport = ( nameOrType ) => styleSupportKeys.some( ( key ) => hasBlockSupport( nameOrType, key ) ); + +/** + * Returns true if the block defines support for block class name. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasBlockClassNameSupport = ( nameOrType ) => { + const classNameSupport = getBlockSupport( nameOrType, 'className', true ); + + if ( typeof classNameSupport === 'boolean' ) { + return classNameSupport; + } + + // classNameSupport can be an object. If it doesn't have a `block` key, + // we default to true. + return ( + ! Object.hasOwn( classNameSupport, 'block' ) || + classNameSupport.block === true + ); +}; + +/** + * Returns true if the block defines support for variation class name. + * + * @param {string|Object} nameOrType Block name or type object. + * @return {boolean} Whether the block supports the feature. + */ +export const hasVariationClassNameSupport = ( nameOrType ) => + hasBlockSupport( nameOrType, 'className.variation', false ); diff --git a/packages/block-editor/src/hooks/test/generated-class-name.js b/packages/block-editor/src/hooks/test/generated-class-name.js index a37ceb74c87bdc..3b17d0abc42bd9 100644 --- a/packages/block-editor/src/hooks/test/generated-class-name.js +++ b/packages/block-editor/src/hooks/test/generated-class-name.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { applyFilters } from '@wordpress/hooks'; +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -12,12 +13,47 @@ const noop = () => {}; describe( 'generated className', () => { const blockSettings = { - name: 'chicken/ribs', + name: 'produce/fruit', save: noop, category: 'text', title: 'block title', + attributes: { + fruit: { + type: 'string', + default: 'apple', + }, + }, }; + const variations = [ + { + name: 'apple', + attributes: { + fruit: 'apple', + }, + isActive: [ 'fruit' ], + isDefault: true, + }, + { + name: 'banana', + attributes: { + fruit: 'banana', + }, + isActive: [ 'fruit' ], + }, + ]; + + beforeAll( () => { + registerBlockType( 'produce/fruit', { + ...blockSettings, + variations, + } ); + } ); + + afterAll( () => { + unregisterBlockType( 'produce/fruit' ); + } ); + describe( 'addSaveProps', () => { const addSaveProps = applyFilters.bind( null, @@ -40,6 +76,24 @@ describe( 'generated className', () => { expect( extraProps ).not.toHaveProperty( 'className' ); } ); + it( 'should do nothing if the block settings opt out of generated className support via the block property', () => { + const attributes = { className: 'foo' }; + const extraProps = addSaveProps( + {}, + { + ...blockSettings, + supports: { + className: { + block: false, + }, + }, + }, + attributes + ); + + expect( extraProps ).not.toHaveProperty( 'className' ); + } ); + it( 'should inject the generated className', () => { const attributes = { className: 'bar' }; const extraProps = addSaveProps( @@ -48,18 +102,78 @@ describe( 'generated className', () => { attributes ); - expect( extraProps.className ).toBe( 'wp-block-chicken-ribs foo' ); + expect( extraProps.className ).toBe( 'wp-block-produce-fruit foo' ); } ); it( 'should not inject duplicates into className', () => { const attributes = { className: 'bar' }; const extraProps = addSaveProps( - { className: 'foo wp-block-chicken-ribs' }, + { className: 'foo wp-block-produce-fruit' }, blockSettings, attributes ); - expect( extraProps.className ).toBe( 'wp-block-chicken-ribs foo' ); + expect( extraProps.className ).toBe( 'wp-block-produce-fruit foo' ); + } ); + + it( "should not inject the generated variation className if support isn't enabled", () => { + const attributes = { className: 'foo', fruit: 'banana' }; + const extraProps = addSaveProps( + {}, + { + ...blockSettings, + variations, + supports: { + className: false, + }, + }, + attributes + ); + + expect( extraProps ).not.toHaveProperty( 'className' ); + } ); + + it( 'should inject generated classNames for both block and variation', () => { + const attributes = { className: 'bar', fruit: 'banana' }; + const extraProps = addSaveProps( + { className: 'foo' }, + { + ...blockSettings, + variations, + supports: { + className: { + variation: true, + }, + }, + }, + attributes + ); + + expect( extraProps.className ).toBe( + 'wp-block-produce-fruit wp-block-produce-fruit__banana foo' + ); + } ); + + it( 'should only inject the generated variation className if the block property is explicitly set to false', () => { + const attributes = { className: 'bar', fruit: 'banana' }; + const extraProps = addSaveProps( + { className: 'foo' }, + { + ...blockSettings, + variations, + supports: { + className: { + block: false, + variation: true, + }, + }, + }, + attributes + ); + + expect( extraProps.className ).toBe( + 'wp-block-produce-fruit__banana foo' + ); } ); } ); } ); diff --git a/packages/block-editor/src/hooks/test/supports.js b/packages/block-editor/src/hooks/test/supports.js new file mode 100644 index 00000000000000..8e4295cdee8f60 --- /dev/null +++ b/packages/block-editor/src/hooks/test/supports.js @@ -0,0 +1,97 @@ +/** + * Internal dependencies + */ +import { + hasBlockClassNameSupport, + hasVariationClassNameSupport, +} from '../supports'; + +describe( 'hasBlockClassNameSupport', () => { + const blockName = 'block/name'; + + it( 'should default to true', () => { + const block = { + name: blockName, + }; + expect( hasBlockClassNameSupport( block ) ).toBe( true ); + } ); + + it( 'should return false if the block does not support className', () => { + const block = { + name: blockName, + supports: { + className: false, + }, + }; + expect( hasBlockClassNameSupport( block ) ).toBe( false ); + } ); + + it( 'should reflect the nested supports property if true', () => { + const block = { + name: blockName, + supports: { + className: { + block: true, + }, + }, + }; + expect( hasBlockClassNameSupport( block ) ).toBe( true ); + } ); + + it( 'should reflect the nested supports property if false', () => { + const block = { + name: blockName, + supports: { + className: { + block: false, + }, + }, + }; + expect( hasBlockClassNameSupport( block ) ).toBe( false ); + } ); +} ); + +describe( 'hasVariationClassNameSupport', () => { + const blockName = 'block/name'; + + it( 'should default to false', () => { + const block = { + name: blockName, + }; + expect( hasVariationClassNameSupport( block ) ).toBe( false ); + } ); + + it( 'should return false if the block does not explicitly support variation class names', () => { + const block = { + name: blockName, + supports: { + className: true, + }, + }; + expect( hasVariationClassNameSupport( block ) ).toBe( false ); + } ); + + it( 'should reflect the nested supports property if true', () => { + const block = { + name: blockName, + supports: { + className: { + variation: true, + }, + }, + }; + expect( hasVariationClassNameSupport( block ) ).toBe( true ); + } ); + + it( 'should reflect the nested supports property if false', () => { + const block = { + name: blockName, + supports: { + className: { + variation: false, + }, + }, + }; + expect( hasVariationClassNameSupport( block ) ).toBe( false ); + } ); +} ); diff --git a/packages/blocks/README.md b/packages/blocks/README.md index d724f986b0ca81..8659f869e3a547 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -118,6 +118,8 @@ _Returns_ Returns the block's default classname from its name. +The return value can be filtered using the `blocks.getBlockDefaultClassName` filter. + _Parameters_ - _blockName_ `string`: The block name. @@ -198,6 +200,23 @@ _Returns_ - `Array`: Block settings. +### getBlockVariationClassName + +Returns a block variation specific classname. + +If the given block matches a variation, the classname will be the block's default classname with the variation name appended (separated by a double underscore, i.e. `__`). + +Note that the block's default classname is affected by the `blocks.getBlockDefaultClassName` filter. + +_Parameters_ + +- _blockName_ `string`: The block name. +- _attributes_ `Object`: Block attributes. + +_Returns_ + +- `string|null`: The block variation classname, or null if the block doesn't match any variation. + ### getChildBlockNames Returns an array with the child blocks of a given block. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 803467cb2187e2..0b440a5a2ca30e 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -70,6 +70,7 @@ export { default as serialize, getBlockInnerHTML as getBlockContent, getBlockDefaultClassName, + getBlockVariationClassName, getBlockMenuDefaultClassName, getSaveElement, getSaveContent, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index b0f5ae350759f0..973a342d8c0789 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -693,6 +693,26 @@ export const getBlockVariations = ( blockName, scope ) => { return select( blocksStore ).getBlockVariations( blockName, scope ); }; +/** + * Returns the active block variation for a given block based on its attributes. + * Ignored from documentation as the recommended usage is via useSelect from @wordpress/data. + * + * @ignore + * + * @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 const getActiveBlockVariation = ( blockName, attributes, scope ) => { + return select( blocksStore ).getActiveBlockVariation( + blockName, + attributes, + scope + ); +}; + /** * Registers a new block variation for the given block type. * diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index 2e7246ce9584a9..526b91a4cfa37b 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -15,6 +15,7 @@ import { removep } from '@wordpress/autop'; * Internal dependencies */ import { + getActiveBlockVariation, getBlockType, getFreeformContentHandlerName, getUnregisteredTypeHandlerName, @@ -33,6 +34,8 @@ import { isUnmodifiedDefaultBlock, normalizeBlockType } from './utils'; /** * Returns the block's default classname from its name. * + * The return value can be filtered using the `blocks.getBlockDefaultClassName` filter. + * * @param {string} blockName The block name. * * @return {string} The block's default class. @@ -50,6 +53,28 @@ export function getBlockDefaultClassName( blockName ) { ); } +/** + * Returns a block variation specific classname. + * + * If the given block matches a variation, the classname will be the block's default classname + * with the variation name appended (separated by a double underscore, i.e. `__`). + * + * Note that the block's default classname is affected by the `blocks.getBlockDefaultClassName` filter. + * + * @param {string} blockName The block name. + * @param {Object} attributes Block attributes. + * + * @return {string|null} The block variation classname, or null if the block doesn't match any variation. + */ +export function getBlockVariationClassName( blockName, attributes ) { + const activeVariation = getActiveBlockVariation( blockName, attributes ); + if ( ! activeVariation ) { + return null; + } + + return getBlockDefaultClassName( blockName ) + '__' + activeVariation.name; +} + /** * Returns the block's default menu item classname from its name. * diff --git a/packages/blocks/src/api/test/serializer.js b/packages/blocks/src/api/test/serializer.js index 7fed23041daaa6..14480b78c33d7b 100644 --- a/packages/blocks/src/api/test/serializer.js +++ b/packages/blocks/src/api/test/serializer.js @@ -13,6 +13,8 @@ import serialize, { getCommentDelimitedContent, serializeBlock, getBlockInnerHTML, + getBlockDefaultClassName, + getBlockVariationClassName, } from '../serializer'; import { getBlockTypes, @@ -472,4 +474,74 @@ describe( 'block serializer', () => { expect( getBlockInnerHTML( block ) ).toBe( 'chicken' ); } ); } ); + + describe( 'getBlockDefaultClassName', () => { + it( 'should return the default class name for a block without the core namespace', () => { + expect( getBlockDefaultClassName( 'core/test-block' ) ).toBe( + 'wp-block-test-block' + ); + } ); + + it( 'should return the default class name for a block', () => { + expect( getBlockDefaultClassName( 'plugin/test-block' ) ).toBe( + 'wp-block-plugin-test-block' + ); + } ); + } ); + + describe( 'getBlockVariationClassName', () => { + const blockSettings = { + title: 'Fruit', + attributes: { + fruit: { + type: 'string', + default: 'apple', + }, + }, + }; + const variations = [ + { + name: 'apple', + attributes: { + fruit: 'apple', + }, + isActive: [ 'fruit' ], + isDefault: true, + }, + { + name: 'banana', + attributes: { + fruit: 'banana', + }, + isActive: [ 'fruit' ], + }, + ]; + + it( 'should return null if the block does not have any variations', () => { + registerBlockType( 'core/fruit', blockSettings ); + expect( + getBlockVariationClassName( 'core/fruit', { + fruit: 'orange', + } ) + ).toBeNull(); + } ); + + it( 'should return null if the given attributes do not match any variation', () => { + registerBlockType( 'core/fruit', { ...blockSettings, variations } ); + expect( + getBlockVariationClassName( 'core/fruit', { + fruit: 'orange', + } ) + ).toBeNull(); + } ); + + it( 'should return the correct variation class name for a block', () => { + registerBlockType( 'core/fruit', { ...blockSettings, variations } ); + expect( + getBlockVariationClassName( 'core/fruit', { + fruit: 'banana', + } ) + ).toBe( 'wp-block-fruit__banana' ); + } ); + } ); } ); diff --git a/schemas/json/block.json b/schemas/json/block.json index 8e314a45ae1cff..49f57627f27e01 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -256,7 +256,22 @@ }, "className": { "description": "By default, the class .wp-block-your-block-name is added to the root element of your saved markup. This helps having a consistent mechanism for styling blocks that themes and plugins can rely on. If, for whatever reason, a class is not desired on the markup, this functionality can be disabled.", - "type": "boolean", + "oneOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { + "block": { + "description": "Add the .wp-block-your-block-name class to the block's wrapper element", + "type": "boolean" + }, + "variation": { + "description": "Add the .wp-block-your-block-name__your-block-variation class to the block's wrapper element", + "type": "boolean" + } + } + } + ], "default": true }, "color": {