From fa6ce11653fbf05a4555c924db5bb5527cc4f7d0 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 14 Dec 2022 23:07:34 +1100 Subject: [PATCH] Block Selection Toolbar: Support fixed and sticky blocks by re-rendering on scroll and flipping based on available space at top of viewport --- .../src/components/block-popover/index.js | 26 ++++++++++ .../use-block-toolbar-popover-props.js | 50 ++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 5382949684abd9..0c994d51c3f776 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -7,6 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useMergeRefs } from '@wordpress/compose'; +import { getScrollContainer } from '@wordpress/dom'; import { Popover } from '@wordpress/components'; import { forwardRef, @@ -75,6 +76,31 @@ function BlockPopover( }; }, [ selectedElement ] ); + const scrollContainer = useMemo( () => { + if ( ! __unstableContentRef?.current ) { + return; + } + return getScrollContainer( __unstableContentRef?.current ); + }, [ __unstableContentRef?.current ] ); + + useLayoutEffect( () => { + if ( ! scrollContainer ) { + return; + } + + scrollContainer?.addEventListener?.( + 'scroll', + forceRecomputePopoverDimensions + ); + + return () => { + scrollContainer?.removeEventHandler?.( + 'scroll', + forceRecomputePopoverDimensions + ); + }; + }, [ scrollContainer ] ); + const style = useMemo( () => { if ( // popoverDimensionsRecomputeCounter is by definition always equal or greater diff --git a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js index d218b1104139cf..4703e059855290 100644 --- a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js +++ b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js @@ -3,7 +3,13 @@ */ import { useRefEffect } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; -import { useCallback, useLayoutEffect, useState } from '@wordpress/element'; +import { getScrollContainer } from '@wordpress/dom'; +import { + useCallback, + useLayoutEffect, + useMemo, + useState, +} from '@wordpress/element'; /** * Internal dependencies @@ -40,24 +46,40 @@ const RESTRICTED_HEIGHT_PROPS = { * * @param {Element} contentElement The DOM element that represents the editor content or canvas. * @param {Element} selectedBlockElement The outer DOM element of the first selected block. + * @param {Element} scrollContainer The scrollable container for the contentElement. * @param {number} toolbarHeight The height of the toolbar in pixels. * * @return {Object} The popover props used to determine the position of the toolbar. */ -function getProps( contentElement, selectedBlockElement, toolbarHeight ) { +function getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight +) { if ( ! contentElement || ! selectedBlockElement ) { return DEFAULT_PROPS; } + // Get how far the content area has been scrolled. + const scrollTop = scrollContainer?.scrollTop || 0; + const blockRect = selectedBlockElement.getBoundingClientRect(); const contentRect = contentElement.getBoundingClientRect(); + // Get the vertical position of top of the visible content area. + const topOfContentElementInViewport = scrollTop + contentRect.top; + // The document element's clientHeight represents the viewport height. const viewportHeight = contentElement.ownerDocument.documentElement.clientHeight; - const hasSpaceForToolbarAbove = - blockRect.top - contentRect.top > toolbarHeight; + // The restricted height area is calculated as the sum of the + // vertical position of the visible content area, plus the height + // of the block toolbar. + const restrictedTopArea = topOfContentElementInViewport + toolbarHeight; + const hasSpaceForToolbarAbove = blockRect.top > restrictedTopArea; + const isBlockTallerThanViewport = blockRect.height > viewportHeight - toolbarHeight; @@ -83,8 +105,19 @@ export default function useBlockToolbarPopoverProps( { } ) { const selectedBlockElement = useBlockElement( clientId ); const [ toolbarHeight, setToolbarHeight ] = useState( 0 ); + const scrollContainer = useMemo( () => { + if ( ! contentElement ) { + return; + } + return getScrollContainer( contentElement ); + }, [ contentElement ] ); const [ props, setProps ] = useState( () => - getProps( contentElement, selectedBlockElement, toolbarHeight ) + getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight + ) ); const blockIndex = useSelect( ( select ) => select( blockEditorStore ).getBlockIndex( clientId ), @@ -98,7 +131,12 @@ export default function useBlockToolbarPopoverProps( { const updateProps = useCallback( () => setProps( - getProps( contentElement, selectedBlockElement, toolbarHeight ) + getProps( + contentElement, + selectedBlockElement, + scrollContainer, + toolbarHeight + ) ), [ contentElement, selectedBlockElement, toolbarHeight ] );