Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Render Selected Block Tools in Header when using Top Toolbar (#55787)
Browse files Browse the repository at this point in the history
The Top Toolbar was relying on position: absolute; CSS and layoutEffect calculations to display the selected block tools visually within the top toolbar. This PR places it in the header DOM where we want it, allowing us to use native CSS and DOM flow to improve accessibility (tab order matches visual order) and have more maintainable code.

* Imports `<BlockContextualToolbar />` via private-api and the <Popover /> for image captions to the edit site, edit post, edit widgets, and customize widget headers.
* Removes position: absolute; and layoutEffect block toolbar positioning hacks.
* CSS for the top toolbar to use flex-shrink and overflow-x: hidden; to allow for the block toolbar to fit its current space and scroll to reveal the hidden tools.
* With top toolbar mode, Shift+Tab does not go directly to the toolbar but the first tabstop outside of the editor.

Co-authored-by: Alex Lende <ajlende@gmail.com>
Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>
3 people authored and cbravobernal committed Nov 14, 2023
1 parent 73c0403 commit a5f6e19
Showing 26 changed files with 578 additions and 659 deletions.
8 changes: 8 additions & 0 deletions packages/block-editor/src/components/block-toolbar/style.scss
Original file line number Diff line number Diff line change
@@ -56,6 +56,14 @@
}
}

.block-editor-block-contextual-toolbar.is-fixed {
position: sticky;
top: 0;
z-index: z-index(".block-editor-block-popover");
display: block;
width: 100%;
}

// on desktop browsers the fixed toolbar has tweaked borders
@include break-medium() {
.block-editor-block-contextual-toolbar.is-fixed {
Original file line number Diff line number Diff line change
@@ -7,22 +7,8 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
forwardRef,
useLayoutEffect,
useEffect,
useRef,
useState,
} from '@wordpress/element';
import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import {
ToolbarItem,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { next, previous } from '@wordpress/icons';
import { useViewportMatch } from '@wordpress/compose';

/**
* Internal dependencies
@@ -32,141 +18,46 @@ import BlockToolbar from '../block-toolbar';
import { store as blockEditorStore } from '../../store';
import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls';

function UnforwardedBlockContextualToolbar(
{ focusOnMount, isFixed, ...props },
ref
) {
// When the toolbar is fixed it can be collapsed
const [ isCollapsed, setIsCollapsed ] = useState( false );
const toolbarButtonRef = useRef();

const isLargeViewport = useViewportMatch( 'medium' );
const {
blockType,
blockEditingMode,
hasParents,
showParentSelector,
selectedBlockClientId,
} = useSelect( ( select ) => {
const {
getBlockName,
getBlockParents,
getSelectedBlockClientIds,
getBlockEditingMode,
} = select( blockEditorStore );
const { getBlockType } = select( blocksStore );
const selectedBlockClientIds = getSelectedBlockClientIds();
const _selectedBlockClientId = selectedBlockClientIds[ 0 ];
const parents = getBlockParents( _selectedBlockClientId );
const firstParentClientId = parents[ parents.length - 1 ];
const parentBlockName = getBlockName( firstParentClientId );
const parentBlockType = getBlockType( parentBlockName );

return {
selectedBlockClientId: _selectedBlockClientId,
blockType:
_selectedBlockClientId &&
getBlockType( getBlockName( _selectedBlockClientId ) ),
blockEditingMode: getBlockEditingMode( _selectedBlockClientId ),
hasParents: parents.length,
showParentSelector:
parentBlockType &&
getBlockEditingMode( firstParentClientId ) === 'default' &&
hasBlockSupport(
parentBlockType,
'__experimentalParentSelector',
true
) &&
selectedBlockClientIds.length <= 1 &&
getBlockEditingMode( _selectedBlockClientId ) === 'default',
};
}, [] );

useEffect( () => {
setIsCollapsed( false );
}, [ selectedBlockClientId ] );

const isLargerThanTabletViewport = useViewportMatch( 'large', '>=' );
const isFullscreen =
document.body.classList.contains( 'is-fullscreen-mode' );

/**
* The following code is a workaround to fix the width of the toolbar
* it should be removed when the toolbar will be rendered inline
* FIXME: remove this layout effect when the toolbar is no longer
* absolutely positioned
*/
useLayoutEffect( () => {
// don't do anything if not fixed toolbar
if ( ! isFixed ) {
return;
}

const blockToolbar = document.querySelector(
'.block-editor-block-contextual-toolbar'
);

if ( ! blockToolbar ) {
return;
}

if ( ! blockType ) {
blockToolbar.style.width = 'initial';
return;
}

if ( ! isLargerThanTabletViewport ) {
// set the width of the toolbar to auto
blockToolbar.style = {};
return;
}

if ( isCollapsed ) {
// set the width of the toolbar to auto
blockToolbar.style.width = 'auto';
return;
}

// get the width of the pinned items in the post editor or widget editor
const pinnedItems = document.querySelector(
'.edit-post-header__settings, .edit-widgets-header__actions'
);
// get the width of the left header in the site editor
const leftHeader = document.querySelector(
'.edit-site-header-edit-mode__end'
);

const computedToolbarStyle = window.getComputedStyle( blockToolbar );
const computedPinnedItemsStyle = pinnedItems
? window.getComputedStyle( pinnedItems )
: false;
const computedLeftHeaderStyle = leftHeader
? window.getComputedStyle( leftHeader )
: false;

const marginLeft = parseFloat( computedToolbarStyle.marginLeft );
const pinnedItemsWidth = computedPinnedItemsStyle
? parseFloat( computedPinnedItemsStyle.width )
: 0;
const leftHeaderWidth = computedLeftHeaderStyle
? parseFloat( computedLeftHeaderStyle.width )
: 0;

// set the new witdth of the toolbar
blockToolbar.style.width = `calc(100% - ${
leftHeaderWidth +
pinnedItemsWidth +
marginLeft +
( pinnedItems || leftHeader ? 2 : 0 ) + // Prevents button focus border from being cut off
( isFullscreen ? 0 : 160 ) // the width of the admin sidebar expanded
}px)`;
}, [
isFixed,
isLargerThanTabletViewport,
isCollapsed,
isFullscreen,
blockType,
] );
export default function BlockContextualToolbar( {
focusOnMount,
isFixed,
...props
} ) {
const { blockType, blockEditingMode, hasParents, showParentSelector } =
useSelect( ( select ) => {
const {
getBlockName,
getBlockParents,
getSelectedBlockClientIds,
getBlockEditingMode,
} = select( blockEditorStore );
const { getBlockType } = select( blocksStore );
const selectedBlockClientIds = getSelectedBlockClientIds();
const _selectedBlockClientId = selectedBlockClientIds[ 0 ];
const parents = getBlockParents( _selectedBlockClientId );
const firstParentClientId = parents[ parents.length - 1 ];
const parentBlockName = getBlockName( firstParentClientId );
const parentBlockType = getBlockType( parentBlockName );

return {
selectedBlockClientId: _selectedBlockClientId,
blockType:
_selectedBlockClientId &&
getBlockType( getBlockName( _selectedBlockClientId ) ),
blockEditingMode: getBlockEditingMode( _selectedBlockClientId ),
hasParents: parents.length,
showParentSelector:
parentBlockType &&
getBlockEditingMode( firstParentClientId ) === 'default' &&
hasBlockSupport(
parentBlockType,
'__experimentalParentSelector',
true
) &&
selectedBlockClientIds.length <= 1 &&
getBlockEditingMode( _selectedBlockClientId ) === 'default',
};
}, [] );

const isToolbarEnabled =
blockType &&
@@ -183,12 +74,10 @@ function UnforwardedBlockContextualToolbar(
const classes = classnames( 'block-editor-block-contextual-toolbar', {
'has-parent': hasParents && showParentSelector,
'is-fixed': isFixed,
'is-collapsed': isCollapsed,
} );

return (
<NavigableToolbar
ref={ ref }
focusOnMount={ focusOnMount }
focusEditorOnEscape
className={ classes }
@@ -200,37 +89,7 @@ function UnforwardedBlockContextualToolbar(
key={ selectedBlockClientId }
{ ...props }
>
{ ! isCollapsed && <BlockToolbar hideDragHandle={ isFixed } /> }
{ isFixed && isLargeViewport && blockType && (
<ToolbarGroup
className={
isCollapsed
? 'block-editor-block-toolbar__group-expand-fixed-toolbar'
: 'block-editor-block-toolbar__group-collapse-fixed-toolbar'
}
>
<ToolbarItem
as={ ToolbarButton }
ref={ toolbarButtonRef }
icon={ isCollapsed ? next : previous }
onClick={ () => {
setIsCollapsed( ( collapsed ) => ! collapsed );
toolbarButtonRef.current.focus();
} }
label={
isCollapsed
? __( 'Show block tools' )
: __( 'Hide block tools' )
}
/>
</ToolbarGroup>
) }
<BlockToolbar hideDragHandle={ isFixed } />
</NavigableToolbar>
);
}

export const BlockContextualToolbar = forwardRef(
UnforwardedBlockContextualToolbar
);

export default BlockContextualToolbar;
30 changes: 18 additions & 12 deletions packages/block-editor/src/components/block-tools/index.js
Original file line number Diff line number Diff line change
@@ -88,8 +88,6 @@ export default function BlockTools( {
moveBlocksDown,
} = useDispatch( blockEditorStore );

const selectedBlockToolsRef = useRef( null );

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

@@ -132,7 +130,7 @@ export default function BlockTools( {
insertBeforeBlock( clientIds[ 0 ] );
}
} else if ( isMatch( 'core/block-editor/unselect', event ) ) {
if ( selectedBlockToolsRef?.current?.contains( event.target ) ) {
if ( event.target.closest( '[role=toolbar]' ) ) {
// 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.
@@ -164,6 +162,12 @@ export default function BlockTools( {
const blockToolbarRef = usePopoverScroll( __unstableContentRef );
const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef );

// Conditions for fixed toolbar
// 1. Not zoom out mode
// 2. It's a large viewport. If it's a smaller viewport, let the floating toolbar handle it as it already has styles attached to make it render that way.
// 3. Fixed toolbar is enabled
const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport;

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div { ...props } onKeyDown={ onKeyDown }>
@@ -173,13 +177,11 @@ export default function BlockTools( {
__unstableContentRef={ __unstableContentRef }
/>
) }
{ ! isZoomOutMode &&
( hasFixedToolbar || ! isLargeViewport ) && (
<BlockContextualToolbar
ref={ selectedBlockToolsRef }
isFixed
/>
) }
{ /* If there is no slot available, such as in the standalone block editor, render within the editor */ }

{ ! isLargeViewport && ( // Small viewports always get a fixed toolbar
<BlockContextualToolbar isFixed />
) }

{ showEmptyBlockSideInserter && (
<EmptyBlockInserter
@@ -191,14 +193,18 @@ export default function BlockTools( {
needed for navigation and zoom-out mode. */ }
{ ! showEmptyBlockSideInserter && hasSelectedBlock && (
<SelectedBlockTools
ref={ selectedBlockToolsRef }
__unstableContentRef={ __unstableContentRef }
clientId={ clientId }
/>
) }

{ /* Used for the inline rich text toolbar. */ }
<Popover.Slot name="block-toolbar" ref={ blockToolbarRef } />
{ ! isTopToolbar && (
<Popover.Slot
name="block-toolbar"
ref={ blockToolbarRef }
/>
) }
{ children }
{ /* Used for inline rich text popovers. */ }
<Popover.Slot
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { forwardRef, useRef, useEffect } from '@wordpress/element';
import { useRef, useEffect } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { useShortcut } from '@wordpress/keyboard-shortcuts';

@@ -21,10 +21,11 @@ import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props';
import useSelectedBlockToolProps from './use-selected-block-tool-props';
import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show';

function UnforwardedSelectedBlockTools(
{ clientId, showEmptyBlockSideInserter, __unstableContentRef },
ref
) {
export default function SelectedBlockTools( {
clientId,
showEmptyBlockSideInserter,
__unstableContentRef,
} ) {
const {
capturingClientId,
isInsertionPointVisible,
@@ -101,7 +102,6 @@ function UnforwardedSelectedBlockTools(
>
{ 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 }
@@ -125,7 +125,3 @@ function UnforwardedSelectedBlockTools(

return null;
}

export const SelectedBlockTools = forwardRef( UnforwardedSelectedBlockTools );

export default SelectedBlockTools;
10 changes: 0 additions & 10 deletions packages/block-editor/src/components/block-tools/style.scss
Original file line number Diff line number Diff line change
@@ -118,16 +118,6 @@
}
}

// Add a scrim to the right of the collapsed button.
&.is-collapsed::after {
content: "";
position: absolute;
left: 100%;
width: $grid-unit-60;
height: 100%;
background: linear-gradient(to right, $white, transparent);
}

@include break-medium() {
&.is-fixed {
& > .block-editor-block-toolbar {
30 changes: 10 additions & 20 deletions packages/block-editor/src/components/navigable-toolbar/index.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
*/
import { NavigableMenu, Toolbar } from '@wordpress/components';
import {
forwardRef,
useState,
useRef,
useLayoutEffect,
@@ -196,21 +195,16 @@ function useToolbarFocus( {
}, [ focusEditorOnEscape, lastFocus, toolbarRef ] );
}

function UnforwardedNavigableToolbar(
{
children,
focusOnMount,
focusEditorOnEscape = false,
shouldUseKeyboardFocusShortcut = true,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
...props
},
ref
) {
const maybeRef = useRef();
// If a ref was not forwarded, we create one.
const toolbarRef = ref || maybeRef;
export default function NavigableToolbar( {
children,
focusOnMount,
focusEditorOnEscape = false,
shouldUseKeyboardFocusShortcut = true,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
...props
} ) {
const toolbarRef = useRef();
const isAccessibleToolbar = useIsAccessibleToolbar( toolbarRef );

useToolbarFocus( {
@@ -246,7 +240,3 @@ function UnforwardedNavigableToolbar(
</NavigableMenu>
);
}

export const NavigableToolbar = forwardRef( UnforwardedNavigableToolbar );

export default NavigableToolbar;
2 changes: 2 additions & 0 deletions packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
@@ -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 BlockContextualToolbar from './components/block-tools/block-contextual-toolbar';
import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show';
import { cleanEmptyObject } from './hooks/utils';
import BlockQuickNavigation from './components/block-quick-navigation';
@@ -41,6 +42,7 @@ lock( privateApis, {
PrivateListView,
ResizableBoxPopover,
BlockInfo,
BlockContextualToolbar,
useShouldContextualToolbarShow,
cleanEmptyObject,
BlockQuickNavigation,
29 changes: 25 additions & 4 deletions packages/customize-widgets/src/components/header/index.js
Original file line number Diff line number Diff line change
@@ -6,18 +6,25 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { createPortal, useState, useEffect } from '@wordpress/element';
import { __, _x, isRTL } from '@wordpress/i18n';
import { ToolbarButton } from '@wordpress/components';
import { NavigableToolbar } from '@wordpress/block-editor';
import { Popover, ToolbarButton } from '@wordpress/components';
import { useViewportMatch } from '@wordpress/compose';
import {
NavigableToolbar,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { createPortal, useEffect, useRef, useState } from '@wordpress/element';
import { displayShortcut, isAppleOS } from '@wordpress/keycodes';
import { __, _x, isRTL } from '@wordpress/i18n';
import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons';

/**
* Internal dependencies
*/
import Inserter from '../inserter';
import MoreMenu from '../more-menu';
import { unlock } from '../../lock-unlock';

const { BlockContextualToolbar } = unlock( blockEditorPrivateApis );

function Header( {
sidebar,
@@ -26,6 +33,8 @@ function Header( {
setIsInserterOpened,
isFixedToolbarActive,
} ) {
const isLargeViewport = useViewportMatch( 'medium' );
const blockToolbarRef = useRef();
const [ [ hasUndo, hasRedo ], setUndoRedo ] = useState( [
sidebar.hasUndo(),
sidebar.hasRedo(),
@@ -98,6 +107,18 @@ function Header( {
<Inserter setIsOpened={ setIsInserterOpened } />,
inserter.contentContainer[ 0 ]
) }

{ isFixedToolbarActive && isLargeViewport && (
<>
<div className="selected-block-tools-wrapper">
<BlockContextualToolbar isFixed />
</div>
<Popover.Slot
ref={ blockToolbarRef }
name="block-toolbar"
/>
</>
) }
</>
);
}
31 changes: 0 additions & 31 deletions packages/customize-widgets/src/style.scss
Original file line number Diff line number Diff line change
@@ -17,34 +17,3 @@
.customize-widgets-popover {
@include reset;
}

/**
Fixed bloock toolbar overrides. We can't detect each editor instance
in the styles of the block editor component so we need to override
the fixed styles here because the breakpoint css does not fire in the
customizer's left panel.
*/
.block-editor-block-contextual-toolbar {
&.is-fixed {
position: sticky;
top: 0;
left: 0;
z-index: z-index(".block-editor-block-list__insertion-point");
width: calc(100% + 2 * 12px); //12px is the padding of customizer sidebar content

overflow-y: hidden;

border: none;
border-bottom: $border-width solid $gray-200;
border-radius: 0;

.block-editor-block-toolbar .components-toolbar-group,
.block-editor-block-toolbar .components-toolbar {
border-right-color: $gray-200;
}

&.is-collapsed {
margin-left: -12px; //12px is the padding of customizer sidebar content
}
}
}
Original file line number Diff line number Diff line change
@@ -20,21 +20,18 @@ import { displayShortcut } from '@wordpress/keycodes';
import { store as editPostStore } from '../../../store';

function DocumentActions() {
const { template, isEditing } = useSelect( ( select ) => {
const { isEditingTemplate, getEditedPostTemplate } =
select( editPostStore );
const _isEditing = isEditingTemplate();
const { template } = useSelect( ( select ) => {
const { getEditedPostTemplate } = select( editPostStore );

return {
template: _isEditing ? getEditedPostTemplate() : null,
isEditing: _isEditing,
template: getEditedPostTemplate(),
};
}, [] );
const { clearSelectedBlock } = useDispatch( blockEditorStore );
const { setIsEditingTemplate } = useDispatch( editPostStore );
const { open: openCommandCenter } = useDispatch( commandsStore );

if ( ! isEditing || ! template ) {
if ( ! template ) {
return null;
}

Original file line number Diff line number Diff line change
@@ -19,7 +19,6 @@ import { Button, ToolbarItem } from '@wordpress/components';
import { listView, plus } from '@wordpress/icons';
import { useRef, useCallback } from '@wordpress/element';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
@@ -33,7 +32,7 @@ const preventDefault = ( event ) => {
event.preventDefault();
};

function HeaderToolbar( { setListViewToggleElement } ) {
function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) {
const inserterButton = useRef();
const { setIsInserterOpened, setIsListViewOpened } =
useDispatch( editPostStore );
@@ -44,15 +43,13 @@ function HeaderToolbar( { setListViewToggleElement } ) {
showIconLabels,
isListViewOpen,
listViewShortcut,
hasFixedToolbar,
} = useSelect( ( select ) => {
const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } =
select( blockEditorStore );
const { getEditorSettings } = select( editorStore );
const { getEditorMode, isFeatureActive, isListViewOpened } =
select( editPostStore );
const { getShortcutRepresentation } = select( keyboardShortcutsStore );
const { get: getPreference } = select( preferencesStore );

return {
// This setting (richEditingEnabled) should not live in the block editor's setting.
@@ -69,7 +66,6 @@ function HeaderToolbar( { setListViewToggleElement } ) {
listViewShortcut: getShortcutRepresentation(
'core/edit-post/toggle-list-view'
),
hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ),
};
}, [] );

121 changes: 104 additions & 17 deletions packages/edit-post/src/components/header/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import {
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { PostSavedState, PostPreviewButton } from '@wordpress/editor';
import { useEffect, useRef, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { next, previous } from '@wordpress/icons';
import { PinnedItems } from '@wordpress/interface';
import { useViewportMatch } from '@wordpress/compose';
import { __unstableMotion as motion } from '@wordpress/components';
import {
Button,
__unstableMotion as motion,
Popover,
} from '@wordpress/components';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
@@ -19,6 +36,9 @@ import ViewLink from '../view-link';
import MainDashboardButton from './main-dashboard-button';
import { store as editPostStore } from '../../store';
import DocumentActions from './document-actions';
import { unlock } from '../../lock-unlock';

const { BlockContextualToolbar } = unlock( blockEditorPrivateApis );

const slideY = {
hidden: { y: '-50px' },
@@ -36,18 +56,43 @@ function Header( {
setEntitiesSavedStatesCallback,
setListViewToggleElement,
} ) {
const isLargeViewport = useViewportMatch( 'large' );
const { hasActiveMetaboxes, isPublishSidebarOpened, showIconLabels } =
useSelect(
( select ) => ( {
hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
isPublishSidebarOpened:
select( editPostStore ).isPublishSidebarOpened(),
showIconLabels:
select( editPostStore ).isFeatureActive( 'showIconLabels' ),
} ),
[]
);
const isWideViewport = useViewportMatch( 'large' );
const isLargeViewport = useViewportMatch( 'medium' );
const blockToolbarRef = useRef();
const {
blockSelectionStart,
hasActiveMetaboxes,
hasFixedToolbar,
isEditingTemplate,
isPublishSidebarOpened,
showIconLabels,
} = useSelect( ( select ) => {
const { get: getPreference } = select( preferencesStore );

return {
blockSelectionStart:
select( blockEditorStore ).getBlockSelectionStart(),
hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
isEditingTemplate: select( editPostStore ).isEditingTemplate(),
isPublishSidebarOpened:
select( editPostStore ).isPublishSidebarOpened(),
hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ),
showIconLabels:
select( editPostStore ).isFeatureActive( 'showIconLabels' ),
};
}, [] );

const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] =
useState( true );

const hasBlockSelected = !! blockSelectionStart;

useEffect( () => {
// If we have a new block selection, show the block tools
if ( blockSelectionStart ) {
setIsBlockToolsCollapsed( false );
}
}, [ blockSelectionStart ] );

return (
<div className="edit-post-header">
@@ -65,10 +110,52 @@ function Header( {
className="edit-post-header__toolbar"
>
<HeaderToolbar
hasFixedToolbar={ hasFixedToolbar }
setListViewToggleElement={ setListViewToggleElement }
/>
<div className="edit-post-header__center">
<DocumentActions />
{ hasFixedToolbar && isLargeViewport && (
<>
<div
className={ classnames(
'selected-block-tools-wrapper',
{
'is-collapsed': isBlockToolsCollapsed,
}
) }
>
<BlockContextualToolbar isFixed />
</div>
<Popover.Slot
ref={ blockToolbarRef }
name="block-toolbar"
/>
{ isEditingTemplate && hasBlockSelected && (
<Button
className="edit-post-header__block-tools-toggle"
icon={ isBlockToolsCollapsed ? next : previous }
onClick={ () => {
setIsBlockToolsCollapsed(
( collapsed ) => ! collapsed
);
} }
label={
isBlockToolsCollapsed
? __( 'Show block tools' )
: __( 'Hide block tools' )
}
/>
) }
</>
) }
<div
className={ classnames( 'edit-post-header__center', {
'is-collapsed':
! isBlockToolsCollapsed &&
isLargeViewport &&
isEditingTemplate,
} ) }
>
{ isEditingTemplate && <DocumentActions /> }
</div>
</motion.div>
<motion.div
@@ -96,13 +183,13 @@ function Header( {
setEntitiesSavedStatesCallback
}
/>
{ ( isLargeViewport || ! showIconLabels ) && (
{ ( isWideViewport || ! showIconLabels ) && (
<>
<PinnedItems.Slot scope="core/edit-post" />
<MoreMenu showIconLabels={ showIconLabels } />
</>
) }
{ showIconLabels && ! isLargeViewport && (
{ showIconLabels && ! isWideViewport && (
<MoreMenu showIconLabels={ showIconLabels } />
) }
</motion.div>
33 changes: 31 additions & 2 deletions packages/edit-post/src/components/header/style.scss
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
align-items: center;
// The header should never be wider than the viewport, or buttons might be hidden. Especially relevant at high zoom levels. Related to https://core.trac.wordpress.org/ticket/47603#ticket.
max-width: 100vw;
justify-content: space-between;

// Make toolbar sticky on larger breakpoints
@include break-zoomed-in {
@@ -33,7 +34,15 @@

.edit-post-header__toolbar {
display: flex;
flex-grow: 1;
// Allow this area to shrink to fit the toolbar buttons.
flex-shrink: 8;
// Take up the space of the toolbar so it can be justified to the left side of the toolbar.
flex-grow: 3;
// Hide the overflow so flex will limit its width. Block toolbar will allow scrolling on fixed toolbar.
overflow: hidden;
// Leave enough room for the focus ring to show.
padding: 2px 0;
align-items: center;

.table-of-contents {
display: none;
@@ -42,12 +51,32 @@
display: block;
}
}

.block-editor-block-contextual-toolbar.is-fixed {
border: none;
}

.selected-block-tools-wrapper {
overflow-x: hidden;

&.is-collapsed {
display: none;
}
}
}

.edit-post-header__block-tools-toggle {
margin-left: 2px; // Allow focus ring to be fully visible
}

.edit-post-header__center {
flex-grow: 1;
display: flex;
justify-content: center;

&.is-collapsed {
display: none;
}
}

/**
@@ -57,7 +86,7 @@
.edit-post-header__settings {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
flex-wrap: nowrap;
padding-right: $grid-unit-05;

@include break-small () {
11 changes: 0 additions & 11 deletions packages/edit-post/src/components/layout/style.scss
Original file line number Diff line number Diff line change
@@ -99,14 +99,3 @@
.edit-post-layout .entities-saved-states__panel-header {
height: $header-height + $border-width;
}

.edit-post-layout.has-fixed-toolbar {
// making the header be lower than the content
// so the fixed toolbar can be positioned on top of it
// but only on desktop
@include break-medium() {
.interface-interface-skeleton__header:not(:focus-within) {
z-index: 19;
}
}
}
89 changes: 0 additions & 89 deletions packages/edit-post/src/components/visual-editor/style.scss
Original file line number Diff line number Diff line change
@@ -67,92 +67,3 @@
// See also https://www.w3.org/TR/CSS22/visudet.html#the-height-property
flex-grow: 1;
}

// Fixed contextual toolbar
@include editor-left(".edit-post-visual-editor .block-editor-block-contextual-toolbar.is-fixed");

.edit-post-visual-editor .block-editor-block-contextual-toolbar.is-fixed {
position: sticky;
top: 0;
z-index: z-index(".block-editor-block-popover");
display: block;
width: 100%;

// on desktop and tablet viewports the toolbar is fixed
// on top of interface header
$toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05;

@include break-medium() {
// leave room for block inserter, undo and redo, list view
margin-left: $toolbar-margin;
// position on top of interface header
position: fixed;
top: $admin-bar-height;
// Don't fill up when empty
min-height: initial;
// remove the border
border-bottom: none;
// has to be flex for collapse button to fit
display: flex;

// Mimic the height of the parent, vertically align center, and provide a max-height.
height: $header-height;
align-items: center;


// on tablet viewports the toolbar is fixed
// on top of interface header and covers the whole header
// except for the inserter on the left
width: calc(100% - #{$toolbar-margin});

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}

.is-fullscreen-mode & {
// leave room for block inserter, undo and redo, list view
// and some margin left
margin-left: $grid-unit-80 * 4 - 2 * $grid-unit;

top: 0;

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}
}

.show-icon-labels & {
width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons
margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin

.is-fullscreen-mode & {
margin-left: $grid-unit * 18; // site hub, inserter and margin
}
}
}

// on desktop viewports the toolbar is fixed
// on top of interface header and leaves room
// for the block inserter the publish button
@include break-large() {
width: auto;
.show-icon-labels & {
width: auto; //there are no undo, redo and list view buttons
}

.is-fullscreen-mode & {
// in full screen mode we need to account for
// the combined with of the tools at the right of the header and the margin left
// of the toolbar which includes four buttons
width: calc(100% - 280px - #{4 * $grid-unit-80});
}
}
}
87 changes: 0 additions & 87 deletions packages/edit-site/src/components/block-editor/style.scss
Original file line number Diff line number Diff line change
@@ -172,90 +172,3 @@
box-shadow: inset 0 0 0 2px var(--wp-admin-theme-color);
}
}

// Fixed contextual toolbar
@include editor-left(".edit-site-visual-editor .block-editor-block-contextual-toolbar.is-fixed");

.edit-site-visual-editor .block-editor-block-contextual-toolbar.is-fixed {
position: sticky;
top: 0;
z-index: z-index(".block-editor-block-popover");
display: block;
width: 100%;

// on desktop and tablet viewports the toolbar is fixed
// on top of interface header
$toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05;

@include break-medium() {
// leave room for block inserter, undo and redo, list view
margin-left: $toolbar-margin;
// position on top of interface header
position: fixed;
top: $admin-bar-height;
// Don't fill up when empty
min-height: initial;
// has to be flex for collapse button to fit
display: flex;

// Mimic the height of the parent, vertically align center, and provide a max-height.
height: $header-height;
align-items: center;


// on tablet viewports the toolbar is fixed
// on top of interface header and covers the whole header
// except for the inserter on the left
width: calc(100% - #{$toolbar-margin});

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}

.is-fullscreen-mode & {
// leave room for block inserter, undo and redo, list view
// and some margin left
margin-left: $grid-unit-80 * 4 - 2 * $grid-unit;

top: 0;

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}
}

.show-icon-labels & {
margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin
width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons

.is-fullscreen-mode & {
margin-left: $grid-unit * 18; // site hub, inserter and margin
}
}
}

// on desktop viewports the toolbar is fixed
// on top of interface header and leaves room
// for the block inserter the publish button
@include break-large() {
width: auto;
.show-icon-labels & {
width: auto; //there are no undo, redo and list view buttons
}

.is-fullscreen-mode & {
// in full screen mode we need to account for
// the combined with of the tools at the right of the header and the margin left
// of the toolbar which includes four buttons
width: calc(100% - 280px - #{4 * $grid-unit-80});
}
}
}
Original file line number Diff line number Diff line change
@@ -14,7 +14,6 @@ import { _x, __ } from '@wordpress/i18n';
import { listView, plus, chevronUpDown } from '@wordpress/icons';
import { Button, ToolbarItem } from '@wordpress/components';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
@@ -32,42 +31,34 @@ const preventDefault = ( event ) => {

export default function DocumentTools( {
blockEditorMode,
hasFixedToolbar,
isDistractionFree,
showIconLabels,
setListViewToggleElement,
} ) {
const inserterButton = useRef();
const {
isInserterOpen,
isListViewOpen,
listViewShortcut,
isVisualMode,
hasFixedToolbar,
} = useSelect( ( select ) => {
const {
__experimentalGetPreviewDeviceType,
isInserterOpened,
isListViewOpened,
getEditorMode,
} = select( editSiteStore );
const { getShortcutRepresentation } = select( keyboardShortcutsStore );

const { get: getPreference } = select( preferencesStore );
const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } =
useSelect( ( select ) => {
const {
__experimentalGetPreviewDeviceType,
isInserterOpened,
isListViewOpened,
getEditorMode,
} = select( editSiteStore );
const { getShortcutRepresentation } = select(
keyboardShortcutsStore
);

return {
deviceType: __experimentalGetPreviewDeviceType(),
isInserterOpen: isInserterOpened(),
isListViewOpen: isListViewOpened(),
listViewShortcut: getShortcutRepresentation(
'core/edit-site/toggle-list-view'
),
isVisualMode: getEditorMode() === 'visual',
hasFixedToolbar: getPreference(
editSiteStore.name,
'fixedToolbar'
),
};
}, [] );
return {
deviceType: __experimentalGetPreviewDeviceType(),
isInserterOpen: isInserterOpened(),
isListViewOpen: isListViewOpened(),
listViewShortcut: getShortcutRepresentation(
'core/edit-site/toggle-list-view'
),
isVisualMode: getEditorMode() === 'visual',
};
}, [] );

const {
__experimentalSetPreviewDeviceType: setPreviewDeviceType,
@@ -120,7 +111,7 @@ export default function DocumentTools( {

return (
<NavigableToolbar
className="edit-site-header-edit-mode__start"
className="edit-site-header-edit-mode__document-tools"
aria-label={ __( 'Document tools' ) }
shouldUseKeyboardFocusShortcut={ ! blockToolbarCanBeFocused }
>
85 changes: 81 additions & 4 deletions packages/edit-site/src/components/header-edit-mode/index.js
Original file line number Diff line number Diff line change
@@ -6,20 +6,24 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useReducedMotion } from '@wordpress/compose';
import { useViewportMatch, useReducedMotion } from '@wordpress/compose';
import { store as coreStore } from '@wordpress/core-data';
import {
__experimentalPreviewOptions as PreviewOptions,
privateApis as blockEditorPrivateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect, useRef, useState } from '@wordpress/element';
import { PinnedItems } from '@wordpress/interface';
import { __ } from '@wordpress/i18n';
import { external } from '@wordpress/icons';
import { external, next, previous } from '@wordpress/icons';
import {
Button,
__unstableMotion as motion,
MenuGroup,
MenuItem,
Popover,
VisuallyHidden,
} from '@wordpress/components';
import { store as preferencesStore } from '@wordpress/preferences';
@@ -39,19 +43,25 @@ import {
import { unlock } from '../../lock-unlock';
import { FOCUSABLE_ENTITIES } from '../../utils/constants';

const { BlockContextualToolbar } = unlock( blockEditorPrivateApis );

export default function HeaderEditMode( { setListViewToggleElement } ) {
const {
deviceType,
templateType,
isDistractionFree,
blockEditorMode,
blockSelectionStart,
homeUrl,
showIconLabels,
editorCanvasView,
hasFixedToolbar,
isZoomOutMode,
} = useSelect( ( select ) => {
const { __experimentalGetPreviewDeviceType, getEditedPostType } =
select( editSiteStore );
const { __unstableGetEditorMode } = select( blockEditorStore );
const { getBlockSelectionStart, __unstableGetEditorMode } =
select( blockEditorStore );

const postType = getEditedPostType();

@@ -65,6 +75,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) {
deviceType: __experimentalGetPreviewDeviceType(),
templateType: postType,
blockEditorMode: __unstableGetEditorMode(),
blockSelectionStart: getBlockSelectionStart(),
homeUrl: getUnstableBase()?.home,
showIconLabels: getPreference(
editSiteStore.name,
@@ -73,13 +84,22 @@ export default function HeaderEditMode( { setListViewToggleElement } ) {
editorCanvasView: unlock(
select( editSiteStore )
).getEditorCanvasContainerView(),
hasFixedToolbar: getPreference(
editSiteStore.name,
'fixedToolbar'
),
isDistractionFree: getPreference(
editSiteStore.name,
'distractionFree'
),
isZoomOutMode: __unstableGetEditorMode() === 'zoom-out',
};
}, [] );

const isLargeViewport = useViewportMatch( 'medium' );
const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport;
const blockToolbarRef = useRef();

const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } =
useDispatch( editSiteStore );
const disableMotion = useReducedMotion();
@@ -90,6 +110,18 @@ export default function HeaderEditMode( { setListViewToggleElement } ) {

const isZoomedOutView = blockEditorMode === 'zoom-out';

const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] =
useState( true );

const hasBlockSelected = !! blockSelectionStart;

useEffect( () => {
// If we have a new block selection, show the block tools
if ( blockSelectionStart ) {
setIsBlockToolsCollapsed( false );
}
}, [ blockSelectionStart ] );

const toolbarVariants = {
isDistractionFree: { y: '-50px' },
isDistractionFreeHovering: { y: 0 },
@@ -111,6 +143,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) {
>
{ hasDefaultEditorCanvasView && (
<motion.div
className="edit-site-header-edit-mode__start"
variants={ toolbarVariants }
transition={ toolbarTransition }
>
@@ -120,11 +153,55 @@ export default function HeaderEditMode( { setListViewToggleElement } ) {
showIconLabels={ showIconLabels }
setListViewToggleElement={ setListViewToggleElement }
/>
{ isTopToolbar && (
<>
<div
className={ classnames(
'selected-block-tools-wrapper',
{
'is-collapsed': isBlockToolsCollapsed,
}
) }
>
<BlockContextualToolbar isFixed />
</div>
<Popover.Slot
ref={ blockToolbarRef }
name="block-toolbar"
/>
{ hasBlockSelected && (
<Button
className="edit-site-header-edit-mode__block-tools-toggle"
icon={
isBlockToolsCollapsed ? next : previous
}
onClick={ () => {
setIsBlockToolsCollapsed(
( collapsed ) => ! collapsed
);
} }
label={
isBlockToolsCollapsed
? __( 'Show block tools' )
: __( 'Hide block tools' )
}
/>
) }
</>
) }
</motion.div>
) }

{ ! isDistractionFree && (
<div className="edit-site-header-edit-mode__center">
<div
className={ classnames(
'edit-site-header-edit-mode__center',
{
'is-collapsed':
! isBlockToolsCollapsed && isLargeViewport,
}
) }
>
{ ! hasDefaultEditorCanvasView ? (
getEditorCanvasContainerTitle( editorCanvasView )
) : (
37 changes: 35 additions & 2 deletions packages/edit-site/src/components/header-edit-mode/style.scss
Original file line number Diff line number Diff line change
@@ -15,6 +15,17 @@ $header-toolbar-min-width: 335px;
.edit-site-header-edit-mode__start {
display: flex;
border: none;
align-items: center;
flex-shrink: 2;
// We need this to be overflow hidden so the block toolbar can
// overflow scroll. If the overflow is visible, flexbox allows
// the toolbar to grow outside of the allowed container space.
overflow: hidden;
// Take up the full height of the header so the border focus
// is visible on toolbar buttons.
height: 100%;
// Allow focus ring to be fully visible on furthest right button.
padding-right: 2px;
}

.edit-site-header-edit-mode__end {
@@ -35,6 +46,10 @@ $header-toolbar-min-width: 335px;
// See https://dev.w3.org/csswg/css-flexbox/#min-size-auto
min-width: 0;
}

.block-editor-block-contextual-toolbar.is-fixed {
border: none;
}
}

.edit-site-header-edit-mode__toolbar {
@@ -99,7 +114,7 @@ $header-toolbar-min-width: 335px;
}
}

.edit-site-header-edit-mode__start {
.edit-site-header-edit-mode__document-tools {
display: flex;
border: none;

@@ -183,7 +198,25 @@ $header-toolbar-min-width: 335px;
padding: 0 $grid-unit-10;
}

.edit-site-header-edit-mode__start .edit-site-header-edit-mode__toolbar > * + * {
.edit-site-header-edit-mode__document-tools .edit-site-header-edit-mode__toolbar > * + * {
margin-left: $grid-unit-10;
}
}

.has-fixed-toolbar {
.selected-block-tools-wrapper {
overflow-x: scroll;

&.is-collapsed {
display: none;
}
}

.edit-site-header-edit-mode__center.is-collapsed {
display: none;
}
}

.edit-site-header-edit-mode__block-tools-toggle {
margin-left: 2px; // Allow focus ring to be fully visible
}
1 change: 1 addition & 0 deletions packages/edit-site/src/components/layout/index.js
Original file line number Diff line number Diff line change
@@ -192,6 +192,7 @@ export default function Layout() {
'is-full-canvas': isFullCanvas,
'is-edit-mode': isEditing,
'has-fixed-toolbar': hasFixedToolbar,
'is-block-toolbar-visible': hasBlockSelected,
}
) }
>
17 changes: 0 additions & 17 deletions packages/edit-site/src/components/layout/style.scss
Original file line number Diff line number Diff line change
@@ -254,23 +254,6 @@
}
}

.edit-site-layout.has-fixed-toolbar {
// making the header be lower than the content
// so the fixed toolbar can be positioned on top of it
// but only on desktop
@include break-medium() {
.edit-site-layout__canvas-container {
z-index: 5;
}
.edit-site-site-hub {
z-index: 4;
}
.edit-site-layout__header:focus-within {
z-index: 3;
}
}
}

.is-edit-mode.is-distraction-free {

.edit-site-layout__header-container {
36 changes: 32 additions & 4 deletions packages/edit-widgets/src/components/header/index.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
/**
* WordPress dependencies
*/
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { VisuallyHidden } from '@wordpress/components';
import { Popover, VisuallyHidden } from '@wordpress/components';
import { PinnedItems } from '@wordpress/interface';
import { useViewportMatch } from '@wordpress/compose';
import { store as preferencesStore } from '@wordpress/preferences';

/**
* Internal dependencies
*/
import DocumentTools from './document-tools';
import SaveButton from '../save-button';
import MoreMenu from '../more-menu';
import { unlock } from '../../lock-unlock';

const { BlockContextualToolbar } = unlock( blockEditorPrivateApis );

function Header( { setListViewToggleElement } ) {
const isMediumViewport = useViewportMatch( 'medium' );
const isLargeViewport = useViewportMatch( 'medium' );
const blockToolbarRef = useRef();
const { hasFixedToolbar } = useSelect(
( select ) => ( {
hasFixedToolbar: !! select( preferencesStore ).get(
'core/edit-widgets',
'fixedToolbar'
),
} ),
[]
);

return (
<>
<div className="edit-widgets-header">
<div className="edit-widgets-header__navigable-toolbar-wrapper">
{ isMediumViewport && (
{ isLargeViewport && (
<h1 className="edit-widgets-header__title">
{ __( 'Widgets' ) }
</h1>
) }
{ ! isMediumViewport && (
{ ! isLargeViewport && (
<VisuallyHidden
as="h1"
className="edit-widgets-header__title"
@@ -36,6 +53,17 @@ function Header( { setListViewToggleElement } ) {
<DocumentTools
setListViewToggleElement={ setListViewToggleElement }
/>
{ hasFixedToolbar && isLargeViewport && (
<>
<div className="selected-block-tools-wrapper">
<BlockContextualToolbar isFixed />
</div>
<Popover.Slot
ref={ blockToolbarRef }
name="block-toolbar"
/>
</>
) }
</div>
<div className="edit-widgets-header__actions">
<SaveButton />
10 changes: 10 additions & 0 deletions packages/edit-widgets/src/components/header/style.scss
Original file line number Diff line number Diff line change
@@ -9,12 +9,22 @@
@include break-small {
overflow: visible;
}

.selected-block-tools-wrapper {
overflow-x: hidden;
}

.block-editor-block-contextual-toolbar.is-fixed {
border: none;
}
}

.edit-widgets-header__navigable-toolbar-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 2;
overflow-x: hidden;
padding-left: $grid-unit-20;
}

12 changes: 0 additions & 12 deletions packages/edit-widgets/src/components/layout/style.scss
Original file line number Diff line number Diff line change
@@ -22,15 +22,3 @@
height: 100%;
}
}

.blocks-widgets-container {
// making the header be lower than the content
// so the fixed toolbar can be positioned on top of it
// but only on desktop
@include break-medium() {
.interface-interface-skeleton__header:not(:focus-within) {
z-index: 19;
}
}

}
Original file line number Diff line number Diff line change
@@ -34,101 +34,3 @@
}
}
}

// Fixed contextual toolbar
@include editor-left(".edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed");


.edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed {
position: sticky;
top: 0;
z-index: z-index(".block-editor-block-popover");
display: block;
width: 100%;

// on desktop and tablet viewports the toolbar is fixed
// on top of interface header
$toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05;

@include break-medium() {
// leave room for block inserter, undo and redo, list view
margin-left: $toolbar-margin;
// position on top of interface header
position: fixed;
top: $admin-bar-height;
// Don't fill up when empty
min-height: initial;
// remove the border
border-bottom: none;
// has to be flex for collapse button to fit
display: flex;

// Mimic the height of the parent, vertically align center, and provide a max-height.
height: $header-height;
align-items: center;


// on tablet viewports the toolbar is fixed
// on top of interface header and covers the whole header
// except for the inserter on the left
width: calc(100% - #{$toolbar-margin});

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}

.is-fullscreen-mode & {
// leave room for block inserter, undo and redo, list view
// and some margin left
margin-left: $grid-unit-80 * 4 - 2 * $grid-unit;

top: 0;

&.is-collapsed {
width: initial;
}

&:empty {
width: initial;
}
}

.show-icon-labels & {
margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin
width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons

.is-fullscreen-mode & {
margin-left: $grid-unit * 18; // site hub, inserter and margin
}
}

.blocks-widgets-container & {
margin-left: $grid-unit-80 * 2.4;

&.is-collapsed {
margin-left: $grid-unit-80 * 4.2;
}
}
}

// on desktop viewports the toolbar is fixed
// on top of interface header and leaves room
// for the block inserter the publish button
@include break-large() {
width: auto;
.show-icon-labels & {
width: auto; //there are no undo, redo and list view buttons
}

.is-fullscreen-mode & {
// in full screen mode we need to account for
// the combined with of the tools at the right of the header and the margin left
// of the toolbar which includes four buttons
width: calc(100% - 280px - #{4 * $grid-unit-80});
}
}
}
149 changes: 146 additions & 3 deletions test/e2e/specs/editor/various/navigable-toolbar.spec.js
Original file line number Diff line number Diff line change
@@ -4,8 +4,8 @@
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.use( {
BlockToolbarUtils: async ( { page, pageUtils }, use ) => {
await use( new BlockToolbarUtils( { page, pageUtils } ) );
BlockToolbarUtils: async ( { editor, page, pageUtils }, use ) => {
await use( new BlockToolbarUtils( { editor, page, pageUtils } ) );
},
} );

@@ -127,10 +127,124 @@ test.describe( 'Block Toolbar', () => {
.getByRole( 'button', { name: 'Paragraph' } )
).toBeFocused();
} );

// If this test breaks, it's likely that a new div has been added to wrap the top toolbar, which will need an additional
// overflow-x property set to allow the block toolbar to scroll.
test( 'Block toolbar will scroll to reveal hidden buttons with fixed toolbar', async ( {
editor,
BlockToolbarUtils,
page,
pageUtils,
} ) => {
/* eslint-disable playwright/expect-expect */
/* eslint-disable playwright/no-wait-for-timeout */
// Set the fixed toolbar
await editor.setIsFixedToolbar( true );
// Insert a block with a lot of tool buttons
await editor.insertBlock( { name: 'core/buttons' } );
// Set the locators we'll need to check for visibility
const blockButton = page.getByRole( 'button', {
name: 'Button',
exact: true,
} );

// Yes, this is the way to get the block toolbar, and yes, it is annoying.
const blockToolbar = page.getByRole( 'toolbar', {
name: 'Block tools',
} );

// Test: Top Toolbar can scroll to reveal hidden block tools.
await pageUtils.setBrowserViewport( { width: 960, height: 700 } );

// Test: Block toolbar can scroll on top toolbar mode
await BlockToolbarUtils.testScrollable( blockToolbar, blockButton );

// Test: Fixed toolbar can scroll.

// Make the viewport very small to force the fixed to bottom toolbar overflow
await pageUtils.setBrowserViewport( { width: 400, height: 700 } );

await BlockToolbarUtils.testScrollable( blockToolbar, blockButton );

// Test cleanup
await editor.setIsFixedToolbar( false );
await pageUtils.setBrowserViewport( 'large' );
/* eslint-enable playwright/expect-expect */
/* eslint-enable playwright/no-wait-for-timeout */
} );

test( 'Tab order of the block toolbar aligns with visual order', async ( {
editor,
BlockToolbarUtils,
page,
pageUtils,
} ) => {
// On default floating toolbar
await editor.insertBlock( { name: 'core/paragraph' } );
await page.keyboard.type( 'Paragraph' );

// shift + tab
await pageUtils.pressKeys( 'shift+Tab' );
// check focus is within the block toolbar
const blockToolbarParagraphButton = page.getByRole( 'button', {
name: 'Paragraph',
exact: true,
} );
await expect( blockToolbarParagraphButton ).toBeFocused();
await pageUtils.pressKeys( 'Tab' );
// check focus is on the block
await BlockToolbarUtils.expectLabelToHaveFocus( 'Block: Paragraph' );

// set the screen size to mobile
await pageUtils.setBrowserViewport( 'small' );

// TEST: Small screen toolbar without fixed toolbar setting should be the first tabstop before the editor
await pageUtils.pressKeys( 'shift+Tab' );
// check focus is within the block toolbar
await expect( blockToolbarParagraphButton ).toBeFocused();
await pageUtils.pressKeys( 'Tab' );
// check focus is on the block
await BlockToolbarUtils.expectLabelToHaveFocus( 'Block: Paragraph' );
// TEST: Fixed toolbar should be within the header dom
// Changed to Fixed top toolbar setting and large viewport to test fixed toolbar
await pageUtils.setBrowserViewport( 'large' );
await editor.setIsFixedToolbar( true );
// shift + tab
await pageUtils.pressKeys( 'shift+Tab' );

// Options button is the last one in the top toolbar, the first item outside of the editor canvas, so it should get focused.
await BlockToolbarUtils.expectLabelToHaveFocus( 'Options' );

await pageUtils.pressKeys( 'Tab' );
// check focus is on the block
await BlockToolbarUtils.expectLabelToHaveFocus( 'Block: Paragraph' );
// Move to block, alt + f10
await pageUtils.pressKeys( 'alt+F10' );
// check focus in block toolbar
await expect( blockToolbarParagraphButton ).toBeFocused();
// escape back to block
await pageUtils.pressKeys( 'Escape' );
// check block focus
await BlockToolbarUtils.expectLabelToHaveFocus( 'Block: Paragraph' );

// TEST: Small screen toolbar with fixed toolbar setting should be the first tabstop before the editor. Even though the fixed toolbar setting is on, it should not render within the header since it's visually after it.
await pageUtils.setBrowserViewport( 'small' );
await pageUtils.pressKeys( 'shift+Tab' );
// check focus is within the block toolbar
await expect( blockToolbarParagraphButton ).toBeFocused();
await pageUtils.pressKeys( 'Tab' );
// check focus is on the block
await BlockToolbarUtils.expectLabelToHaveFocus( 'Block: Paragraph' );

// Test cleanup
await editor.setIsFixedToolbar( false );
await pageUtils.setBrowserViewport( 'large' );
} );
} );

class BlockToolbarUtils {
constructor( { page, pageUtils } ) {
constructor( { editor, page, pageUtils } ) {
this.editor = editor;
this.page = page;
this.pageUtils = pageUtils;
}
@@ -155,4 +269,33 @@ class BlockToolbarUtils {

expect( ariaLabel ).toBe( label );
}

async testScrollable( scrollableElement, elementToTest ) {
// We can't use `not.toBeVisible()` here since Playwright's definition of visible or not visible is not the same
// as being human visible. It will pass if the element is off screen, but not human visible. Instead, we check the x
// position of the element. It should change as we scroll. But we also can't programmatically use scroll, as it will
// allow a scroll even if the element is not scrollable. So we use the mouse wheel event to scroll the element.
const initialBox = await elementToTest.boundingBox();

// Scroll the block toolbar to the right to reveal the hidden block tools
await scrollableElement.hover();
await this.page.mouse.wheel( 60, 0 );
// Wait for the scroll to complete. Playwright doesn't wait for the scroll from the mouse event to complete before returning.
await this.editor.page.waitForTimeout( 500 );

let currentBox = await elementToTest.boundingBox();

// The x position of the button should now be 60px lower.
expect( currentBox.x ).toEqual( initialBox.x - 60 );

// Scroll the block toolbar back to the left to hide the block tools again
await this.page.mouse.wheel( -60, 0 );
// Wait for the scroll to complete. Playwright doesn't wait for the scroll from the mouse event to complete before returning.
await this.editor.page.waitForTimeout( 500 );

currentBox = await elementToTest.boundingBox();

// The x positions should return to their initial values
expect( initialBox.x ).toEqual( currentBox.x );
}
}

0 comments on commit a5f6e19

Please sign in to comment.