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

List View: Add multi-select behaviour for blocks when shift key is selected #38314

Merged
merged 24 commits into from
Feb 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8749179
List View: Add multi-select behaviour when shift key is selected
andrewserong Jan 28, 2022
4e75817
Ensure shift clicking a block when no blocks are selected selects tha…
andrewserong Jan 31, 2022
cfba501
Add support for dragging multiple selected blocks
andrewserong Jan 31, 2022
51376c5
Remove duplication by moving multi-select behaviour to the ListView c…
andrewserong Feb 1, 2022
d2fd70d
Add inline comments
andrewserong Feb 1, 2022
b6bb2db
Update documentation, add changelog entry
andrewserong Feb 1, 2022
06c731c
Add e2e test for multi-select in the list view
andrewserong Feb 2, 2022
edcff8b
Remove stray line from changelog
andrewserong Feb 2, 2022
1362dba
Ensure that clicked on blocks that aren't a part of a selection can s…
andrewserong Feb 3, 2022
74602ca
Try a naive approach to keyboard handling for shift + up/down to sele…
andrewserong Feb 3, 2022
62f32e4
Move block selection to its own useBlockSelection hook
andrewserong Feb 7, 2022
8fbb905
Refactor start and end id calculation to its own function, add unit t…
andrewserong Feb 7, 2022
999568e
Move utility function to utils file, add doc comment, update usage
andrewserong Feb 8, 2022
d884e0e
Update multiSelect behavior to support keeping focus within the ListV…
andrewserong Feb 11, 2022
27d63bc
Update unit tests, add additional e2e test for keyboard behaviour
andrewserong Feb 11, 2022
c5853bc
Revert change to focus for when shift key is not held
andrewserong Feb 11, 2022
6cb4fb3
Defer calculation of the next/prev clientId to the TreeGrid component…
andrewserong Feb 14, 2022
38b0e5b
Update initialPosition param to be experimental
andrewserong Feb 15, 2022
025087f
Update TreeGrid readme with documentation for the three callback func…
andrewserong Feb 15, 2022
c3b6a1e
Add changelog entry
andrewserong Feb 15, 2022
b321851
Pass in selectedClientIds to dropdown
andrewserong Feb 16, 2022
db31ffd
Prevent shift+click on expand toggle from opening a new window
andrewserong Feb 16, 2022
4f0e0bb
Try announcing deselected blocks
andrewserong Feb 16, 2022
5511896
Rename onChangeRow to onFocusRow, update changelog entry
andrewserong Feb 18, 2022
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
1 change: 1 addition & 0 deletions docs/reference-guides/data/data-core-block-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,7 @@ _Parameters_

- _start_ `string`: First block of the multi selection.
- _end_ `string`: Last block of the multiselection.
- _\_\_experimentalInitialPosition_ `number|null`: Optional initial position. Pass as null to skip focus within editor canvas.

### receiveBlocks

Expand Down
4 changes: 4 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

- Removed unused `@wordpress/block-serialization-default-parser`, `css-mediaquery`, `memize` and `redux-multi` dependencies ([#38388](https://github.com/WordPress/gutenberg/pull/38388)).

### New Features

- List View now supports selecting and dragging multiple blocks via `SHIFT` clicking items in the list [#38314](https://github.com/WordPress/gutenberg/pull/38314).

## 8.1.0 (2022-01-27)

## 8.0.0 (2021-11-07)
Expand Down
4 changes: 2 additions & 2 deletions packages/block-editor/src/components/list-view/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The ListView component provides an overview of the hierarchical structure of all

Blocks that have child blocks (such as group or column blocks) are presented with the parent at the top and the nested children below.

In addition to presenting the structure of the blocks in the editor, the ListView component lets users navigate to each block by clicking on its line in the hierarchy tree.
In addition to presenting the structure of the blocks in the editor, the ListView component lets users navigate to each block by clicking on its line in the hierarchy tree. Multiple blocks at the same level of nesting can be selected by holding down the `SHIFT` key and clicking blocks within the list.

![List view](https://make.wordpress.org/core/files/2020/08/block-navigation.png)
![View of a group list view](https://make.wordpress.org/core/files/2020/08/view-of-group-block-navigation.png)
Expand All @@ -23,7 +23,7 @@ Renders a list view with default syles.
```jsx
import { ListView } from '@wordpress/block-editor';

const MyNavigation = () => <ListView onSelect={ onClose } />;
const MyNavigation = () => <ListView />;
```

## Related components
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const ListViewBlockContents = forwardRef(
siblingBlockCount,
level,
isExpanded,
selectedClientIds,
...props
},
ref
Expand All @@ -36,12 +37,10 @@ const ListViewBlockContents = forwardRef(
const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect(
( select ) => {
const {
getBlockRootClientId,
hasBlockMovingClientId,
getSelectedBlockClientId,
} = select( blockEditorStore );
return {
rootClientId: getBlockRootClientId( clientId ) || '',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As of #33320 the rootClientId is no longer used in this function, so I think it can safely be removed here.

blockMovingClientId: hasBlockMovingClientId(),
selectedBlockInBlockEditor: getSelectedBlockClientId(),
};
Expand All @@ -56,8 +55,16 @@ const ListViewBlockContents = forwardRef(
'is-dropping-before': isBlockMoveTarget,
} );

// Only include all selected blocks if the currently clicked on block
// is one of the selected blocks. This ensures that if a user attempts
// to drag a block that isn't part of the selection, they're still able
// to drag it and rearrange its position.
const draggableClientIds = selectedClientIds.includes( clientId )
? selectedClientIds
: [ clientId ];

return (
<BlockDraggable clientIds={ [ block.clientId ] }>
<BlockDraggable clientIds={ draggableClientIds }>
{ ( { draggable, onDragStart, onDragEnd } ) => (
<ListViewBlockSelectButton
ref={ ref }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ function ListViewBlockSelectButton(

function onKeyDownHandler( event ) {
if ( event.keyCode === ENTER || event.keyCode === SPACE ) {
event.preventDefault();
onClick( event );
}
}
Expand Down
26 changes: 22 additions & 4 deletions packages/block-editor/src/components/list-view/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function ListViewBlock( {
showBlockMovers,
path,
isExpanded,
selectedClientIds,
} ) {
const cellRef = useRef( null );
const [ isHovered, setIsHovered ] = useState( false );
Expand Down Expand Up @@ -104,14 +105,22 @@ function ListViewBlock( {

const selectEditorBlock = useCallback(
( event ) => {
event.stopPropagation();
selectBlock( clientId );
selectBlock( event, clientId );
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the background behind this change?

Just an FYI, there was a bug that someone noticed yesterday that's related to selectEditorBlock - #38760.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the heads-up about that PR! I'll rebase this PR tomorrow and do some extra testing to see if I need to refactor a little more there to preserve that fix. One of the main changes in this PR is that we need to pass up the click and keyboard events to where we use the updateBlockSelection function, so that the useBlockSelection hook can factor in the type of event, whether the shift key was held, and whether it was a click or keyboard event.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've adjusted the logic slightly (added in a separate selectDuplicatedBlock callback so that selectBlock is called correctly), but the behaviour from that fix is now preserved.

},
[ clientId, selectBlock ]
);

const selectDuplicatedBlock = useCallback(
( newClientId ) => {
selectBlock( undefined, newClientId );
},
[ selectBlock ]
);

const toggleExpanded = useCallback(
( event ) => {
// Prevent shift+click from opening link in a new window when toggling.
event.preventDefault();
event.stopPropagation();
if ( isExpanded === true ) {
collapse( clientId );
Expand Down Expand Up @@ -154,6 +163,14 @@ function ListViewBlock( {
)
: __( 'Options' );

// Only include all selected blocks if the currently clicked on block
// is one of the selected blocks. This ensures that if a user attempts
// to alter a block that isn't part of the selection, they're still able
// to do so.
const dropdownClientIds = selectedClientIds.includes( clientId )
? selectedClientIds
: [ clientId ];

return (
<ListViewLeaf
className={ classes }
Expand Down Expand Up @@ -188,6 +205,7 @@ function ListViewBlock( {
tabIndex={ tabIndex }
onFocus={ onFocus }
isExpanded={ isExpanded }
selectedClientIds={ selectedClientIds }
/>
</div>
) }
Expand Down Expand Up @@ -228,7 +246,7 @@ function ListViewBlock( {
<TreeGridCell className={ listViewBlockSettingsClassName }>
{ ( { ref, tabIndex, onFocus } ) => (
<BlockSettingsDropdown
clientIds={ [ clientId ] }
clientIds={ dropdownClientIds }
icon={ moreVertical }
label={ settingsAriaLabel }
toggleProps={ {
Expand All @@ -238,7 +256,7 @@ function ListViewBlock( {
onFocus,
} }
disableOpenOnArrowDown
__experimentalSelectBlock={ selectBlock }
__experimentalSelectBlock={ selectDuplicatedBlock }
/>
) }
</TreeGridCell>
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/components/list-view/branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ function ListViewBranch( props ) {
path={ updatedPath }
isExpanded={ isExpanded }
listPosition={ nextPosition }
selectedClientIds={ selectedClientIds }
/>
) }
{ ! showBlock && (
Expand Down
49 changes: 28 additions & 21 deletions packages/block-editor/src/components/list-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
__experimentalUseFixedWindowList as useFixedWindowList,
} from '@wordpress/compose';
import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components';
import { AsyncModeProvider, useDispatch, useSelect } from '@wordpress/data';
import { AsyncModeProvider, useSelect } from '@wordpress/data';
import {
useCallback,
useEffect,
Expand All @@ -23,11 +23,11 @@ import { __ } from '@wordpress/i18n';
import ListViewBranch from './branch';
import { ListViewContext } from './context';
import ListViewDropIndicator from './drop-indicator';
import useBlockSelection from './use-block-selection';
import useListViewClientIds from './use-list-view-client-ids';
import useListViewDropZone from './use-list-view-drop-zone';
import { store as blockEditorStore } from '../../store';

const noop = () => {};
const expanded = ( state, action ) => {
switch ( action.type ) {
case 'expand':
Expand All @@ -44,20 +44,18 @@ const expanded = ( state, action ) => {
* recursive component (it renders itself), so this ensures TreeGrid is only
* present at the very top of the navigation grid.
*
* @param {Object} props Components props.
* @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy.
* @param {Function} props.onSelect Block selection callback.
* @param {boolean} props.showNestedBlocks Flag to enable displaying nested blocks.
* @param {boolean} props.showBlockMovers Flag to enable block movers
* @param {boolean} props.__experimentalFeatures Flag to enable experimental features.
* @param {boolean} props.__experimentalPersistentListViewFeatures Flag to enable features for the Persistent List View experiment.
* @param {boolean} props.__experimentalHideContainerBlockActions Flag to hide actions of top level blocks (like core/widget-area)
* @param {Object} ref Forwarded ref
* @param {Object} props Components props.
* @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy.
* @param {boolean} props.showNestedBlocks Flag to enable displaying nested blocks.
* @param {boolean} props.showBlockMovers Flag to enable block movers
* @param {boolean} props.__experimentalFeatures Flag to enable experimental features.
* @param {boolean} props.__experimentalPersistentListViewFeatures Flag to enable features for the Persistent List View experiment.
* @param {boolean} props.__experimentalHideContainerBlockActions Flag to hide actions of top level blocks (like core/widget-area)
* @param {Object} ref Forwarded ref
*/
function ListView(
{
blocks,
onSelect = noop,
andrewserong marked this conversation as resolved.
Show resolved Hide resolved
__experimentalFeatures,
__experimentalPersistentListViewFeatures,
__experimentalHideContainerBlockActions,
Expand All @@ -72,7 +70,7 @@ function ListView(
draggedClientIds,
selectedClientIds,
} = useListViewClientIds( blocks );
const { selectBlock } = useDispatch( blockEditorStore );

const { visibleBlockCount } = useSelect(
( select ) => {
const { getGlobalBlockCount, getClientIdsOfDescendants } = select(
Expand All @@ -88,13 +86,9 @@ function ListView(
},
[ draggedClientIds ]
);
const selectEditorBlock = useCallback(
( clientId ) => {
selectBlock( clientId );
onSelect( clientId );
},
[ selectBlock, onSelect ]
);

const { updateBlockSelection } = useBlockSelection();

const [ expandedState, setExpandedState ] = useReducer( expanded, {} );

const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone();
Expand Down Expand Up @@ -149,6 +143,18 @@ function ListView(
},
[ collapse ]
);
const focusRow = useCallback(
( event, startRow, endRow ) => {
if ( event.shiftKey ) {
updateBlockSelection(
event,
startRow?.dataset?.block,
endRow?.dataset?.block
);
}
},
[ updateBlockSelection ]
);

const contextValue = useMemo(
() => ( {
Expand Down Expand Up @@ -185,11 +191,12 @@ function ListView(
ref={ treeGridRef }
onCollapseRow={ collapseRow }
onExpandRow={ expandRow }
onFocusRow={ focusRow }
>
<ListViewContext.Provider value={ contextValue }>
<ListViewBranch
blocks={ clientIdsTree }
selectBlock={ selectEditorBlock }
selectBlock={ updateBlockSelection }
showNestedBlocks={ showNestedBlocks }
showBlockMovers={ showBlockMovers }
fixedListWindow={ fixedListWindow }
Expand Down
50 changes: 50 additions & 0 deletions packages/block-editor/src/components/list-view/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Internal dependencies
*/
import { getCommonDepthClientIds } from '../utils';

describe( 'getCommonDepthClientIds', () => {
it( 'should return start and end when no depth is provided', () => {
const result = getCommonDepthClientIds(
'start-id',
'clicked-id',
[],
[]
);

expect( result ).toEqual( { start: 'start-id', end: 'clicked-id' } );
} );

it( 'should return deepest start and end when depths match', () => {
const result = getCommonDepthClientIds(
'start-id',
'clicked-id',
[ 'start-1', 'start-2', 'start-3' ],
[ 'end-1', 'end-2', 'end-3' ]
);

expect( result ).toEqual( { start: 'start-id', end: 'clicked-id' } );
} );

it( 'should return shallower ids when start is shallower', () => {
const result = getCommonDepthClientIds(
'start-id',
'clicked-id',
[ 'start-1' ],
[ 'end-1', 'end-2', 'end-3' ]
);

expect( result ).toEqual( { start: 'start-id', end: 'end-2' } );
} );

it( 'should return shallower ids when end is shallower', () => {
const result = getCommonDepthClientIds(
'start-id',
'clicked-id',
[ 'start-1', 'start-2', 'start-3' ],
[ 'end-1', 'end-2' ]
);

expect( result ).toEqual( { start: 'start-3', end: 'clicked-id' } );
} );
} );
Loading