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
27 changes: 24 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,9 @@ 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';
import { speak } from '@wordpress/a11y';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
Expand Down Expand Up @@ -64,9 +67,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 +83,7 @@ export default function BlockTools( {
const {
duplicateBlocks,
removeBlocks,
replaceBlocks,
insertAfterBlock,
insertBeforeBlock,
selectBlock,
Expand Down Expand Up @@ -159,6 +167,19 @@ 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 );
speak( __( 'Selected blocks are grouped.' ) );
}
}
}

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
23 changes: 22 additions & 1 deletion packages/block-editor/src/components/list-view/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { hasBlockSupport } from '@wordpress/blocks';
import {
hasBlockSupport,
switchToBlockType,
store as blocksStore,
} from '@wordpress/blocks';
import {
__experimentalTreeGridCell as TreeGridCell,
__experimentalTreeGridItem as TreeGridItem,
Expand All @@ -25,6 +29,7 @@ import { __ } from '@wordpress/i18n';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts';
import { speak } from '@wordpress/a11y';

/**
* Internal dependencies
Expand Down Expand Up @@ -85,6 +90,7 @@ function ListViewBlock( {
toggleBlockHighlight,
duplicateBlocks,
multiSelect,
replaceBlocks,
removeBlocks,
insertAfterBlock,
insertBeforeBlock,
Expand All @@ -100,7 +106,9 @@ function ListViewBlock( {
getBlockParents,
getBlocksByClientId,
canRemoveBlocks,
isGroupable,
} = useSelect( blockEditorStore );
const { getGroupingBlockName } = useSelect( blocksStore );

const blockInformation = useBlockDisplayInformation( clientId );

Expand Down Expand Up @@ -324,6 +332,19 @@ function ListViewBlock( {
collapseAll();
// Expand all parents of the current block.
expand( blockParents );
} else if ( isMatch( 'core/block-editor/group', event ) ) {
const clientIds = getSelectedBlockClientIds();
Copy link
Contributor

@andrewserong andrewserong May 8, 2024

Choose a reason for hiding this comment

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

In the List View, rather than using the selected block client ids, I think it's generally better to use const { blocksToUpdate } = getBlocksToUpdate(); as used in the insert before and after keyboard shortcuts above. This will ensure that if the user is focused on a block that isn't part of the selection, then the action is performed on the currently focused block. If the focused block is part of the selection, then the whole selection is used.

Copy link
Contributor

Choose a reason for hiding this comment

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

I just realised this might not be particularly clear as to why it matters. Here's a quick example video of the behaviour as it is currently, where if I move up the list view to outside the selection, the keyboard shortcut acts on the selected blocks instead of the focused one:

2024-05-08.16.12.33.mp4

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.

To fix the issue of the canvas stealing focus that Alex mentioned, we might be able to use a similar approach as in the insert before and after keyboard shortcuts above, where we grab the selected blocks after calling replaceBlocks and then call the following to shift focus back to the list view again:

			const newlySelectedBlocks = getSelectedBlockClientIds();
			// Focus the first block of the newly inserted blocks, to keep focus within the list view.
			updateFocusAndSelection( newlySelectedBlocks[ 0 ], false );

speak( __( 'Selected blocks are grouped.' ) );
}
}
}

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' },
},
] );
} );
} );
} );
40 changes: 40 additions & 0 deletions test/e2e/specs/editor/various/list-view.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,46 @@ test.describe( 'List View', () => {
] );
} );

test( 'should create a group block from the selected multiple blocks', async ( {
editor,
pageUtils,
listViewUtils,
} ) => {
// Insert some blocks of different types.
await editor.insertBlock( { name: 'core/paragraph' } );
await editor.insertBlock( { name: 'core/heading' } );
await editor.insertBlock( { name: 'core/file' } );

await listViewUtils.openListView();

// Group Heading and File blocks.
await pageUtils.pressKeys( 'shift+ArrowUp' );
await pageUtils.pressKeys( 'primary+g' );
await expect
.poll( listViewUtils.getBlocksWithA11yAttributes )
.toMatchObject( [
{ name: 'core/paragraph', selected: false, focused: false },

{
name: 'core/group',
selected: true,
focused: false,
innerBlocks: [
{
name: 'core/heading',
selected: false,
focused: false,
},
{
name: 'core/file',
selected: false,
focused: false,
},
],
},
] );
} );

test( 'block settings dropdown menu', async ( {
editor,
page,
Expand Down
Loading