diff --git a/packages/block-editor/src/components/block-popover/cover.js b/packages/block-editor/src/components/block-popover/cover.js index 6d2d5b8ce1ac0..02c31ca1f2dfe 100644 --- a/packages/block-editor/src/components/block-popover/cover.js +++ b/packages/block-editor/src/components/block-popover/cover.js @@ -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; @@ -26,7 +33,10 @@ function BlockPopoverCover( { ...props } > { selectedElement && clientId === bottomClientId ? ( - + { children } ) : ( @@ -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 ); @@ -54,8 +68,9 @@ function CoverContainer( { selectedElement, children } ) { position: 'absolute', width, height, + ...additionalStyles, }; - }, [ width, height ] ); + }, [ width, height, additionalStyles ] ); return
{ children }
; } diff --git a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js index 33d677910a712..bc82cb9d8efc0 100644 --- a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js +++ b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { ResizableBox } from '@wordpress/components'; +import { useState, useRef, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -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 ( + + ); +} + +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. + */ + 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 = { + 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 ( { + /* + * 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. + */ + const rootElementParent = + rootBlockElement.closest( 'body' ); + rootElementParent.addEventListener( + '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(); } } /> diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss index 45140e59c7af9..9d5d306eadaa7 100644 --- a/packages/block-editor/src/components/grid-visualizer/style.scss +++ b/packages/block-editor/src/components/grid-visualizer/style.scss @@ -31,3 +31,4 @@ pointer-events: all !important; } } +