diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 2413601a590e2e..47022e336e4869 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 { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import usePopoverScroll from './use-popover-scroll'; +import { rectUnion, getVisibleElementBounds } 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( + getVisibleElementBounds( selectedElement ), + getVisibleElementBounds( lastSelectedElement ) + ) + : getVisibleElementBounds( 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 8428222268408a..c6378130b7da42 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 @@ -47,15 +47,19 @@ export default function BlockToolbarPopover( { isToolbarForcedRef.current = false; } ); + // If the block has a parent with __experimentalCaptureToolbars enabled, + // the toolbar should be positioned over the topmost capturing parent. + const clientIdToPositionOver = capturingClientId || clientId; + const popoverProps = useBlockToolbarPopoverProps( { contentElement: __unstableContentRef?.current, - clientId, + clientId: clientIdToPositionOver, } ); return ( ! isTyping && ( component. + if ( element.classList.contains( 'components-visually-hidden' ) ) { + return false; + } + + const bounds = element.getBoundingClientRect(); + if ( bounds.width === 0 || bounds.height === 0 ) { + return false; + } + + return element.checkVisibility( { + opacityProperty: true, + contentVisibilityAuto: true, + visibilityProperty: true, + } ); +} + +/** + * Returns the rect of the element including all visible nested elements. + * + * Visible nested elements, including elements that overflow the parent, are + * taken into account. + * + * 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 represents the full extent of the element and its visible + * children, which may extend beyond the viewport. + * + * @param {Element} element Element. + * @return {DOMRect} Bounding client rect of the element and its visible children. + */ +export function getVisibleElementBounds( element ) { + const viewport = element.ownerDocument.defaultView; + if ( ! viewport ) { + return new window.DOMRect(); + } + + let bounds = element.getBoundingClientRect(); + const viewportRect = new window.DOMRectReadOnly( + 0, + 0, + viewport.innerWidth, + viewport.innerHeight + ); + + 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, viewportRect ); + stack.push( child ); + } + } + } + + return bounds; +}