-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Fix resizing items to top and left with GridItemResizer #60986
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
Changes from 8 commits
c0d99f0
333eefc
f434924
0a6f97b
84f1cb7
7e4dbb8
19e8ae2
6957c76
4bc0167
e026dde
69368e4
795d661
20315d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
* WordPress dependencies | ||
*/ | ||
import { ResizableBox } from '@wordpress/components'; | ||
import { useState, useRef } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
|
@@ -12,14 +13,89 @@ import { getComputedCSS } from './utils'; | |
|
||
export function GridItemResizer( { clientId, onChange } ) { | ||
const blockElement = useBlockElement( clientId ); | ||
const rootBlockElement = blockElement?.parentElement; | ||
const [ resizeDirection, setResizeDirection ] = useState( null ); | ||
|
||
/* | ||
* Resizer dummy is an empty div that exists only so we can | ||
* get the bounding client rect of the resizer element. This is | ||
* necessary because the resizer 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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It lets us avoid adding the new There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ], | ||
} ), | ||
}; | ||
|
||
const blockClientRect = blockElement.getBoundingClientRect(); | ||
const rootBlockClientRect = rootBlockElement.getBoundingClientRect(); | ||
|
||
/* | ||
* 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 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, | ||
}; | ||
}, | ||
}; | ||
|
||
/* | ||
* Only enable resizing to a side if that side is not on the | ||
* edge of the grid. | ||
*/ | ||
const enableTop = blockClientRect.top > rootBlockClientRect.top; | ||
const enableBottom = blockClientRect.bottom < rootBlockClientRect.bottom; | ||
const enableLeft = blockClientRect.left > rootBlockClientRect.left; | ||
const enableRight = blockClientRect.right < rootBlockClientRect.right; | ||
|
||
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" | ||
|
@@ -28,15 +104,41 @@ export function GridItemResizer( { clientId, onChange } ) { | |
height: '100%', | ||
} } | ||
enable={ { | ||
bottom: true, | ||
bottom: enableBottom, | ||
bottomLeft: false, | ||
bottomRight: false, | ||
left: false, | ||
right: true, | ||
top: false, | ||
left: enableLeft, | ||
right: enableRight, | ||
top: enableTop, | ||
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. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this fix live in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm good question. This issue only surfaces when we set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The But yeah, not sure if it's needed in There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.parentElement; | ||
rootElementParent.addEventListener( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } ) | ||
); | ||
}, | ||
true | ||
); | ||
} } | ||
onResizeStop={ ( event, direction, boxElement ) => { | ||
const gridElement = blockElement.parentElement; | ||
const columnGap = parseFloat( | ||
|
@@ -53,28 +155,22 @@ export function GridItemResizer( { clientId, onChange } ) { | |
getComputedCSS( gridElement, '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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,3 +31,4 @@ | |
pointer-events: all !important; | ||
} | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.