Skip to content
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

Support both horizontal and vertical in-between inserters #27860

Merged
merged 11 commits into from
Jan 19, 2021
268 changes: 172 additions & 96 deletions packages/block-editor/src/components/block-list/insertion-point.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,40 @@
* External dependencies
*/
import classnames from 'classnames';
import { last } from 'lodash';

/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { useState, useRef, useEffect, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import {
useState,
useEffect,
useCallback,
useRef,
useMemo,
} from '@wordpress/element';
import { Popover } from '@wordpress/components';
import { placeCaretAtVerticalEdge } from '@wordpress/dom';
import { isRTL } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import Inserter from '../inserter';
import { getClosestTabbable } from '../writing-flow';
import { getBlockDOMNode } from '../../utils/dom';

function InsertionPointInserter( {
clientId,
setIsInserterForced,
containerRef,
} ) {
const ref = useRef();
// Hide the inserter above the selected block and during multi-selection.
const isInserterHidden = useSelect(
( select ) => {
const {
getMultiSelectedBlockClientIds,
getSelectedBlockClientId,
hasMultiSelection,
getSettings,
} = select( 'core/block-editor' );
const { hasReducedUI } = getSettings();
if ( hasReducedUI ) {
return true;
}
const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds();
const selectedBlockClientId = getSelectedBlockClientId();
return hasMultiSelection()
? multiSelectedBlockClientIds.includes( clientId )
: clientId === selectedBlockClientId;
},
[ clientId ]
);

function focusClosestTabbable( event ) {
const { clientX, clientY, target } = event;

// Only handle click on the wrapper specifically, and not an event
// bubbled from the inserter itself.
if ( target !== ref.current ) {
return;
}

const { ownerDocument } = containerRef.current;
const targetRect = target.getBoundingClientRect();
const isReverse = clientY < targetRect.top + targetRect.height / 2;
const blockNode = getBlockDOMNode( clientId, ownerDocument );
const container = isReverse ? containerRef.current : blockNode;
const closest =
getClosestTabbable( blockNode, true, container ) || blockNode;
const rect = new window.DOMRect( clientX, clientY, 0, 16 );

placeCaretAtVerticalEdge( closest, isReverse, rect, false );
Comment on lines -59 to -68
Copy link
Contributor

@stokesman stokesman Feb 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will want to bring this logic back though it probably needs adaptation for horizontal contexts. See #28682 (comment)

}

function InsertionPointInserter( { clientId, setIsInserterForced } ) {
return (
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<div
ref={ ref }
onFocus={ () => setIsInserterForced( true ) }
onBlur={ () => setIsInserterForced( false ) }
onClick={ focusClosestTabbable }
// 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
tabIndex={ -1 }
className={ classnames(
'block-editor-block-list__insertion-point-inserter',
{
'is-inserter-hidden': isInserterHidden,
}
'block-editor-block-list__insertion-point-inserter'
) }
>
<Inserter
position="bottom center"
clientId={ clientId }
__experimentalIsQuick
onToggle={ setIsInserterForced }
onSelectOrClose={ () => setIsInserterForced( false ) }
/>
</div>
);
Expand All @@ -107,56 +50,159 @@ function InsertionPointPopover( {
containerRef,
showInsertionPoint,
} ) {
const element = useSelect(
const { selectBlock } = useDispatch( 'core/block-editor' );
const ref = useRef();

const { previousElement, nextElement, orientation, isHidden } = useSelect(
( select ) => {
const { getBlockOrder } = select( 'core/block-editor' );
const {
getBlockOrder,
getBlockRootClientId,
getBlockListSettings,
getMultiSelectedBlockClientIds,
getSelectedBlockClientId,
hasMultiSelection,
getSettings,
} = select( 'core/block-editor' );
const { ownerDocument } = containerRef.current;
const targetClientId =
clientId || last( getBlockOrder( rootClientId ) );
const targetRootClientId = clientId
? getBlockRootClientId( clientId )
: rootClientId;
Comment on lines +68 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why the logic wasn't something like:

const targetRootClientId = rootClientId ? rootClientId : getBlockRootClientId( clientId );

I'm sure there's a good reason, but might be worth leaving a comment for others reading the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to do with the fact that this is both used for the insertion point when the inserter is opened (use the insertion poin t from there) and the hovered insertion point.

const blockOrder = getBlockOrder( targetRootClientId );
if ( blockOrder.length < 2 ) {
return {};
}
const next = clientId
? clientId
: blockOrder[ blockOrder.length - 1 ];
const previous = blockOrder[ blockOrder.indexOf( next ) - 1 ];
const { hasReducedUI } = getSettings();
const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds();
const selectedBlockClientId = getSelectedBlockClientId();
const blockOrientation =
getBlockListSettings( targetRootClientId )?.orientation ||
'vertical';

return getBlockDOMNode( targetClientId, ownerDocument );
return {
previousElement: getBlockDOMNode( previous, ownerDocument ),
nextElement: getBlockDOMNode( next, ownerDocument ),
isHidden:
hasReducedUI ||
( hasMultiSelection()
? multiSelectedBlockClientIds.includes( clientId )
: blockOrientation === 'vertical' &&
clientId === selectedBlockClientId ),
orientation: blockOrientation,
};
},
[ clientId, rootClientId ]
);

const position = clientId ? 'top' : 'bottom';
const className = classnames( 'block-editor-block-list__insertion-point', {
'is-insert-after': ! clientId,
} );
const style = useMemo( () => {
if ( ! previousElement || ! nextElement ) {
return {};
}
const previousRect = previousElement.getBoundingClientRect();
const nextRect = nextElement.getBoundingClientRect();

return orientation === 'vertical'
? {
width: previousElement.offsetWidth,
height: nextRect.top - previousRect.bottom,
}
: {
width: isRTL()
? previousRect.left - nextRect.right
: nextRect.left - previousRect.right,
height: previousElement.offsetHeight,
};
}, [ previousElement, nextElement ] );

const getAnchorRect = useCallback( () => {
const previousRect = previousElement.getBoundingClientRect();
const nextRect = nextElement.getBoundingClientRect();
if ( orientation === 'vertical' ) {
return {
top: previousRect.bottom,
left: previousRect.left,
right: previousRect.right,
bottom: nextRect.top,
};
}
return {
top: previousRect.top,
left: isRTL() ? nextRect.right : previousRect.right,
right: isRTL() ? previousRect.left : nextRect.left,
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 ) {
selectBlock( clientId, -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 ) {
setIsInserterForced( true );
}
}

/* 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 }
anchorRef={ element }
position={ `${ position } right left` }
getAnchorRect={ getAnchorRect }
focusOnMount={ false }
className="block-editor-block-list__insertion-point-popover"
__unstableSlotName="block-toolbar"
__unstableForcePosition={ true }
>
<div
ref={ ref }
tabIndex={ -1 }
onClick={ onClick }
onFocus={ onFocus }
className={ className }
style={ { width: element?.offsetWidth } }
style={ style }
>
{ ( showInsertionPoint ||
isInserterShown ||
isInserterForced ) && (
<div className="block-editor-block-list__insertion-point-indicator" />
) }
{ ( isInserterShown || isInserterForced ) && (
{ ! isHidden &&
( showInsertionPoint ||
isInserterShown ||
isInserterForced ) && (
<div className="block-editor-block-list__insertion-point-indicator" />
) }
{ ! isHidden && ( isInserterShown || isInserterForced ) && (
<InsertionPointInserter
clientId={ clientId }
setIsInserterForced={ setIsInserterForced }
containerRef={ containerRef }
/>
) }
</div>
</Popover>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
}

export default function InsertionPoint( ref ) {
export default function useInsertionPoint( ref ) {
const [ isInserterShown, setIsInserterShown ] = useState( false );
const [ isInserterForced, setIsInserterForced ] = useState( false );
const [ inserterClientId, setInserterClientId ] = useState( null );
Expand All @@ -165,18 +211,21 @@ export default function InsertionPoint( ref ) {
isInserterVisible,
selectedClientId,
selectedRootClientId,
getBlockListSettings,
} = useSelect( ( select ) => {
const {
isMultiSelecting: _isMultiSelecting,
isBlockInsertionPointVisible,
getBlockInsertionPoint,
getBlockOrder,
getBlockListSettings: _getBlockListSettings,
} = select( 'core/block-editor' );

const insertionPoint = getBlockInsertionPoint();
const order = getBlockOrder( insertionPoint.rootClientId );

return {
getBlockListSettings: _getBlockListSettings,
isMultiSelecting: _isMultiSelecting(),
isInserterVisible: isBlockInsertionPointVisible(),
selectedClientId: order[ insertionPoint.index ],
Expand All @@ -197,11 +246,29 @@ export default function InsertionPoint( ref ) {
return;
}

let rootClientId;
if ( ! event.target.classList.contains( 'is-root-container' ) ) {
const blockElement = !! event.target.getAttribute(
'data-block'
)
? event.target
: event.target.closest( '[data-block]' );
rootClientId = blockElement.getAttribute( 'data-block' );
}

const orientation =
getBlockListSettings( rootClientId )?.orientation || 'vertical';
const rect = event.target.getBoundingClientRect();
const offset = event.clientY - rect.top;
const offsetTop = event.clientY - rect.top;
const offsetLeft = event.clientX - rect.left;
let element = Array.from( event.target.children ).find(
( blockEl ) => {
return blockEl.offsetTop > offset;
return (
( orientation === 'vertical' &&
blockEl.offsetTop > offsetTop ) ||
( orientation === 'horizontal' &&
blockEl.offsetLeft > offsetLeft )
);
}
);

Expand All @@ -228,8 +295,12 @@ export default function InsertionPoint( ref ) {
const elementRect = element.getBoundingClientRect();

if (
event.clientX > elementRect.right ||
event.clientX < elementRect.left
( orientation === 'horizontal' &&
( event.clientY > elementRect.bottom ||
event.clientY < elementRect.top ) ) ||
( orientation === 'vertical' &&
( event.clientX > elementRect.right ||
event.clientX < elementRect.left ) )
) {
if ( isInserterShown ) {
setIsInserterShown( false );
Expand Down Expand Up @@ -269,7 +340,12 @@ export default function InsertionPoint( ref ) {
rootClientId={ selectedRootClientId }
isInserterShown={ isInserterShown }
isInserterForced={ isInserterForced }
setIsInserterForced={ setIsInserterForced }
setIsInserterForced={ ( value ) => {
setIsInserterForced( value );
if ( ! value ) {
setIsInserterShown( value );
}
} }
containerRef={ ref }
showInsertionPoint={ isInserterVisible }
/>
Expand Down
Loading