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

Fix resizing items to top and left with GridItemResizer #60986

Merged
merged 13 commits into from
May 10, 2024
Merged
23 changes: 19 additions & 4 deletions packages/block-editor/src/components/block-popover/cover.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-
import BlockPopover from '.';

function BlockPopoverCover(
{ clientId, bottomClientId, children, shift = false, ...props },
{
clientId,
bottomClientId,
children,
shift = false,
additionalStyles,
...props
},
ref
) {
bottomClientId ??= clientId;
Expand All @@ -26,7 +33,10 @@ function BlockPopoverCover(
{ ...props }
>
{ selectedElement && clientId === bottomClientId ? (
<CoverContainer selectedElement={ selectedElement }>
<CoverContainer
selectedElement={ selectedElement }
additionalStyles={ additionalStyles }
>
{ children }
</CoverContainer>
) : (
Expand All @@ -36,7 +46,11 @@ function BlockPopoverCover(
);
}

function CoverContainer( { selectedElement, children } ) {
function CoverContainer( {
selectedElement,
additionalStyles = {},
children,
} ) {
const [ width, setWidth ] = useState( selectedElement.offsetWidth );
const [ height, setHeight ] = useState( selectedElement.offsetHeight );

Expand All @@ -54,8 +68,9 @@ function CoverContainer( { selectedElement, children } ) {
position: 'absolute',
width,
height,
...additionalStyles,
};
}, [ width, height ] );
}, [ width, height, additionalStyles ] );

return <div style={ style }>{ children }</div>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { ResizableBox } from '@wordpress/components';
import { useState, useRef, useEffect } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -12,14 +13,125 @@ import { getComputedCSS } from './utils';

export function GridItemResizer( { clientId, onChange } ) {
const blockElement = useBlockElement( clientId );
const rootBlockElement = blockElement?.parentElement;

if ( ! blockElement || ! rootBlockElement ) {
return null;
}

return (
<GridItemResizerInner
clientId={ clientId }
blockElement={ blockElement }
rootBlockElement={ rootBlockElement }
onChange={ onChange }
/>
);
}

function GridItemResizerInner( {
clientId,
blockElement,
rootBlockElement,
onChange,
} ) {
const [ resizeDirection, setResizeDirection ] = useState( null );
const [ enableSide, setEnableSide ] = useState( {
top: false,
bottom: false,
left: false,
right: false,
} );

useEffect( () => {
const observer = new window.ResizeObserver( () => {
const blockClientRect = blockElement.getBoundingClientRect();
const rootBlockClientRect =
rootBlockElement.getBoundingClientRect();
setEnableSide( {
top: blockClientRect.top > rootBlockClientRect.top,
bottom: blockClientRect.bottom < rootBlockClientRect.bottom,
left: blockClientRect.left > rootBlockClientRect.left,
right: blockClientRect.right < rootBlockClientRect.right,
} );
} );
observer.observe( blockElement );
return () => observer.disconnect();
}, [ blockElement, rootBlockElement ] );

/*
* This ref is necessary get the bounding client rect of the resizer,
* because it exists outside of the iframe, so its bounding client
* rect isn't the same as the block element's.
*/
noisysocks marked this conversation as resolved.
Show resolved Hide resolved
const resizerRef = useRef( null );

if ( ! blockElement ) {
return null;
}

const justification = {
right: 'flex-start',
left: 'flex-end',
};

const alignment = {
top: 'flex-end',
bottom: 'flex-start',
};

const styles = {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about doing this with a CSS classname instead of a styles object? BlockPopoverCover could have a .block-popover-cover and .block-popover-cover__cover class and .block-editor-grid-item-resizer could have .is-right-justified and .is-top-aligned variations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mean we could do that but is there any advantage to it? Using styles is more consistent with how the component works, like with the width and height props.

Copy link
Member

@noisysocks noisysocks May 8, 2024

Choose a reason for hiding this comment

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

It lets us avoid adding the new additionalStyles prop. Not a big deal since BlockPopoverCover is not a public component but nice to keep API surface area low where possible. Up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm yeah. I think the additional prop is a worthwhile trade-off for better legibility. Also the inline style means it's less likely some over-specific CSS will break this at any point 😄

display: 'flex',
justifyContent: 'center',
alignItems: 'center',
...( justification[ resizeDirection ] && {
justifyContent: justification[ resizeDirection ],
} ),
...( alignment[ resizeDirection ] && {
alignItems: alignment[ resizeDirection ],
} ),
};

/*
* The bounding element is equivalent to the root block element, but
* its bounding client rect is modified to account for the resizer
* being outside of the editor iframe.
*/
const boundingElement = {
offsetWidth: rootBlockElement.offsetWidth,
offsetHeight: rootBlockElement.offsetHeight,
getBoundingClientRect: () => {
const blockClientRect = blockElement.getBoundingClientRect();
const rootBlockClientRect =
rootBlockElement.getBoundingClientRect();
const resizerTop = resizerRef.current?.getBoundingClientRect()?.top;
// Fallback value of 60 to account for editor top bar height.
const heightDifference = resizerTop
? resizerTop - blockClientRect.top
: 60;
return {
bottom: rootBlockClientRect.bottom + heightDifference,
height: rootBlockElement.offsetHeight,
left: rootBlockClientRect.left,
right: rootBlockClientRect.right,
top: rootBlockClientRect.top + heightDifference,
width: rootBlockClientRect.width,
x: rootBlockClientRect.x,
y: rootBlockClientRect.y + heightDifference,
};
},
};

// Controller to remove event listener on resize stop.
const controller = new AbortController();

return (
<BlockPopoverCover
className="block-editor-grid-item-resizer"
clientId={ clientId }
__unstablePopoverSlot="block-toolbar"
additionalStyles={ styles }
__unstableContentRef={ resizerRef }
>
<ResizableBox
className="block-editor-grid-item-resizer__box"
Expand All @@ -28,57 +140,85 @@ export function GridItemResizer( { clientId, onChange } ) {
height: '100%',
} }
enable={ {
bottom: true,
bottom: enableSide.bottom,
bottomLeft: false,
bottomRight: false,
left: false,
right: true,
top: false,
left: enableSide.left,
right: enableSide.right,
top: enableSide.top,
topLeft: false,
topRight: false,
} }
bounds={ boundingElement }
boundsByDirection
onResizeStart={ ( event, direction ) => {
/*
* The container justification and alignment need to be set
* according to the direction the resizer is being dragged in,
* so that it resizes in the right direction.
*/
setResizeDirection( direction );

/*
* The mouseup event on the resize handle doesn't trigger if the mouse
* isn't directly above the handle, so we try to detect if it happens
* outside the grid and dispatch a mouseup event on the handle.
*/
Copy link
Member

Choose a reason for hiding this comment

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

Should this fix live in ResizableBox so that future users of the component don't run into the same issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm good question. This issue only surfaces when we set bounds on the box, so it's not really useful for all the instances. And this fix depends on knowing about a parent element, which ResizableBox doesn't. It doesn't even implement its own onResizeStart, just passes it directly to Resizable. So I'm not sure it's worth adding complexity to the component for what so far is a pretty niche use case. Maybe worth doing it if similar instances pop up in other places?

Copy link
Member

Choose a reason for hiding this comment

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

The mouseup handler could be added to ownerWindow or ownerDocument to avoid it having to know about a parent element.

But yeah, not sure if it's needed in ResizableBox, up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm not sure that'll work if we have the iframe in the way. The parent element I added the event listener to is in the actual editor canvas, but the component isn't.

const rootElementParent =
rootBlockElement.closest( 'body' );
rootElementParent.addEventListener(
Copy link
Member

Choose a reason for hiding this comment

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

We need to remove this event listener somewhere to avoid memory leaks.

'mouseup',
() => {
event.target.dispatchEvent(
new Event( 'mouseup', { bubbles: true } )
);
},
{ signal: controller.signal, capture: true }
);
} }
onResizeStop={ ( event, direction, boxElement ) => {
const gridElement = blockElement.parentElement;
const columnGap = parseFloat(
getComputedCSS( gridElement, 'column-gap' )
getComputedCSS( rootBlockElement, 'column-gap' )
);
const rowGap = parseFloat(
getComputedCSS( gridElement, 'row-gap' )
getComputedCSS( rootBlockElement, 'row-gap' )
);
const gridColumnTracks = getGridTracks(
getComputedCSS( gridElement, 'grid-template-columns' ),
getComputedCSS(
rootBlockElement,
'grid-template-columns'
),
columnGap
);
const gridRowTracks = getGridTracks(
getComputedCSS( gridElement, 'grid-template-rows' ),
getComputedCSS(
rootBlockElement,
'grid-template-rows'
),
rowGap
);
const rect = new window.DOMRect(
blockElement.offsetLeft + boxElement.offsetLeft,
blockElement.offsetTop + boxElement.offsetTop,
boxElement.offsetWidth,
boxElement.offsetHeight
);
const columnStart =
getClosestTrack(
gridColumnTracks,
blockElement.offsetLeft
) + 1;
getClosestTrack( gridColumnTracks, rect.left ) + 1;
const rowStart =
getClosestTrack(
gridRowTracks,
blockElement.offsetTop
) + 1;
getClosestTrack( gridRowTracks, rect.top ) + 1;
const columnEnd =
getClosestTrack(
gridColumnTracks,
blockElement.offsetLeft + boxElement.offsetWidth,
'end'
) + 1;
getClosestTrack( gridColumnTracks, rect.right, 'end' ) +
1;
const rowEnd =
getClosestTrack(
gridRowTracks,
blockElement.offsetTop + boxElement.offsetHeight,
'end'
) + 1;
getClosestTrack( gridRowTracks, rect.bottom, 'end' ) +
1;
onChange( {
columnSpan: columnEnd - columnStart + 1,
rowSpan: rowEnd - rowStart + 1,
} );
// Removes event listener added in onResizeStart.
controller.abort();
} }
/>
</BlockPopoverCover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
pointer-events: all !important;
}
}

Loading