-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Move navigation and selection logic to WritingFlow #19397
Changes from 14 commits
f4727bc
5c1bdba
5fb2ddf
dadd96d
815e7ca
8178ca9
bdd923f
43ca6ea
9ad9c44
90d30ca
c1239e3
dbfb783
6c6acc0
82be361
3ddc1d5
0491a00
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 |
---|---|---|
|
@@ -12,10 +12,13 @@ import { getDefaultBlockName } from '@wordpress/blocks'; | |
/** | ||
* Internal dependencies | ||
*/ | ||
import IgnoreNestedEvents from '../ignore-nested-events'; | ||
import DefaultBlockAppender from '../default-block-appender'; | ||
import ButtonBlockAppender from '../button-block-appender'; | ||
|
||
function stopPropagation( event ) { | ||
event.stopPropagation(); | ||
} | ||
|
||
function BlockListAppender( { | ||
blockClientIds, | ||
rootClientId, | ||
|
@@ -51,26 +54,24 @@ function BlockListAppender( { | |
); | ||
} | ||
|
||
// IgnoreNestedEvents is used to treat interactions within the appender as | ||
// subject to the same conditions as those which occur within nested blocks. | ||
// Notably, this effectively prevents event bubbling to block ancestors | ||
// which can otherwise interfere with the intended behavior of the appender | ||
// (e.g. focus handler on the ancestor block). | ||
// | ||
// A `tabIndex` is used on the wrapping `div` element in order to force a | ||
// focus event to occur when an appender `button` element is clicked. In | ||
// some browsers (Firefox, Safari), button clicks do not emit a focus event, | ||
// which could cause this event to propagate unexpectedly. The `tabIndex` | ||
// ensures that the interaction is captured as a focus, without also adding | ||
// an extra tab stop. | ||
// | ||
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus | ||
return ( | ||
<IgnoreNestedEvents childHandledEvents={ [ 'onFocus', 'onClick', 'onKeyDown' ] }> | ||
<div tabIndex={ -1 } className="block-list-appender"> | ||
{ appender } | ||
</div> | ||
</IgnoreNestedEvents> | ||
<div | ||
// A `tabIndex` is used on the wrapping `div` element in order to | ||
// force a focus event to occur when an appender `button` element | ||
// is clicked. In some browsers (Firefox, Safari), button clicks do | ||
// not emit a focus event, which could cause this event to propagate | ||
// unexpectedly. The `tabIndex` ensures that the interaction is | ||
// captured as a focus, without also adding an extra tab stop. | ||
// | ||
// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus | ||
tabIndex={ -1 } | ||
// Prevent the block from being selected when the appender is | ||
// clicked. | ||
onFocus={ stopPropagation } | ||
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. @aduth This fixed the issue you found (#19397 (comment)). Does everything now work correctly? 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. Initial impression: I imagine this could work, yes, considering that 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. Thanks! It would be nice the eventually get rid of this as well. I'll look into it sometime separately. 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.
Yeah, that's fair. I'm very reluctant about any 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. Right, same here. At least it's now explicit that propagation is stopped here. 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 a give and take, I think. One nice thing about |
||
className="block-list-appender" | ||
> | ||
{ appender } | ||
</div> | ||
); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,13 +8,13 @@ import { animated } from 'react-spring/web.cjs'; | |
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef, useEffect, useLayoutEffect, useState, useCallback } from '@wordpress/element'; | ||
import { useRef, useEffect, useLayoutEffect, useState, useCallback, useContext } from '@wordpress/element'; | ||
import { | ||
focus, | ||
isTextField, | ||
placeCaretAtHorizontalEdge, | ||
} from '@wordpress/dom'; | ||
import { BACKSPACE, DELETE, ENTER, ESCAPE } from '@wordpress/keycodes'; | ||
import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; | ||
import { | ||
getBlockType, | ||
getSaveElement, | ||
|
@@ -45,20 +45,11 @@ import BlockHtml from './block-html'; | |
import BlockBreadcrumb from './breadcrumb'; | ||
import BlockContextualToolbar from './block-contextual-toolbar'; | ||
import BlockInsertionPoint from './insertion-point'; | ||
import IgnoreNestedEvents from '../ignore-nested-events'; | ||
import Inserter from '../inserter'; | ||
import { isInsideRootBlock } from '../../utils/dom'; | ||
import useMovingAnimation from './moving-animation'; | ||
import { ChildToolbar, ChildToolbarSlot } from './block-child-toolbar'; | ||
/** | ||
* Prevents default dragging behavior within a block to allow for multi- | ||
* selection to take effect unhampered. | ||
* | ||
* @param {DragEvent} event Drag event. | ||
*/ | ||
const preventDrag = ( event ) => { | ||
event.preventDefault(); | ||
}; | ||
import { Context } from './root-container'; | ||
|
||
function BlockListBlock( { | ||
mode, | ||
|
@@ -94,17 +85,15 @@ function BlockListBlock( { | |
onRemove, | ||
onInsertDefaultBlockAfter, | ||
toggleSelection, | ||
onShiftSelection, | ||
onSelectionStart, | ||
animateOnChange, | ||
enableAnimation, | ||
isNavigationMode, | ||
setNavigationMode, | ||
isMultiSelecting, | ||
isLargeViewport, | ||
hasSelectedUI = true, | ||
hasMovers = true, | ||
} ) { | ||
const onSelectionStart = useContext( Context ); | ||
// In addition to withSelect, we should favor using useSelect in this component going forward | ||
// to avoid leaking new props to the public API (editor.BlockListBlock filter) | ||
const { isDraggingBlocks } = useSelect( ( select ) => { | ||
|
@@ -231,17 +220,6 @@ function BlockListBlock( { | |
|
||
// Other event handlers | ||
|
||
/** | ||
* Marks the block as selected when focused and not already selected. This | ||
* specifically handles the case where block does not set focus on its own | ||
* (via `setFocus`), typically if there is no focusable input in the block. | ||
*/ | ||
const onFocus = () => { | ||
if ( ! isSelected && ! isPartOfMultiSelection ) { | ||
onSelect(); | ||
} | ||
}; | ||
|
||
/** | ||
* Interprets keydown event intent to remove or insert after block if key | ||
* event occurs on wrapper node. This can occur when the block has no text | ||
|
@@ -253,13 +231,9 @@ function BlockListBlock( { | |
const onKeyDown = ( event ) => { | ||
const { keyCode, target } = event; | ||
|
||
// ENTER/BACKSPACE Shortcuts are only available if the wrapper is focused | ||
// and the block is not locked. | ||
const canUseShortcuts = ( | ||
isSelected && | ||
! isLocked && | ||
( target === wrapper.current || target === breadcrumb.current ) | ||
); | ||
// ENTER/BACKSPACE Shortcuts are only available if the wrapper or | ||
// breadcrumb is focused. | ||
const canUseShortcuts = ( target === wrapper.current || target === breadcrumb.current ); | ||
const isEditMode = ! isNavigationMode; | ||
|
||
switch ( keyCode ) { | ||
|
@@ -279,51 +253,6 @@ function BlockListBlock( { | |
event.preventDefault(); | ||
} | ||
break; | ||
case ESCAPE: | ||
if ( | ||
isSelected && | ||
isEditMode | ||
) { | ||
setNavigationMode( true ); | ||
wrapper.current.focus(); | ||
} | ||
break; | ||
} | ||
}; | ||
|
||
/** | ||
* Begins tracking cursor multi-selection when clicking down within block. | ||
* | ||
* @param {MouseEvent} event A mousedown event. | ||
*/ | ||
const onMouseDown = ( event ) => { | ||
// Not the main button. | ||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button | ||
if ( event.button !== 0 ) { | ||
return; | ||
} | ||
|
||
if ( | ||
isNavigationMode && | ||
isSelected && | ||
isInsideRootBlock( blockNodeRef.current, event.target ) | ||
) { | ||
setNavigationMode( false ); | ||
} | ||
|
||
if ( event.shiftKey ) { | ||
if ( ! isSelected ) { | ||
onShiftSelection(); | ||
event.preventDefault(); | ||
} | ||
|
||
// Allow user to escape out of a multi-selection to a singular | ||
// selection of a block via click. This is handled here since | ||
// onFocus excludes blocks involved in a multiselection, as | ||
// focus can be incurred by starting a multiselection (focus | ||
// moved to first block's multi-controls). | ||
} else if ( isPartOfMultiSelection ) { | ||
onSelect(); | ||
} | ||
}; | ||
|
||
|
@@ -333,7 +262,7 @@ function BlockListBlock( { | |
// cases where Firefox might always set `buttons` to `0`. | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which | ||
if ( isSelected && ( buttons || which ) === 1 ) { | ||
if ( ( buttons || which ) === 1 ) { | ||
onSelectionStart( clientId ); | ||
} | ||
}; | ||
|
@@ -469,18 +398,16 @@ function BlockListBlock( { | |
); | ||
|
||
return ( | ||
<IgnoreNestedEvents | ||
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. @aduth Might be of interest to you as you originally created this component. |
||
<animated.div | ||
id={ blockElementId } | ||
ref={ wrapper } | ||
className={ wrapperClassName } | ||
data-type={ name } | ||
onFocus={ onFocus } | ||
onKeyDown={ onKeyDown } | ||
// Only allow shortcuts when a blocks is selected and not locked. | ||
onKeyDown={ isSelected && ! isLocked ? onKeyDown : undefined } | ||
tabIndex="0" | ||
aria-label={ blockLabel } | ||
role="group" | ||
childHandledEvents={ [ 'onDragStart', 'onMouseDown' ] } | ||
tagName={ animated.div } | ||
{ ...wrapperProps } | ||
style={ | ||
wrapperProps && wrapperProps.style ? | ||
|
@@ -542,11 +469,10 @@ function BlockListBlock( { | |
) } | ||
</Popover> | ||
) } | ||
<IgnoreNestedEvents | ||
<div | ||
ref={ blockNodeRef } | ||
onDragStart={ preventDrag } | ||
onMouseDown={ onMouseDown } | ||
onMouseLeave={ onMouseLeave } | ||
// Only allow selection to be started from a selected block. | ||
onMouseLeave={ isSelected ? onMouseLeave : undefined } | ||
Comment on lines
+474
to
+475
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. Is this related to multi-selection? And if it is, would any of it be expected to be handled in WritingFlow instead? Considering you mention it at https://github.com/WordPress/gutenberg/pull/19397/files#r363036970 as being partly responsible for handling multi-selection. 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. That's right, this is for multi selection. It would be nice to move this to |
||
data-block={ clientId } | ||
> | ||
<BlockCrashBoundary onError={ onBlockError }> | ||
|
@@ -565,7 +491,7 @@ function BlockListBlock( { | |
] } | ||
</BlockCrashBoundary> | ||
{ !! hasError && <BlockCrashWarning /> } | ||
</IgnoreNestedEvents> | ||
</div> | ||
{ showEmptyBlockSideInserter && ( | ||
<div className="block-editor-block-list__empty-block-inserter"> | ||
<Inserter | ||
|
@@ -576,7 +502,7 @@ function BlockListBlock( { | |
/> | ||
</div> | ||
) } | ||
</IgnoreNestedEvents> | ||
</animated.div> | ||
); | ||
} | ||
|
||
|
@@ -680,14 +606,12 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { | |
const { | ||
updateBlockAttributes, | ||
selectBlock, | ||
multiSelect, | ||
insertBlocks, | ||
insertDefaultBlock, | ||
removeBlock, | ||
mergeBlocks, | ||
replaceBlocks, | ||
toggleSelection, | ||
setNavigationMode, | ||
__unstableMarkLastChangeAsPersistent, | ||
} = dispatch( 'core/block-editor' ); | ||
|
||
|
@@ -750,25 +674,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { | |
} | ||
replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect ); | ||
}, | ||
onShiftSelection() { | ||
if ( ! ownProps.isSelectionEnabled ) { | ||
return; | ||
} | ||
|
||
const { | ||
getBlockSelectionStart, | ||
} = select( 'core/block-editor' ); | ||
|
||
if ( getBlockSelectionStart() ) { | ||
multiSelect( getBlockSelectionStart(), ownProps.clientId ); | ||
} else { | ||
selectBlock( ownProps.clientId ); | ||
} | ||
}, | ||
toggleSelection( selectionEnabled ) { | ||
toggleSelection( selectionEnabled ); | ||
}, | ||
setNavigationMode, | ||
}; | ||
} ); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,6 @@ import classnames from 'classnames'; | |
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useRef } from '@wordpress/element'; | ||
import { AsyncModeProvider, useSelect } from '@wordpress/data'; | ||
|
||
/** | ||
|
@@ -15,7 +14,7 @@ import { AsyncModeProvider, useSelect } from '@wordpress/data'; | |
import BlockListBlock from './block'; | ||
import BlockListAppender from '../block-list-appender'; | ||
import __experimentalBlockListFooter from '../block-list-footer'; | ||
import useMultiSelection from './use-multi-selection'; | ||
import RootContainer from './root-container'; | ||
|
||
/** | ||
* If the block count exceeds the threshold, we disable the reordering animation | ||
|
@@ -71,18 +70,17 @@ function BlockList( { | |
hasMultiSelection, | ||
enableAnimation, | ||
} = useSelect( selector, [ rootClientId ] ); | ||
const ref = useRef(); | ||
const onSelectionStart = useMultiSelection( { ref, rootClientId } ); | ||
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. Notice that this is no longer rendered for every block list, but rather only for the root block list. |
||
|
||
const uiParts = { | ||
hasMovers: true, | ||
hasSelectedUI: true, | ||
...__experimentalUIParts, | ||
}; | ||
|
||
const Container = rootClientId ? 'div' : RootContainer; | ||
|
||
return ( | ||
<div | ||
ref={ ref } | ||
<Container | ||
className={ classnames( | ||
'block-editor-block-list__layout', | ||
className | ||
|
@@ -98,7 +96,6 @@ function BlockList( { | |
<BlockListBlock | ||
rootClientId={ rootClientId } | ||
clientId={ clientId } | ||
onSelectionStart={ onSelectionStart } | ||
isDraggable={ isDraggable } | ||
moverDirection={ moverDirection } | ||
isMultiSelecting={ isMultiSelecting } | ||
|
@@ -118,7 +115,7 @@ function BlockList( { | |
renderAppender={ renderAppender } | ||
/> | ||
<__experimentalBlockListFooter.Slot /> | ||
</div> | ||
</Container> | ||
); | ||
} | ||
|
||
|
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 was actually surprised to find that in the latest version of the plugin / core version, the behavior of clicking on the inserter in the column block is such that it requires a second click to open. I don't think it's always been like this, since I distinctly remember trying to require only a single click. One potential difference now is that, while it does only require a single click, it doesn't actually select the block. I don't particularly see this as being problematic, and in some ways it's actually an ideal behavior (since the user's intent is to insert, not necessarily to select the block).