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 editor: add a keyboard shortcut to create group from the selected blocks #46972

Merged
merged 12 commits into from
May 10, 2024
Merged
5 changes: 5 additions & 0 deletions docs/getting-started/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ This is the canonical list of keyboard shortcuts:
<td><kbd>/</kbd></td>
<td><kbd>/</kbd></td>
</tr>
<tr>
<td>Create a group block from the selected multiple blocks.</td>
<td><kbd>Ctrl</kbd>+<kbd>G</kbd></td>
<td><kbd>⌘</kbd><kbd>⇧</kbd><kbd>G</kbd></td>
</tr>
<tr>
<td>Remove multiple selected blocks.</td>
<td><kbd>del</kbd><kbd>backspace</kbd></td>
Expand Down
24 changes: 21 additions & 3 deletions packages/block-editor/src/components/block-tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isTextField } from '@wordpress/dom';
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
Expand Down Expand Up @@ -64,9 +65,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,
Expand All @@ -76,6 +81,7 @@ export default function BlockTools( {
const {
duplicateBlocks,
removeBlocks,
replaceBlocks,
insertAfterBlock,
insertBeforeBlock,
selectBlock,
Expand Down Expand Up @@ -159,6 +165,18 @@ export default function BlockTools( {
}
event.preventDefault();
expandBlock( clientId );
} 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 );
}
Copy link
Contributor

Choose a reason for hiding this comment

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

A spoken message when grouping is successful might be a good affordance. I tested with voiceover and there's not any indication that anything happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated by 8cf7f34

When you run the shortcut, you should hear "Selected blocks are grouped.".

Video tested with NVDA:

7274217df9cd891d1cc3d9bcedd95b7d.mp4

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By the way, we may need to add a spoken message to other shortcuts as well. See #61168.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import { MenuItem } from '@wordpress/components';
import { _x } from '@wordpress/i18n';
import { switchToBlockType } from '@wordpress/blocks';
import { useDispatch } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { displayShortcut } from '@wordpress/keycodes';

/**
* Internal dependencies
Expand All @@ -22,6 +23,7 @@ function ConvertToGroupButton( {
groupingBlockName,
onClose = () => {},
} ) {
const { getSelectedBlockClientIds } = useSelect( blockEditorStore );
const { replaceBlocks } = useDispatch( blockEditorStore );
const onConvertToGroup = () => {
// Activate the `transform` on the Grouping Block which does the conversion.
Expand Down Expand Up @@ -52,10 +54,17 @@ function ConvertToGroupButton( {
return null;
}

const selectedBlockClientIds = getSelectedBlockClientIds();

return (
<>
{ isGroupable && (
<MenuItem
shortcut={
selectedBlockClientIds.length > 1
? displayShortcut.primary( 'g' )
: undefined
}
onClick={ () => {
onConvertToGroup();
onClose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ function KeyboardShortcutsRegister() {
character: 'l',
},
} );

registerShortcut( {
name: 'core/block-editor/group',
category: 'block',
description: __(
'Create a group block from the selected multiple blocks.'
),
keyCombination: {
modifier: 'primary',
character: 'g',
},
} );
}, [ registerShortcut ] );

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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( 'primary+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( 'primary+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' },
},
] );
} );
} );
} );
Loading