Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,15 @@ export default function useArrowNav() {
const {
getMultiSelectedBlocksStartClientId,
getMultiSelectedBlocksEndClientId,
getNextBlockClientId,
getPreviousBlockClientId,
getSelectedBlockClientId,
getSelectionStart,
getSettings,
hasMultiSelection,
__unstableIsFullySelected,
} = useSelect( blockEditorStore );
const { selectBlock } = useDispatch( blockEditorStore );
const { selectBlock, multiSelect } = useDispatch( blockEditorStore );
return useRefEffect( ( node ) => {
// Here a DOMRect is stored while moving the caret vertically so
// vertical position of the start position can be restored. This is to
Expand Down Expand Up @@ -244,6 +248,32 @@ export default function useArrowNav() {
// selection to the start or end of the selection.
if ( hasMultiSelection() ) {
if ( shiftKey ) {
// A fully selected multi-selection has no native
// selection to extend (use-multi-selection cleared it),
// so grow or shrink it by one block at the focus end.
// Only without a usable native selection: a selection
// that is fully selected because it resolves to a
// nesting ancestor keeps its native selection, which
// the browser extends natively (and the observer
// promotes to the common level).
const selection = defaultView.getSelection();
if (
__unstableIsFullySelected() &&
( ! selection.rangeCount || selection.isCollapsed )
) {
const anchorClientId =
getMultiSelectedBlocksStartClientId();
const focusClientId =
getMultiSelectedBlocksEndClientId();
const nextClientId = isReverse
? getPreviousBlockClientId( focusClientId )
: getNextBlockClientId( focusClientId );

if ( nextClientId ) {
multiSelect( anchorClientId, nextClientId );
event.preventDefault();
}
}
return;
}

Expand All @@ -264,6 +294,26 @@ export default function useArrowNav() {
return;
}

// A block selected without a text selection within it (e.g. an
// image or spacer) has no native selection to extend: start a
// block multi-selection with the adjacent block.
if (
shiftKey &&
getSelectedBlockClientId() &&
! getSelectionStart().attributeKey
) {
const selectedClientId = getSelectedBlockClientId();
const nextClientId = isReverse
? getPreviousBlockClientId( selectedClientId )
: getNextBlockClientId( selectedClientId );

if ( nextClientId ) {
multiSelect( selectedClientId, nextClientId );
event.preventDefault();
}
return;
}

// Abort if our current target is not a candidate for navigation
// (e.g. preserve native input behaviors).
if ( ! isNavigationCandidate( target, keyCode, hasModifier ) ) {
Expand Down
77 changes: 77 additions & 0 deletions test/e2e/specs/editor/various/multi-block-selection.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,83 @@ test.describe( 'Multi-block selection (@firefox, @webkit)', () => {
] );
} );

test( 'should grow and shrink a full selection with shift+arrow', async ( {
editor,
page,
multiBlockSelectionUtils,
} ) => {
await editor.insertBlock( { name: 'core/spacer' } );
await editor.insertBlock( { name: 'core/spacer' } );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: 'a paragraph' },
} );

const spacerBlocks = editor.canvas.getByRole( 'document', {
name: 'Block: Spacer',
} );

await spacerBlocks.nth( 0 ).click();
await spacerBlocks.nth( 1 ).click( { modifiers: [ 'Shift' ] } );

await expect
.poll( multiBlockSelectionUtils.getSelectedBlocks )
.toMatchObject( [
{ name: 'core/spacer' },
{ name: 'core/spacer' },
] );

// A fully selected multi-selection grows by one block at the
// focus end.
await page.keyboard.press( 'Shift+ArrowDown' );
await expect
.poll( multiBlockSelectionUtils.getSelectedBlocks )
.toMatchObject( [
{ name: 'core/spacer' },
{ name: 'core/spacer' },
{
name: 'core/paragraph',
attributes: { content: 'a paragraph' },
},
] );

// And shrinks the same way.
await page.keyboard.press( 'Shift+ArrowUp' );
await expect
.poll( multiBlockSelectionUtils.getSelectedBlocks )
.toMatchObject( [
{ name: 'core/spacer' },
{ name: 'core/spacer' },
] );
} );

test( 'should extend the selection from a block without a text selection', async ( {
page,
editor,
multiBlockSelectionUtils,
} ) => {
await editor.insertBlock( { name: 'core/spacer' } );
await editor.insertBlock( {
name: 'core/paragraph',
attributes: { content: 'a paragraph' },
} );

await editor.canvas
.getByRole( 'document', { name: 'Block: Spacer' } )
.click();

await page.keyboard.press( 'Shift+ArrowDown' );
await expect
.poll( multiBlockSelectionUtils.getSelectedBlocks )
.toMatchObject( [
{ name: 'core/spacer' },
{
name: 'core/paragraph',
attributes: { content: 'a paragraph' },
},
] );
} );

test( 'should not scroll the canvas when the selection spans a scrolled page', async ( {
editor,
page,
Expand Down
Loading