diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js
index 6b88fddb1163fa..033201d7facadb 100644
--- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js
+++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js
@@ -14,6 +14,7 @@ import { useState, useMemo } from '@wordpress/element';
*/
import BlockIcon from '../block-icon';
import PreviewBlockPopover from './preview-block-popover';
+import BlockVariationTransformations from './block-variation-transformations';
/**
* Helper hook to group transformations to display them in a specific order in the UI.
@@ -65,7 +66,9 @@ function useGroupedTransforms( possibleBlockTransformations ) {
const BlockTransformationsMenu = ( {
className,
possibleBlockTransformations,
+ possibleBlockVariationTransformations,
onSelect,
+ onSelectVariation,
blocks,
} ) => {
const [ hoveredTransformItemName, setHoveredTransformItemName ] =
@@ -95,6 +98,15 @@ const BlockTransformationsMenu = ( {
) }
/>
) }
+ { !! possibleBlockVariationTransformations?.length && (
+
+ ) }
{ priorityTextTransformations.map( ( item ) => (
{
+ const {
+ getBlockRootClientId,
+ getBlockAttributes,
+ canRemoveBlocks,
+ } = select( blockEditorStore );
+ const { getActiveBlockVariation, getBlockVariations } =
+ select( blocksStore );
+ const rootClientId = getBlockRootClientId(
+ Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds
+ );
+ const canRemove = canRemoveBlocks( clientIds, rootClientId );
+ // Only handle single selected blocks for now.
+ if ( blocks.length !== 1 || ! canRemove ) {
+ return EMPTY_OBJECT;
+ }
+ const [ firstBlock ] = blocks;
+ return {
+ blockVariationTransformations: getBlockVariations(
+ firstBlock.name,
+ 'transform'
+ ),
+ activeBlockVariation: getActiveBlockVariation(
+ firstBlock.name,
+ getBlockAttributes( firstBlock.clientId )
+ ),
+ };
+ },
+ [ clientIds, blocks ]
+ );
+ const transformations = useMemo( () => {
+ return blockVariationTransformations?.filter(
+ ( { name } ) => name !== activeBlockVariation?.name
+ );
+ }, [ blockVariationTransformations, activeBlockVariation ] );
+ return transformations;
+}
+
+const BlockVariationTransformations = ( {
+ transformations,
+ onSelect,
+ blocks,
+} ) => {
+ const [ hoveredTransformItemName, setHoveredTransformItemName ] =
+ useState();
+ return (
+ <>
+ { hoveredTransformItemName && (
+ name === hoveredTransformItemName
+ ).attributes
+ ) }
+ />
+ ) }
+ { transformations?.map( ( item ) => (
+
+ ) ) }
+ >
+ );
+};
+
+function BlockVariationTranformationItem( {
+ item,
+ onSelect,
+ setHoveredTransformItemName,
+} ) {
+ const { name, icon, title } = item;
+ return (
+
+ );
+}
+
+export default BlockVariationTransformations;
diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js
index 696b097757d335..a4c15a9e17062a 100644
--- a/packages/block-editor/src/components/block-switcher/index.js
+++ b/packages/block-editor/src/components/block-switcher/index.js
@@ -24,12 +24,14 @@ import { store as blockEditorStore } from '../../store';
import useBlockDisplayInformation from '../use-block-display-information';
import BlockIcon from '../block-icon';
import BlockTransformationsMenu from './block-transformations-menu';
+import { useBlockVariationTransforms } from './block-variation-transformations';
import BlockStylesMenu from './block-styles-menu';
import PatternTransformationsMenu from './pattern-transformations-menu';
import useBlockDisplayTitle from '../block-title/use-block-display-title';
export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
- const { replaceBlocks, multiSelect } = useDispatch( blockEditorStore );
+ const { replaceBlocks, multiSelect, updateBlockAttributes } =
+ useDispatch( blockEditorStore );
const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId );
const {
possibleBlockTransformations,
@@ -43,9 +45,9 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
getBlockRootClientId,
getBlockTransformItems,
__experimentalGetPatternTransformItems,
+ canRemoveBlocks,
} = select( blockEditorStore );
const { getBlockStyles, getBlockType } = select( blocksStore );
- const { canRemoveBlocks } = select( blockEditorStore );
const rootClientId = getBlockRootClientId(
Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds
);
@@ -82,6 +84,11 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
[ clientIds, blocks, blockInformation?.icon ]
);
+ const blockVariationTransformations = useBlockVariationTransforms( {
+ clientIds,
+ blocks,
+ } );
+
const blockTitle = useBlockDisplayTitle( {
clientId: Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds,
maximumLength: 35,
@@ -105,6 +112,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
selectForMultipleBlocks( newBlocks );
}
+ function onBlockVariationTransform( name ) {
+ updateBlockAttributes( blocks[ 0 ].clientId, {
+ ...blockVariationTransformations.find(
+ ( { name: variationName } ) => variationName === name
+ ).attributes,
+ } );
+ }
+
// Pattern transformation through the `Patterns` API.
function onPatternTransform( transformedBlocks ) {
replaceBlocks( clientIds, transformedBlocks );
@@ -118,8 +133,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
*/
const hasPossibleBlockTransformations =
!! possibleBlockTransformations.length && canRemove && ! isTemplate;
+ const hasPossibleBlockVariationTransformations =
+ !! blockVariationTransformations?.length;
const hasPatternTransformation = !! patterns?.length && canRemove;
- if ( ! hasBlockStyles && ! hasPossibleBlockTransformations ) {
+ if (
+ ! hasBlockStyles &&
+ ! hasPossibleBlockTransformations &&
+ ! hasPossibleBlockVariationTransformations
+ ) {
return (
{
blocks.length
);
+ const hasBlockOrBlockVariationTransforms =
+ hasPossibleBlockTransformations ||
+ hasPossibleBlockVariationTransformations;
const showDropDown =
hasBlockStyles ||
- hasPossibleBlockTransformations ||
+ hasBlockOrBlockVariationTransforms ||
hasPatternTransformation;
return (
@@ -213,17 +237,26 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
} }
/>
) }
- { hasPossibleBlockTransformations && (
+ { hasBlockOrBlockVariationTransforms && (
{
onBlockTransform( name );
onClose();
} }
+ onSelectVariation={ ( name ) => {
+ onBlockVariationTransform(
+ name
+ );
+ onClose();
+ } }
/>
) }
{ hasBlockStyles && (
diff --git a/test/e2e/specs/editor/various/block-switcher-test.spec.js b/test/e2e/specs/editor/various/block-switcher-test.spec.js
new file mode 100644
index 00000000000000..12fd843ed2ed12
--- /dev/null
+++ b/test/e2e/specs/editor/various/block-switcher-test.spec.js
@@ -0,0 +1,60 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Block Switcher', () => {
+ test.beforeEach( async ( { admin } ) => {
+ await admin.createNewPost();
+ } );
+
+ test( 'Block variation transforms', async ( { editor, page } ) => {
+ // This is the `stack` Group variation.
+ await editor.insertBlock( {
+ name: 'core/group',
+ attributes: {
+ layout: {
+ type: 'flex',
+ orientation: 'vertical',
+ },
+ },
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '1' },
+ },
+ ],
+ } );
+ // Transform to `Stack` variation.
+ await editor.clickBlockToolbarButton( 'Stack' );
+ const variations = page
+ .getByRole( 'menu', { name: 'Stack' } )
+ .getByRole( 'group' );
+ await expect(
+ variations.getByRole( 'menuitem', { name: 'Stack' } )
+ ).toBeHidden();
+ await variations.getByRole( 'menuitem', { name: 'Row' } ).click();
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/group',
+ attributes: expect.objectContaining( {
+ layout: {
+ type: 'flex',
+ flexWrap: 'nowrap',
+ orientation: undefined,
+ },
+ } ),
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '1' },
+ },
+ ],
+ },
+ ] );
+ await editor.clickBlockToolbarButton( 'Row' );
+ await expect(
+ page.locator( 'role=menuitem[name="Stack"i]' )
+ ).toBeVisible();
+ } );
+} );