Skip to content

Commit

Permalink
Position BlockToolbar below all of the selected block's descendants
Browse files Browse the repository at this point in the history
  • Loading branch information
noisysocks committed Jun 21, 2024
1 parent 5738e9b commit 69e3b3c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 30 deletions.
35 changes: 7 additions & 28 deletions packages/block-editor/src/components/block-popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function BlockToolbarPopover( {

const popoverProps = useBlockToolbarPopoverProps( {
contentElement: __unstableContentRef?.current,
clientId,
clientId: capturingClientId || clientId,
} );

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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.
Expand Down
98 changes: 98 additions & 0 deletions packages/block-editor/src/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

0 comments on commit 69e3b3c

Please sign in to comment.