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": {