diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index e80d2a93487419..bf024b1fb758bf 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -27,6 +27,7 @@ import { sprintf, __ } from '@wordpress/i18n'; * Internal dependencies */ import ListViewLeaf from './leaf'; +import useListViewScrollIntoView from './use-list-view-scroll-into-view'; import { BlockMoverUpButton, BlockMoverDownButton, @@ -57,6 +58,7 @@ function ListViewBlock( { isSyncedBranch, } ) { const cellRef = useRef( null ); + const rowRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); const { clientId } = block; @@ -220,6 +222,15 @@ function ListViewBlock( { ? selectedClientIds : [ clientId ]; + // Pass in a ref to the row, so that it can be scrolled + // into view when selected. For long lists, the placeholder for the + // selected block is also observed, within ListViewLeafPlaceholder. + useListViewScrollIntoView( { + isSelected, + rowItemRef: rowRef, + selectedClientIds, + } ); + return ( { showBlock && ( diff --git a/packages/block-editor/src/components/list-view/leaf.js b/packages/block-editor/src/components/list-view/leaf.js index 41bf4bc34cc665..63c154fd620379 100644 --- a/packages/block-editor/src/components/list-view/leaf.js +++ b/packages/block-editor/src/components/list-view/leaf.js @@ -8,6 +8,8 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __experimentalTreeGridRow as TreeGridRow } from '@wordpress/components'; +import { useMergeRefs } from '@wordpress/compose'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies @@ -16,33 +18,45 @@ import useMovingAnimation from '../use-moving-animation'; const AnimatedTreeGridRow = animated( TreeGridRow ); -export default function ListViewLeaf( { - isSelected, - position, - level, - rowCount, - children, - className, - path, - ...props -} ) { - const ref = useMovingAnimation( { - isSelected, - adjustScrolling: false, - enableAnimation: true, - triggerAnimationOnChange: path, - } ); +const ListViewLeaf = forwardRef( + ( + { + isSelected, + position, + level, + rowCount, + children, + className, + path, + ...props + }, + ref + ) => { + const animationRef = useMovingAnimation( { + isSelected, + adjustScrolling: false, + enableAnimation: true, + triggerAnimationOnChange: path, + } ); - return ( - - { children } - - ); -} + const mergedRef = useMergeRefs( [ ref, animationRef ] ); + + return ( + + { children } + + ); + } +); + +export default ListViewLeaf; diff --git a/packages/block-editor/src/components/list-view/use-list-view-scroll-into-view.js b/packages/block-editor/src/components/list-view/use-list-view-scroll-into-view.js new file mode 100644 index 00000000000000..bee9c3b87a729a --- /dev/null +++ b/packages/block-editor/src/components/list-view/use-list-view-scroll-into-view.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { getScrollContainer } from '@wordpress/dom'; +import { useLayoutEffect } from '@wordpress/element'; + +export default function useListViewScrollIntoView( { + isSelected, + selectedClientIds, + rowItemRef, +} ) { + const isSingleSelection = selectedClientIds.length === 1; + + useLayoutEffect( () => { + // Skip scrolling into view if this particular block isn't selected, + // or if more than one block is selected overall. This is to avoid + // scrolling the view in a multi selection where the user has intentionally + // selected multiple blocks within the list view, but the initially + // selected block may be out of view. + if ( ! isSelected || ! isSingleSelection || ! rowItemRef.current ) { + return; + } + + const scrollContainer = getScrollContainer( rowItemRef.current ); + const { ownerDocument } = rowItemRef.current; + + const windowScroll = + scrollContainer === ownerDocument.body || + scrollContainer === ownerDocument.documentElement; + + // If the there is no scroll container, of if the scroll container is the window, + // do not scroll into view, as the block is already in view. + if ( windowScroll || ! scrollContainer ) { + return; + } + + const rowRect = rowItemRef.current.getBoundingClientRect(); + const scrollContainerRect = scrollContainer.getBoundingClientRect(); + + // If the selected block is not currently visible, scroll to it. + if ( + rowRect.top < scrollContainerRect.top || + rowRect.bottom > scrollContainerRect.bottom + ) { + rowItemRef.current.scrollIntoView(); + } + }, [ isSelected, isSingleSelection, rowItemRef ] ); +}