Skip to content

Commit

Permalink
Position BlockToolbar below all of the selected block's descendants (W…
Browse files Browse the repository at this point in the history
…ordPress#62711)

* Position BlockToolbar below all of the selected block's descendants

* Fix scrolling

* Don't use window global

* Explain what capturingClientId is

* No need to clip bounds to viewport

* Use explicit check for VisuallyHidden

* To calculate visible bounds using rectUnion, take into account the outer limits of the container in which an element is supposed to be "visible"
For example, if an element is positioned -10px to the left of the window x value (0), we should discount the negative overhang because it's not visible and therefore to be counted in the visible calculations.

* switch to checkVisibility DOM method

---------

Co-authored-by: noisysocks <noisysocks@git.wordpress.org>
Co-authored-by: ramonopoly <ramonopoly@git.wordpress.org>
Co-authored-by: kevin940726 <kevin940726@git.wordpress.org>
Co-authored-by: aaronrobertshaw <aaronrobertshaw@git.wordpress.org>
  • Loading branch information
5 people authored and bph committed Aug 31, 2024
1 parent e1d364c commit 59fdcb3
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 31 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 { 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;

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(
getVisibleElementBounds( selectedElement ),
getVisibleElementBounds( lastSelectedElement )
)
: getVisibleElementBounds( selectedElement );
},
contextElement: selectedElement,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
<BlockPopover
clientId={ capturingClientId || clientId }
clientId={ clientIdToPositionOver }
bottomClientId={ lastClientId }
className={ clsx( 'block-editor-block-list__block-popover', {
'is-insertion-point-visible': isInsertionPointVisible,
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 { useBlockElement } from '../block-list/use-block-props/use-block-refs';
import { hasStickyOrFixedPositionValue } from '../../hooks/position';
import { getVisibleElementBounds } 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 = getVisibleElementBounds( selectedBlockElement );
const contentRect = contentElement.getBoundingClientRect();

// Get the vertical position of top of the visible content area.
Expand Down
107 changes: 107 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,110 @@ export function getBlockClientId( node ) {

return blockNode.id.slice( 'block-'.length );
}

/**
* Calculates the union of two rectangles, and optionally constrains this union within a containerRect's
* left and right values.
* The function returns a new DOMRect object representing this union.
*
* @param {DOMRect} rect1 First rectangle.
* @param {DOMRect} rect2 Second rectangle.
* @param {DOMRectReadOnly?} containerRect An optional container rectangle. The union will be clipped to this rectangle.
* @return {DOMRect} Union of the two rectangles.
*/
export function rectUnion( rect1, rect2, containerRect ) {
let left = Math.min( rect1.left, rect2.left );
let right = Math.max( rect1.right, rect2.right );
const bottom = Math.max( rect1.bottom, rect2.bottom );
const top = Math.min( rect1.top, rect2.top );

/*
* To calculate visible bounds using rectUnion, take into account the outer
* horizontal limits of the container in which an element is supposed to be "visible".
* For example, if an element is positioned -10px to the left of the window x value (0),
* this function discounts the negative overhang because it's not visible and
* therefore not to be counted in the visibility calculations.
* Top and bottom values are not accounted for to accommodate vertical scroll.
*/
if ( containerRect ) {
left = Math.max( left, containerRect.left );
right = Math.min( right, containerRect.right );
}

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 viewport = element.ownerDocument.defaultView;
if ( ! viewport ) {
return false;
}

// Check for <VisuallyHidden> 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;
}

0 comments on commit 59fdcb3

Please sign in to comment.