Skip to content

Commit 59fdcb3

Browse files
noisysocksramonjdkevin940726aaronrobertshaw
authored andcommitted
Position BlockToolbar below all of the selected block's descendants (WordPress#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>
1 parent e1d364c commit 59fdcb3

File tree

4 files changed

+122
-31
lines changed

4 files changed

+122
-31
lines changed

packages/block-editor/src/components/block-popover/index.js

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
*/
2121
import { useBlockElement } from '../block-list/use-block-props/use-block-refs';
2222
import usePopoverScroll from './use-popover-scroll';
23+
import { rectUnion, getVisibleElementBounds } from '../../utils/dom';
2324

2425
const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER;
2526

@@ -87,34 +88,12 @@ function BlockPopover(
8788

8889
return {
8990
getBoundingClientRect() {
90-
const selectedBCR = selectedElement.getBoundingClientRect();
91-
const lastSelectedBCR =
92-
lastSelectedElement?.getBoundingClientRect();
93-
94-
// Get the biggest rectangle that encompasses completely the currently
95-
// selected element and the last selected element:
96-
// - for top/left coordinates, use the smaller numbers
97-
// - for the bottom/right coordinates, use the largest numbers
98-
const left = Math.min(
99-
selectedBCR.left,
100-
lastSelectedBCR?.left ?? Infinity
101-
);
102-
const top = Math.min(
103-
selectedBCR.top,
104-
lastSelectedBCR?.top ?? Infinity
105-
);
106-
const right = Math.max(
107-
selectedBCR.right,
108-
lastSelectedBCR.right ?? -Infinity
109-
);
110-
const bottom = Math.max(
111-
selectedBCR.bottom,
112-
lastSelectedBCR.bottom ?? -Infinity
113-
);
114-
const width = right - left;
115-
const height = bottom - top;
116-
117-
return new window.DOMRect( left, top, width, height );
91+
return lastSelectedElement
92+
? rectUnion(
93+
getVisibleElementBounds( selectedElement ),
94+
getVisibleElementBounds( lastSelectedElement )
95+
)
96+
: getVisibleElementBounds( selectedElement );
11897
},
11998
contextElement: selectedElement,
12099
};

packages/block-editor/src/components/block-tools/block-toolbar-popover.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,19 @@ export default function BlockToolbarPopover( {
4747
isToolbarForcedRef.current = false;
4848
} );
4949

50+
// If the block has a parent with __experimentalCaptureToolbars enabled,
51+
// the toolbar should be positioned over the topmost capturing parent.
52+
const clientIdToPositionOver = capturingClientId || clientId;
53+
5054
const popoverProps = useBlockToolbarPopoverProps( {
5155
contentElement: __unstableContentRef?.current,
52-
clientId,
56+
clientId: clientIdToPositionOver,
5357
} );
5458

5559
return (
5660
! isTyping && (
5761
<BlockPopover
58-
clientId={ capturingClientId || clientId }
62+
clientId={ clientIdToPositionOver }
5963
bottomClientId={ lastClientId }
6064
className={ clsx( 'block-editor-block-list__block-popover', {
6165
'is-insertion-point-visible': isInsertionPointVisible,

packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { store as blockEditorStore } from '../../store';
1818
import { useBlockElement } from '../block-list/use-block-props/use-block-refs';
1919
import { hasStickyOrFixedPositionValue } from '../../hooks/position';
20+
import { getVisibleElementBounds } from '../../utils/dom';
2021

2122
const COMMON_PROPS = {
2223
placement: 'top-start',
@@ -67,7 +68,7 @@ function getProps(
6768
// Get how far the content area has been scrolled.
6869
const scrollTop = scrollContainer?.scrollTop || 0;
6970

70-
const blockRect = selectedBlockElement.getBoundingClientRect();
71+
const blockRect = getVisibleElementBounds( selectedBlockElement );
7172
const contentRect = contentElement.getBoundingClientRect();
7273

7374
// Get the vertical position of top of the visible content area.

packages/block-editor/src/utils/dom.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,110 @@ export function getBlockClientId( node ) {
5757

5858
return blockNode.id.slice( 'block-'.length );
5959
}
60+
61+
/**
62+
* Calculates the union of two rectangles, and optionally constrains this union within a containerRect's
63+
* left and right values.
64+
* The function returns a new DOMRect object representing this union.
65+
*
66+
* @param {DOMRect} rect1 First rectangle.
67+
* @param {DOMRect} rect2 Second rectangle.
68+
* @param {DOMRectReadOnly?} containerRect An optional container rectangle. The union will be clipped to this rectangle.
69+
* @return {DOMRect} Union of the two rectangles.
70+
*/
71+
export function rectUnion( rect1, rect2, containerRect ) {
72+
let left = Math.min( rect1.left, rect2.left );
73+
let right = Math.max( rect1.right, rect2.right );
74+
const bottom = Math.max( rect1.bottom, rect2.bottom );
75+
const top = Math.min( rect1.top, rect2.top );
76+
77+
/*
78+
* To calculate visible bounds using rectUnion, take into account the outer
79+
* horizontal limits of the container in which an element is supposed to be "visible".
80+
* For example, if an element is positioned -10px to the left of the window x value (0),
81+
* this function discounts the negative overhang because it's not visible and
82+
* therefore not to be counted in the visibility calculations.
83+
* Top and bottom values are not accounted for to accommodate vertical scroll.
84+
*/
85+
if ( containerRect ) {
86+
left = Math.max( left, containerRect.left );
87+
right = Math.min( right, containerRect.right );
88+
}
89+
90+
return new window.DOMRect( left, top, right - left, bottom - top );
91+
}
92+
93+
/**
94+
* Returns whether an element is visible.
95+
*
96+
* @param {Element} element Element.
97+
* @return {boolean} Whether the element is visible.
98+
*/
99+
function isElementVisible( element ) {
100+
const viewport = element.ownerDocument.defaultView;
101+
if ( ! viewport ) {
102+
return false;
103+
}
104+
105+
// Check for <VisuallyHidden> component.
106+
if ( element.classList.contains( 'components-visually-hidden' ) ) {
107+
return false;
108+
}
109+
110+
const bounds = element.getBoundingClientRect();
111+
if ( bounds.width === 0 || bounds.height === 0 ) {
112+
return false;
113+
}
114+
115+
return element.checkVisibility( {
116+
opacityProperty: true,
117+
contentVisibilityAuto: true,
118+
visibilityProperty: true,
119+
} );
120+
}
121+
122+
/**
123+
* Returns the rect of the element including all visible nested elements.
124+
*
125+
* Visible nested elements, including elements that overflow the parent, are
126+
* taken into account.
127+
*
128+
* This function is useful for calculating the visible area of a block that
129+
* contains nested elements that overflow the block, e.g. the Navigation block,
130+
* which can contain overflowing Submenu blocks.
131+
*
132+
* The returned rect represents the full extent of the element and its visible
133+
* children, which may extend beyond the viewport.
134+
*
135+
* @param {Element} element Element.
136+
* @return {DOMRect} Bounding client rect of the element and its visible children.
137+
*/
138+
export function getVisibleElementBounds( element ) {
139+
const viewport = element.ownerDocument.defaultView;
140+
if ( ! viewport ) {
141+
return new window.DOMRect();
142+
}
143+
144+
let bounds = element.getBoundingClientRect();
145+
const viewportRect = new window.DOMRectReadOnly(
146+
0,
147+
0,
148+
viewport.innerWidth,
149+
viewport.innerHeight
150+
);
151+
152+
const stack = [ element ];
153+
let currentElement;
154+
155+
while ( ( currentElement = stack.pop() ) ) {
156+
for ( const child of currentElement.children ) {
157+
if ( isElementVisible( child ) ) {
158+
const childBounds = child.getBoundingClientRect();
159+
bounds = rectUnion( bounds, childBounds, viewportRect );
160+
stack.push( child );
161+
}
162+
}
163+
}
164+
165+
return bounds;
166+
}

0 commit comments

Comments
 (0)