@wordpress/block-editor
Version:
265 lines (242 loc) • 7.34 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import {
useCallback,
useRef,
useMemo,
createContext,
useContext,
} from '@wordpress/element';
import { Popover } from '@wordpress/components';
import { isRTL } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import Inserter from '../inserter';
import { store as blockEditorStore } from '../../store';
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
export const InsertionPointOpenRef = createContext();
function InsertionPointPopover() {
const { selectBlock } = useDispatch( blockEditorStore );
const openRef = useContext( InsertionPointOpenRef );
const ref = useRef();
const {
orientation,
isHidden,
previousClientId,
nextClientId,
rootClientId,
isInserterShown,
} = useSelect( ( select ) => {
const {
getBlockOrder,
getBlockListSettings,
getMultiSelectedBlockClientIds,
getSelectedBlockClientId,
hasMultiSelection,
getSettings,
getBlockInsertionPoint,
} = select( blockEditorStore );
const insertionPoint = getBlockInsertionPoint();
const order = getBlockOrder( insertionPoint.rootClientId );
const targetClientId = order[ insertionPoint.index - 1 ];
const targetRootClientId = insertionPoint.rootClientId;
const blockOrder = getBlockOrder( targetRootClientId );
if ( ! blockOrder.length ) {
return {};
}
const previous = targetClientId
? targetClientId
: blockOrder[ blockOrder.length - 1 ];
const isLast = previous === blockOrder[ blockOrder.length - 1 ];
const next = isLast
? null
: blockOrder[ blockOrder.indexOf( previous ) + 1 ];
const { hasReducedUI } = getSettings();
const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds();
const selectedBlockClientId = getSelectedBlockClientId();
const blockOrientation =
getBlockListSettings( targetRootClientId )?.orientation ||
'vertical';
return {
previousClientId: previous,
nextClientId: next,
isHidden:
hasReducedUI ||
( hasMultiSelection()
? next && multiSelectedBlockClientIds.includes( next )
: next &&
blockOrientation === 'vertical' &&
next === selectedBlockClientId ),
orientation: blockOrientation,
clientId: targetClientId,
rootClientId: targetRootClientId,
isInserterShown: insertionPoint?.__unstableWithInserter,
};
}, [] );
const previousElement = useBlockElement( previousClientId );
const nextElement = useBlockElement( nextClientId );
const style = useMemo( () => {
if ( ! previousElement ) {
return {};
}
const previousRect = previousElement.getBoundingClientRect();
const nextRect = nextElement
? nextElement.getBoundingClientRect()
: null;
if ( orientation === 'vertical' ) {
return {
width: previousElement.offsetWidth,
height: nextRect ? nextRect.top - previousRect.bottom : 0,
};
}
let width = 0;
if ( nextElement ) {
width = isRTL()
? previousRect.left - nextRect.right
: nextRect.left - previousRect.right;
}
return {
width,
height: previousElement.offsetHeight,
};
}, [ previousElement, nextElement ] );
const getAnchorRect = useCallback( () => {
const previousRect = previousElement.getBoundingClientRect();
const nextRect = nextElement
? nextElement.getBoundingClientRect()
: null;
if ( orientation === 'vertical' ) {
if ( isRTL() ) {
return {
top: previousRect.bottom,
left: previousRect.right,
right: previousRect.left,
bottom: nextRect ? nextRect.top : previousRect.bottom,
};
}
return {
top: previousRect.bottom,
left: previousRect.left,
right: previousRect.right,
bottom: nextRect ? nextRect.top : previousRect.bottom,
};
}
if ( isRTL() ) {
return {
top: previousRect.top,
left: nextRect ? nextRect.right : previousRect.left,
right: previousRect.left,
bottom: previousRect.bottom,
};
}
return {
top: previousRect.top,
left: previousRect.right,
right: nextRect ? nextRect.left : previousRect.right,
bottom: previousRect.bottom,
};
}, [ previousElement, nextElement ] );
if ( ! previousElement ) {
return null;
}
const className = classnames(
'block-editor-block-list__insertion-point',
'is-' + orientation
);
function onClick( event ) {
if ( event.target === ref.current && nextClientId ) {
selectBlock( nextClientId, -1 );
}
}
function onFocus( event ) {
// Only handle click on the wrapper specifically, and not an event
// bubbled from the inserter itself.
if ( event.target !== ref.current ) {
openRef.current = true;
}
}
// Only show the inserter when there's a `nextElement` (a block after the
// insertion point). At the end of the block list the trailing appender
// should serve the purpose of inserting blocks.
const showInsertionPointInserter =
! isHidden && nextElement && isInserterShown;
// Show the indicator if the insertion point inserter is visible, or if
// the `showInsertionPoint` state is `true`. The latter is generally true
// when hovering blocks for insertion in the block library.
const showInsertionPointIndicator =
showInsertionPointInserter || ! isHidden;
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
// While ideally it would be enough to capture the
// bubbling focus event from the Inserter, due to the
// characteristics of click focusing of `button`s in
// Firefox and Safari, it is not reliable.
//
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
return (
<Popover
noArrow
animate={ false }
getAnchorRect={ getAnchorRect }
focusOnMount={ false }
className="block-editor-block-list__insertion-point-popover"
__unstableSlotName="block-toolbar"
>
<div
ref={ ref }
tabIndex={ -1 }
onClick={ onClick }
onFocus={ onFocus }
className={ classnames( className, {
'is-with-inserter': showInsertionPointInserter,
} ) }
style={ style }
>
{ showInsertionPointIndicator && (
<div className="block-editor-block-list__insertion-point-indicator" />
) }
{ showInsertionPointInserter && (
<div
className={ classnames(
'block-editor-block-list__insertion-point-inserter'
) }
>
<Inserter
position="bottom center"
clientId={ nextClientId }
rootClientId={ rootClientId }
__experimentalIsQuick
onToggle={ ( isOpen ) => {
openRef.current = isOpen;
} }
onSelectOrClose={ () => {
openRef.current = false;
} }
/>
</div>
) }
</div>
</Popover>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
}
export default function InsertionPoint( { children } ) {
const isVisible = useSelect( ( select ) => {
const { isMultiSelecting, isBlockInsertionPointVisible } = select(
blockEditorStore
);
return isBlockInsertionPointVisible() && ! isMultiSelecting();
}, [] );
return (
<InsertionPointOpenRef.Provider value={ useRef( false ) }>
{ isVisible && <InsertionPointPopover /> }
{ children }
</InsertionPointOpenRef.Provider>
);
}