diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 6727851e1647bf..d9799849b13990 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -99,6 +99,7 @@ const BlockToolbar = ( { hideDragHandle } ) => { // header area and not contextually to the block. const displayHeaderToolbar = useViewportMatch( 'medium', '<' ) || hasFixedToolbar; + const isLargeViewport = ! useViewportMatch( 'medium', '<' ); if ( blockType ) { if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { @@ -124,9 +125,9 @@ const BlockToolbar = ( { hideDragHandle } ) => { return (
- { ! isMultiToolbar && - ! displayHeaderToolbar && - ! isContentLocked && } + { ! isMultiToolbar && isLargeViewport && ! isContentLocked && ( + + ) }
{ ( shouldShowVisualToolbar || isMultiToolbar ) && ! isContentLocked && ( diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 554c5d96d65dbb..fa838928ba7b9e 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -56,26 +56,46 @@ } } -.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { - margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); +// on desktop browsers the fixed toolbar has tweaked borders +@include break-medium() { + .block-editor-block-contextual-toolbar.is-fixed { + .block-editor-block-toolbar { + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } - .show-icon-labels & { - margin-left: 0; + > :last-child, + > :last-child .components-toolbar-group, + > :last-child .components-toolbar { + &::after { + display: none; + } + } + } } } -.block-editor-block-parent-selector { - position: absolute; - top: -$border-width; - left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); +.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { + margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); .show-icon-labels & { - position: relative; - left: auto; - top: auto; - margin-top: -$border-width; - margin-left: -$border-width; - margin-bottom: -$border-width; + margin-left: 0; } } diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index ba31043ada2dd8..c8eee6b48cbd40 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -7,8 +7,16 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { forwardRef, 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 { levelUp } from '@wordpress/icons'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -16,10 +24,57 @@ import { useSelect } from '@wordpress/data'; import NavigableToolbar from '../navigable-toolbar'; import BlockToolbar from '../block-toolbar'; import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; + +const CollapseFixedToolbarButton = forwardRef( ( { onClick }, ref ) => { + return ( + + ); +} ); + +const ExpandFixedToolbarButton = forwardRef( ( { onClick, icon }, ref ) => { + return ( + } + onClick={ onClick } + ref={ ref } + label={ __( 'Show block tools' ) } + /> + ); +} ); function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { - const { blockType, hasParents, showParentSelector } = useSelect( - ( select ) => { + // When the toolbar is fixed it can be collapsed + const [ isCollapsed, setIsCollapsed ] = useState( false ); + const expandFixedToolbarButtonRef = useRef(); + const collapseFixedToolbarButtonRef = useRef(); + + // Don't focus the block toolbar just because it mounts + const initialRender = useRef( true ); + useEffect( () => { + if ( initialRender.current ) { + initialRender.current = false; + return; + } + if ( isCollapsed && expandFixedToolbarButtonRef.current ) { + expandFixedToolbarButtonRef.current.focus(); + } + if ( ! isCollapsed && collapseFixedToolbarButtonRef.current ) { + collapseFixedToolbarButtonRef.current.focus(); + } + }, [ isCollapsed ] ); + + const { blockType, hasParents, showParentSelector, selectedBlockClientId } = + useSelect( ( select ) => { const { getBlockName, getBlockParents, @@ -28,16 +83,17 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { } = select( blockEditorStore ); const { getBlockType } = select( blocksStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); - const selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const parents = getBlockParents( selectedBlockClientId ); + 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 ) ), + _selectedBlockClientId && + getBlockType( getBlockName( _selectedBlockClientId ) ), hasParents: parents.length, showParentSelector: parentBlockType && @@ -48,12 +104,16 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { ) && selectedBlockClientIds.length <= 1 && ! __unstableGetContentLockingParent( - selectedBlockClientId + _selectedBlockClientId ), }; - }, - [] - ); + }, [] ); + + useEffect( () => { + setIsCollapsed( false ); + }, [ selectedBlockClientId ] ); + + const isLargeViewport = useViewportMatch( 'medium' ); if ( blockType ) { if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { @@ -65,6 +125,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { const classes = classnames( 'block-editor-block-contextual-toolbar', { 'has-parent': hasParents && showParentSelector, 'is-fixed': isFixed, + 'is-collapsed': isCollapsed, } ); return ( @@ -75,7 +136,29 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { aria-label={ __( 'Block tools' ) } { ...props } > - + { isFixed && isLargeViewport && blockType && ( + + { isCollapsed ? ( + setIsCollapsed( false ) } + icon={ blockType.icon } + ref={ expandFixedToolbarButtonRef } + /> + ) : ( + setIsCollapsed( true ) } + ref={ collapseFixedToolbarButtonRef } + /> + ) } + + ) } + { ! isCollapsed && } ); } diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 099bf14987dcba..9194935c616b27 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -104,11 +104,10 @@ &.is-fixed { position: sticky; top: 0; - width: 100%; + left: 0; z-index: z-index(".block-editor-block-popover"); - // Fill up when empty - min-height: $block-toolbar-height; display: block; + width: 100%; border: none; border-bottom: $border-width solid $gray-200; @@ -119,6 +118,226 @@ border-right-color: $gray-200; } } + + // on desktop and tablet viewports the toolbar is fixed + // on top of interface header + @include break-medium() { + &.is-fixed { + + // position on top of interface header + position: fixed; + top: $grid-unit-50 - 2; + // leave room for block inserter + left: $grid-unit-80 + $grid-unit-40; + // 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; + + &.is-collapsed { + left: $grid-unit-10 * 32; + } + + .is-fullscreen-mode & { + top: $grid-unit - 2; + // leave room for block inserter + left: $grid-unit-80 + $grid-unit-70; + &.is-collapsed { + left: $grid-unit-10 * 35; + } + } + + & > .block-editor-block-toolbar__group-collapse-fixed-toolbar { + border: none; + + // Add a border as separator in the block toolbar. + &::after { + content: ""; + width: $border-width; + height: 24px; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: absolute; + left: 44px; + top: -1px; + } + } + + & > .block-editor-block-toolbar__group-expand-fixed-toolbar { + border: none; + + // Add a border as separator in the block toolbar. + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: relative; + left: -12px; //the padding of buttons + height: 24px; + } + } + + .show-icon-labels & { + left: $grid-unit-80 + $grid-unit-50; + + &.is-collapsed { + left: $grid-unit-10 * 56; + } + + .is-fullscreen-mode & { + left: $grid-unit-80 + $grid-unit-80; + } + + .block-editor-block-parent-selector .block-editor-block-parent-selector__button::after { + left: 0; + } + + .block-editor-block-toolbar__block-controls .block-editor-block-mover { + border-left: none; + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + position: relative; + } + } + } + } + + &.is-fixed .block-editor-block-parent-selector { + .block-editor-block-parent-selector__button { + position: relative; + top: -1px; + border: 0; + padding-right: 6px; + padding-left: 6px; + &::after { + content: "\00B7"; + font-size: 16px; + line-height: $grid-unit-40 + $grid-unit-10; + position: absolute; + left: $grid-unit-40 + $grid-unit-15 + 2px; + bottom: $grid-unit-05; + } + } + } + + &:not(.is-fixed) .block-editor-block-parent-selector { + position: absolute; + top: -$border-width; + left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); + + .show-icon-labels & { + position: relative; + left: auto; + top: auto; + margin-top: -$border-width; + margin-left: -$border-width; + margin-bottom: -$border-width; + } + } + } + + // on tablet vewports the toolbar is fixed + // on top of interface header and covers the whole header + // except for the inserter on the left + @include break-medium() { + &.is-fixed { + + left: 28 * $grid-unit; + width: calc(100% - #{28 * $grid-unit}); + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 48; + } + + // collapsed wp admin sidebar when not in full screen mode + .auto-fold & { + left: $grid-unit-80 + $grid-unit-40; + width: calc(100% - #{$grid-unit-80 + $grid-unit-40}); + + &.is-collapsed { + left: $grid-unit * 32; + } + } + + .is-fullscreen-mode & { + width: calc(100% - #{$grid-unit-80 + $grid-unit-70}); + left: $grid-unit-80 + $grid-unit-70; + &.is-collapsed { + left: $grid-unit * 36; + // when collapsed minimize area + width: initial; + } + } + } + } + + // 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() { + &.is-fixed { + + .auto-fold & { + // Don't fill the whole header, minimize area + width: initial; + + // leave room for block inserter and the dashboard navigation + left: $grid-unit-80 + $grid-unit-40 + ( $grid-unit-80 * 2 ); + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 48; + } + + } + + // collapsed wp admin sidebar when not in full screen mode + .auto-fold.folded & { + width: initial; + left: $grid-unit-80 + $grid-unit-40; + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 32; + } + + } + + .auto-fold.is-fullscreen-mode & { + // Don't fill the whole header, minimize area + width: initial; + left: $grid-unit-80 + $grid-unit-70; + + &.is-collapsed { + // when collapsed minimize area + width: initial; + left: $grid-unit * 36; + } + } + + .auto-fold.is-fullscreen-mode .show-icon-labels & { + left: $grid-unit-80 * 2; + &.is-collapsed { + left: $grid-unit * 48; + } + } + + } + } + } /** diff --git a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js index ec93851707849c..8fb2edf4f6768d 100644 --- a/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js +++ b/packages/e2e-tests/specs/editor/various/navigable-toolbar.test.js @@ -11,38 +11,12 @@ async function isInBlockToolbar() { } ); } -describe.each( [ - [ 'unified', true ], - [ 'contextual', false ], -] )( 'block toolbar (%s: %p)', ( label, isUnifiedToolbar ) => { +describe( 'Block Toolbar', () => { beforeEach( async () => { await createNewPost(); - - await page.evaluate( ( _isUnifiedToolbar ) => { - const { select, dispatch } = wp.data; - const isCurrentlyUnified = - select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); - if ( isCurrentlyUnified !== _isUnifiedToolbar ) { - dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); - } - }, isUnifiedToolbar ); } ); - it( 'navigates in and out of toolbar by keyboard (Alt+F10, Escape)', async () => { - // Assumes new post focus starts in title. Create first new - // block by Enter. - await page.keyboard.press( 'Enter' ); - - // [TEMPORARY]: A new paragraph is not technically a block yet - // until starting to type within it. - await page.keyboard.type( 'Example' ); - - // Upward. - await pressKeyWithModifier( 'alt', 'F10' ); - expect( await isInBlockToolbar() ).toBe( true ); - } ); - - if ( ! isUnifiedToolbar ) { + describe( 'Contextual Toolbar', () => { it( 'should not scroll page', async () => { while ( await page.evaluate( () => { @@ -74,5 +48,57 @@ describe.each( [ expect( scrollTopBefore ).toBe( scrollTopAfter ); } ); - } + + it( 'navigates into the toolbar by keyboard (Alt+F10)', async () => { + // Assumes new post focus starts in title. Create first new + // block by Enter. + await page.keyboard.press( 'Enter' ); + + // [TEMPORARY]: A new paragraph is not technically a block yet + // until starting to type within it. + await page.keyboard.type( 'Example' ); + + // Upward. + await pressKeyWithModifier( 'alt', 'F10' ); + + expect( await isInBlockToolbar() ).toBe( true ); + } ); + } ); + + describe( 'Unified Toolbar', () => { + beforeEach( async () => { + // Enable unified toolbar + await page.evaluate( () => { + const { select, dispatch } = wp.data; + const isCurrentlyUnified = + select( 'core/edit-post' ).isFeatureActive( + 'fixedToolbar' + ); + if ( ! isCurrentlyUnified ) { + dispatch( 'core/edit-post' ).toggleFeature( + 'fixedToolbar' + ); + } + } ); + } ); + + it( 'navigates into the toolbar by keyboard (Alt+F10)', async () => { + // Assumes new post focus starts in title. Create first new + // block by Enter. + await page.keyboard.press( 'Enter' ); + + // [TEMPORARY]: A new paragraph is not technically a block yet + // until starting to type within it. + await page.keyboard.type( 'Example' ); + + // Upward. + await pressKeyWithModifier( 'alt', 'F10' ); + + expect( + await page.evaluate( () => { + return document.activeElement.getAttribute( 'aria-label' ); + } ) + ).toBe( 'Show document tools' ); + } ); + } ); } ); diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 229ab58a4e14b0..3fdb90dea8555f 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -99,3 +99,15 @@ .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; + } + } + +} diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 7ea454da4f150a..609d677c07b3d6 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -23,6 +23,7 @@ import { useState, useRef } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { CommandMenu } from '@wordpress/commands'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -68,8 +69,8 @@ export default function Layout() { const { params } = useLocation(); const isListPage = getIsListPage( params ); const isEditorPage = ! isListPage; - const { canvasMode, previousShortcut, nextShortcut } = useSelect( - ( select ) => { + const { hasFixedToolbar, canvasMode, previousShortcut, nextShortcut } = + useSelect( ( select ) => { const { getAllShortcutKeyCombinations } = select( keyboardShortcutsStore ); @@ -82,10 +83,10 @@ export default function Layout() { nextShortcut: getAllShortcutKeyCombinations( 'core/edit-site/next-region' ), + hasFixedToolbar: + select( preferencesStore ).get( 'fixedToolbar' ), }; - }, - [] - ); + }, [] ); const navigateRegionsProps = useNavigateRegions( { previous: previousShortcut, next: nextShortcut, @@ -139,6 +140,7 @@ export default function Layout() { { 'is-full-canvas': isFullCanvas, 'is-edit-mode': canvasMode === 'edit', + 'has-fixed-toolbar': hasFixedToolbar, } ) } > diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 03c206eaf7f5e5..c069c471a361ee 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -221,3 +221,18 @@ border-left: $border-width solid $gray-300; } } + +.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-site-hub { + z-index: 4; + } + .edit-site-layout__header:focus-within { + z-index: 3; + } + } + +} diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index ba9d28ee266f12..c3f0380728e55c 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -120,6 +120,7 @@ export { default as key } from './library/key'; export { default as keyboardClose } from './library/keyboard-close'; export { default as keyboardReturn } from './library/keyboard-return'; export { default as layout } from './library/layout'; +export { default as levelUp } from './library/level-up'; export { default as lifesaver } from './library/lifesaver'; export { default as lineDashed } from './library/line-dashed'; export { default as lineDotted } from './library/line-dotted'; diff --git a/packages/icons/src/library/level-up.js b/packages/icons/src/library/level-up.js new file mode 100644 index 00000000000000..fc992c8dbada56 --- /dev/null +++ b/packages/icons/src/library/level-up.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const levelUp = ( + + + +); + +export default levelUp;