Skip to content

Commit

Permalink
List View: Add keyboard shortcut to select all blocks (#54899)
Browse files Browse the repository at this point in the history
* List View: Try adding keyboard shortcut to select all blocks

* Move up each leve of the hierarchy incrementally

* Fix bug if focused on a different part of the list view from the block selection

* Add e2e tests

* Simplify e2e tests

* Clarify the group block state a little more in the e2e test
  • Loading branch information
andrewserong authored Nov 13, 2023
1 parent 4d3b148 commit f927f2d
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { SPACE, ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts';
import { __, sprintf } from '@wordpress/i18n';
import isShallowEqual from '@wordpress/is-shallow-equal';

/**
* Internal dependencies
Expand All @@ -30,6 +31,7 @@ import ListViewExpander from './expander';
import { useBlockLock } from '../block-lock';
import { store as blockEditorStore } from '../../store';
import useListViewImages from './use-list-view-images';
import { useListViewContext } from './context';

function ListViewBlockSelectButton(
{
Expand Down Expand Up @@ -64,10 +66,12 @@ function ListViewBlockSelectButton(
getBlocksByClientId,
canRemoveBlocks,
} = useSelect( blockEditorStore );
const { duplicateBlocks, removeBlocks } = useDispatch( blockEditorStore );
const { duplicateBlocks, multiSelect, removeBlocks } =
useDispatch( blockEditorStore );
const isMatch = useShortcutEventMatch();
const isSticky = blockInformation?.positionType === 'sticky';
const images = useListViewImages( { clientId, isExpanded } );
const { rootClientId } = useListViewContext();

const positionLabel = blockInformation?.positionLabel
? sprintf(
Expand Down Expand Up @@ -183,6 +187,45 @@ function ListViewBlockSelectButton(
updateFocusAndSelection( updatedBlocks[ 0 ], false );
}
}
} else if ( isMatch( 'core/block-editor/select-all', event ) ) {
if ( event.defaultPrevented ) {
return;
}
event.preventDefault();

const { firstBlockRootClientId, selectedBlockClientIds } =
getBlocksToUpdate();
const blockClientIds = getBlockOrder( firstBlockRootClientId );
if ( ! blockClientIds.length ) {
return;
}

// If we have selected all sibling nested blocks, try selecting up a level.
// This is a similar implementation to that used by `useSelectAll`.
// `isShallowEqual` is used for the list view instead of a length check,
// as the array of siblings of the currently focused block may be a different
// set of blocks from the current block selection if the user is focused
// on a different part of the list view from the block selection.
if ( isShallowEqual( selectedBlockClientIds, blockClientIds ) ) {
// Only select up a level if the first block is not the root block.
// This ensures that the block selection can't break out of the root block
// used by the list view, if the list view is only showing a partial hierarchy.
if (
firstBlockRootClientId &&
firstBlockRootClientId !== rootClientId
) {
updateFocusAndSelection( firstBlockRootClientId, true );
return;
}
}

// Select all while passing `null` to skip focusing to the editor canvas,
// and retain focus within the list view.
multiSelect(
blockClientIds[ 0 ],
blockClientIds[ blockClientIds.length - 1 ],
null
);
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/block-editor/src/components/list-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function ListViewComponent(
insertedBlock,
setInsertedBlock,
treeGridElementRef: elementRef,
rootClientId,
} ),
[
draggedClientIds,
Expand All @@ -233,6 +234,7 @@ function ListViewComponent(
AdditionalBlockContent,
insertedBlock,
setInsertedBlock,
rootClientId,
]
);

Expand Down
112 changes: 111 additions & 1 deletion test/e2e/specs/editor/various/list-view.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ test.describe( 'List View', () => {
).toBeFocused();
} );

test( 'should duplicate, delete, and deselect blocks using keyboard', async ( {
test( 'should select, duplicate, delete, and deselect blocks using keyboard', async ( {
editor,
page,
pageUtils,
Expand Down Expand Up @@ -464,6 +464,116 @@ test.describe( 'List View', () => {
{ name: 'core/file', selected: true, focused: true },
] );

// Move up to columns block, expand, and then move to the first column block.
await page.keyboard.press( 'ArrowUp' );
await page.keyboard.press( 'ArrowRight' );
await page.keyboard.press( 'ArrowDown' );

await expect
.poll(
listViewUtils.getBlocksWithA11yAttributes,
'The last inserted block should be selected, while the first column block should be focused.'
)
.toMatchObject( [
{ name: 'core/group' },
{
name: 'core/columns',
innerBlocks: [
{ name: 'core/column', selected: false, focused: true },
{ name: 'core/column' },
],
},
{ name: 'core/file', selected: true, focused: false },
] );

// Select all sibling column blocks at current level.
await pageUtils.pressKeys( 'primary+a' );
await expect
.poll(
listViewUtils.getBlocksWithA11yAttributes,
'All column blocks should be selected, with the first one focused.'
)
.toMatchObject( [
{ name: 'core/group', selected: false, focused: false },
{
name: 'core/columns',
innerBlocks: [
{ name: 'core/column', selected: true, focused: true },
{ name: 'core/column', selected: true, focused: false },
],
selected: false,
},
{ name: 'core/file', selected: false, focused: false },
] );

// Select next parent (the columns block).
await pageUtils.pressKeys( 'primary+a' );
await expect
.poll(
listViewUtils.getBlocksWithA11yAttributes,
'The columns block should be selected and focused.'
)
.toMatchObject( [
{ name: 'core/group', selected: false, focused: false },
{
name: 'core/columns',
innerBlocks: [
{ name: 'core/column' },
{ name: 'core/column' },
],
selected: true,
focused: true,
},
{ name: 'core/file', selected: false, focused: false },
] );

// Select all siblings at root level.
await pageUtils.pressKeys( 'primary+a' );
await expect
.poll(
listViewUtils.getBlocksWithA11yAttributes,
'All blocks should be selected.'
)
.toMatchObject( [
{ name: 'core/group', selected: true, focused: false },
{
name: 'core/columns',
innerBlocks: [
{ name: 'core/column' },
{ name: 'core/column' },
],
selected: true,
focused: true,
},
{ name: 'core/file', selected: true, focused: false },
] );

// Deselect blocks via Escape key.
await page.keyboard.press( 'Escape' );
// Collapse the columns block.
await page.keyboard.press( 'ArrowLeft' );

await expect
.poll(
listViewUtils.getBlocksWithA11yAttributes,
'All blocks should be deselected, with focus on the Columns block.'
)
.toMatchObject( [
{ name: 'core/group', selected: false, focused: false },
{
name: 'core/columns',
selected: false,
focused: true,
},
{ name: 'core/file', selected: false, focused: false },
] );

// Move focus and selection to the file block to set up for testing duplication.
await listView
.getByRole( 'gridcell', { name: 'File', exact: true } )
.dblclick();

// Test duplication behaviour.
await pageUtils.pressKeys( 'primaryShift+d' );

await expect
Expand Down

0 comments on commit f927f2d

Please sign in to comment.