@wordpress/block-editor
Version:
174 lines (154 loc) • 4.92 kB
JavaScript
/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { __, sprintf } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { UP, DOWN, HOME, END, ESCAPE } from '@wordpress/keycodes';
import { store as blocksStore } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { getCommonDepthClientIds } from './utils';
export default function useBlockSelection() {
const { clearSelectedBlock, multiSelect, selectBlock } =
useDispatch( blockEditorStore );
const {
getBlockName,
getBlockParents,
getBlockSelectionStart,
getSelectedBlockClientIds,
hasMultiSelection,
hasSelectedBlock,
} = useSelect( blockEditorStore );
const { getBlockType } = useSelect( blocksStore );
const updateBlockSelection = useCallback(
async ( event, clientId, destinationClientId, focusPosition ) => {
if ( ! event?.shiftKey && event?.keyCode !== ESCAPE ) {
selectBlock( clientId, focusPosition );
return;
}
// To handle multiple block selection via the `SHIFT` key, prevent
// the browser default behavior of opening the link in a new window.
event.preventDefault();
const isOnlyDeselection =
event.type === 'keydown' && event.keyCode === ESCAPE;
const isKeyPress =
event.type === 'keydown' &&
( event.keyCode === UP ||
event.keyCode === DOWN ||
event.keyCode === HOME ||
event.keyCode === END );
// Handle clicking on a block when no blocks are selected, and return early.
if (
! isKeyPress &&
! hasSelectedBlock() &&
! hasMultiSelection()
) {
selectBlock( clientId, null );
return;
}
const selectedBlocks = getSelectedBlockClientIds();
const clientIdWithParents = [
...getBlockParents( clientId ),
clientId,
];
if (
isOnlyDeselection ||
( isKeyPress &&
! selectedBlocks.some( ( blockId ) =>
clientIdWithParents.includes( blockId )
) )
) {
// Ensure that shift-selecting blocks via the keyboard only
// expands the current selection if focusing over already
// selected blocks. Otherwise, clear the selection so that
// a user can create a new selection entirely by keyboard.
await clearSelectedBlock();
}
// Update selection, if not only clearing the selection.
if ( ! isOnlyDeselection ) {
let startTarget = getBlockSelectionStart();
let endTarget = clientId;
// Handle keyboard behavior for selecting multiple blocks.
if ( isKeyPress ) {
if ( ! hasSelectedBlock() && ! hasMultiSelection() ) {
// Set the starting point of the selection to the currently
// focused block, if there are no blocks currently selected.
// This ensures that as the selection is expanded or contracted,
// the starting point of the selection is anchored to that block.
startTarget = clientId;
}
if ( destinationClientId ) {
// If the user presses UP or DOWN, we want to ensure that the block they're
// moving to is the target for selection, and not the currently focused one.
endTarget = destinationClientId;
}
}
const startParents = getBlockParents( startTarget );
const endParents = getBlockParents( endTarget );
const { start, end } = getCommonDepthClientIds(
startTarget,
endTarget,
startParents,
endParents
);
await multiSelect( start, end, null );
}
// Announce deselected block, or number of deselected blocks if
// the total number of blocks deselected is greater than one.
const updatedSelectedBlocks = getSelectedBlockClientIds();
// If the selection is greater than 1 and the Home or End keys
// were used to generate the selection, then skip announcing the
// deselected blocks.
if (
( event.keyCode === HOME || event.keyCode === END ) &&
updatedSelectedBlocks.length > 1
) {
return;
}
const selectionDiff = selectedBlocks.filter(
( blockId ) => ! updatedSelectedBlocks.includes( blockId )
);
let label;
if ( selectionDiff.length === 1 ) {
const title = getBlockType(
getBlockName( selectionDiff[ 0 ] )
)?.title;
if ( title ) {
label = sprintf(
/* translators: %s: block name */
__( '%s deselected.' ),
title
);
}
} else if ( selectionDiff.length > 1 ) {
label = sprintf(
/* translators: %s: number of deselected blocks */
__( '%s blocks deselected.' ),
selectionDiff.length
);
}
if ( label ) {
speak( label, 'assertive' );
}
},
[
clearSelectedBlock,
getBlockName,
getBlockType,
getBlockParents,
getBlockSelectionStart,
getSelectedBlockClientIds,
hasMultiSelection,
hasSelectedBlock,
multiSelect,
selectBlock,
]
);
return {
updateBlockSelection,
};
}