Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Escape on Block Toolbar returns focus to Editor Canvas (#55712)
* Move lastFocus into redux store Stores the element that had last focus from the editor when focus leaves the editor canvas * ForwardRef for selected block tools Adding the escape keypress from the navigable block tools will require forwarding the ref from the BlockTools all the way down to the NavigableToolbar. * Handle escape on toolbar via toolbar event listener This method requires forwarding a ref from the editor level down to the navigable toolbar so that the escape unselect shortcut can be blocked and the navigable toolbar event listener will still fire. Blocking the global escape event shouldn't be necessary, but we have a combination of a few things all combining to create a situation where: - Because the block toolbar uses createPortal to populate the block toolbar fills, we can't rely on the React event bubbling to hit the onKeyDown listener for the block toolbar - Since we can't use the React tree, we use the DOM tree which _should_ handle the event bubbling correctly from a `createPortal` element. - This bubbles via the React tree, which hits this `unselect` escape keypress before the block toolbar DOM event listener has access to it. Also, this is better than attaching it to all of the children in the block toolbar because when new items are attached to the toolbar (such as via the bubblesVirtually slots or the apply/cancel buttons when cropping an image) we can still catch the events. Otherwise, those buttons are added after the mount, and the children don't receive the listener. * Refactor to object named notation * Pass selectedBlockToolsRef to fixed toolbar * Add keyboard navigation test to cover escape and alt+10 keypresses * Remove onIndexChange from useEffect dependency array I added it since it was passed in, but it is a callback and doesn't change. It was causing unnecssary rerenders that was messing with the focus position when selecting an item from a dropdown, such as when changing the heading level on a site title block.
- Loading branch information
Showing
11 changed files
with
563 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
274 changes: 274 additions & 0 deletions
274
packages/block-editor/src/components/block-tools/selected-block-popover.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { forwardRef, useRef, useEffect } from '@wordpress/element'; | ||
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; | ||
import { useDispatch, useSelect } from '@wordpress/data'; | ||
import { useShortcut } from '@wordpress/keyboard-shortcuts'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import BlockSelectionButton from './block-selection-button'; | ||
import BlockContextualToolbar from './block-contextual-toolbar'; | ||
import { store as blockEditorStore } from '../../store'; | ||
import BlockPopover from '../block-popover'; | ||
import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; | ||
import Inserter from '../inserter'; | ||
import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; | ||
|
||
function selector( select ) { | ||
const { | ||
__unstableGetEditorMode, | ||
hasMultiSelection, | ||
isTyping, | ||
getLastMultiSelectedBlockClientId, | ||
} = select( blockEditorStore ); | ||
|
||
return { | ||
editorMode: __unstableGetEditorMode(), | ||
hasMultiSelection: hasMultiSelection(), | ||
isTyping: isTyping(), | ||
lastClientId: hasMultiSelection() | ||
? getLastMultiSelectedBlockClientId() | ||
: null, | ||
}; | ||
} | ||
|
||
function UnforwardSelectedBlockPopover( | ||
{ | ||
clientId, | ||
rootClientId, | ||
isEmptyDefaultBlock, | ||
capturingClientId, | ||
__unstablePopoverSlot, | ||
__unstableContentRef, | ||
}, | ||
ref | ||
) { | ||
const { editorMode, hasMultiSelection, isTyping, lastClientId } = useSelect( | ||
selector, | ||
[] | ||
); | ||
|
||
const isInsertionPointVisible = useSelect( | ||
( select ) => { | ||
const { | ||
isBlockInsertionPointVisible, | ||
getBlockInsertionPoint, | ||
getBlockOrder, | ||
} = select( blockEditorStore ); | ||
|
||
if ( ! isBlockInsertionPointVisible() ) { | ||
return false; | ||
} | ||
|
||
const insertionPoint = getBlockInsertionPoint(); | ||
const order = getBlockOrder( insertionPoint.rootClientId ); | ||
return order[ insertionPoint.index ] === clientId; | ||
}, | ||
[ clientId ] | ||
); | ||
const isToolbarForced = useRef( false ); | ||
const { shouldShowContextualToolbar, canFocusHiddenToolbar } = | ||
useShouldContextualToolbarShow(); | ||
|
||
const { stopTyping } = useDispatch( blockEditorStore ); | ||
|
||
const showEmptyBlockSideInserter = | ||
! isTyping && editorMode === 'edit' && isEmptyDefaultBlock; | ||
const shouldShowBreadcrumb = | ||
! hasMultiSelection && | ||
( editorMode === 'navigation' || editorMode === 'zoom-out' ); | ||
|
||
useShortcut( | ||
'core/block-editor/focus-toolbar', | ||
() => { | ||
isToolbarForced.current = true; | ||
stopTyping( true ); | ||
}, | ||
{ | ||
isDisabled: ! canFocusHiddenToolbar, | ||
} | ||
); | ||
|
||
useEffect( () => { | ||
isToolbarForced.current = false; | ||
} ); | ||
|
||
// Stores the active toolbar item index so the block toolbar can return focus | ||
// to it when re-mounting. | ||
const initialToolbarItemIndexRef = useRef(); | ||
|
||
useEffect( () => { | ||
// Resets the index whenever the active block changes so this is not | ||
// persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 | ||
initialToolbarItemIndexRef.current = undefined; | ||
}, [ clientId ] ); | ||
|
||
const popoverProps = useBlockToolbarPopoverProps( { | ||
contentElement: __unstableContentRef?.current, | ||
clientId, | ||
} ); | ||
|
||
if ( showEmptyBlockSideInserter ) { | ||
return ( | ||
<BlockPopover | ||
clientId={ capturingClientId || clientId } | ||
__unstableCoverTarget | ||
bottomClientId={ lastClientId } | ||
className={ classnames( | ||
'block-editor-block-list__block-side-inserter-popover', | ||
{ | ||
'is-insertion-point-visible': isInsertionPointVisible, | ||
} | ||
) } | ||
__unstablePopoverSlot={ __unstablePopoverSlot } | ||
__unstableContentRef={ __unstableContentRef } | ||
resize={ false } | ||
shift={ false } | ||
{ ...popoverProps } | ||
> | ||
<div className="block-editor-block-list__empty-block-inserter"> | ||
<Inserter | ||
position="bottom right" | ||
rootClientId={ rootClientId } | ||
clientId={ clientId } | ||
__experimentalIsQuick | ||
/> | ||
</div> | ||
</BlockPopover> | ||
); | ||
} | ||
|
||
if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { | ||
return ( | ||
<BlockPopover | ||
clientId={ capturingClientId || clientId } | ||
bottomClientId={ lastClientId } | ||
className={ classnames( | ||
'block-editor-block-list__block-popover', | ||
{ | ||
'is-insertion-point-visible': isInsertionPointVisible, | ||
} | ||
) } | ||
__unstablePopoverSlot={ __unstablePopoverSlot } | ||
__unstableContentRef={ __unstableContentRef } | ||
resize={ false } | ||
{ ...popoverProps } | ||
> | ||
{ shouldShowContextualToolbar && ( | ||
<BlockContextualToolbar | ||
ref={ ref } | ||
// If the toolbar is being shown because of being forced | ||
// it should focus the toolbar right after the mount. | ||
focusOnMount={ isToolbarForced.current } | ||
__experimentalInitialIndex={ | ||
initialToolbarItemIndexRef.current | ||
} | ||
__experimentalOnIndexChange={ ( index ) => { | ||
initialToolbarItemIndexRef.current = index; | ||
} } | ||
// Resets the index whenever the active block changes so | ||
// this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 | ||
key={ clientId } | ||
/> | ||
) } | ||
{ shouldShowBreadcrumb && ( | ||
<BlockSelectionButton | ||
clientId={ clientId } | ||
rootClientId={ rootClientId } | ||
/> | ||
) } | ||
</BlockPopover> | ||
); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
const SelectedBlockPopover = forwardRef( UnforwardSelectedBlockPopover ); | ||
|
||
function wrapperSelector( select ) { | ||
const { | ||
getSelectedBlockClientId, | ||
getFirstMultiSelectedBlockClientId, | ||
getBlockRootClientId, | ||
getBlock, | ||
getBlockParents, | ||
__experimentalGetBlockListSettingsForBlocks, | ||
} = select( blockEditorStore ); | ||
|
||
const clientId = | ||
getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); | ||
|
||
if ( ! clientId ) { | ||
return; | ||
} | ||
|
||
const { name, attributes = {} } = getBlock( clientId ) || {}; | ||
const blockParentsClientIds = getBlockParents( clientId ); | ||
|
||
// Get Block List Settings for all ancestors of the current Block clientId. | ||
const parentBlockListSettings = __experimentalGetBlockListSettingsForBlocks( | ||
blockParentsClientIds | ||
); | ||
|
||
// Get the clientId of the topmost parent with the capture toolbars setting. | ||
const capturingClientId = blockParentsClientIds.find( | ||
( parentClientId ) => | ||
parentBlockListSettings[ parentClientId ] | ||
?.__experimentalCaptureToolbars | ||
); | ||
|
||
return { | ||
clientId, | ||
rootClientId: getBlockRootClientId( clientId ), | ||
name, | ||
isEmptyDefaultBlock: | ||
name && isUnmodifiedDefaultBlock( { name, attributes } ), | ||
capturingClientId, | ||
}; | ||
} | ||
|
||
function UnforwardWrappedBlockPopover( | ||
{ __unstablePopoverSlot, __unstableContentRef }, | ||
ref | ||
) { | ||
const selected = useSelect( wrapperSelector, [] ); | ||
|
||
if ( ! selected ) { | ||
return null; | ||
} | ||
|
||
const { | ||
clientId, | ||
rootClientId, | ||
name, | ||
isEmptyDefaultBlock, | ||
capturingClientId, | ||
} = selected; | ||
|
||
if ( ! name ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<SelectedBlockPopover | ||
ref={ ref } | ||
clientId={ clientId } | ||
rootClientId={ rootClientId } | ||
isEmptyDefaultBlock={ isEmptyDefaultBlock } | ||
capturingClientId={ capturingClientId } | ||
__unstablePopoverSlot={ __unstablePopoverSlot } | ||
__unstableContentRef={ __unstableContentRef } | ||
/> | ||
); | ||
} | ||
|
||
export default forwardRef( UnforwardWrappedBlockPopover ); |
Oops, something went wrong.
ec83e45
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.
Flaky tests detected in ec83e45.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.
🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6735338786
📝 Reported issues:
/test/e2e/specs/editor/various/multi-block-selection.spec.js