-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
d259057
b833a45
7c8d2d0
43407e6
933764c
a7c5f81
0efa459
c9f8abe
b703638
9b10854
25153c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 ); | ||
} | ||
|
||
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> | ||
); | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ); | ||
|
@@ -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 ], | ||
|
@@ -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 ) | ||
); | ||
} | ||
); | ||
|
||
|
@@ -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 ); | ||
|
@@ -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 } | ||
/> | ||
|
There was a problem hiding this comment.
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)