diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index fca1eb9747d1c8..e2b734ef4fc654 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -16,16 +16,16 @@ import { forwardRef, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { getScrollContainer } from '@wordpress/dom'; /** * Internal dependencies */ -import ListViewBranch, { countBlocks } from './branch'; +import ListViewBranch from './branch'; import { ListViewContext } from './context'; import ListViewDropIndicator from './drop-indicator'; import useListViewClientIds from './use-list-view-client-ids'; import useListViewDropZone from './use-list-view-drop-zone'; +import useListViewOpenSelectedItem from './use-list-view-open-selected-item'; import { store as blockEditorStore } from '../../store'; const noop = () => {}; @@ -45,7 +45,7 @@ const expanded = ( state, action ) => { return state; }; -const BLOCK_LIST_ITEM_HEIGHT = 36; +export const BLOCK_LIST_ITEM_HEIGHT = 36; /** * Wrap `ListViewRows` with `TreeGrid`. ListViewRows is a @@ -79,9 +79,7 @@ function ListView( clientIdsTree, draggedClientIds, selectedClientIds, - selectedBlockParentClientIds, } = useListViewClientIds( blocks ); - const selectedTreeId = useRef( null ); const { selectBlock } = useDispatch( blockEditorStore ); const { visibleBlockCount } = useSelect( ( select ) => { @@ -98,20 +96,26 @@ function ListView( }, [ draggedClientIds ] ); + const [ expandedState, setExpandedState ] = useReducer( expanded, {} ); + const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone(); + const elementRef = useRef(); + const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); + const isMounted = useRef( false ); + const { setSelectedTreeId } = useListViewOpenSelectedItem( { + firstSelectedBlockClientId: selectedClientIds[ 0 ], + clientIdsTree, + scrollContainerElement: elementRef?.current, + expandedState, + setExpandedState, + } ); const selectEditorBlock = useCallback( ( clientId ) => { selectBlock( clientId ); onSelect( clientId ); - selectedTreeId.current = clientId; + setSelectedTreeId( clientId ); }, [ selectBlock, onSelect ] ); - const [ expandedState, setExpandedState ] = useReducer( expanded, {} ); - const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone(); - const elementRef = useRef(); - const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); - const isMounted = useRef( false ); - useEffect( () => { isMounted.current = true; }, [] ); @@ -182,59 +186,6 @@ function ListView( collapse, ] ); - // @TODO create custom hooks. - useEffect( () => { - // If the selectedTreeId is the same as the selected block, - // it means that the block was selected usin the block list tree. - if ( selectedTreeId.current === selectedClientIds[ 0 ] ) { - return; - } - - // If the selected block has parents, get the top-level parent. - if ( - Array.isArray( selectedBlockParentClientIds ) && - selectedBlockParentClientIds.length - ) { - // If the selected block has parents, - // expand the tree branch. - setExpandedState( { - type: 'expand', - clientIds: selectedBlockParentClientIds, - } ); - } - - if ( Array.isArray( selectedClientIds ) && selectedClientIds.length ) { - const scrollContainer = getScrollContainer( elementRef.current ); - - // Grab the selected id. This is the point at which we can - // stop counting blocks in the tree. - let selectedId = selectedClientIds[ 0 ]; - - // If the selected block has parents, get the top-level parent. - if ( - Array.isArray( selectedBlockParentClientIds ) && - selectedBlockParentClientIds.length - ) { - selectedId = selectedBlockParentClientIds[ 0 ]; - } - - // Count expanded blocks in the tree up until the selected block, - // so we can calculate the scroll container top. - let listItemHeightFactor = 0; - clientIdsTree.every( ( item ) => { - if ( item?.clientId === selectedId ) { - return false; - } - listItemHeightFactor += countBlocks( item, expandedState, [] ); - return true; - } ); - - // @TODO if selected block is already visible in the list prevent scroll. - scrollContainer?.scrollTo( { - top: listItemHeightFactor * BLOCK_LIST_ITEM_HEIGHT, - } ); - } - }, [ selectedClientIds[ 0 ] ] ); return ( diff --git a/packages/block-editor/src/components/list-view/use-list-view-client-ids.js b/packages/block-editor/src/components/list-view/use-list-view-client-ids.js index e009eba4e9aea2..5dafa765f16ea5 100644 --- a/packages/block-editor/src/components/list-view/use-list-view-client-ids.js +++ b/packages/block-editor/src/components/list-view/use-list-view-client-ids.js @@ -16,18 +16,12 @@ export default function useListViewClientIds( blocks ) { getDraggedBlockClientIds, getSelectedBlockClientIds, __unstableGetClientIdsTree, - getBlockParents, } = select( blockEditorStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); return { selectedClientIds: getSelectedBlockClientIds(), draggedClientIds: getDraggedBlockClientIds(), clientIdsTree: blocks ? blocks : __unstableGetClientIdsTree(), - selectedBlockParentClientIds: getBlockParents( - selectedBlockClientIds[ 0 ], - false - ), }; }, [ blocks ] diff --git a/packages/block-editor/src/components/list-view/use-list-view-open-selected-item.js b/packages/block-editor/src/components/list-view/use-list-view-open-selected-item.js new file mode 100644 index 00000000000000..33aa221e14cb71 --- /dev/null +++ b/packages/block-editor/src/components/list-view/use-list-view-open-selected-item.js @@ -0,0 +1,139 @@ +/** + * WordPress dependencies + */ +import { useLayoutEffect, useEffect, useState } from '@wordpress/element'; +import { getScrollContainer } from '@wordpress/dom'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { BLOCK_LIST_ITEM_HEIGHT } from './'; +import { countBlocks } from './branch'; +import { store as blockEditorStore } from '../../store'; + +export default function useListViewOpenSelectedItem( { + firstSelectedBlockClientId, + clientIdsTree, + blockListItemHeight = BLOCK_LIST_ITEM_HEIGHT, + scrollContainerElement, + expandedState, + setExpandedState, +} ) { + const [ selectedTreeId, setSelectedTreeId ] = useState( null ); + const scrollContainer = getScrollContainer( scrollContainerElement ); + const { selectedBlockParentClientIds } = useSelect( + ( select ) => { + const { getBlockParents } = select( blockEditorStore ); + return { + selectedBlockParentClientIds: getBlockParents( + firstSelectedBlockClientId, + false + ), + }; + }, + [ firstSelectedBlockClientId ] + ); + + const parentClientIds = + Array.isArray( selectedBlockParentClientIds ) && + selectedBlockParentClientIds.length + ? selectedBlockParentClientIds + : null; + + // Track the expanded state of any parents. + // To calculate the number of expanded items correctly. + let parentExpandedState = null; + if ( parentClientIds ) { + parentExpandedState = expandedState[ parentClientIds[ 0 ] ]; + } + + useEffect( () => { + // If the selectedTreeId is the same as the selected block, + // it means that the block was selected using the block list tree. + if ( selectedTreeId === firstSelectedBlockClientId ) { + return; + } + + // If the selected block has parents, get the top-level parent. + if ( parentClientIds ) { + // If the selected block has parents, + // expand the tree branch. + setExpandedState( { + type: 'expand', + clientIds: selectedBlockParentClientIds, + } ); + } + }, [ firstSelectedBlockClientId ] ); + + useLayoutEffect( () => { + // If the selectedTreeId is the same as the selected block, + // it means that the block was selected using the block list tree. + if ( selectedTreeId === firstSelectedBlockClientId ) { + return; + } + if ( + scrollContainer && + !! firstSelectedBlockClientId && + Array.isArray( clientIdsTree ) && + clientIdsTree.length + ) { + // Grab the selected id. This is the point at which we can + // stop counting blocks in the tree. + let selectedId = firstSelectedBlockClientId; + + // If the selected block has parents, get the top-level parent. + if ( parentClientIds ) { + selectedId = parentClientIds[ 0 ]; + // If the selected block has parents, + // check to see if the selected tree is expanded + // so we can accurately calculate the scroll container top value. + if ( ! parentExpandedState ) { + return; + } + } + + // Count expanded blocks in the tree up until the selected block, + // so we can calculate the scroll container top value. + let listItemHeightFactor = 0; + clientIdsTree.every( ( item ) => { + if ( item?.clientId === selectedId ) { + return false; + } + listItemHeightFactor += countBlocks( item, expandedState, [] ); + return true; + } ); + + // New scroll value is the number of expanded items + // multiplied by the item height + // plus the number of expanded children in the selected block + // multiplied by the item height. + const newScrollTopValue = + listItemHeightFactor * blockListItemHeight + + ( parentClientIds ? parentClientIds.length : 1 ) * + blockListItemHeight; + + const shouldScrollDown = + newScrollTopValue > + scrollContainer.scrollTop + scrollContainer.clientHeight; + + const shouldScrollUp = + newScrollTopValue < scrollContainer.scrollTop; + + if ( ! shouldScrollUp && ! shouldScrollDown ) { + return; + } + + // @TODO This doesn't yet work when nested blocks are selected. + // We're still using the top parent block to calculate/trigger redraw. + // If selected block is already visible in the list prevent scroll. + scrollContainer?.scrollTo( { + top: newScrollTopValue, + } ); + } + }, [ firstSelectedBlockClientId, parentExpandedState ] ); + + return { + setSelectedTreeId, + }; +}