From c3f78204dc1042d3abe237e6a6494ad2f17b8a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Mon, 13 Jan 2020 13:58:17 +0100 Subject: [PATCH] Block: split out toolbar rendering (#19564) * wip * Wait for block to mount * Fix some tests * Revert moving focus effects * Fix align error * Fix navigation mode * Fix capturing toolbar * Remove has-toolbar-captured * Add docs * Simplify multi selection selector * Fix alignment * Clean up * Only add block node for selected block * Simplify block DOM node state * Fix alignment tests * Clean up * Clean up * Avoid node in state --- .../block-list/block-child-toolbar.js | 19 -- .../components/block-list/block-popover.js | 235 ++++++++++++++++++ .../src/components/block-list/block.js | 184 ++------------ .../components/block-list/root-container.js | 2 + .../src/components/block-list/style.scss | 8 +- packages/block-editor/src/store/actions.js | 12 + packages/block-editor/src/store/reducer.js | 20 ++ packages/block-editor/src/store/selectors.js | 11 + 8 files changed, 299 insertions(+), 192 deletions(-) delete mode 100644 packages/block-editor/src/components/block-list/block-child-toolbar.js create mode 100644 packages/block-editor/src/components/block-list/block-popover.js diff --git a/packages/block-editor/src/components/block-list/block-child-toolbar.js b/packages/block-editor/src/components/block-list/block-child-toolbar.js deleted file mode 100644 index aa01285c8ebf3..0000000000000 --- a/packages/block-editor/src/components/block-list/block-child-toolbar.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * WordPress dependencies - */ -import { createSlotFill } from '@wordpress/components'; - -const { Fill, Slot } = createSlotFill( 'ChildToolbar' ); - -export const ChildToolbar = ( { children } ) => ( - - { children } - -); - -// `bubblesVirtually` is required in order to avoid -// events triggered on the child toolbar from bubbling -// up to the parent Block. -export const ChildToolbarSlot = () => ( - -); diff --git a/packages/block-editor/src/components/block-list/block-popover.js b/packages/block-editor/src/components/block-list/block-popover.js new file mode 100644 index 0000000000000..b41f0e22d0f12 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-popover.js @@ -0,0 +1,235 @@ +/** + * External dependencies + */ +import { findIndex } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useState, useCallback } from '@wordpress/element'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { Popover } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { useViewportMatch } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import BlockBreadcrumb from './breadcrumb'; +import BlockContextualToolbar from './block-contextual-toolbar'; +import Inserter from '../inserter'; + +function selector( select ) { + const { + isNavigationMode, + isMultiSelecting, + hasMultiSelection, + isTyping, + isCaretWithinFormattedText, + getSettings, + } = select( 'core/block-editor' ); + return { + isNavigationMode: isNavigationMode(), + isMultiSelecting: isMultiSelecting(), + isTyping: isTyping(), + isCaretWithinFormattedText: isCaretWithinFormattedText(), + hasMultiSelection: hasMultiSelection(), + hasFixedToolbar: getSettings().hasFixedToolbar, + }; +} + +function BlockPopover( { + clientId, + rootClientId, + name, + align, + isValid, + moverDirection, + isEmptyDefaultBlock, + capturingClientId, + hasMovers = true, +} ) { + const { + isNavigationMode, + isMultiSelecting, + isTyping, + isCaretWithinFormattedText, + hasMultiSelection, + hasFixedToolbar, + } = useSelect( selector, [] ); + const isLargeViewport = useViewportMatch( 'medium' ); + const [ isToolbarForced, setIsToolbarForced ] = useState( false ); + + const showEmptyBlockSideInserter = ! isNavigationMode && isEmptyDefaultBlock && isValid; + const shouldShowBreadcrumb = isNavigationMode; + const shouldShowContextualToolbar = + ! isNavigationMode && + ! hasFixedToolbar && + isLargeViewport && + ! showEmptyBlockSideInserter && + ! isMultiSelecting && + ( ! isTyping || isCaretWithinFormattedText ); + const canFocusHiddenToolbar = + ! isNavigationMode && + ! shouldShowContextualToolbar && + ! hasFixedToolbar && + ! isEmptyDefaultBlock; + + useShortcut( + 'core/block-editor/focus-toolbar', + useCallback( () => setIsToolbarForced( true ), [] ), + { bindGlobal: true, eventName: 'keydown', isDisabled: ! canFocusHiddenToolbar } + ); + + if ( + ! shouldShowBreadcrumb && + ! shouldShowContextualToolbar && + ! isToolbarForced && + ! showEmptyBlockSideInserter + ) { + return null; + } + + const node = document.getElementById( 'block-' + capturingClientId ); + + if ( ! node ) { + return null; + } + + // Position above the anchor, pop out towards the right, and position in the + // left corner. For the side inserter, pop out towards the left, and + // position in the right corner. + // To do: refactor `Popover` to make this prop clearer. + const popoverPosition = showEmptyBlockSideInserter ? 'top left right' : 'top right left'; + const popoverIsSticky = hasMultiSelection ? '.wp-block.is-multi-selected' : true; + + return ( + setIsToolbarForced( false ) } + > + { ( shouldShowContextualToolbar || isToolbarForced ) && ( + + ) } + { shouldShowBreadcrumb && ( + + ) } + { showEmptyBlockSideInserter && ( +
+ +
+ ) } +
+ ); +} + +function wrapperSelector( select ) { + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlockRootClientId, + __unstableGetSelectedMountedBlock, + __unstableGetBlockWithoutInnerBlocks, + getBlockParents, + getBlockListSettings, + __experimentalGetBlockListSettingsForBlocks, + } = select( 'core/block-editor' ); + + const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); + + if ( ! clientId ) { + return; + } + + const rootClientId = getBlockRootClientId( clientId ); + const { name, attributes = {}, isValid } = __unstableGetBlockWithoutInnerBlocks( clientId ) || {}; + const blockParentsClientIds = getBlockParents( clientId ); + const { __experimentalMoverDirection } = getBlockListSettings( rootClientId ) || {}; + + // Get Block List Settings for all ancestors of the current Block clientId + const ancestorBlockListSettings = __experimentalGetBlockListSettingsForBlocks( blockParentsClientIds ); + + // Find the index of the first Block with the `captureDescendantsToolbars` prop defined + // This will be the top most ancestor because getBlockParents() returns tree from top -> bottom + const topmostAncestorWithCaptureDescendantsToolbarsIndex = findIndex( ancestorBlockListSettings, [ '__experimentalCaptureToolbars', true ] ); + + let capturingClientId = clientId; + + if ( topmostAncestorWithCaptureDescendantsToolbarsIndex !== -1 ) { + capturingClientId = blockParentsClientIds[ topmostAncestorWithCaptureDescendantsToolbarsIndex ]; + } + + return { + clientId, + rootClientId: getBlockRootClientId( clientId ), + isMounted: __unstableGetSelectedMountedBlock() === clientId, + name, + align: attributes.align, + isValid, + moverDirection: __experimentalMoverDirection, + isEmptyDefaultBlock: name && isUnmodifiedDefaultBlock( { name, attributes } ), + capturingClientId, + }; +} + +export default function WrappedBlockPopover() { + const selected = useSelect( wrapperSelector, [] ); + + if ( ! selected ) { + return null; + } + + const { + clientId, + rootClientId, + isMounted, + name, + align, + isValid, + moverDirection, + isEmptyDefaultBlock, + capturingClientId, + } = selected; + + if ( ! name || ! isMounted ) { + return null; + } + + return ( + + ); +} diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 1b94eb0d59291..89be3dd2c6b66 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -2,13 +2,13 @@ * External dependencies */ import classnames from 'classnames'; -import { first, last, findIndex } from 'lodash'; +import { first, last } from 'lodash'; import { animated } from 'react-spring/web.cjs'; /** * WordPress dependencies */ -import { useRef, useEffect, useLayoutEffect, useState, useCallback, useContext } from '@wordpress/element'; +import { useRef, useEffect, useLayoutEffect, useState, useContext } from '@wordpress/element'; import { focus, isTextField, @@ -23,15 +23,15 @@ import { getUnregisteredTypeHandlerName, __experimentalGetAccessibleBlockLabel as getAccessibleBlockLabel, } from '@wordpress/blocks'; -import { withFilters, Popover } from '@wordpress/components'; +import { withFilters } from '@wordpress/components'; import { withDispatch, withSelect, useSelect, + useDispatch, } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { compose, pure, ifCondition } from '@wordpress/compose'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies @@ -41,12 +41,8 @@ import BlockInvalidWarning from './block-invalid-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; -import BlockBreadcrumb from './breadcrumb'; -import BlockContextualToolbar from './block-contextual-toolbar'; -import Inserter from '../inserter'; import { isInsideRootBlock } from '../../utils/dom'; import useMovingAnimation from './moving-animation'; -import { ChildToolbar, ChildToolbarSlot } from './block-child-toolbar'; import { Context } from './root-container'; /** @@ -78,27 +74,21 @@ const useDebouncedAccessibleBlockLabel = ( blockType, attributes, index, moverDi function BlockListBlock( { mode, isFocusMode, - hasFixedToolbar, moverDirection, isLocked, clientId, - rootClientId, isSelected, isMultiSelected, isPartOfMultiSelection, isFirstMultiSelected, isTypingWithinBlock, - isCaretWithinFormattedText, isEmptyDefaultBlock, isAncestorOfSelectedBlock, - isCapturingDescendantToolbars, - hasAncestorCapturingToolbars, isSelectionEnabled, className, name, index, isValid, - isLast, attributes, initialPosition, wrapperProps, @@ -106,7 +96,6 @@ function BlockListBlock( { onReplace, onInsertBlocksAfter, onMerge, - onSelect, onRemove, onInsertDefaultBlockAfter, toggleSelection, @@ -114,9 +103,7 @@ function BlockListBlock( { enableAnimation, isNavigationMode, isMultiSelecting, - isLargeViewport, hasSelectedUI = true, - hasMovers = true, } ) { const onSelectionStart = useContext( Context ); // In addition to withSelect, we should favor using useSelect in this component going forward @@ -126,10 +113,19 @@ function BlockListBlock( { isDraggingBlocks: select( 'core/block-editor' ).isDraggingBlocks(), }; }, [] ); + const { + __unstableSetSelectedMountedBlock, + } = useDispatch( 'core/block-editor' ); // Reference of the wrapper const wrapper = useRef( null ); + useLayoutEffect( () => { + if ( isSelected || isFirstMultiSelected ) { + __unstableSetSelectedMountedBlock( clientId ); + } + }, [ isSelected, isFirstMultiSelected ] ); + // Reference to the block edit node const blockNodeRef = useRef(); @@ -137,8 +133,6 @@ function BlockListBlock( { const [ hasError, setErrorState ] = useState( false ); const onBlockError = () => setErrorState( true ); - const [ isToolbarForced, setIsToolbarForced ] = useState( false ); - const blockType = getBlockType( name ); const blockAriaLabel = useDebouncedAccessibleBlockLabel( blockType, attributes, index, moverDirection, 400 ); @@ -181,11 +175,11 @@ function BlockListBlock( { // Focus the selected block's wrapper or inner input on mount and update const isMounting = useRef( true ); useEffect( () => { - if ( isSelected && ! isMultiSelecting ) { + if ( isSelected && ! isMultiSelecting && ! isNavigationMode ) { focusTabbable( ! isMounting.current ); } isMounting.current = false; - }, [ isSelected, isMultiSelecting ] ); + }, [ isSelected, isMultiSelecting, isNavigationMode ] ); // Focus the first multi selected block useEffect( () => { @@ -248,46 +242,16 @@ function BlockListBlock( { } }; - const selectOnOpen = ( open ) => { - if ( open && ! isSelected ) { - onSelect(); - } - }; - - const canFocusHiddenToolbar = ( - ! isNavigationMode && - ! shouldShowContextualToolbar && - isSelected && - ! hasFixedToolbar && - ! isEmptyDefaultBlock - ); - useShortcut( - 'core/block-editor/focus-toolbar', - useCallback( () => setIsToolbarForced( true ), [] ), - { bindGlobal: true, eventName: 'keydown', isDisabled: ! canFocusHiddenToolbar } - ); - const isUnregisteredBlock = name === getUnregisteredTypeHandlerName(); // If the block is selected and we're typing the block should not appear. // Empty paragraph blocks should always show up as unselected. - const showEmptyBlockSideInserter = ! isNavigationMode && ( isSelected || isLast ) && isEmptyDefaultBlock && isValid; + const showEmptyBlockSideInserter = ! isNavigationMode && isSelected && isEmptyDefaultBlock && isValid; const shouldAppearSelected = ! isFocusMode && ! showEmptyBlockSideInserter && isSelected && ! isTypingWithinBlock; - const shouldShowBreadcrumb = isNavigationMode && isSelected; - const shouldShowContextualToolbar = - ! isNavigationMode && - ! hasFixedToolbar && - isLargeViewport && - ! showEmptyBlockSideInserter && - ! isMultiSelecting && - ( - ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || - isFirstMultiSelected - ); const isDragging = isDraggingBlocks && ( isSelected || isPartOfMultiSelection ); @@ -307,7 +271,6 @@ function BlockListBlock( { 'is-focused': isFocusMode && ( isSelected || isAncestorOfSelectedBlock ), 'is-focus-mode': isFocusMode, 'has-child-selected': isAncestorOfSelectedBlock, - 'has-toolbar-captured': hasAncestorCapturingToolbars, }, className ); @@ -343,32 +306,6 @@ function BlockListBlock( { blockEdit =
{ blockEdit }
; } - /** - * Renders an individual `BlockContextualToolbar` component. - * This needs to be a function which generates the component - * on demand as we can only have a single toolbar for each render. - * This is because of the `isForcingContextualToolbar` logic which - * relies on a single toolbar being rendered to update the boolean - * value of the ref used to track the "force" state. - */ - const renderBlockContextualToolbar = () => ( - - ); - - // Position above the anchor, pop out towards the right, and position in the - // left corner. For the side inserter, pop out towards the left, and - // position in the right corner. - // To do: refactor `Popover` to make this prop clearer. - const popoverPosition = showEmptyBlockSideInserter ? 'top left right' : 'top right left'; - const popoverIsSticky = isPartOfMultiSelection ? '.wp-block.is-multi-selected' : true; - return ( - { hasAncestorCapturingToolbars && ( shouldShowContextualToolbar || isToolbarForced ) && ( - // If the parent Block is set to consume toolbars of the child Blocks - // then render the child Block's toolbar into the Slot provided - // by the parent. - - { renderBlockContextualToolbar() } - - ) } - { ( - shouldShowBreadcrumb || - shouldShowContextualToolbar || - isToolbarForced || - showEmptyBlockSideInserter || - isCapturingDescendantToolbars - ) && ( - setIsToolbarForced( false ) } - > - { ! hasAncestorCapturingToolbars && ( shouldShowContextualToolbar || isToolbarForced ) && renderBlockContextualToolbar() } - { ( isCapturingDescendantToolbars ) && ( - // A slot made available on all ancestors of the selected Block - // to allow child Blocks to render their toolbars into the DOM - // of the appropriate parent. - - ) } - { shouldShowBreadcrumb && ( - - ) } - { showEmptyBlockSideInserter && ( -
- -
- ) } -
- ) }
bottom - const topmostAncestorWithCaptureDescendantsToolbarsIndex = findIndex( ancestorBlockListSettings, [ '__experimentalCaptureToolbars', true ] ); - - // Boolean to indicate whether current Block has a parent with `captureDescendantsToolbars` set - const hasAncestorCapturingToolbars = topmostAncestorWithCaptureDescendantsToolbarsIndex !== -1 ? true : false; - - // Is the *current* Block the one capturing all its descendant toolbars? - // If there is no `topmostAncestorWithCaptureDescendantsToolbarsIndex` then - // we're at the top of the tree - const isCapturingDescendantToolbars = isAncestorOfSelectedBlock && ( currentBlockListSettings && currentBlockListSettings.__experimentalCaptureToolbars ) && ! hasAncestorCapturingToolbars; // The fallback to `{}` is a temporary fix. // This function should never be called when a block is not present in the state. @@ -539,7 +398,6 @@ const applyWithSelect = withSelect( // Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected. isTypingWithinBlock: ( isSelected || isAncestorOfSelectedBlock ) && isTyping(), - isCaretWithinFormattedText: isSelected && isCaretWithinFormattedText(), mode: getBlockMode( clientId ), isSelectionEnabled: isSelectionEnabled(), @@ -548,8 +406,6 @@ const applyWithSelect = withSelect( name && isUnmodifiedDefaultBlock( { name, attributes } ), isLocked: !! templateLock, isFocusMode: focusMode && isLargeViewport, - hasFixedToolbar: hasFixedToolbar && isLargeViewport, - isLast: index === blockOrder.length - 1, isNavigationMode: isNavigationMode(), index, isRTL, @@ -564,8 +420,6 @@ const applyWithSelect = withSelect( isValid, isSelected, isAncestorOfSelectedBlock, - isCapturingDescendantToolbars, - hasAncestorCapturingToolbars, }; } ); @@ -573,7 +427,6 @@ const applyWithSelect = withSelect( const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { const { updateBlockAttributes, - selectBlock, insertBlocks, insertDefaultBlock, removeBlock, @@ -588,9 +441,6 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { const { clientId } = ownProps; updateBlockAttributes( clientId, newAttributes ); }, - onSelect( clientId = ownProps.clientId, initialPosition ) { - selectBlock( clientId, initialPosition ); - }, onInsertBlocks( blocks, index ) { const { rootClientId } = ownProps; insertBlocks( blocks, index, rootClientId ); diff --git a/packages/block-editor/src/components/block-list/root-container.js b/packages/block-editor/src/components/block-list/root-container.js index 437be8f1cd795..74d03524edfe3 100644 --- a/packages/block-editor/src/components/block-list/root-container.js +++ b/packages/block-editor/src/components/block-list/root-container.js @@ -10,6 +10,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import useMultiSelection from './use-multi-selection'; import { getBlockClientId } from '../../utils/dom'; import InsertionPoint from './insertion-point'; +import BlockPopover from './block-popover'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ @@ -78,6 +79,7 @@ function RootContainer( { children, className }, ref ) { isMultiSelecting={ isMultiSelecting } selectedBlockClientId={ selectedBlockClientId } > +