Skip to content

Commit

Permalink
Ensure block toolbar doesn't overlap block by modifying forcePosition…
Browse files Browse the repository at this point in the history
… and shift popover props (#42887)

* Flip popover placement in SelectedBlockPopover component

* Adjust shift/forcePosition props instead of placement

* Remount popover when attached to a different block

* Remove key prop

* Update hook to use an effect and handle window resize event

* Fix props not changing when elements are changed. Tidy up code and add comments.

* Remove any unrelated changes

* Handle content ref being undefined - fixes widget editor errors

* Update toolbar popover props on block movement

* Use dynamic toolbar height in hook

* Improve comments

* Add resize observer for block

* Refactor into seperate hooks

* Revert "Refactor into seperate hooks"

This reverts commit 59b192f.

* Ensure the resize observer is using the correct view

* Refactor update on block move to a different effect to reduce event binding

* Fix doc block description

* Add missing hook dependency

* Switch to useLayoutEffect

* Use lazy initial state
  • Loading branch information
talldan committed Aug 23, 2022
1 parent c692b4e commit dc2ba4b
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 13 deletions.
35 changes: 22 additions & 13 deletions packages/block-editor/src/components/block-popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,35 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useMergeRefs } from '@wordpress/compose';
import { Popover } from '@wordpress/components';
import { useMemo } from '@wordpress/element';
import { forwardRef, useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
import usePopoverScroll from './use-popover-scroll';

export default function BlockPopover( {
clientId,
bottomClientId,
children,
__unstableRefreshSize,
__unstableCoverTarget = false,
__unstablePopoverSlot,
__unstableContentRef,
...props
} ) {
function BlockPopover(
{
clientId,
bottomClientId,
children,
__unstableRefreshSize,
__unstableCoverTarget = false,
__unstablePopoverSlot,
__unstableContentRef,
...props
},
ref
) {
const selectedElement = useBlockElement( clientId );
const lastSelectedElement = useBlockElement( bottomClientId ?? clientId );
const popoverScrollRef = usePopoverScroll( __unstableContentRef );
const mergedRefs = useMergeRefs( [
ref,
usePopoverScroll( __unstableContentRef ),
] );
const style = useMemo( () => {
if ( ! selectedElement || lastSelectedElement !== selectedElement ) {
return {};
Expand All @@ -51,7 +58,7 @@ export default function BlockPopover( {

return (
<Popover
ref={ popoverScrollRef }
ref={ mergedRefs }
animate={ false }
position="top right left"
focusOnMount={ false }
Expand All @@ -74,3 +81,5 @@ export default function BlockPopover( {
</Popover>
);
}

export default forwardRef( BlockPopover );
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import BlockSelectionButton from './block-selection-button';
import BlockContextualToolbar from './block-contextual-toolbar';
import { store as blockEditorStore } from '../../store';
import BlockPopover from '../block-popover';
import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props';

function selector( select ) {
const {
Expand Down Expand Up @@ -113,6 +114,11 @@ function SelectedBlockPopover( {
// to it when re-mounting.
const initialToolbarItemIndexRef = useRef();

const popoverProps = useBlockToolbarPopoverProps( {
contentElement: __unstableContentRef?.current,
clientId,
} );

if ( ! shouldShowBreadcrumb && ! shouldShowContextualToolbar ) {
return null;
}
Expand All @@ -126,6 +132,7 @@ function SelectedBlockPopover( {
} ) }
__unstablePopoverSlot={ __unstablePopoverSlot }
__unstableContentRef={ __unstableContentRef }
{ ...popoverProps }
>
{ shouldShowContextualToolbar && (
<BlockContextualToolbar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* WordPress dependencies
*/
import { useRefEffect } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { useCallback, useLayoutEffect, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';

// By default the toolbar sets the `shift` prop. If the user scrolls the page
// down the toolbar will stay on screen by adopting a sticky position at the
// top of the viewport.
const DEFAULT_PROPS = { __unstableForcePosition: true, __unstableShift: true };

// When there isn't enough height between the top of the block and the editor
// canvas, the `shift` prop is set to `false`, as it will cause the block to be
// obscured. The `flip` behavior is enabled (by setting `forcePosition` to
// `false`), which positions the toolbar below the block.
const RESTRICTED_HEIGHT_PROPS = {
__unstableForcePosition: false,
__unstableShift: false,
};

/**
* Get the popover props for the block toolbar, determined by the space at the top of the canvas and the toolbar height.
*
* @param {Element} contentElement The DOM element that represents the editor content or canvas.
* @param {Element} selectedBlockElement The outer DOM element of the first selected block.
* @param {number} toolbarHeight The height of the toolbar in pixels.
*
* @return {Object} The popover props used to determine the position of the toolbar.
*/
function getProps( contentElement, selectedBlockElement, toolbarHeight ) {
if ( ! contentElement || ! selectedBlockElement ) {
return DEFAULT_PROPS;
}

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

if ( blockRect.top - contentRect.top > toolbarHeight ) {
return DEFAULT_PROPS;
}

return RESTRICTED_HEIGHT_PROPS;
}

/**
* Determines the desired popover positioning behavior, returning a set of appropriate props.
*
* @param {Object} elements
* @param {Element} elements.contentElement The DOM element that represents the editor content or canvas.
* @param {string} elements.clientId The clientId of the first selected block.
*
* @return {Object} The popover props used to determine the position of the toolbar.
*/
export default function useBlockToolbarPopoverProps( {
contentElement,
clientId,
} ) {
const selectedBlockElement = useBlockElement( clientId );
const [ toolbarHeight, setToolbarHeight ] = useState( 0 );
const [ props, setProps ] = useState( () =>
getProps( contentElement, selectedBlockElement, toolbarHeight )
);
const blockIndex = useSelect(
( select ) => select( blockEditorStore ).getBlockIndex( clientId ),
[ clientId ]
);

const popoverRef = useRefEffect( ( popoverNode ) => {
setToolbarHeight( popoverNode.offsetHeight );
}, [] );

const updateProps = useCallback(
() =>
setProps(
getProps( contentElement, selectedBlockElement, toolbarHeight )
),
[ contentElement, selectedBlockElement, toolbarHeight ]
);

// Update props when the block is moved. This also ensures the props are
// correct on initial mount, and when the selected block or content element
// changes (since the callback ref will update).
useLayoutEffect( updateProps, [ blockIndex, updateProps ] );

// Update props when the viewport is resized or the block is resized.
useLayoutEffect( () => {
if ( ! contentElement || ! selectedBlockElement ) {
return;
}

// Update the toolbar props on viewport resize.
const contentView = contentElement?.ownerDocument?.defaultView;
contentView?.addEventHandler?.( 'resize', updateProps );

// Update the toolbar props on block resize.
let resizeObserver;
const blockView = selectedBlockElement?.ownerDocument?.defaultView;
if ( blockView.ResizeObserver ) {
resizeObserver = new blockView.ResizeObserver( updateProps );
resizeObserver.observe( selectedBlockElement );
}

return () => {
contentView?.removeEventHandler?.( 'resize', updateProps );

if ( resizeObserver ) {
resizeObserver.disconnect();
}
};
}, [ updateProps, contentElement, selectedBlockElement ] );

return {
...props,
ref: popoverRef,
};
}

0 comments on commit dc2ba4b

Please sign in to comment.