diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 370959d1b145c..597f61eb94910 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -6,8 +6,9 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { useMergeRefs } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; -import { useMemo } from '@wordpress/element'; +import { forwardRef, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -15,19 +16,25 @@ import { useMemo } from '@wordpress/element'; import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; import usePopoverScroll from './use-popover-scroll'; -export default function BlockPopover( { - clientId, - bottomClientId, - children, - __unstableRefreshSize, - __unstableCoverTarget = false, - __unstablePopoverSlot, - __unstableContentRef, - ...props -} ) { +function BlockPopover( + { + clientId, + bottomClientId, + children, + __unstableRefreshSize, + __unstableCoverTarget = false, + __unstablePopoverSlot, + __unstableContentRef, + ...props + }, + ref +) { const selectedElement = useBlockElement( clientId ); const lastSelectedElement = useBlockElement( bottomClientId ?? clientId ); - const popoverScrollRef = usePopoverScroll( __unstableContentRef ); + const mergedRefs = useMergeRefs( [ + ref, + usePopoverScroll( __unstableContentRef ), + ] ); const style = useMemo( () => { if ( ! selectedElement || lastSelectedElement !== selectedElement ) { return {}; @@ -51,7 +58,7 @@ export default function BlockPopover( { return ( ); } + +export default forwardRef( BlockPopover ); diff --git a/packages/block-editor/src/components/block-tools/selected-block-popover.js b/packages/block-editor/src/components/block-tools/selected-block-popover.js index 962063ba284b6..652c04efe6aed 100644 --- a/packages/block-editor/src/components/block-tools/selected-block-popover.js +++ b/packages/block-editor/src/components/block-tools/selected-block-popover.js @@ -20,6 +20,7 @@ import BlockSelectionButton from './block-selection-button'; import BlockContextualToolbar from './block-contextual-toolbar'; import { store as blockEditorStore } from '../../store'; import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; function selector( select ) { const { @@ -113,6 +114,11 @@ function SelectedBlockPopover( { // to it when re-mounting. const initialToolbarItemIndexRef = useRef(); + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + if ( ! shouldShowBreadcrumb && ! shouldShowContextualToolbar ) { return null; } @@ -126,6 +132,7 @@ function SelectedBlockPopover( { } ) } __unstablePopoverSlot={ __unstablePopoverSlot } __unstableContentRef={ __unstableContentRef } + { ...popoverProps } > { shouldShowContextualToolbar && ( toolbarHeight ) { + return DEFAULT_PROPS; + } + + return RESTRICTED_HEIGHT_PROPS; +} + +/** + * Determines the desired popover positioning behavior, returning a set of appropriate props. + * + * @param {Object} elements + * @param {Element} elements.contentElement The DOM element that represents the editor content or canvas. + * @param {string} elements.clientId The clientId of the first selected block. + * + * @return {Object} The popover props used to determine the position of the toolbar. + */ +export default function useBlockToolbarPopoverProps( { + contentElement, + clientId, +} ) { + const selectedBlockElement = useBlockElement( clientId ); + const [ toolbarHeight, setToolbarHeight ] = useState( 0 ); + const [ props, setProps ] = useState( () => + getProps( contentElement, selectedBlockElement, toolbarHeight ) + ); + const blockIndex = useSelect( + ( select ) => select( blockEditorStore ).getBlockIndex( clientId ), + [ clientId ] + ); + + const popoverRef = useRefEffect( ( popoverNode ) => { + setToolbarHeight( popoverNode.offsetHeight ); + }, [] ); + + const updateProps = useCallback( + () => + setProps( + getProps( contentElement, selectedBlockElement, toolbarHeight ) + ), + [ contentElement, selectedBlockElement, toolbarHeight ] + ); + + // Update props when the block is moved. This also ensures the props are + // correct on initial mount, and when the selected block or content element + // changes (since the callback ref will update). + useLayoutEffect( updateProps, [ blockIndex, updateProps ] ); + + // Update props when the viewport is resized or the block is resized. + useLayoutEffect( () => { + if ( ! contentElement || ! selectedBlockElement ) { + return; + } + + // Update the toolbar props on viewport resize. + const contentView = contentElement?.ownerDocument?.defaultView; + contentView?.addEventHandler?.( 'resize', updateProps ); + + // Update the toolbar props on block resize. + let resizeObserver; + const blockView = selectedBlockElement?.ownerDocument?.defaultView; + if ( blockView.ResizeObserver ) { + resizeObserver = new blockView.ResizeObserver( updateProps ); + resizeObserver.observe( selectedBlockElement ); + } + + return () => { + contentView?.removeEventHandler?.( 'resize', updateProps ); + + if ( resizeObserver ) { + resizeObserver.disconnect(); + } + }; + }, [ updateProps, contentElement, selectedBlockElement ] ); + + return { + ...props, + ref: popoverRef, + }; +}