Skip to content
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
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,
};
}