Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block toolbar: restrict visible child calculation to known blocks #66702

Merged
merged 6 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/block-editor/src/components/block-popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +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';
import { rectUnion, getElementBounds } from '../../utils/dom';

const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER;

Expand Down Expand Up @@ -90,10 +90,10 @@ function BlockPopover(
getBoundingClientRect() {
return lastSelectedElement
? rectUnion(
getVisibleElementBounds( selectedElement ),
getVisibleElementBounds( lastSelectedElement )
getElementBounds( selectedElement ),
getElementBounds( lastSelectedElement )
)
: getVisibleElementBounds( selectedElement );
: getElementBounds( selectedElement );
},
contextElement: selectedElement,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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';
import { getElementBounds } from '../../utils/dom';

const COMMON_PROPS = {
placement: 'top-start',
Expand Down Expand Up @@ -68,7 +68,7 @@ function getProps(
// Get how far the content area has been scrolled.
const scrollTop = scrollContainer?.scrollTop || 0;

const blockRect = getVisibleElementBounds( selectedBlockElement );
const blockRect = getElementBounds( selectedBlockElement );
const contentRect = contentElement.getBoundingClientRect();

// Get the vertical position of top of the visible content area.
Expand Down
46 changes: 26 additions & 20 deletions packages/block-editor/src/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,41 +134,47 @@ function isScrollable( element ) {
);
}

export const WITH_OVERFLOW_ELEMENT_BLOCKS = [ 'core/navigation' ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you considered this elsewhere, but I wonder whether in the future we could make this list extensible as an editor setting? That way it can be modified in PHP for custom blocks?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for cherry picking this one!

@t-hamano had an idea to add it to the block api

I think it's worth investigating if it the issue arises again.

/**
* 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.
* Returns the bounding rectangle of an element, with special handling for blocks
* that have visible overflowing children (defined in WITH_OVERFLOW_ELEMENT_BLOCKS).
*
* For blocks like Navigation that can have overflowing elements (e.g. submenus),
* this function calculates the combined bounds of both the parent and its visible
* children. The returned rect may extend beyond the viewport.
* 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 ) {
export function getElementBounds( element ) {
const viewport = element.ownerDocument.defaultView;

if ( ! viewport ) {
return new window.DOMRectReadOnly();
}

let bounds = element.getBoundingClientRect();
const stack = [ element ];
let currentElement;

while ( ( currentElement = stack.pop() ) ) {
// Children won’t affect bounds unless the element is not scrollable.
if ( ! isScrollable( currentElement ) ) {
for ( const child of currentElement.children ) {
if ( isElementVisible( child ) ) {
const childBounds = child.getBoundingClientRect();
bounds = rectUnion( bounds, childBounds );
stack.push( child );
const dataType = element.getAttribute( 'data-type' );

/*
* For blocks with overflowing elements (like Navigation), include the bounds
* of visible children that extend beyond the parent container.
*/
if ( dataType && WITH_OVERFLOW_ELEMENT_BLOCKS.includes( dataType ) ) {
const stack = [ element ];
let currentElement;

while ( ( currentElement = stack.pop() ) ) {
// Children won’t affect bounds unless the element is not scrollable.
if ( ! isScrollable( currentElement ) ) {
for ( const child of currentElement.children ) {
if ( isElementVisible( child ) ) {
const childBounds = child.getBoundingClientRect();
bounds = rectUnion( bounds, childBounds );
stack.push( child );
}
}
}
}
Expand Down
224 changes: 224 additions & 0 deletions packages/block-editor/src/utils/test/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/**
* Internal dependencies
*/
import { getElementBounds, WITH_OVERFLOW_ELEMENT_BLOCKS } from '../dom';
describe( 'dom', () => {
describe( 'getElementBounds', () => {
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
it( 'should return a DOMRectReadOnly object if the viewport is not available', () => {
const element = {
ownerDocument: {
defaultView: null,
},
};
expect( getElementBounds( element ) ).toEqual(
new window.DOMRectReadOnly()
);
} );
it( 'should return a DOMRectReadOnly object if the viewport is available', () => {
const element = {
ownerDocument: {
defaultView: {
getComputedStyle: () => ( {
display: 'block',
visibility: 'visible',
opacity: '1',
} ),
},
},
getBoundingClientRect: () => ( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
} ),
getAttribute: ( x ) => x,
};
expect( getElementBounds( element ) ).toEqual(
new window.DOMRectReadOnly( 0, 0, 100, 100 )
);
} );
it( 'should clip left and right values when an element is larger than the viewport width', () => {
const element = window.document.createElement( 'div' );
element.getBoundingClientRect = jest.fn().mockReturnValue( {
left: -10,
top: 0,
right: window.innerWidth + 10,
bottom: 100,
width: window.innerWidth,
height: 100,
} );
expect( getElementBounds( element ).toJSON() ).toEqual( {
left: 0, // Reset to min left bound.
top: 0,
right: window.innerWidth, // Reset to max right bound.
bottom: 100,
width: window.innerWidth,
height: 100,
x: 0,
y: 0,
} );
} );
it( 'should return the parent DOMRectReadOnly object if the parent block type is not supported', () => {
const element = window.document.createElement( 'div' );
element.getBoundingClientRect = jest.fn().mockReturnValue( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
} );
element.setAttribute( 'data-type', 'test' );
const childElement = window.document.createElement( 'div' );
childElement.getBoundingClientRect = jest.fn().mockReturnValue( {
left: 0,
top: 0,
right: 333,
bottom: 333,
width: 333,
height: 333,
x: 0,
y: 0,
} );
element.appendChild( childElement );

expect( getElementBounds( element ).toJSON() ).toEqual( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
x: 0,
y: 0,
} );
} );
describe( 'With known block type', () => {
it( 'should return the child DOMRectReadOnly object if it is visible and a known block type', () => {
const element = window.document.createElement( 'div' );
element.getBoundingClientRect = jest.fn().mockReturnValue( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
} );
element.setAttribute(
'data-type',
WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ]
);
const childElement = window.document.createElement( 'div' );
childElement.getBoundingClientRect = jest
.fn()
.mockReturnValue( {
left: 0,
top: 0,
right: 333,
bottom: 333,
width: 333,
height: 333,
x: 0,
y: 0,
} );
element.appendChild( childElement );

expect( getElementBounds( element ).toJSON() ).toEqual( {
left: 0,
top: 0,
right: 333,
bottom: 333,
width: 333,
height: 333,
x: 0,
y: 0,
} );
} );
it( 'should return the parent DOMRectReadOnly if the child is scrollable', () => {
const element = window.document.createElement( 'div' );
element.setAttribute(
'data-type',
WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ]
);
element.style.overflowX = 'auto';
element.style.overflowY = 'auto';
element.getBoundingClientRect = jest.fn().mockReturnValue( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
} );
const childElement = window.document.createElement( 'div' );
childElement.getBoundingClientRect = jest
.fn()
.mockReturnValue( {
left: 0,
top: 0,
right: 333,
bottom: 333,
width: 333,
height: 333,
x: 0,
y: 0,
} );
element.appendChild( childElement );

expect( getElementBounds( element ).toJSON() ).toEqual( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
x: 0,
y: 0,
} );
} );
it( 'should return the parent DOMRectReadOnly object if the child element is not visible', () => {
const element = window.document.createElement( 'div' );
element.getBoundingClientRect = jest.fn().mockReturnValue( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
} );
element.setAttribute(
'data-type',
WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ]
);
const childElement = window.document.createElement( 'div' );
childElement.getBoundingClientRect = jest
.fn()
.mockReturnValue( {
left: 0,
top: 0,
right: 333,
bottom: 333,
width: 333,
height: 333,
x: 0,
y: 0,
} );
childElement.style.display = 'none';
element.appendChild( childElement );

expect( getElementBounds( element ).toJSON() ).toEqual( {
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
x: 0,
y: 0,
} );
} );
} );
} );
} );
Loading