From a009ab1c13f9c7936473e74f7e9a5aba90b8c7c2 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 17 Jun 2024 16:15:35 +1000 Subject: [PATCH] Position BlockToolbar below all of the selected block's descendants --- .../src/components/block-popover/index.js | 35 ++----- .../block-tools/block-toolbar-popover.js | 2 +- .../use-block-toolbar-popover-props.js | 3 +- packages/block-editor/src/utils/dom.js | 98 +++++++++++++++++++ 4 files changed, 108 insertions(+), 30 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index cc8d832c31bc70..f08ef99622fa00 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -20,6 +20,7 @@ import { */ import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; import usePopoverScroll from './use-popover-scroll'; +import { rectUnion, getVisibleBoundingRect } from '../../utils/dom'; const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER; @@ -87,34 +88,12 @@ function BlockPopover( return { getBoundingClientRect() { - const selectedBCR = selectedElement.getBoundingClientRect(); - const lastSelectedBCR = - lastSelectedElement?.getBoundingClientRect(); - - // Get the biggest rectangle that encompasses completely the currently - // selected element and the last selected element: - // - for top/left coordinates, use the smaller numbers - // - for the bottom/right coordinates, use the largest numbers - const left = Math.min( - selectedBCR.left, - lastSelectedBCR?.left ?? Infinity - ); - const top = Math.min( - selectedBCR.top, - lastSelectedBCR?.top ?? Infinity - ); - const right = Math.max( - selectedBCR.right, - lastSelectedBCR.right ?? -Infinity - ); - const bottom = Math.max( - selectedBCR.bottom, - lastSelectedBCR.bottom ?? -Infinity - ); - const width = right - left; - const height = bottom - top; - - return new window.DOMRect( left, top, width, height ); + return lastSelectedElement + ? rectUnion( + getVisibleBoundingRect( selectedElement ), + getVisibleBoundingRect( lastSelectedElement ) + ) + : getVisibleBoundingRect( selectedElement ); }, contextElement: selectedElement, }; diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js index eef5b5afca304d..f97a4fa3b2555c 100644 --- a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js +++ b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js @@ -49,7 +49,7 @@ export default function BlockToolbarPopover( { const popoverProps = useBlockToolbarPopoverProps( { contentElement: __unstableContentRef?.current, - clientId, + clientId: capturingClientId || clientId, } ); return ( 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 f99323dd5c80a7..bc4c0e6aa8a4d1 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 @@ -17,6 +17,7 @@ import { import { store as blockEditorStore } from '../../store'; import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; import { hasStickyOrFixedPositionValue } from '../../hooks/position'; +import { getVisibleBoundingRect } from '../../utils/dom'; const COMMON_PROPS = { placement: 'top-start', @@ -67,7 +68,7 @@ function getProps( // Get how far the content area has been scrolled. const scrollTop = scrollContainer?.scrollTop || 0; - const blockRect = selectedBlockElement.getBoundingClientRect(); + const blockRect = getVisibleBoundingRect( selectedBlockElement ); const contentRect = contentElement.getBoundingClientRect(); // Get the vertical position of top of the visible content area. diff --git a/packages/block-editor/src/utils/dom.js b/packages/block-editor/src/utils/dom.js index 6af35aff730155..df6ab797c98130 100644 --- a/packages/block-editor/src/utils/dom.js +++ b/packages/block-editor/src/utils/dom.js @@ -57,3 +57,101 @@ export function getBlockClientId( node ) { return blockNode.id.slice( 'block-'.length ); } + +/** + * Returns the union of two DOMRect objects. + * + * @param {DOMRect} rect1 First rectangle. + * @param {DOMRect} rect2 Second rectangle. + * @return {DOMRect} Union of the two rectangles. + */ +export function rectUnion( rect1, rect2 ) { + const left = Math.min( rect1.left, rect2.left ); + const top = Math.min( rect1.top, rect2.top ); + const right = Math.max( rect1.right, rect2.right ); + const bottom = Math.max( rect1.bottom, rect2.bottom ); + return new window.DOMRect( left, top, right - left, bottom - top ); +} + +/** + * Returns the intersection of two DOMRect objects. + * + * @param {DOMRect} rect1 First rectangle. + * @param {DOMRect} rect2 Second rectangle. + * @return {DOMRect} Intersection of the two rectangles. + */ +function rectIntersect( rect1, rect2 ) { + const left = Math.max( rect1.left, rect2.left ); + const top = Math.max( rect1.top, rect2.top ); + const right = Math.min( rect1.right, rect2.right ); + const bottom = Math.min( rect1.bottom, rect2.bottom ); + return new window.DOMRect( left, top, right - left, bottom - top ); +} + +/** + * Returns whether an element is visible. + * + * @param {Element} element Element. + * @return {boolean} Whether the element is visible. + */ +function isElementVisible( element ) { + const style = window.getComputedStyle( element ); + if ( + style.display === 'none' || + style.visibility === 'hidden' || + style.opacity === '0' + ) { + return false; + } + + const bounds = element.getBoundingClientRect(); + return ( + bounds.width > 0 && + bounds.height > 0 && + bounds.right >= 0 && + bounds.bottom >= 0 && + bounds.left <= window.innerWidth && + bounds.top <= window.innerHeight + ); +} + +/** + * Returns the rect of the element that is visible in the viewport. + * + * Visible nested elements, including elements that overflow the parent, are + * taken into account. The returned rect is clipped to the viewport. + * + * This function is useful for calculating the visible area of a block that + * contains nested elements that overflow the block, e.g. the Navigation block, + * which can contain overflowing Submenu blocks. + * + * The returned rect is suitable for passing to the Popover component to + * position the popover relative to the visible area of the block. + * + * @param {Element} element Element. + * @return {DOMRect} Bounding client rect. + */ +export function getVisibleBoundingRect( element ) { + let bounds = element.getBoundingClientRect(); + + const stack = [ element ]; + let currentElement; + + while ( ( currentElement = stack.pop() ) ) { + for ( const child of currentElement.children ) { + if ( isElementVisible( child ) ) { + const childBounds = child.getBoundingClientRect(); + bounds = rectUnion( bounds, childBounds ); + stack.push( child ); + } + } + } + + const viewportRect = new window.DOMRect( + 0, + 0, + window.innerWidth, + window.innerHeight + ); + return rectIntersect( bounds, viewportRect ); +}