diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md
index 95ddf3b0c616ee..e0847ba9b29aec 100644
--- a/docs/getting-started/faq.md
+++ b/docs/getting-started/faq.md
@@ -224,6 +224,11 @@ This is the canonical list of keyboard shortcuts:
/ |
/ |
+
+ Create a group block from the selected block(s). |
+ Ctrl+Alt+G |
+ ⌥⌘⇧G |
+
Remove multiple selected blocks. |
delbackspace |
diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js
index 3959257ecf4e86..b17a8ba2b02de5 100644
--- a/packages/block-editor/src/components/block-tools/index.js
+++ b/packages/block-editor/src/components/block-tools/index.js
@@ -5,6 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { Popover } from '@wordpress/components';
import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts';
import { useRef } from '@wordpress/element';
+import { switchToBlockType, store as blocksStore } from '@wordpress/blocks';
/**
* Internal dependencies
@@ -62,9 +63,13 @@ export default function BlockTools( {
[]
);
const isMatch = useShortcutEventMatch();
- const { getSelectedBlockClientIds, getBlockRootClientId } =
- useSelect( blockEditorStore );
-
+ const {
+ getBlocksByClientId,
+ getSelectedBlockClientIds,
+ getBlockRootClientId,
+ isGroupable,
+ } = useSelect( blockEditorStore );
+ const { getGroupingBlockName } = useSelect( blocksStore );
const {
showEmptyBlockSideInserter,
showBreadcrumb,
@@ -74,6 +79,7 @@ export default function BlockTools( {
const {
duplicateBlocks,
removeBlocks,
+ replaceBlocks,
insertAfterBlock,
insertBeforeBlock,
selectBlock,
@@ -140,6 +146,18 @@ export default function BlockTools( {
// In effect, to the user this feels like deselecting the multi-selection.
selectBlock( clientIds[ 0 ] );
}
+ } else if ( isMatch( 'core/block-editor/group', event ) ) {
+ const clientIds = getSelectedBlockClientIds();
+ if ( clientIds.length > 1 && isGroupable( clientIds ) ) {
+ event.preventDefault();
+ const blocks = getBlocksByClientId( clientIds );
+ const groupingBlockName = getGroupingBlockName();
+ const newBlocks = switchToBlockType(
+ blocks,
+ groupingBlockName
+ );
+ replaceBlocks( clientIds, newBlocks );
+ }
}
}
diff --git a/packages/block-editor/src/components/keyboard-shortcuts/index.js b/packages/block-editor/src/components/keyboard-shortcuts/index.js
index 0e0a57257becca..162ee97c56ebc4 100644
--- a/packages/block-editor/src/components/keyboard-shortcuts/index.js
+++ b/packages/block-editor/src/components/keyboard-shortcuts/index.js
@@ -132,6 +132,18 @@ function KeyboardShortcutsRegister() {
character: 'y',
},
} );
+
+ registerShortcut( {
+ name: 'core/block-editor/group',
+ category: 'block',
+ description: __(
+ 'Create a group block from the selected block(s).'
+ ),
+ keyCombination: {
+ modifier: 'primaryAlt',
+ character: 'g',
+ },
+ } );
}, [ registerShortcut ] );
return null;
diff --git a/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js
index 0e8c5c8e7bf537..df6d1e793c2fb7 100644
--- a/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js
+++ b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js
@@ -217,4 +217,68 @@ test.describe( 'Block editor keyboard shortcuts', () => {
] );
} );
} );
+
+ test.describe( 'create a group block from the selected blocks', () => {
+ test( 'should propagate properly if multiple blocks are selected.', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await addTestParagraphBlocks( { editor, page } );
+
+ // Multiselect via keyboard.
+ await pageUtils.pressKeys( 'primary+a', { times: 2 } );
+
+ await pageUtils.pressKeys( 'primaryAlt+g' ); // Keyboard shortcut for Insert before.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/group',
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '1st' },
+ },
+ {
+ name: 'core/paragraph',
+ attributes: { content: '2nd' },
+ },
+ {
+ name: 'core/paragraph',
+ attributes: { content: '3rd' },
+ },
+ ],
+ },
+ ] );
+ } );
+
+ test( 'should prevent if a single block is selected.', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await addTestParagraphBlocks( { editor, page } );
+ const firstParagraphBlock = editor.canvas
+ .getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } )
+ .first();
+ await editor.selectBlocks( firstParagraphBlock );
+ await pageUtils.pressKeys( 'primaryAlt+g' );
+
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '1st' },
+ },
+ {
+ name: 'core/paragraph',
+ attributes: { content: '2nd' },
+ },
+ {
+ name: 'core/paragraph',
+ attributes: { content: '3rd' },
+ },
+ ] );
+ } );
+ } );
} );