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

Base Tracking Branch: Top Toolbar DOM Updates #55780

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/reference-guides/data/data-core-block-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,18 @@ _Properties_
- _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item.
- _frecency_ `number`: Heuristic that combines frequency and recency.

### getLastFocus

Returns the element of the last element that had focus when focus left the editor canvas.

_Parameters_

- _state_ `Object`: Block editor state.

_Returns_

- `Object`: Element.

### getLastMultiSelectedBlockClientId

Returns the client ID of the last block in the multi-selection set, or null if there is no multi-selection.
Expand Down Expand Up @@ -1651,6 +1663,18 @@ _Parameters_
- _clientId_ `string`: The block's clientId.
- _hasControlledInnerBlocks_ `boolean`: True if the block's inner blocks are controlled.

### setLastFocus

Action that sets the element that had focus when focus leaves the editor canvas.

_Parameters_

- _lastFocus_ `Object`: The last focused element.

_Returns_

- `Object`: Action object.

### setNavigationMode

Action that enables or disables the navigation mode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classnames from 'classnames';
*/
import { __ } from '@wordpress/i18n';
import {
forwardRef,
useLayoutEffect,
useEffect,
useRef,
Expand All @@ -31,7 +32,10 @@ import BlockToolbar from '../block-toolbar';
import { store as blockEditorStore } from '../../store';
import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls';

function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) {
function UnforwardBlockContextualToolbar(
{ focusOnMount, isFixed, ...props },
ref
) {
// When the toolbar is fixed it can be collapsed
const [ isCollapsed, setIsCollapsed ] = useState( false );
const toolbarButtonRef = useRef();
Expand Down Expand Up @@ -184,7 +188,9 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) {

return (
<NavigableToolbar
ref={ ref }
focusOnMount={ focusOnMount }
focusEditorOnEscape
className={ classes }
/* translators: accessibility text for the block toolbar */
aria-label={ __( 'Block tools' ) }
Expand Down Expand Up @@ -220,4 +226,4 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) {
);
}

export default BlockContextualToolbar;
export default forwardRef( UnforwardBlockContextualToolbar );
17 changes: 16 additions & 1 deletion packages/block-editor/src/components/block-tools/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export default function BlockTools( {
moveBlocksDown,
} = useDispatch( blockEditorStore );

const selectedBlockToolsRef = useRef( null );

function onKeyDown( event ) {
if ( event.defaultPrevented ) return;

Expand Down Expand Up @@ -130,6 +132,15 @@ export default function BlockTools( {
insertBeforeBlock( clientIds[ 0 ] );
}
} else if ( isMatch( 'core/block-editor/unselect', event ) ) {
if ( selectedBlockToolsRef?.current?.contains( event.target ) ) {
// This 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.
// An alternative would be to remove the addEventListener on the navigableToolbar and use this event to handle it directly right here. That feels hacky too though.
return;
}

const clientIds = getSelectedBlockClientIds();
if ( clientIds.length ) {
event.preventDefault();
Expand Down Expand Up @@ -164,7 +175,10 @@ export default function BlockTools( {
) }
{ ! isZoomOutMode &&
( hasFixedToolbar || ! isLargeViewport ) && (
<BlockContextualToolbar isFixed />
<BlockContextualToolbar
ref={ selectedBlockToolsRef }
isFixed
/>
) }

{ showEmptyBlockSideInserter && (
Expand All @@ -177,6 +191,7 @@ export default function BlockTools( {
needed for navigation and zoom-out mode. */ }
{ ! showEmptyBlockSideInserter && hasSelectedBlock && (
<SelectedBlockTools
ref={ selectedBlockToolsRef }
__unstableContentRef={ __unstableContentRef }
clientId={ clientId }
/>
Expand Down
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 );