-
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
Lighter block DOM: put sibling inserter in popover #19456
Changes from all commits
43f4a7c
bc18435
0f3ac15
8064208
886ca5d
996b9e0
a70d0ea
29dfe1b
15fd90e
c3415fd
5aae9ca
4c12937
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 |
---|---|---|
@@ -1,74 +1,119 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState } from '@wordpress/element'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { useState } from '@wordpress/element'; | ||
import { Popover } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import Inserter from '../inserter'; | ||
|
||
export default function BlockInsertionPoint( { rootClientId, clientId } ) { | ||
const [ isInserterFocused, setInserterFocused ] = useState( false ); | ||
function Indicator( { clientId } ) { | ||
const showInsertionPoint = useSelect( ( select ) => { | ||
const { | ||
getBlockIndex, | ||
getBlockInsertionPoint, | ||
isBlockInsertionPointVisible, | ||
getBlockRootClientId, | ||
} = select( 'core/block-editor' ); | ||
const rootClientId = getBlockRootClientId( clientId ); | ||
const blockIndex = getBlockIndex( clientId, rootClientId ); | ||
const insertionPoint = getBlockInsertionPoint(); | ||
return ( | ||
isBlockInsertionPointVisible() && | ||
insertionPoint.index === blockIndex && | ||
insertionPoint.rootClientId === rootClientId | ||
); | ||
}, [ clientId, rootClientId ] ); | ||
}, [ clientId ] ); | ||
|
||
function onFocus( event ) { | ||
// Stop propagation of the focus event to avoid selecting the current | ||
// block while inserting a new block, as it is not relevant to sibling | ||
// insertion and conflicts with contextual toolbar placement. | ||
event.stopPropagation(); | ||
setInserterFocused( true ); | ||
if ( ! showInsertionPoint ) { | ||
return null; | ||
} | ||
|
||
function onBlur() { | ||
setInserterFocused( false ); | ||
return <div className="block-editor-block-list__insertion-point-indicator" />; | ||
} | ||
|
||
export default function InsertionPoint( { | ||
className, | ||
isMultiSelecting, | ||
selectedBlockClientId, | ||
children, | ||
} ) { | ||
const [ isInserterShown, setIsInserterShown ] = useState( false ); | ||
const [ isInserterForced, setIsInserterForced ] = useState( false ); | ||
const [ inserterElement, setInserterElement ] = useState( null ); | ||
const [ inserterClientId, setInserterClientId ] = useState( null ); | ||
|
||
function onMouseMove( event ) { | ||
if ( event.target.className !== className ) { | ||
if ( isInserterShown ) { | ||
setIsInserterShown( false ); | ||
} | ||
return; | ||
} | ||
|
||
const rect = event.target.getBoundingClientRect(); | ||
const offset = event.clientY - rect.top; | ||
const element = Array.from( event.target.children ).find( ( blockEl ) => { | ||
return blockEl.offsetTop > offset; | ||
} ); | ||
|
||
if ( ! element ) { | ||
return; | ||
} | ||
|
||
const clientId = element.id.slice( 'block-'.length ); | ||
|
||
if ( ! clientId || clientId === selectedBlockClientId ) { | ||
return; | ||
} | ||
|
||
const elementRect = element.getBoundingClientRect(); | ||
|
||
if ( event.clientX > elementRect.right || event.clientX < elementRect.left ) { | ||
if ( isInserterShown ) { | ||
setIsInserterShown( false ); | ||
} | ||
return; | ||
} | ||
|
||
setIsInserterShown( true ); | ||
setInserterElement( element ); | ||
setInserterClientId( clientId ); | ||
Comment on lines
+82
to
+84
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. While it might not be too much an issue since 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. Would it make much difference, you think? I thought the only thing we'd avoid is an extra function call. 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's hard to say. I'm not too sure what all React does internally to decide how to handle |
||
} | ||
|
||
return ( | ||
<div className="block-editor-block-list__insertion-point"> | ||
{ showInsertionPoint && ( | ||
<div className="block-editor-block-list__insertion-point-indicator" /> | ||
) } | ||
<div | ||
onFocus={ onFocus } | ||
onBlur={ onBlur } | ||
// 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-visible': isInserterFocused, | ||
} ) | ||
} | ||
> | ||
<Inserter | ||
rootClientId={ rootClientId } | ||
clientId={ clientId } | ||
/> | ||
return <> | ||
{ ! isMultiSelecting && ( isInserterShown || isInserterForced ) && <Popover | ||
noArrow | ||
animate={ false } | ||
anchorRef={ inserterElement } | ||
position="top right left" | ||
focusOnMount={ false } | ||
className="block-editor-block-list__block-popover" | ||
__unstableSlotName="block-toolbar" | ||
> | ||
<div className="block-editor-block-list__insertion-point" style={ { width: inserterElement.offsetWidth } }> | ||
<Indicator clientId={ inserterClientId } /> | ||
<div | ||
onFocus={ () => setIsInserterForced( true ) } | ||
onBlur={ () => setIsInserterForced( false ) } | ||
// 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="block-editor-block-list__insertion-point-inserter" | ||
> | ||
<Inserter clientId={ inserterClientId } /> | ||
</div> | ||
</div> | ||
</Popover> } | ||
<div onMouseMove={ ! isInserterForced && ! isMultiSelecting ? onMouseMove : undefined }> | ||
{ children } | ||
</div> | ||
); | ||
</>; | ||
} |
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.
What is this logic meant to be doing? It's quite unclear to me in reading this. A few inline comments or named utility functions could help clarify here.
FWIW, it raises some curiosity how reliable
className
is here, whether that might hurt maintainability or interoperability (I know of at least a few Chrome extensions which apply classes to the live DOM, etc). Could be something whereevent.target.classList.contains
could prove more durable.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.
Yeah, if we need to take into account outside changes to the class name, then checking with
contains
is better. Even better would be to keep a list of all the reference check if it's included.