diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js index 3597bc07b07f79..029419926e9ed5 100644 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ b/packages/block-editor/src/components/block-tools/back-compat.js @@ -9,7 +9,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockPopover from './selected-block-popover'; +import BlockPopover from './selected-block-tools'; export default function BlockToolsBackCompat( { children } ) { const openRef = useContext( InsertionPointOpenRef ); diff --git a/packages/block-editor/src/components/block-tools/empty-block-inserter.js b/packages/block-editor/src/components/block-tools/empty-block-inserter.js new file mode 100644 index 00000000000000..1d520ed72b1c69 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/empty-block-inserter.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import Inserter from '../inserter'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; + +export default function EmptyBlockInserter( { + clientId, + __unstableContentRef, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + 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..3a570712778182 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -6,28 +6,48 @@ import { useViewportMatch } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies */ +import EmptyBlockInserter from './empty-block-inserter'; import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockPopover from './selected-block-popover'; +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'; function selector( select ) { - const { __unstableGetEditorMode, getSettings, isTyping } = - select( blockEditorStore ); + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlock, + getSettings, + __unstableGetEditorMode, + isTyping, + } = select( blockEditorStore ); + + const clientId = + getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); + + const { name = '', attributes = {} } = getBlock( clientId ) || {}; return { - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + clientId, hasFixedToolbar: getSettings().hasFixedToolbar, + hasSelectedBlock: clientId && name, isTyping: isTyping(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + showEmptyBlockSideInserter: + clientId && + ! isTyping() && + __unstableGetEditorMode() === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ), }; } @@ -46,10 +66,14 @@ export default function BlockTools( { ...props } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { hasFixedToolbar, isZoomOutMode, isTyping } = useSelect( - selector, - [] - ); + const { + clientId, + hasFixedToolbar, + hasSelectedBlock, + isTyping, + isZoomOutMode, + showEmptyBlockSideInserter, + } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = useSelect( blockEditorStore ); @@ -142,11 +166,22 @@ export default function BlockTools( { ( hasFixedToolbar || ! isLargeViewport ) && ( ) } + + { showEmptyBlockSideInserter && ( + + ) } { /* Even if the toolbar is fixed, the block popover is still needed for navigation and zoom-out mode. */ } - + { ! showEmptyBlockSideInserter && hasSelectedBlock && ( + + ) } + { /* Used for the inline rich text toolbar. */ } { children } diff --git a/packages/block-editor/src/components/block-tools/selected-block-popover.js b/packages/block-editor/src/components/block-tools/selected-block-popover.js deleted file mode 100644 index 8c87d8ea3a4739..00000000000000 --- a/packages/block-editor/src/components/block-tools/selected-block-popover.js +++ /dev/null @@ -1,265 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useRef, useEffect } from '@wordpress/element'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import BlockSelectionButton from './block-selection-button'; -import BlockContextualToolbar from './block-contextual-toolbar'; -import { store as blockEditorStore } from '../../store'; -import BlockPopover from '../block-popover'; -import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; -import Inserter from '../inserter'; -import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; - -function selector( select ) { - const { - __unstableGetEditorMode, - hasMultiSelection, - isTyping, - getLastMultiSelectedBlockClientId, - } = select( blockEditorStore ); - - return { - editorMode: __unstableGetEditorMode(), - hasMultiSelection: hasMultiSelection(), - isTyping: isTyping(), - lastClientId: hasMultiSelection() - ? getLastMultiSelectedBlockClientId() - : null, - }; -} - -function SelectedBlockPopover( { - clientId, - rootClientId, - isEmptyDefaultBlock, - capturingClientId, - __unstablePopoverSlot, - __unstableContentRef, -} ) { - const { editorMode, hasMultiSelection, isTyping, lastClientId } = useSelect( - selector, - [] - ); - - const isInsertionPointVisible = useSelect( - ( select ) => { - const { - isBlockInsertionPointVisible, - getBlockInsertionPoint, - getBlockOrder, - } = select( blockEditorStore ); - - if ( ! isBlockInsertionPointVisible() ) { - return false; - } - - const insertionPoint = getBlockInsertionPoint(); - const order = getBlockOrder( insertionPoint.rootClientId ); - return order[ insertionPoint.index ] === clientId; - }, - [ clientId ] - ); - const isToolbarForced = useRef( false ); - const { shouldShowContextualToolbar, canFocusHiddenToolbar } = - useShouldContextualToolbarShow(); - - const { stopTyping } = useDispatch( blockEditorStore ); - - const showEmptyBlockSideInserter = - ! isTyping && editorMode === 'edit' && isEmptyDefaultBlock; - const shouldShowBreadcrumb = - ! hasMultiSelection && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ); - - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: ! canFocusHiddenToolbar, - } - ); - - useEffect( () => { - isToolbarForced.current = false; - } ); - - // Stores the active toolbar item index so the block toolbar can return focus - // to it when re-mounting. - const initialToolbarItemIndexRef = useRef(); - - useEffect( () => { - // Resets the index whenever the active block changes so this is not - // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - initialToolbarItemIndexRef.current = undefined; - }, [ clientId ] ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - if ( showEmptyBlockSideInserter ) { - return ( - -
- -
-
- ); - } - - if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { - return ( - - { shouldShowContextualToolbar && ( - { - initialToolbarItemIndexRef.current = index; - } } - // Resets the index whenever the active block changes so - // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - key={ clientId } - /> - ) } - { shouldShowBreadcrumb && ( - - ) } - - ); - } - - 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 WrappedBlockPopover( { - __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/selected-block-tools.js b/packages/block-editor/src/components/block-tools/selected-block-tools.js new file mode 100644 index 00000000000000..8fa0ac97d5c941 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/selected-block-tools.js @@ -0,0 +1,130 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import BlockContextualToolbar from './block-contextual-toolbar'; +import { store as blockEditorStore } from '../../store'; +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; +import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; + +export default function SelectedBlockTools( { + clientId, + showEmptyBlockSideInserter, + __unstableContentRef, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const { shouldShowBreadcrumb } = useSelect( ( select ) => { + const { hasMultiSelection, __unstableGetEditorMode } = + select( blockEditorStore ); + + const editorMode = __unstableGetEditorMode(); + + return { + shouldShowBreadcrumb: + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ), + }; + }, [] ); + + const isToolbarForced = useRef( false ); + const { shouldShowContextualToolbar, canFocusHiddenToolbar } = + useShouldContextualToolbarShow(); + + const { stopTyping } = useDispatch( blockEditorStore ); + + useShortcut( + 'core/block-editor/focus-toolbar', + () => { + isToolbarForced.current = true; + stopTyping( true ); + }, + { + isDisabled: ! canFocusHiddenToolbar, + } + ); + + useEffect( () => { + isToolbarForced.current = false; + } ); + + // Stores the active toolbar item index so the block toolbar can return focus + // to it when re-mounting. + const initialToolbarItemIndexRef = useRef(); + + useEffect( () => { + // Resets the index whenever the active block changes so this is not + // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + initialToolbarItemIndexRef.current = undefined; + }, [ clientId ] ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + if ( showEmptyBlockSideInserter ) { + return null; + } + + if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { + return ( + + { shouldShowContextualToolbar && ( + { + initialToolbarItemIndexRef.current = index; + } } + // Resets the index whenever the active block changes so + // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + key={ clientId } + /> + ) } + { shouldShowBreadcrumb && ( + + ) } + + ); + } + + return null; +} diff --git a/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js new file mode 100644 index 00000000000000..a2783f49bb5389 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** + * Returns props for the selected block tools and empty block inserter. + * + * @param {string} clientId Selected block client ID. + */ +export default function useSelectedBlockToolProps( clientId ) { + const selectedBlockProps = useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockParents, + __experimentalGetBlockListSettingsForBlocks, + isBlockInsertionPointVisible, + getBlockInsertionPoint, + getBlockOrder, + hasMultiSelection, + getLastMultiSelectedBlockClientId, + } = select( blockEditorStore ); + + 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 + ); + + let isInsertionPointVisible = false; + if ( isBlockInsertionPointVisible() ) { + const insertionPoint = getBlockInsertionPoint(); + const order = getBlockOrder( insertionPoint.rootClientId ); + isInsertionPointVisible = + order[ insertionPoint.index ] === clientId; + } + + return { + capturingClientId, + isInsertionPointVisible, + lastClientId: hasMultiSelection() + ? getLastMultiSelectedBlockClientId() + : null, + rootClientId: getBlockRootClientId( clientId ), + }; + }, + [ clientId ] + ); + + return selectedBlockProps; +}