Skip to content

Commit

Permalink
Fix multiple tooltips showing on NavigableToolbars (#49644)
Browse files Browse the repository at this point in the history
* Add useKeyboardShortcut prop to NavigableToolbar and useToolbarFocus

The 'core/block-editor/focus-toolbar' shortcut (alt + F10) selects the active navigable toolbar's fi
rst tabbable item, but when there are multiple navigable toolbars, it places focus on all of the act
ive navigable toolbar components. This results in an odd state of two tooltips being open. This comm
it scaffolds the ability to disable the keyboard shortcut from applying.

* Conditionally useKeyboardFocusShortcut in HeaderToolbar

I couldn't find a reliable way of determining if there is a block toolbar showing, so I approximated it by checking if there are no selected blocks or we're not in 'edit' mode. If one of those conditions applies, then a focusable block toolbar won't be present (there may other instances I'm missing though), and we can enable the shortcut for the header toolbar.

* Remove accidentally added line break

* Check all the conditions when a toolbar might be showing

I hope I'm missing a reliable isBlockToolbarShowing() instead of doingn all these checks, but I didn't see a function like that. Maybe it's worth adding?

* Remove unnecesary line break

* Change logic to use isBlockWithToolbarSelected instead of isUnmodifiedDefaultBlock

We don't care if it's an unmodified default block or not in the condition to use the shortcut for the top level toolbar, we need to know if there's a chance the toolbar is showing or not. This simplifies the logic so anytime a block with a toolbar is selected, we set maybeBlockToolbarShowing to true, then check the extra condition of a fixed toolbar with ANY block selected.

* Start on e2e tests to cover various scenarios

* e2e test: should focus the top level toolbar when in select mode

* ede test: should focus the top level toolbar when on an empty block

* ede test: grouping into describe test sections

* Scaffold top  toolbar mode e2e tests

* e2e tests: Top Toolbar in edit mode

* e2e tests: Top toolbar + select mode

* Refactor toggle fixed toolbar to playwright editor class

* Refactor: consolodate e2e locators

* Refactor: Combine tests into fewer cases to save execution time

There were 12 tests and each time had to go through all of the test set-up. I combined the tests into groups based into view and mode for four total tests. This cuts the test time down by about half, (18s for the previous 12 tests vs 9s for these 4 tests). This is recommended by Playwright Best Practices of "write fewer tests but longer tests": https://playwright.dev/docs/best-practices#write-fewer-tests-but-longer-tests. I think the time saved per commit is worth having more assertions per test.

* Use POM-style ToolbarUtils for e2e test

* Improve naming of isFixed to setIsFixedToolbar

* Improve naming to shouldUseKeyboardFocusShortcut

* Simplify logic for setting isBlockWithToolbarSelected

* Rename toggleFixedToolbar util to setIsFixedToolbar

* Refactor shouldShowContextualToolbar and canFocusHiddenToolbar into useShouldContextualToolbarShow hook

In order to decide if the block toolbar or document toolbar should be focused from the alt+F10 shortcut, we need to do duplicate the same code in both toolbar wrappers. This moves them into a shared hook to keep things consistent.

* Export useShouldContextualToolbarShow

* Add hasClientId check, as we can't focus a block toolbar without a block ID

* Implement useShouldContextualToolbarShow in header toolbar

* Add tests for smaller viewports

* Update e2e tests to address new unified toolbar view

* Make useShouldContextualToolbarShow private
  • Loading branch information
jeryj committed Apr 19, 2023
1 parent 24300c6 commit 6446ff7
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { useRef, useEffect } from '@wordpress/element';
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks';
import { useDispatch, useSelect } from '@wordpress/data';
import { useShortcut } from '@wordpress/keyboard-shortcuts';
import { useViewportMatch } from '@wordpress/compose';

/**
* Internal dependencies
Expand All @@ -22,26 +21,20 @@ import BlockPopover from '../block-popover';
import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props';
import Inserter from '../inserter';
import { unlock } from '../../lock-unlock';
import { privateApis } from '../../private-apis';

function selector( select ) {
const {
__unstableGetEditorMode,
isMultiSelecting,
hasMultiSelection,
isTyping,
isBlockInterfaceHidden,
getSettings,
getLastMultiSelectedBlockClientId,
} = unlock( select( blockEditorStore ) );

return {
editorMode: __unstableGetEditorMode(),
hasMultiSelection: hasMultiSelection(),
isMultiSelecting: isMultiSelecting(),
isTyping: isTyping(),
isBlockInterfaceHidden: isBlockInterfaceHidden(),
hasFixedToolbar: getSettings().hasFixedToolbar,
isDistractionFree: getSettings().isDistractionFree,
lastClientId: hasMultiSelection()
? getLastMultiSelectedBlockClientId()
: null,
Expand All @@ -52,21 +45,16 @@ function SelectedBlockPopover( {
clientId,
rootClientId,
isEmptyDefaultBlock,
showContents, // we may need to mount an empty popover because we reuse
capturingClientId,
__unstablePopoverSlot,
__unstableContentRef,
} ) {
const {
editorMode,
hasMultiSelection,
isMultiSelecting,
isTyping,
isBlockInterfaceHidden,
hasFixedToolbar,
isDistractionFree,
lastClientId,
} = useSelect( selector, [] );
const { editorMode, hasMultiSelection, isTyping, lastClientId } = useSelect(
selector,
[]
);

const { useShouldContextualToolbarShow } = unlock( privateApis );
const isInsertionPointVisible = useSelect(
( select ) => {
const {
Expand All @@ -85,29 +73,17 @@ function SelectedBlockPopover( {
},
[ clientId ]
);
const isLargeViewport = useViewportMatch( 'medium' );
const isToolbarForced = useRef( false );
const { shouldShowContextualToolbar, canFocusHiddenToolbar } =
useShouldContextualToolbarShow( clientId );

const { stopTyping } = useDispatch( blockEditorStore );

const showEmptyBlockSideInserter =
! isTyping && editorMode === 'edit' && isEmptyDefaultBlock;
const shouldShowBreadcrumb =
! hasMultiSelection &&
( editorMode === 'navigation' || editorMode === 'zoom-out' );
const shouldShowContextualToolbar =
editorMode === 'edit' &&
! hasFixedToolbar &&
isLargeViewport &&
! isMultiSelecting &&
! showEmptyBlockSideInserter &&
! isTyping &&
! isBlockInterfaceHidden;
const canFocusHiddenToolbar =
editorMode === 'edit' &&
! shouldShowContextualToolbar &&
! hasFixedToolbar &&
! isDistractionFree &&
! isEmptyDefaultBlock;

useShortcut(
'core/block-editor/focus-toolbar',
Expand Down Expand Up @@ -179,7 +155,7 @@ function SelectedBlockPopover( {
resize={ false }
{ ...popoverProps }
>
{ shouldShowContextualToolbar && showContents && (
{ shouldShowContextualToolbar && (
<BlockContextualToolbar
// If the toolbar is being shown because of being forced
// it should focus the toolbar right after the mount.
Expand Down Expand Up @@ -215,8 +191,6 @@ function wrapperSelector( select ) {
getBlockRootClientId,
getBlock,
getBlockParents,
getSettings,
isNavigationMode: _isNavigationMode,
__experimentalGetBlockListSettingsForBlocks,
} = select( blockEditorStore );

Expand All @@ -242,14 +216,10 @@ function wrapperSelector( select ) {
?.__experimentalCaptureToolbars
);

const settings = getSettings();

return {
clientId,
rootClientId: getBlockRootClientId( clientId ),
name,
isDistractionFree: settings.isDistractionFree,
isNavigationMode: _isNavigationMode(),
isEmptyDefaultBlock:
name && isUnmodifiedDefaultBlock( { name, attributes } ),
capturingClientId,
Expand All @@ -272,8 +242,6 @@ export default function WrappedBlockPopover( {
name,
isEmptyDefaultBlock,
capturingClientId,
isDistractionFree,
isNavigationMode,
} = selected;

if ( ! name ) {
Expand All @@ -285,7 +253,6 @@ export default function WrappedBlockPopover( {
clientId={ clientId }
rootClientId={ rootClientId }
isEmptyDefaultBlock={ isEmptyDefaultBlock }
showContents={ ! isDistractionFree || isNavigationMode }
capturingClientId={ capturingClientId }
__unstablePopoverSlot={ __unstablePopoverSlot }
__unstableContentRef={ __unstableContentRef }
Expand Down
15 changes: 12 additions & 3 deletions packages/block-editor/src/components/navigable-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ function useToolbarFocus(
focusOnMount,
isAccessibleToolbar,
defaultIndex,
onIndexChange
onIndexChange,
shouldUseKeyboardFocusShortcut
) {
// Make sure we don't use modified versions of this prop.
const [ initialFocusOnMount ] = useState( focusOnMount );
Expand All @@ -103,8 +104,14 @@ function useToolbarFocus(
focusFirstTabbableIn( ref.current );
}, [] );

const focusToolbarViaShortcut = () => {
if ( shouldUseKeyboardFocusShortcut ) {
focusToolbar();
}
};

// Focus on toolbar when pressing alt+F10 when the toolbar is visible.
useShortcut( 'core/block-editor/focus-toolbar', focusToolbar );
useShortcut( 'core/block-editor/focus-toolbar', focusToolbarViaShortcut );

useEffect( () => {
if ( initialFocusOnMount ) {
Expand Down Expand Up @@ -147,6 +154,7 @@ function useToolbarFocus(
function NavigableToolbar( {
children,
focusOnMount,
shouldUseKeyboardFocusShortcut = true,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
...props
Expand All @@ -159,7 +167,8 @@ function NavigableToolbar( {
focusOnMount,
isAccessibleToolbar,
initialIndex,
onIndexChange
onIndexChange,
shouldUseKeyboardFocusShortcut
);

if ( isAccessibleToolbar ) {
Expand Down
2 changes: 2 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ResizableBoxPopover from './components/resizable-box-popover';
import { ComposedPrivateInserter as PrivateInserter } from './components/inserter';
import { PrivateListView } from './components/list-view';
import BlockInfo from './components/block-info-slot-fill';
import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show';

/**
* Private @wordpress/block-editor APIs.
Expand All @@ -24,4 +25,5 @@ lock( privateApis, {
PrivateListView,
ResizableBoxPopover,
BlockInfo,
useShouldContextualToolbarShow,
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* WordPress dependencies
*/
import { useSelect } from '@wordpress/data';
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks';
import { useViewportMatch } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../store';
import { unlock } from '../lock-unlock';

/**
* Returns true if the contextual block toolbar should show, or false if it should be hidden.
*
* @param {string} clientId The client ID of the block.
*
* @return {boolean} Whether the block toolbar is hidden.
*/
export function useShouldContextualToolbarShow( clientId ) {
const isLargeViewport = useViewportMatch( 'medium' );

const { shouldShowContextualToolbar, canFocusHiddenToolbar } = useSelect(
( select ) => {
const {
__unstableGetEditorMode,
isMultiSelecting,
isTyping,
isBlockInterfaceHidden,
getBlock,
getSettings,
isNavigationMode,
} = unlock( select( blockEditorStore ) );

const isEditMode = __unstableGetEditorMode() === 'edit';
const hasFixedToolbar = getSettings().hasFixedToolbar;
const isDistractionFree = getSettings().isDistractionFree;
const hasClientId = !! clientId;
const isEmptyDefaultBlock = isUnmodifiedDefaultBlock(
getBlock( clientId ) || {}
);

const _shouldShowContextualToolbar =
isEditMode &&
! hasFixedToolbar &&
( ! isDistractionFree || isNavigationMode() ) &&
isLargeViewport &&
! isMultiSelecting() &&
! isTyping() &&
hasClientId &&
! isEmptyDefaultBlock &&
! isBlockInterfaceHidden();

const _canFocusHiddenToolbar =
isEditMode &&
hasClientId &&
! _shouldShowContextualToolbar &&
! hasFixedToolbar &&
! isDistractionFree &&
! isEmptyDefaultBlock;

return {
shouldShowContextualToolbar: _shouldShowContextualToolbar,
canFocusHiddenToolbar: _canFocusHiddenToolbar,
};
},
[ clientId, isLargeViewport ]
);

return {
shouldShowContextualToolbar,
canFocusHiddenToolbar,
};
}
4 changes: 4 additions & 0 deletions packages/e2e-test-utils-playwright/src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { selectBlocks } from './select-blocks';
import { setContent } from './set-content';
import { showBlockToolbar } from './show-block-toolbar';
import { saveSiteEditorEntities } from './site-editor';
import { setIsFixedToolbar } from './set-is-fixed-toolbar';
import { transformBlockTo } from './transform-block-to';

type EditorConstructorProps = {
Expand Down Expand Up @@ -68,6 +69,9 @@ export class Editor {
setContent: typeof setContent = setContent.bind( this );
/** @borrows showBlockToolbar as this.showBlockToolbar */
showBlockToolbar: typeof showBlockToolbar = showBlockToolbar.bind( this );
/** @borrows setIsFixedToolbar as this.setIsFixedToolbar */
setIsFixedToolbar: typeof setIsFixedToolbar =
setIsFixedToolbar.bind( this );
/** @borrows transformBlockTo as this.transformBlockTo */
transformBlockTo: typeof transformBlockTo = transformBlockTo.bind( this );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Internal dependencies
*/
import type { Editor } from './index';

/**
* Toggles the fixed toolbar option.
*
* @param this
* @param isFixed Boolean value true/false for on/off.
*/
export async function setIsFixedToolbar( this: Editor, isFixed: boolean ) {
await this.page.evaluate( ( _isFixed ) => {
const { select, dispatch } = window.wp.data;
const isCurrentlyFixed =
select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' );
if ( isCurrentlyFixed !== _isFixed ) {
dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' );
}
}, isFixed );
}
31 changes: 28 additions & 3 deletions packages/edit-post/src/components/header/header-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NavigableToolbar,
ToolSelector,
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import {
EditorHistoryRedo,
Expand All @@ -23,6 +24,7 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
* Internal dependencies
*/
import { store as editPostStore } from '../../../store';
import { unlock } from '../../../private-apis';

const preventDefault = ( event ) => {
event.preventDefault();
Expand All @@ -39,15 +41,27 @@ function HeaderToolbar() {
showIconLabels,
isListViewOpen,
listViewShortcut,
selectedBlockId,
hasFixedToolbar,
} = useSelect( ( select ) => {
const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } =
select( blockEditorStore );
const {
hasInserterItems,
getBlockRootClientId,
getBlockSelectionEnd,
getSelectedBlockClientId,
getFirstMultiSelectedBlockClientId,
getSettings,
} = select( blockEditorStore );
const { getEditorSettings } = select( editorStore );
const { getEditorMode, isFeatureActive, isListViewOpened } =
select( editPostStore );
const { getShortcutRepresentation } = select( keyboardShortcutsStore );

return {
hasFixedToolbar: getSettings().hasFixedToolbar,
selectedBlockId:
getSelectedBlockClientId() ||
getFirstMultiSelectedBlockClientId(),
// This setting (richEditingEnabled) should not live in the block editor's setting.
isInserterEnabled:
getEditorMode() === 'visual' &&
Expand All @@ -64,9 +78,19 @@ function HeaderToolbar() {
),
};
}, [] );

const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis );

const isLargeViewport = useViewportMatch( 'medium' );
const isWideViewport = useViewportMatch( 'wide' );

const { shouldShowContextualToolbar, canFocusHiddenToolbar } =
useShouldContextualToolbarShow( selectedBlockId );
// If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar.
// There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport.
const blockToolbarCanBeFocused =
shouldShowContextualToolbar ||
canFocusHiddenToolbar ||
( ( hasFixedToolbar || ! isLargeViewport ) && selectedBlockId );
/* translators: accessibility text for the editor toolbar */
const toolbarAriaLabel = __( 'Document tools' );

Expand Down Expand Up @@ -114,6 +138,7 @@ function HeaderToolbar() {
<NavigableToolbar
className="edit-post-header-toolbar"
aria-label={ toolbarAriaLabel }
shouldUseKeyboardFocusShortcut={ ! blockToolbarCanBeFocused }
>
<div className="edit-post-header-toolbar__left">
<ToolbarItem
Expand Down
Loading

0 comments on commit 6446ff7

Please sign in to comment.