Skip to content
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: Add block-supports flag to add variation specific classname #61864

Open
wants to merge 41 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6fe71ec
Block Variations: Add block-support for variation-specific classnames
ockham Jul 4, 2024
d42c4d1
Add new helpers
ockham Jul 4, 2024
1081fe4
Use new helpers
ockham Jul 8, 2024
6cd7199
Change attribute from class to className
ockham Jul 8, 2024
608bfd6
Fix variation attributes in test
ockham Jul 8, 2024
d9036b6
Fix variation attributes in test
ockham Jul 8, 2024
4de031b
Fix variation attributes in test
ockham Jul 8, 2024
b1bd36c
Remove redundant assertion
ockham Jul 8, 2024
3cb6023
Undo whitespace change
ockham Jul 8, 2024
e384ec7
Fix test
ockham Jul 8, 2024
5dce388
Fix other test
ockham Jul 8, 2024
e99838e
Fix more variation attributes in test
ockham Jul 8, 2024
cb9430a
Remove greedy flag from Regex
ockham Jul 8, 2024
b47cc1f
Update comment
ockham Jul 8, 2024
2bde392
Change test to make it more easily extensible
ockham Jul 8, 2024
1568b02
Absorb variation logic into addGeneratedClassName
ockham Jul 8, 2024
818af29
Reuse addGeneratedClassName
ockham Jul 8, 2024
b34a14e
Use shorthand
ockham Jul 8, 2024
8515de7
Simplify test
ockham Jul 8, 2024
2df5db7
More verbose schema descriptions
ockham Jul 8, 2024
d625240
Revert "Reuse addGeneratedClassName"
ockham Jul 8, 2024
faac58c
Replace slash with hyphen
ockham Jul 8, 2024
c8b9a6a
Add getBlockVariationClassName selector
ockham Jul 10, 2024
2b215d7
Use new selector
ockham Jul 10, 2024
0af2def
Update docs
ockham Jul 10, 2024
962a5ae
Missing comma
ockham Jul 10, 2024
173bef2
Add docs
ockham Jul 10, 2024
5cccab7
Change default to true
ockham Jul 10, 2024
cff8fcc
Fix variation default value in docs
ockham Jul 10, 2024
50a0044
typeof classNameSupport === 'boolean'
ockham Jul 23, 2024
0d34163
Backticks
ockham Jul 23, 2024
aaaa855
Bail early
ockham Jul 23, 2024
e819f54
Remove unnecessary export
ockham Jul 23, 2024
7cf6f8e
Add note that the getBlockDefaultClassName filter also affects variat…
ockham Jul 23, 2024
6ad4153
Remove unnecessary check for blockName
ockham Jul 23, 2024
bc3fb00
Add more notes on block default classname filter
ockham Jul 23, 2024
a37f5d3
Move description before type in schema
ockham Jul 23, 2024
25f7a71
toEqual -> toBe
ockham Sep 11, 2024
6f016b3
toEqual -> toBe
ockham Sep 11, 2024
d369a97
Use __ instead of - to append variation name
ockham Sep 12, 2024
7350ced
Missed one test
ockham Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion docs/reference-guides/block-api/block-supports.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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`
Expand Down
4 changes: 3 additions & 1 deletion docs/reference-guides/filters/block-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
27 changes: 21 additions & 6 deletions packages/block-editor/src/components/block-edit/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -71,12 +75,23 @@ const EditWithGeneratedProps = ( props ) => {
return <EditWithFilters { ...props } context={ context } />;
}

// Generate a class name for the block's editable form.
const generatedClassName = hasBlockSupport( blockType, 'className', true )
? getBlockDefaultClassName( name )
: null;
const generatedClassNames = [];
ockham marked this conversation as resolved.
Show resolved Hide resolved

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
);
Expand Down
43 changes: 43 additions & 0 deletions packages/block-editor/src/components/block-edit/test/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } ) => (
<div data-testid="foo-bar" className={ 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(
<Edit name="core/test-block" attributes={ { fruit: 'Bananas' } } />
);

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', {
Expand Down
27 changes: 23 additions & 4 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
isUnmodifiedBlock,
isReusableBlock,
getBlockDefaultClassName,
getBlockVariationClassName,
hasBlockSupport,
store as blocksStore,
} from '@wordpress/blocks';
Expand All @@ -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';

Expand Down Expand Up @@ -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,
Expand All @@ -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,
};

Expand All @@ -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(
Expand All @@ -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
Expand Down
67 changes: 47 additions & 20 deletions packages/block-editor/src/hooks/generated-class-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' ) {
ockham marked this conversation as resolved.
Show resolved Hide resolved
// 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;
}

Expand Down
30 changes: 30 additions & 0 deletions packages/block-editor/src/hooks/supports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be equivalent with hasBlockSupport( nameOrType, 'className.block', true ); ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, yeah. Are you suggesting we replace it with that? 🙂

(Personally, I find it a bit easier to read if we keep it as-is: We've fetched the value of className block-support and have determined that it's not a boolean, so now we check if it has a block property and return its value if it does; otherwise, we default to true. It also removes the need for separately calling the hasBlockSupport selector, since we already have fetched classNameSupport.)

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 ) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we default to true for this feature? Have you discussed this? This would also affect the default handling when we have supports.className: true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally hoping to do that (and discussed it with @gziolo again today), but I've come to the conclusion that it's too much of a risk. As you mentioned elsewhere, setting this to true will change the serialized markup for existing blocks, so we'll need to add migrations for them.

Having to add those for any block with variations that wants to use this new feature seemed like a bit of a hassle at first, but I now think it's an okay tradeoff. It's probably worse for plugins (and authors) that include a large number of blocks -- and for Core itself -- but even then, not all blocks come with variations, and it's a one time change. IMO, it's reasonable to ask extenders to enable this feature manually (and to add a deprecation) in exchange for a useful enhancement.

Since we might also need a server-side part (which will require a robust way of determining the active variation), it makes even more sense to require extenders to opt into this new feature explicitly, as it gives them control over adding this new class name, and allows them to fix their isActive fields before they do so.

hasBlockSupport( nameOrType, 'className.variation', false );
Loading
Loading