diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 025b4eaf15ab8f..2ec59c486be1a7 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -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. @@ -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. diff --git a/packages/block-editor/src/components/block-controls/slot.js b/packages/block-editor/src/components/block-controls/slot.js index ad800b49ab40db..fb2ace0ba17a10 100644 --- a/packages/block-editor/src/components/block-controls/slot.js +++ b/packages/block-editor/src/components/block-controls/slot.js @@ -42,7 +42,7 @@ export default function BlockControlsSlot( { group = 'default', ...props } ) { return null; } - const slot = ; + const slot = ; if ( group === 'default' ) { return slot; 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 b2087ac2ff5f6d..ac27341df1a973 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,12 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { - useLayoutEffect, - useEffect, - useRef, - useState, -} from '@wordpress/element'; +import { forwardRef, useEffect, useRef, useState } from '@wordpress/element'; import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { @@ -31,7 +26,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(); @@ -82,77 +80,79 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { setIsCollapsed( false ); }, [ selectedBlockClientId ] ); - const isLargerThanTabletViewport = useViewportMatch( 'large', '>=' ); - const isFullscreen = - document.body.classList.contains( 'is-fullscreen-mode' ); - - useLayoutEffect( () => { - // don't do anything if not fixed toolbar - if ( ! isFixed || ! blockType ) { - return; - } - - const blockToolbar = document.querySelector( - '.block-editor-block-contextual-toolbar' - ); - - if ( ! blockToolbar ) { - 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 - const pinnedItems = document.querySelector( - '.edit-post-header__settings' - ); - - // 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, - ] ); + // TODO: Do we need all of this width calculation?? + + // const isLargerThanTabletViewport = useViewportMatch( 'large', '>=' ); + // const isFullscreen = + // document.body.classList.contains( 'is-fullscreen-mode' ); + + // useLayoutEffect( () => { + // // don't do anything if not fixed toolbar + // if ( ! isFixed || ! blockType ) { + // return; + // } + + // const blockToolbar = document.querySelector( + // '.block-editor-block-contextual-toolbar' + // ); + + // if ( ! blockToolbar ) { + // 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 + // const pinnedItems = document.querySelector( + // '.edit-post-header__settings' + // ); + + // // 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 ) + 10 // 10 is the pinned items padding + // : 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, + // ] ); const isToolbarEnabled = ! blockType || @@ -174,6 +174,7 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { return ( { + 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 showEmptyBlockSideInserter = + ! isTyping && editorMode === 'edit' && isEmptyDefaultBlock; + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + if ( showEmptyBlockSideInserter ) { + return ( + +
+ +
+
+ ); + } + + return null; +} + +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, + }; +} + +export default function WrappedEmptyBlockInserter( { + __unstablePopoverSlot, + __unstableContentRef, +} ) { + const selected = useSelect( wrapperSelector, [] ); + + if ( ! selected ) { + return null; + } + + const { + clientId, + rootClientId, + name, + isEmptyDefaultBlock, + capturingClientId, + } = selected; + + if ( ! name ) { + return null; + } + + return ( + + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 8e3b240838fd04..b34b591c111c1f 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -2,8 +2,7 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; -import { Popover } from '@wordpress/components'; +import { Fill, Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; @@ -14,9 +13,9 @@ import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockPopover from './selected-block-popover'; +import EmptyBlockInserter from './empty-block-inserter'; +import SelectedBlockTools from './selected-block-tools'; import { store as blockEditorStore } from '../../store'; -import BlockContextualToolbar from './block-contextual-toolbar'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; @@ -45,7 +44,6 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const isLargeViewport = useViewportMatch( 'medium' ); const { hasFixedToolbar, isZoomOutMode, isTyping } = useSelect( selector, [] @@ -64,6 +62,11 @@ export default function BlockTools( { moveBlocksDown, } = useDispatch( blockEditorStore ); + // If we go with removing bubblesVirtually from the block controls slot, + // we can also remove all of this and all the navigable toolbar forwardRef stuff, + // as we don't need to stop the escape unselect shortcut from hitting first. + const selectedBlockToolsRef = useRef( null ); + function onKeyDown( event ) { if ( event.defaultPrevented ) return; @@ -138,17 +141,19 @@ export default function BlockTools( { __unstableContentRef={ __unstableContentRef } /> ) } - { ! isZoomOutMode && - ( hasFixedToolbar || ! isLargeViewport ) && ( - - ) } - { /* Even if the toolbar is fixed, the block popover is still - needed for navigation and zoom-out mode. */ } - - { /* Used for the inline rich text toolbar. */ } - + + + + + { /* Used for the inline rich text toolbar. */ } + + { children } { /* Used for inline rich text popovers. */ } -
- -
- + { + 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 } + /> ); } + if ( showEmptyBlockSideInserter ) { + return null; + } + if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { return ( { shouldShowContextualToolbar && ( { + const { isFixed } = props; const selected = useSelect( wrapperSelector, [] ); if ( ! selected ) { @@ -253,13 +243,15 @@ export default function WrappedBlockPopover( { } return ( - ); -} +}; + +export default forwardRef( UnforwardWrappedSelectedBlockTools ); diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index a6b7d636f491ba..94abe73406d148 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -90,7 +90,7 @@ */ // Base left position for the toolbar when fixed. -@include editor-left(".block-editor-block-contextual-toolbar.is-fixed"); +// @include editor-left(".block-editor-block-contextual-toolbar.is-fixed"); .block-editor-block-contextual-toolbar { // Block UI appearance. @@ -105,10 +105,8 @@ } &.is-fixed { - position: sticky; - top: 0; - z-index: z-index(".block-editor-block-popover"); - display: block; + position: fixed; + top: $admin-bar-height-big + $header-height; width: 100%; overflow: hidden; @@ -119,6 +117,7 @@ border: none; border-bottom: $border-width solid $gray-200; + border-top: $border-width solid $gray-200; border-radius: 0; .block-editor-block-toolbar .components-toolbar-group, @@ -142,48 +141,19 @@ $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; @include break-medium() { &.is-fixed { - // 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; + position: relative; + top: 0; + display: inline-flex; + width: auto; - // Mimic the height of the parent, vertically align center, and provide a max-height. - height: $header-height; - align-items: center; + // remove the border + border: none; &.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; - } - } - & > .block-editor-block-toolbar { - flex-grow: initial; width: initial; // Add a border as separator in the block toolbar. @@ -332,38 +302,6 @@ } } } - - // on tablet viewports 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 { - width: calc(100% - #{$toolbar-margin}); - - .show-icon-labels & { - width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons - } - - } - } - - // 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 { - width: auto; - .show-icon-labels & { - width: auto; //there are no undo, redo and list view buttons - } - } - .is-fullscreen-mode &.is-fixed { - // 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}); - } - } } /** diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 3e531c93c11989..0314aec454fb96 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -3,16 +3,24 @@ */ import { NavigableMenu, Toolbar } from '@wordpress/components'; import { + forwardRef, useState, useRef, useLayoutEffect, useEffect, useCallback, } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { focus } from '@wordpress/dom'; +import { ESCAPE } from '@wordpress/keycodes'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + function hasOnlyToolbarItem( elements ) { const dataProp = 'toolbarItem'; return ! elements.some( ( element ) => ! ( dataProp in element.dataset ) ); @@ -38,7 +46,7 @@ function focusFirstTabbableIn( container ) { } } -function useIsAccessibleToolbar( ref ) { +function useIsAccessibleToolbar( toolbarRef ) { /* * By default, we'll assume the starting accessible state of the Toolbar * is true, as it seems to be the most common case. @@ -62,7 +70,7 @@ function useIsAccessibleToolbar( ref ) { ); const determineIsAccessibleToolbar = useCallback( () => { - const tabbables = focus.tabbable.find( ref.current ); + const tabbables = focus.tabbable.find( toolbarRef.current ); const onlyToolbarItem = hasOnlyToolbarItem( tabbables ); if ( ! onlyToolbarItem ) { deprecated( 'Using custom components as toolbar controls', { @@ -73,7 +81,7 @@ function useIsAccessibleToolbar( ref ) { } ); } setIsAccessibleToolbar( onlyToolbarItem ); - }, [] ); + }, [ toolbarRef ] ); useLayoutEffect( () => { // Toolbar buttons may be rendered asynchronously, so we use @@ -81,15 +89,18 @@ function useIsAccessibleToolbar( ref ) { const observer = new window.MutationObserver( determineIsAccessibleToolbar ); - observer.observe( ref.current, { childList: true, subtree: true } ); + observer.observe( toolbarRef.current, { + childList: true, + subtree: true, + } ); return () => observer.disconnect(); - }, [ isAccessibleToolbar ] ); + }, [ isAccessibleToolbar, determineIsAccessibleToolbar, toolbarRef ] ); return isAccessibleToolbar; } function useToolbarFocus( - ref, + toolbarRef, focusOnMount, isAccessibleToolbar, defaultIndex, @@ -101,8 +112,8 @@ function useToolbarFocus( const [ initialIndex ] = useState( defaultIndex ); const focusToolbar = useCallback( () => { - focusFirstTabbableIn( ref.current ); - }, [] ); + focusFirstTabbableIn( toolbarRef.current ); + }, [ toolbarRef ] ); const focusToolbarViaShortcut = () => { if ( shouldUseKeyboardFocusShortcut ) { @@ -121,7 +132,7 @@ function useToolbarFocus( useEffect( () => { // Store ref so we have access on useEffect cleanup: https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing - const navigableToolbarRef = ref.current; + const navigableToolbarRef = toolbarRef.current; // If initialIndex is passed, we focus on that toolbar item when the // toolbar gets mounted and initial focus is not forced. // We have to wait for the next browser paint because block controls aren't @@ -150,22 +161,28 @@ function useToolbarFocus( const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; - }, [ initialIndex, initialFocusOnMount ] ); + }, [ initialIndex, initialFocusOnMount, toolbarRef, onIndexChange ] ); } -function NavigableToolbar( { - children, - focusOnMount, - shouldUseKeyboardFocusShortcut = true, - __experimentalInitialIndex: initialIndex, - __experimentalOnIndexChange: onIndexChange, - ...props -} ) { - const ref = useRef(); - const isAccessibleToolbar = useIsAccessibleToolbar( ref ); - +function UnforwardNavigableToolbar( + { + children, + focusOnMount, + handleOnKeyDown, + shouldUseKeyboardFocusShortcut = true, + __experimentalInitialIndex: initialIndex, + __experimentalOnIndexChange: onIndexChange, + focusEditorOnEscape = true, + ...props + }, + ref +) { + const maybeRef = useRef(); + // If a ref was not forwarded, we create one. + const toolbarRef = ref || maybeRef; + const isAccessibleToolbar = useIsAccessibleToolbar( toolbarRef ); useToolbarFocus( - ref, + toolbarRef, focusOnMount, isAccessibleToolbar, initialIndex, @@ -173,9 +190,64 @@ function NavigableToolbar( { shouldUseKeyboardFocusShortcut ); + const { lastFocus } = useSelect( ( select ) => { + const { getLastFocus } = select( blockEditorStore ); + return { + lastFocus: getLastFocus(), + }; + }, [] ); + + useEffect( () => { + const navigableToolbarRef = toolbarRef.current; + + if ( handleOnKeyDown ) { + const handleKeyDown = ( event ) => { + handleOnKeyDown( event ); + }; + + navigableToolbarRef.addEventListener( 'keydown', handleKeyDown ); + + return () => { + navigableToolbarRef.removeEventListener( + 'keydown', + handleKeyDown + ); + }; + } + }, [ handleOnKeyDown, toolbarRef ] ); + + + // TODO: Not sure if this is a good idea... but it's working for now and gets the behavior we're after with the fewest lines of code, but also is limiting. + useEffect( () => { + const navigableToolbarRef = toolbarRef.current; + + if ( focusEditorOnEscape ) { + const handleKeyDown = ( event ) => { + if ( event.keyCode === ESCAPE && lastFocus?.current ) { + // Focus the last focused element when pressing escape. + event.preventDefault(); + lastFocus.current.focus(); + } + }; + + navigableToolbarRef.addEventListener( 'keydown', handleKeyDown ); + + return () => { + navigableToolbarRef.removeEventListener( + 'keydown', + handleKeyDown + ); + }; + } + }, [ focusEditorOnEscape, toolbarRef, lastFocus ] ); + if ( isAccessibleToolbar ) { return ( - + { children } ); @@ -185,7 +257,7 @@ function NavigableToolbar( { { children } @@ -193,4 +265,4 @@ function NavigableToolbar( { ); } -export default NavigableToolbar; +export default forwardRef( UnforwardNavigableToolbar ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index a22b251dd607c2..ebd32a0cb5940b 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -37,6 +37,7 @@ import { useBeforeInputRules } from './use-before-input-rules'; import { useInputRules } from './use-input-rules'; import { useDelete } from './use-delete'; import { useEnter } from './use-enter'; +import { useTab } from './use-tab'; import { useFormatTypes } from './use-format-types'; import { useRemoveBrowserShortcuts } from './use-remove-browser-shortcuts'; import { useShortcuts } from './use-shortcuts'; @@ -399,6 +400,10 @@ function RichTextWrapper( disableLineBreaks, onSplitAtEnd, } ), + useTab( { + value, + onChange, + } ), useFirefoxCompat(), anchorRef, ] ) } diff --git a/packages/block-editor/src/components/rich-text/use-tab.js b/packages/block-editor/src/components/rich-text/use-tab.js new file mode 100644 index 00000000000000..e5ca01f594cdb8 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-tab.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { insert } from '@wordpress/rich-text'; +import { useRefEffect } from '@wordpress/compose'; +import { TAB } from '@wordpress/keycodes'; + +export function useTab( props ) { + const propsRef = useRef( props ); + propsRef.current = props; + return useRefEffect( ( element ) => { + function onKeyDown( event ) { + const { keyCode } = event; + + if ( event.defaultPrevented ) { + return; + } + + const { value, onChange } = propsRef.current; + const _value = { ...value }; + + if ( keyCode === TAB ) { + event.preventDefault(); + + const { start, end } = value; + + onChange( insert( _value, '\t', start, end ) ); + } + } + + element.addEventListener( 'keydown', onKeyDown ); + return () => { + element.removeEventListener( 'keydown', onKeyDown ); + }; + }, [] ); + +} \ No newline at end of file diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 616da1bc758136..b1fb1800a53ea2 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -17,15 +17,20 @@ export default function useTabNav() { const container = useRef(); const focusCaptureBeforeRef = useRef(); const focusCaptureAfterRef = useRef(); - const lastFocus = useRef(); + const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setNavigationMode } = useDispatch( blockEditorStore ); + const { setNavigationMode, setLastFocus } = useDispatch( blockEditorStore ); const isNavigationMode = useSelect( ( select ) => select( blockEditorStore ).isNavigationMode(), [] ); + const lastFocus = useSelect( + ( select ) => select( blockEditorStore ).getLastFocus(), + [] + ); + // Don't allow tabbing to this element in Navigation mode. const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; @@ -158,7 +163,7 @@ export default function useTabNav() { } function onFocusOut( event ) { - lastFocus.current = event.target; + setLastFocus( { ...lastFocus, current: event.target } ); const { ownerDocument } = node; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 32108de713f754..5fea452a9666d6 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1925,3 +1925,18 @@ export function unsetBlockEditingMode( clientId = '' ) { clientId, }; } + +/** + * Action that sets the element that had focus when focus leaves the editor canvas. + * + * @param {Object} lastFocus The last focused element. + * + * + * @return {Object} Action object. + */ +export function setLastFocus( lastFocus = null ) { + return { + type: 'LAST_FOCUS', + lastFocus, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 245aaf7adb0fdf..b1d9e77d118bd5 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1913,6 +1913,23 @@ export function blockEditingModes( state = new Map(), action ) { return state; } +/** + * Reducer setting last focused element + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function lastFocus( state = false, action ) { + switch ( action.type ) { + case 'LAST_FOCUS': + return action.lastFocus; + } + + return state; +} + const combinedReducers = combineReducers( { blocks, isTyping, @@ -1929,6 +1946,7 @@ const combinedReducers = combineReducers( { settings, preferences, lastBlockAttributesChange, + lastFocus, editorMode, hasBlockMovingClientId, highlightedBlock, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 2de9e3f00be75f..408705e2f1919e 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -3006,3 +3006,14 @@ export const isGroupable = createRegistrySelector( ); } ); + +/* + * Returns the element of the last element that had focus when focus left the editor canvas. + * + * @param {Object} state Block editor state. + * + * @return {Object} Element. + */ +export function getLastFocus( state ) { + return state.lastFocus; +} diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index 840067e9fb9b3d..314354c57568a2 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -15,7 +15,7 @@ import { EditorHistoryUndo, store as editorStore, } from '@wordpress/editor'; -import { Button, ToolbarItem } from '@wordpress/components'; +import { Button, Slot, ToolbarItem } from '@wordpress/components'; import { listView, plus } from '@wordpress/icons'; import { useRef, useCallback } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; @@ -131,53 +131,69 @@ function HeaderToolbar() { const shortLabel = ! isInserterOpened ? __( 'Add' ) : __( 'Close' ); return ( - -
- - { ( isWideViewport || ! showIconLabels ) && ( - <> - { isLargeViewport && ! hasFixedToolbar && ( + <> + +
+ + { ( isWideViewport || ! showIconLabels ) && ( + <> + { isLargeViewport && ! hasFixedToolbar && ( + + ) } + - ) } - - - { overflowItems } - - ) } -
-
+ { overflowItems } + + ) } +
+
+ + + ); } diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index ab4bbd4bbc5d15..c1accada0cb3af 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -45,7 +45,7 @@ function Header( { setEntitiesSavedStatesCallback } ) { ); return ( -
+
{ hasDefaultEditorCanvasView && ( - -
- { ! isDistractionFree && ( - - ) } - { isLargeViewport && ( - <> - { ! hasFixedToolbar && ( + <> + +
+ { ! isDistractionFree && ( + + ) } + { isLargeViewport && ( + <> + { ! hasFixedToolbar && ( + + ) } - ) } - - - { ! isDistractionFree && ( - ) } - { isZoomedOutViewExperimentEnabled && - ! isDistractionFree && - ! hasFixedToolbar && ( + { ! isDistractionFree && ( { - setPreviewDeviceType( - 'Desktop' - ); - __unstableSetEditorMode( - isZoomedOutView - ? 'edit' - : 'zoom-out' - ); - } } + label={ __( 'List View' ) } + onClick={ toggleListView } + shortcut={ listViewShortcut } + showTooltip={ ! showIconLabels } + variant={ + showIconLabels + ? 'tertiary' + : undefined + } + aria-expanded={ isListViewOpen } /> ) } - - ) } -
-
+ { isZoomedOutViewExperimentEnabled && + ! isDistractionFree && + ! hasFixedToolbar && ( + { + setPreviewDeviceType( + 'Desktop' + ); + __unstableSetEditorMode( + isZoomedOutView + ? 'edit' + : 'zoom-out' + ); + } } + /> + ) } + + ) } +
+
+ + + ) } { ! isDistractionFree && ( diff --git a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js index 1346041b6a94c1..b8abbedda20f33 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js @@ -35,8 +35,12 @@ function KeyboardShortcutsEditMode() { useDispatch( interfaceStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); - const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = - useSelect( blockEditorStore ); + const { + getBlockName, + getLastFocus, + getSelectedBlockClientId, + getBlockAttributes, + } = useSelect( blockEditorStore ); const handleTextLevelShortcut = ( event, level ) => { event.preventDefault(); @@ -118,6 +122,19 @@ function KeyboardShortcutsEditMode() { toggleDistractionFree(); } ); + useShortcut( 'core/edit-site/focus-editor', ( event ) => { + event.preventDefault(); + const lastFocus = getLastFocus(); + // Only move focus if the selected block is a match with the last focused block + if ( + getSelectedBlockClientId() && + lastFocus?.current && + lastFocus?.current.id.includes( getSelectedBlockClientId() ) + ) { + lastFocus.current.focus(); + } + } ); + return null; } diff --git a/packages/edit-site/src/components/keyboard-shortcuts/register.js b/packages/edit-site/src/components/keyboard-shortcuts/register.js index 8dfd1e3e2a45bf..7923af6bcf09bd 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/register.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/register.js @@ -159,6 +159,18 @@ function KeyboardShortcutsRegister() { character: '\\', }, } ); + + registerShortcut( { + name: 'core/edit-site/focus-editor', + category: 'global', + description: __( + 'Navigate to the last focused element in the editor.' + ), + keyCombination: { + modifier: 'alt', + character: 'F9', + }, + } ); }, [ registerShortcut ] ); return null; diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 11c7bdeeaf2a19..5557266aead4b4 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -249,23 +249,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 { diff --git a/packages/editor/src/components/global-keyboard-shortcuts/index.js b/packages/editor/src/components/global-keyboard-shortcuts/index.js index 4b45fe449123f4..f2b48c285905ae 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/index.js +++ b/packages/editor/src/components/global-keyboard-shortcuts/index.js @@ -3,6 +3,7 @@ */ import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { useDispatch, useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -12,6 +13,8 @@ import { store as editorStore } from '../../store'; export default function EditorKeyboardShortcuts() { const { redo, undo, savePost } = useDispatch( editorStore ); const { isEditedPostDirty, isPostSavingLocked } = useSelect( editorStore ); + const { getLastFocus, getSelectedBlockClientId } = + useSelect( blockEditorStore ); useShortcut( 'core/editor/undo', ( event ) => { undo(); @@ -45,5 +48,18 @@ export default function EditorKeyboardShortcuts() { savePost(); } ); + useShortcut( 'core/editor/focus-editor', ( event ) => { + event.preventDefault(); + const lastFocus = getLastFocus(); + // Only move focus if the selected block is a match with the last focused block + if ( + getSelectedBlockClientId() && + lastFocus?.current && + lastFocus?.current.id.includes( getSelectedBlockClientId() ) + ) { + lastFocus.current.focus(); + } + } ); + return null; } diff --git a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js index 8e8f4c42ca6dd6..456d6b90458cb5 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js +++ b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js @@ -53,6 +53,18 @@ function EditorKeyboardShortcutsRegister() { }, ], } ); + + registerShortcut( { + name: 'core/editor/focus-editor', + category: 'global', + description: __( + 'Navigate to the last focused element in the editor.' + ), + keyCombination: { + modifier: 'alt', + character: 'F9', + }, + } ); }, [ registerShortcut ] ); return ; diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index fa2befc603b7e4..d3b66090e18d2b 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -298,7 +298,7 @@ function filterRange( node, range, filter ) { * @param {string} string */ function collapseWhiteSpace( string ) { - return string.replace( /[\n\r\t]+/g, ' ' ); + return string.replace( /[\n\r]+/g, ' ' ); } /**