Skip to content

Commit

Permalink
List: avoid useSelect in block render (#57077)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix authored Dec 14, 2023
1 parent ea2ee60 commit 30037b5
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 231 deletions.
20 changes: 18 additions & 2 deletions packages/block-library/src/list-item/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useBlockProps,
useInnerBlocksProps,
BlockControls,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { isRTL, __ } from '@wordpress/i18n';
import { ToolbarButton } from '@wordpress/components';
Expand All @@ -16,6 +17,7 @@ import {
formatIndent,
} from '@wordpress/icons';
import { useMergeRefs } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
Expand All @@ -32,8 +34,22 @@ import {
import { convertToListItems } from './utils';

export function IndentUI( { clientId } ) {
const [ canIndent, indentListItem ] = useIndentListItem( clientId );
const [ canOutdent, outdentListItem ] = useOutdentListItem( clientId );
const indentListItem = useIndentListItem( clientId );
const outdentListItem = useOutdentListItem();
const { canIndent, canOutdent } = useSelect(
( select ) => {
const { getBlockIndex, getBlockRootClientId, getBlockName } =
select( blockEditorStore );
return {
canIndent: getBlockIndex( clientId ) > 0,
canOutdent:
getBlockName(
getBlockRootClientId( getBlockRootClientId( clientId ) )
) === 'core/list-item',
};
},
[ clientId ]
);

return (
<>
Expand Down
125 changes: 63 additions & 62 deletions packages/block-library/src/list-item/hooks/use-enter.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,71 +19,72 @@ import useOutdentListItem from './use-outdent-list-item';

export default function useEnter( props ) {
const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore );
const { getBlock, getBlockRootClientId, getBlockIndex } =
const { getBlock, getBlockRootClientId, getBlockIndex, getBlockName } =
useSelect( blockEditorStore );
const propsRef = useRef( props );
propsRef.current = props;
const [ canOutdent, outdentListItem ] = useOutdentListItem(
propsRef.current.clientId
);
return useRefEffect(
( element ) => {
function onKeyDown( event ) {
if ( event.defaultPrevented || event.keyCode !== ENTER ) {
return;
}
const { content, clientId } = propsRef.current;
if ( content.length ) {
return;
}
event.preventDefault();
if ( canOutdent ) {
outdentListItem();
return;
}
// Here we are in top level list so we need to split.
const topParentListBlock = getBlock(
getBlockRootClientId( clientId )
);
const blockIndex = getBlockIndex( clientId );
const head = cloneBlock( {
...topParentListBlock,
innerBlocks: topParentListBlock.innerBlocks.slice(
0,
blockIndex
),
} );
const middle = createBlock( getDefaultBlockName() );
// Last list item might contain a `list` block innerBlock
// In that case append remaining innerBlocks blocks.
const after = [
...( topParentListBlock.innerBlocks[ blockIndex ]
.innerBlocks[ 0 ]?.innerBlocks || [] ),
...topParentListBlock.innerBlocks.slice( blockIndex + 1 ),
];
const tail = after.length
? [
cloneBlock( {
...topParentListBlock,
innerBlocks: after,
} ),
]
: [];
replaceBlocks(
topParentListBlock.clientId,
[ head, middle, ...tail ],
1
);
// We manually change the selection here because we are replacing
// a different block than the selected one.
selectionChange( middle.clientId );
const outdentListItem = useOutdentListItem();
return useRefEffect( ( element ) => {
function onKeyDown( event ) {
if ( event.defaultPrevented || event.keyCode !== ENTER ) {
return;
}
const { content, clientId } = propsRef.current;
if ( content.length ) {
return;
}
event.preventDefault();
const canOutdent =
getBlockName(
getBlockRootClientId(
getBlockRootClientId( propsRef.current.clientId )
)
) === 'core/list-item';
if ( canOutdent ) {
outdentListItem();
return;
}
// Here we are in top level list so we need to split.
const topParentListBlock = getBlock(
getBlockRootClientId( clientId )
);
const blockIndex = getBlockIndex( clientId );
const head = cloneBlock( {
...topParentListBlock,
innerBlocks: topParentListBlock.innerBlocks.slice(
0,
blockIndex
),
} );
const middle = createBlock( getDefaultBlockName() );
// Last list item might contain a `list` block innerBlock
// In that case append remaining innerBlocks blocks.
const after = [
...( topParentListBlock.innerBlocks[ blockIndex ]
.innerBlocks[ 0 ]?.innerBlocks || [] ),
...topParentListBlock.innerBlocks.slice( blockIndex + 1 ),
];
const tail = after.length
? [
cloneBlock( {
...topParentListBlock,
innerBlocks: after,
} ),
]
: [];
replaceBlocks(
topParentListBlock.clientId,
[ head, middle, ...tail ],
1
);
// We manually change the selection here because we are replacing
// a different block than the selected one.
selectionChange( middle.clientId );
}

element.addEventListener( 'keydown', onKeyDown );
return () => {
element.removeEventListener( 'keydown', onKeyDown );
};
},
[ canOutdent ]
);
element.addEventListener( 'keydown', onKeyDown );
return () => {
element.removeEventListener( 'keydown', onKeyDown );
};
}, [] );
}
14 changes: 9 additions & 5 deletions packages/block-library/src/list-item/hooks/use-enter.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ import useOutdentListItem from './use-outdent-list-item';

export default function useEnter( props, preventDefault ) {
const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore );
const { getBlock, getBlockRootClientId, getBlockIndex } =
const { getBlock, getBlockRootClientId, getBlockIndex, getBlockName } =
useSelect( blockEditorStore );
const propsRef = useRef( props );
propsRef.current = props;
const [ canOutdent, outdentListItem ] = useOutdentListItem(
propsRef.current.clientId
);
const outdentListItem = useOutdentListItem();

return {
onEnter() {
Expand All @@ -32,7 +30,13 @@ export default function useEnter( props, preventDefault ) {
return;
}
preventDefault.current = true;
if ( canOutdent ) {
if (
getBlockName(
getBlockRootClientId(
getBlockRootClientId( propsRef.current.clientId )
)
) === 'core/list-item'
) {
outdentListItem();
return;
}
Expand Down
96 changes: 43 additions & 53 deletions packages/block-library/src/list-item/hooks/use-indent-list-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { createBlock, cloneBlock } from '@wordpress/blocks';

export default function useIndentListItem( clientId ) {
const canIndent = useSelect(
( select ) => select( blockEditorStore ).getBlockIndex( clientId ) > 0,
[ clientId ]
);
const { replaceBlocks, selectionChange, multiSelect } =
useDispatch( blockEditorStore );
const {
Expand All @@ -21,55 +17,49 @@ export default function useIndentListItem( clientId ) {
hasMultiSelection,
getMultiSelectedBlockClientIds,
} = useSelect( blockEditorStore );
return [
canIndent,
useCallback( () => {
const _hasMultiSelection = hasMultiSelection();
const clientIds = _hasMultiSelection
? getMultiSelectedBlockClientIds()
: [ clientId ];
const clonedBlocks = clientIds.map( ( _clientId ) =>
cloneBlock( getBlock( _clientId ) )
);
const previousSiblingId = getPreviousBlockClientId( clientId );
const newListItem = cloneBlock( getBlock( previousSiblingId ) );
// If the sibling has no innerBlocks, create a new `list` block.
if ( ! newListItem.innerBlocks?.length ) {
newListItem.innerBlocks = [ createBlock( 'core/list' ) ];
}
// A list item usually has one `list`, but it's possible to have
// more. So we need to preserve the previous `list` blocks and
// merge the new blocks to the last `list`.
newListItem.innerBlocks[
newListItem.innerBlocks.length - 1
].innerBlocks.push( ...clonedBlocks );
return useCallback( () => {
const _hasMultiSelection = hasMultiSelection();
const clientIds = _hasMultiSelection
? getMultiSelectedBlockClientIds()
: [ clientId ];
const clonedBlocks = clientIds.map( ( _clientId ) =>
cloneBlock( getBlock( _clientId ) )
);
const previousSiblingId = getPreviousBlockClientId( clientId );
const newListItem = cloneBlock( getBlock( previousSiblingId ) );
// If the sibling has no innerBlocks, create a new `list` block.
if ( ! newListItem.innerBlocks?.length ) {
newListItem.innerBlocks = [ createBlock( 'core/list' ) ];
}
// A list item usually has one `list`, but it's possible to have
// more. So we need to preserve the previous `list` blocks and
// merge the new blocks to the last `list`.
newListItem.innerBlocks[
newListItem.innerBlocks.length - 1
].innerBlocks.push( ...clonedBlocks );

// We get the selection start/end here, because when
// we replace blocks, the selection is updated too.
const selectionStart = getSelectionStart();
const selectionEnd = getSelectionEnd();
// Replace the previous sibling of the block being indented and the indented blocks,
// with a new block whose attributes are equal to the ones of the previous sibling and
// whose descendants are the children of the previous sibling, followed by the indented blocks.
replaceBlocks(
[ previousSiblingId, ...clientIds ],
[ newListItem ]
// We get the selection start/end here, because when
// we replace blocks, the selection is updated too.
const selectionStart = getSelectionStart();
const selectionEnd = getSelectionEnd();
// Replace the previous sibling of the block being indented and the indented blocks,
// with a new block whose attributes are equal to the ones of the previous sibling and
// whose descendants are the children of the previous sibling, followed by the indented blocks.
replaceBlocks( [ previousSiblingId, ...clientIds ], [ newListItem ] );
if ( ! _hasMultiSelection ) {
selectionChange(
clonedBlocks[ 0 ].clientId,
selectionEnd.attributeKey,
selectionEnd.clientId === selectionStart.clientId
? selectionStart.offset
: selectionEnd.offset,
selectionEnd.offset
);
} else {
multiSelect(
clonedBlocks[ 0 ].clientId,
clonedBlocks[ clonedBlocks.length - 1 ].clientId
);
if ( ! _hasMultiSelection ) {
selectionChange(
clonedBlocks[ 0 ].clientId,
selectionEnd.attributeKey,
selectionEnd.clientId === selectionStart.clientId
? selectionStart.offset
: selectionEnd.offset,
selectionEnd.offset
);
} else {
multiSelect(
clonedBlocks[ 0 ].clientId,
clonedBlocks[ clonedBlocks.length - 1 ].clientId
);
}
}, [ clientId ] ),
];
}
}, [ clientId ] );
}
2 changes: 1 addition & 1 deletion packages/block-library/src/list-item/hooks/use-merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function useMerge( clientId, onMerge ) {
} = useSelect( blockEditorStore );
const { mergeBlocks, moveBlocksToPosition } =
useDispatch( blockEditorStore );
const [ , outdentListItem ] = useOutdentListItem( clientId );
const outdentListItem = useOutdentListItem();

function getTrailingId( id ) {
const order = getBlockOrder( id );
Expand Down
Loading

1 comment on commit 30037b5

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 30037b5.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7213696753
📝 Reported issues:

Please sign in to comment.