diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md
index 1dc03779d4511..4656097189065 100644
--- a/docs/designers-developers/developers/block-api/block-registration.md
+++ b/docs/designers-developers/developers/block-api/block-registration.md
@@ -264,7 +264,10 @@ An object describing a variation defined for the block type can contain the foll
- `attributes` (optional, type `Object`) – Values that override block attributes.
- `innerBlocks` (optional, type `Array[]`) – Initial configuration of nested blocks.
- `example` (optional, type `Object`) – Example provides structured data for the block preview. You can set to `undefined` to disable the preview shown for the block type.
-- `scope` (optional, type `string[]`) - the list of scopes where the variation is applicable. When not provided, it assumes all available scopes. Available options: `block`, `inserter`.
+- `scope` (optional, type `WPBlockVariationScope[]`) - the list of scopes where the variation is applicable. When not provided, it defaults to `block` and `inserter`. Available options:
+ - `inserter` - Block Variation is shown on the inserter.
+ - `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.
It's also possible to override the default block style variation using the `className` attribute when defining block variations.
diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js
index aaff1af27e16d..c209ea39b5c73 100644
--- a/packages/block-editor/src/components/block-card/index.js
+++ b/packages/block-editor/src/components/block-card/index.js
@@ -3,16 +3,14 @@
*/
import BlockIcon from '../block-icon';
-function BlockCard( { blockType } ) {
+function BlockCard( { blockType: { icon, title, description } } ) {
return (
-
+
-
- { blockType.title }
-
+ { title }
- { blockType.description }
+ { description }
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js
index 259f4742febcd..3d68af7c08391 100644
--- a/packages/block-editor/src/components/block-inspector/index.js
+++ b/packages/block-editor/src/components/block-inspector/index.js
@@ -23,6 +23,7 @@ import InspectorAdvancedControls from '../inspector-advanced-controls';
import BlockStyles from '../block-styles';
import MultiSelectionInspector from '../multi-selection-inspector';
import DefaultStylePicker from '../default-style-picker';
+import BlockVariationTransforms from '../block-variation-transforms';
const BlockInspector = ( {
blockType,
count,
@@ -66,6 +67,7 @@ const BlockInspector = ( {
return (
+
{ hasBlockStyles && (
diff --git a/packages/block-editor/src/components/block-variation-transforms/README.md b/packages/block-editor/src/components/block-variation-transforms/README.md
new file mode 100644
index 0000000000000..494c734502979
--- /dev/null
+++ b/packages/block-editor/src/components/block-variation-transforms/README.md
@@ -0,0 +1,54 @@
+# Block Variation Transforms
+
+This component allows to display the selected block's variations which have the `transform` option set in `scope` property and to choose one of them.
+
+By selecting such a variation an update to the selected block's attributes happen, based on the variation's attributes.
+
+## Table of contents
+
+1. [Development guidelines](#development-guidelines)
+2. [Related components](#related-components)
+
+## Development guidelines
+
+### Usage
+
+Renders the block's variations which have the `transform` option set in `scope` property.
+
+```jsx
+import { useSelect } from '@wordpress/data';
+import {
+ __experimentalBlockVariationTransforms as BlockVariationTransforms,
+} from '@wordpress/block-editor';
+
+const MyBlockVariationTransforms = () => {
+ const { selectedBlockClientId } = useSelect(
+ ( select ) => {
+ const { getSelectedBlockClientId } = select(
+ 'core/block-editor'
+ );
+ return {
+ selectedBlockClientId: getSelectedBlockClientId(),
+ };
+ }
+ );
+
+ return (
+
+ );
+};
+```
+
+### Props
+
+#### blockClientId
+
+The block's client id.
+
+- Type: `string`
+
+## Related components
+
+Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [BlockEditorProvider](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree.
diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js
new file mode 100644
index 0000000000000..5d07bbe8d9a82
--- /dev/null
+++ b/packages/block-editor/src/components/block-variation-transforms/index.js
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+import { isMatch } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ DropdownMenu,
+ MenuGroup,
+ MenuItemsChoice,
+} from '@wordpress/components';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useState, useEffect } from '@wordpress/element';
+import { chevronDown } from '@wordpress/icons';
+
+export const getMatchingVariationName = ( blockAttributes, variations ) => {
+ if ( ! variations || ! blockAttributes ) return;
+ const matches = variations.filter( ( { attributes } ) => {
+ if ( ! attributes || ! Object.keys( attributes ).length ) return false;
+ return isMatch( blockAttributes, attributes );
+ } );
+ if ( matches.length !== 1 ) return;
+ return matches[ 0 ].name;
+};
+
+function __experimentalBlockVariationTransforms( { blockClientId } ) {
+ const [ selectedValue, setSelectedValue ] = useState();
+ const { updateBlockAttributes } = useDispatch( 'core/block-editor' );
+ const { variations, blockAttributes } = useSelect(
+ ( select ) => {
+ const { getBlockVariations } = select( 'core/blocks' );
+ const { getBlockName, getBlockAttributes } = select(
+ 'core/block-editor'
+ );
+ const blockName = blockClientId && getBlockName( blockClientId );
+ return {
+ variations:
+ blockName && getBlockVariations( blockName, 'transform' ),
+ blockAttributes: getBlockAttributes( blockClientId ),
+ };
+ },
+ [ blockClientId ]
+ );
+ useEffect( () => {
+ setSelectedValue(
+ getMatchingVariationName( blockAttributes, variations )
+ );
+ }, [ blockAttributes, variations ] );
+ if ( ! variations?.length ) return null;
+
+ const selectOptions = variations.map(
+ ( { name, title, description } ) => ( {
+ value: name,
+ label: title,
+ info: description,
+ } )
+ );
+ const onSelectVariation = ( variationName ) => {
+ updateBlockAttributes( blockClientId, {
+ ...variations.find( ( { name } ) => name === variationName )
+ .attributes,
+ } );
+ };
+ const baseClass = 'block-editor-block-variation-transforms';
+ return (
+
+ { () => (
+
+
+
+
+
+ ) }
+
+ );
+}
+
+export default __experimentalBlockVariationTransforms;
diff --git a/packages/block-editor/src/components/block-variation-transforms/style.scss b/packages/block-editor/src/components/block-variation-transforms/style.scss
new file mode 100644
index 0000000000000..fd929854018f1
--- /dev/null
+++ b/packages/block-editor/src/components/block-variation-transforms/style.scss
@@ -0,0 +1,38 @@
+.block-editor-block-variation-transforms {
+ padding: 0 $grid-unit-20 $grid-unit-20 56px;
+ width: 100%;
+
+ .components-dropdown-menu__toggle {
+ border: 1px solid $gray-700;
+ border-radius: $radius-block-ui;
+ min-height: 30px;
+ width: 100%;
+ position: relative;
+ text-align: left;
+ justify-content: left;
+ padding: 6px 12px;
+
+ // For all button sizes allow sufficient space for the
+ // dropdown "arrow" icon to display.
+ &.components-dropdown-menu__toggle {
+ padding-right: $icon-size;
+ }
+
+ &:focus:not(:disabled) {
+ border-color: var(--wp-admin-theme-color);
+ box-shadow: 0 0 0 ($border-width-focus - $border-width) var(--wp-admin-theme-color);
+ }
+
+ svg {
+ height: 100%;
+ padding: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+}
+
+.block-editor-block-variation-transforms__popover .components-popover__content {
+ min-width: 230px;
+}
diff --git a/packages/block-editor/src/components/block-variation-transforms/test/index.js b/packages/block-editor/src/components/block-variation-transforms/test/index.js
new file mode 100644
index 0000000000000..31b75099919ca
--- /dev/null
+++ b/packages/block-editor/src/components/block-variation-transforms/test/index.js
@@ -0,0 +1,75 @@
+/**
+ * Internal dependencies
+ */
+import { getMatchingVariationName } from '../index';
+
+describe( 'BlockVariationTransforms', () => {
+ describe( 'getMatchingVariationName', () => {
+ describe( 'should not find a match', () => {
+ it( 'when no variations or attributes passed', () => {
+ expect(
+ getMatchingVariationName( null, { content: 'hi' } )
+ ).toBeUndefined();
+ expect( getMatchingVariationName( {} ) ).toBeUndefined();
+ } );
+ it( 'when no variation matched', () => {
+ const variations = [
+ { name: 'one', attributes: { level: 1 } },
+ { name: 'two', attributes: { level: 2 } },
+ ];
+ expect(
+ getMatchingVariationName( { level: 4 }, variations )
+ ).toBeUndefined();
+ } );
+ it( 'when more than one match found', () => {
+ const variations = [
+ { name: 'one', attributes: { level: 1 } },
+ { name: 'two', attributes: { level: 1, content: 'hi' } },
+ ];
+ expect(
+ getMatchingVariationName(
+ { level: 1, content: 'hi', other: 'prop' },
+ variations
+ )
+ ).toBeUndefined();
+ } );
+ it( 'when variation is a superset of attributes', () => {
+ const variations = [
+ { name: 'one', attributes: { level: 1, content: 'hi' } },
+ ];
+ expect(
+ getMatchingVariationName(
+ { level: 1, other: 'prop' },
+ variations
+ )
+ ).toBeUndefined();
+ } );
+ } );
+ describe( 'should find a match', () => {
+ it( 'when variation has one attribute', () => {
+ const variations = [
+ { name: 'one', attributes: { level: 1 } },
+ { name: 'two', attributes: { level: 2 } },
+ ];
+ expect(
+ getMatchingVariationName(
+ { level: 2, content: 'hi', other: 'prop' },
+ variations
+ )
+ ).toEqual( 'two' );
+ } );
+ it( 'when variation has many attributes', () => {
+ const variations = [
+ { name: 'one', attributes: { level: 1, content: 'hi' } },
+ { name: 'two', attributes: { level: 2 } },
+ ];
+ expect(
+ getMatchingVariationName(
+ { level: 1, content: 'hi', other: 'prop' },
+ variations
+ )
+ ).toEqual( 'one' );
+ } );
+ } );
+ } );
+} );
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index c6506c5c58f7a..939cf068e0cc7 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -21,6 +21,7 @@ export { BlockNavigationBlockFill as __experimentalBlockNavigationBlockFill } fr
export { default as __experimentalBlockNavigationEditor } from './block-navigation/editor';
export { default as __experimentalBlockNavigationTree } from './block-navigation/tree';
export { default as __experimentalBlockVariationPicker } from './block-variation-picker';
+export { default as __experimentalBlockVariationTransforms } from './block-variation-transforms';
export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar';
export { default as ButtonBlockerAppender } from './button-block-appender';
export { default as ColorPalette } from './color-palette';
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index bc117ec4307bb..5421186cbdff5 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -26,6 +26,7 @@
@import "./components/block-switcher/style.scss";
@import "./components/block-types-list/style.scss";
@import "./components/block-variation-picker/style.scss";
+@import "./components/block-variation-transforms/style.scss";
@import "./components/button-block-appender/style.scss";
@import "./components/colors-gradients/style.scss";
@import "./components/contrast-checker/style.scss";
diff --git a/packages/block-library/src/navigation/index.js b/packages/block-library/src/navigation/index.js
index 3139a88c908ff..322d7f5cd017e 100644
--- a/packages/block-library/src/navigation/index.js
+++ b/packages/block-library/src/navigation/index.js
@@ -11,6 +11,7 @@ import metadata from './block.json';
import edit from './edit';
import save from './save';
import deprecated from './deprecated';
+import variations from './variations';
const { name } = metadata;
@@ -18,31 +19,12 @@ export { metadata, name };
export const settings = {
title: __( 'Navigation' ),
-
icon,
-
description: __(
'A collection of blocks that allow visitors to get around your site.'
),
-
keywords: [ __( 'menu' ), __( 'navigation' ), __( 'links' ) ],
-
- variations: [
- {
- name: 'horizontal',
- isDefault: true,
- title: __( 'Navigation (horizontal)' ),
- description: __( 'Links shown in a row.' ),
- attributes: { orientation: 'horizontal' },
- },
- {
- name: 'vertical',
- title: __( 'Navigation (vertical)' ),
- description: __( 'Links shown in a column.' ),
- attributes: { orientation: 'vertical' },
- },
- ],
-
+ variations,
example: {
innerBlocks: [
{
@@ -71,10 +53,7 @@ export const settings = {
},
],
},
-
edit,
-
save,
-
deprecated,
};
diff --git a/packages/block-library/src/navigation/variations.js b/packages/block-library/src/navigation/variations.js
new file mode 100644
index 0000000000000..307fa8c0c1204
--- /dev/null
+++ b/packages/block-library/src/navigation/variations.js
@@ -0,0 +1,24 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+const variations = [
+ {
+ name: 'horizontal',
+ isDefault: true,
+ title: __( 'Navigation (horizontal)' ),
+ description: __( 'Links shown in a row.' ),
+ attributes: { orientation: 'horizontal' },
+ scope: [ 'inserter', 'transform' ],
+ },
+ {
+ name: 'vertical',
+ title: __( 'Navigation (vertical)' ),
+ description: __( 'Links shown in a column.' ),
+ attributes: { orientation: 'vertical' },
+ scope: [ 'inserter', 'transform' ],
+ },
+];
+
+export default variations;
diff --git a/packages/block-library/src/post-hierarchical-terms/variations.js b/packages/block-library/src/post-hierarchical-terms/variations.js
index 8ff6dcc07f910..48ef92afc1097 100644
--- a/packages/block-library/src/post-hierarchical-terms/variations.js
+++ b/packages/block-library/src/post-hierarchical-terms/variations.js
@@ -8,7 +8,7 @@ const variations = [
name: 'category',
title: __( 'Post Categories' ),
icon: 'category',
- is_default: true,
+ isDefault: true,
attributes: { term: 'category' },
},
];
diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js
index 9052c76faf694..db9f1a3b7fd61 100644
--- a/packages/blocks/src/api/registration.js
+++ b/packages/blocks/src/api/registration.js
@@ -70,7 +70,7 @@ import { DEPRECATED_ENTRY_KEYS } from './constants';
/**
* Named block variation scopes.
*
- * @typedef {'block'|'inserter'} WPBlockVariationScope
+ * @typedef {'block'|'inserter'|'transform'} WPBlockVariationScope
*/
/**
diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js
index 21e88311b522f..5513ff4e4c488 100644
--- a/packages/blocks/src/store/selectors.js
+++ b/packages/blocks/src/store/selectors.js
@@ -90,7 +90,8 @@ export function getBlockVariations( state, blockName, scope ) {
return variations;
}
return variations.filter( ( variation ) => {
- return ! variation.scope || variation.scope.includes( scope );
+ // For backward compatibility reasons, variation's scope defaults to `block` and `inserter` when not set.
+ return ( variation.scope || [ 'block', 'inserter' ] ).includes( scope );
} );
}
diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js
index fde5d92253622..328293a898fcd 100644
--- a/packages/blocks/src/store/test/selectors.js
+++ b/packages/blocks/src/store/test/selectors.js
@@ -11,6 +11,7 @@ import deepFreeze from 'deep-freeze';
import {
getBlockSupport,
getChildBlockNames,
+ getBlockVariations,
getDefaultBlockVariation,
getGroupingBlockName,
isMatchingSearchTerm,
@@ -220,7 +221,7 @@ describe( 'selectors', () => {
} );
} );
- describe( 'getDefaultBlockVariation', () => {
+ describe( 'Testing block variations selectors', () => {
const blockName = 'block/name';
const createBlockVariationsState = ( variations ) => {
return deepFreeze( {
@@ -238,55 +239,94 @@ describe( 'selectors', () => {
const thirdBlockVariation = {
name: 'third-block-variation',
};
+ describe( 'getBlockVariations', () => {
+ it( 'should return undefined if no variations exists', () => {
+ expect(
+ getBlockVariations( { blockVariations: {} }, blockName )
+ ).toBeUndefined();
+ } );
+ it( 'should return all variations if scope is not provided', () => {
+ const variations = [
+ firstBlockVariation,
+ secondBlockVariation,
+ ];
+ const state = createBlockVariationsState( variations );
+ expect( getBlockVariations( state, blockName ) ).toEqual(
+ variations
+ );
+ } );
+ it( 'should return variations with scope not set at all or explicitly set', () => {
+ const variations = [
+ { ...firstBlockVariation, scope: [ 'inserter' ] },
+ { name: 'only-block', scope: [ 'block' ] },
+ {
+ name: 'multiple-scopes-with-block',
+ scope: [ 'transform', 'block' ],
+ },
+ { name: 'no-scope' },
+ ];
+ const state = createBlockVariationsState( variations );
+ const result = getBlockVariations( state, blockName, 'block' );
+ expect( result ).toHaveLength( 3 );
+ expect( result.map( ( { name } ) => name ) ).toEqual(
+ expect.arrayContaining( [
+ 'only-block',
+ 'multiple-scopes-with-block',
+ 'no-scope',
+ ] )
+ );
+ } );
+ } );
+ describe( 'getDefaultBlockVariation', () => {
+ it( 'should return the default variation when set', () => {
+ const defaultBlockVariation = {
+ ...secondBlockVariation,
+ isDefault: true,
+ };
+ const state = createBlockVariationsState( [
+ firstBlockVariation,
+ defaultBlockVariation,
+ thirdBlockVariation,
+ ] );
- it( 'should return the default variation when set', () => {
- const defaultBlockVariation = {
- ...secondBlockVariation,
- isDefault: true,
- };
- const state = createBlockVariationsState( [
- firstBlockVariation,
- defaultBlockVariation,
- thirdBlockVariation,
- ] );
-
- const result = getDefaultBlockVariation( state, blockName );
+ const result = getDefaultBlockVariation( state, blockName );
- expect( result ).toEqual( defaultBlockVariation );
- } );
+ expect( result ).toEqual( defaultBlockVariation );
+ } );
- it( 'should return the last variation when multiple default variations added', () => {
- const defaultBlockVariation = {
- ...thirdBlockVariation,
- isDefault: true,
- };
- const state = createBlockVariationsState( [
- {
- ...firstBlockVariation,
- isDefault: true,
- },
- {
- ...secondBlockVariation,
+ it( 'should return the last variation when multiple default variations added', () => {
+ const defaultBlockVariation = {
+ ...thirdBlockVariation,
isDefault: true,
- },
- defaultBlockVariation,
- ] );
+ };
+ const state = createBlockVariationsState( [
+ {
+ ...firstBlockVariation,
+ isDefault: true,
+ },
+ {
+ ...secondBlockVariation,
+ isDefault: true,
+ },
+ defaultBlockVariation,
+ ] );
- const result = getDefaultBlockVariation( state, blockName );
+ const result = getDefaultBlockVariation( state, blockName );
- expect( result ).toEqual( defaultBlockVariation );
- } );
+ expect( result ).toEqual( defaultBlockVariation );
+ } );
- it( 'should return the first variation when no default variation set', () => {
- const state = createBlockVariationsState( [
- firstBlockVariation,
- secondBlockVariation,
- thirdBlockVariation,
- ] );
+ it( 'should return the first variation when no default variation set', () => {
+ const state = createBlockVariationsState( [
+ firstBlockVariation,
+ secondBlockVariation,
+ thirdBlockVariation,
+ ] );
- const result = getDefaultBlockVariation( state, blockName );
+ const result = getDefaultBlockVariation( state, blockName );
- expect( result ).toEqual( firstBlockVariation );
+ expect( result ).toEqual( firstBlockVariation );
+ } );
} );
} );
diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md
index 899f3ebade6e0..e28a5aca774f2 100644
--- a/packages/components/src/button/README.md
+++ b/packages/components/src/button/README.md
@@ -239,6 +239,14 @@ If provided with `icon`, sets the icon size.
- Required: No
- Default: `20 when a Dashicon is rendered, 24 for all other icons.`
+#### iconPosition
+
+If provided with `icon`, sets the position of icon relative to the `text`. Available options are `left|right`.
+
+- Type: `string`
+- Required: No
+- Default: `left`
+
#### showTooltip
If provided, renders a [Tooltip](/packages/components/src/tooltip/README.md) component for the button.
diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js
index 32d1b9853a6c2..698c52e453b8a 100644
--- a/packages/components/src/button/index.js
+++ b/packages/components/src/button/index.js
@@ -34,6 +34,7 @@ export function Button( props, ref ) {
className,
disabled,
icon,
+ iconPosition = 'left',
iconSize,
showTooltip,
tooltipPosition,
@@ -111,8 +112,13 @@ export function Button( props, ref ) {
aria-label={ additionalProps[ 'aria-label' ] || label }
ref={ ref }
>
- { icon && }
+ { icon && iconPosition === 'left' && (
+
+ ) }
{ text && <>{ text }> }
+ { icon && iconPosition === 'right' && (
+
+ ) }
{ children }
);
diff --git a/packages/components/src/menu-items-choice/index.js b/packages/components/src/menu-items-choice/index.js
index 299eb2b743f19..617e44161526f 100644
--- a/packages/components/src/menu-items-choice/index.js
+++ b/packages/components/src/menu-items-choice/index.js
@@ -26,6 +26,7 @@ export default function MenuItemsChoice( {
key={ item.value }
role="menuitemradio"
icon={ isSelected && check }
+ info={ item.info }
isSelected={ isSelected }
shortcut={ item.shortcut }
className="components-menu-items-choice"