diff --git a/packages/block-editor/src/components/block-editing-mode/index.js b/packages/block-editor/src/components/block-editing-mode/index.js index 5d916d9816e60..20f93f6b36f90 100644 --- a/packages/block-editor/src/components/block-editing-mode/index.js +++ b/packages/block-editor/src/components/block-editing-mode/index.js @@ -2,13 +2,13 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useContext, useEffect } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { BlockListBlockContext } from '../block-list/block-list-block-context'; +import { useBlockEditContext } from '../block-edit/context'; /** * @typedef {'disabled'|'contentOnly'|'default'} BlockEditingMode @@ -45,7 +45,7 @@ import { BlockListBlockContext } from '../block-list/block-list-block-context'; * @return {BlockEditingMode} The current editing mode. */ export function useBlockEditingMode( mode ) { - const { clientId = '' } = useContext( BlockListBlockContext ) ?? {}; + const { clientId = '' } = useBlockEditContext(); const blockEditingMode = useSelect( ( select ) => select( blockEditorStore ).getBlockEditingMode( clientId ), diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index b38dcf3ef1f2e..0bd5d0b7e199f 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useMemo, useCallback, RawHTML } from '@wordpress/element'; +import { useCallback, RawHTML, useContext } from '@wordpress/element'; import { getBlockType, getSaveContent, @@ -15,16 +15,13 @@ import { switchToBlockType, getDefaultBlockName, isUnmodifiedBlock, + isReusableBlock, + getBlockDefaultClassName, store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; -import { - withDispatch, - withSelect, - useDispatch, - useSelect, -} from '@wordpress/data'; -import { compose, pure, ifCondition } from '@wordpress/compose'; +import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; +import { compose, pure } from '@wordpress/compose'; import { safeHTML } from '@wordpress/dom'; /** @@ -38,7 +35,15 @@ import BlockHtml from './block-html'; import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; -import { BlockListBlockContext } from './block-list-block-context'; +import { PrivateBlockContext } from './private-block-context'; + +import { unlock } from '../../lock-unlock'; + +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; /** * Merges wrapper props with special handling for classNames and styles. @@ -101,44 +106,11 @@ function BlockListBlock( { toggleSelection, } ) { const { - themeSupportsLayout, - isTemporarilyEditingAsBlocks, - blockEditingMode, mayDisplayControls, mayDisplayParentControls, - } = useSelect( - ( select ) => { - const { - getSettings, - __unstableGetTemporarilyEditingAsBlocks, - getBlockEditingMode, - getBlockName, - isFirstMultiSelectedBlock, - getMultiSelectedBlockClientIds, - hasSelectedInnerBlock, - } = select( blockEditorStore ); - const { hasBlockSupport } = select( blocksStore ); - return { - themeSupportsLayout: getSettings().supportsLayout, - isTemporarilyEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks() === clientId, - blockEditingMode: getBlockEditingMode( clientId ), - mayDisplayControls: - isSelected || - ( isFirstMultiSelectedBlock( clientId ) && - getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ) ), - mayDisplayParentControls: - hasBlockSupport( - getBlockName( clientId ), - '__experimentalExposeControlsToChildren', - false - ) && hasSelectedInnerBlock( clientId ), - }; - }, - [ clientId, isSelected, name ] - ); + themeSupportsLayout, + ...context + } = useContext( PrivateBlockContext ); const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); @@ -172,12 +144,6 @@ function BlockListBlock( { const blockType = getBlockType( name ); - if ( blockEditingMode === 'disabled' ) { - wrapperProps = { - ...wrapperProps, - tabIndex: -1, - }; - } // Determine whether the block has props to apply to the wrapper. if ( blockType?.getEditWrapperProps ) { wrapperProps = mergeWrapperProps( @@ -241,30 +207,28 @@ function BlockListBlock( { } else if ( blockType?.apiVersion > 1 ) { block = blockEdit; } else { - block = { blockEdit }; + block = { blockEdit }; } const { 'data-align': dataAlign, ...restWrapperProps } = wrapperProps ?? {}; - const value = { - clientId, - className: classnames( - { - 'is-editing-disabled': blockEditingMode === 'disabled', - 'is-content-locked-temporarily-editing-as-blocks': - isTemporarilyEditingAsBlocks, - }, - dataAlign && themeSupportsLayout && `align${ dataAlign }`, - ! ( dataAlign && isSticky ) && className - ), - wrapperProps: restWrapperProps, - isAligned, - }; - - const memoizedValue = useMemo( () => value, Object.values( value ) ); + restWrapperProps.className = classnames( + restWrapperProps.className, + dataAlign && themeSupportsLayout && `align${ dataAlign }`, + ! ( dataAlign && isSticky ) && className + ); + // We set a new context with the adjusted and filtered wrapperProps (through + // `editor.BlockListBlock`), which the `BlockListBlockProvider` did not have + // access to. return ( - + @@ -274,52 +238,10 @@ function BlockListBlock( { > { block } - + ); } -const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { - const { - isBlockSelected, - getBlockMode, - isSelectionEnabled, - getTemplateLock, - __unstableGetBlockWithoutInnerBlocks, - canRemoveBlock, - canMoveBlock, - } = select( blockEditorStore ); - const block = __unstableGetBlockWithoutInnerBlocks( clientId ); - const isSelected = isBlockSelected( clientId ); - const templateLock = getTemplateLock( rootClientId ); - const canRemove = canRemoveBlock( clientId, rootClientId ); - const canMove = canMoveBlock( clientId, rootClientId ); - - // The fallback to `{}` is a temporary fix. - // This function should never be called when a block is not present in - // the state. It happens now because the order in withSelect rendering - // is not correct. - const { name, attributes, isValid } = block || {}; - - // Do not add new properties here, use `useSelect` instead to avoid - // leaking new props to the public API (editor.BlockListBlock filter). - return { - mode: getBlockMode( clientId ), - isSelectionEnabled: isSelectionEnabled(), - isLocked: !! templateLock, - canRemove, - canMove, - // Users of the editor.BlockListBlock filter used to be able to - // access the block prop. - // Ideally these blocks would rely on the clientId prop only. - // This is kept for backward compatibility reasons. - block, - name, - attributes, - isValid, - isSelected, - }; -} ); - const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { const { updateBlockAttributes, @@ -558,13 +480,285 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { }; } ); -export default compose( - pure, - applyWithSelect, +// This component is used by the BlockListBlockProvider component below. It will +// add the props necessary for the `editor.BlockListBlock` filters. +BlockListBlock = compose( applyWithDispatch, - // Block is sometimes not mounted at the right time, causing it be undefined - // see issue for more info - // https://github.com/WordPress/gutenberg/issues/17013 - ifCondition( ( { block } ) => !! block ), withFilters( 'editor.BlockListBlock' ) )( BlockListBlock ); + +// This component provides all the information we need through a single store +// subscription (useSelect mapping). Only the necesssary props are passed down +// to the BlockListBlock component, which is a filtered component, so these +// props are public API. To avoid adding to the public API, we use a private +// context to pass the rest of the information to the filtered BlockListBlock +// component, and useBlockProps. +function BlockListBlockProvider( props ) { + const { clientId, rootClientId } = props; + const selectedProps = useSelect( + ( select ) => { + const { + isBlockSelected, + getBlockMode, + isSelectionEnabled, + getTemplateLock, + __unstableGetBlockWithoutInnerBlocks, + canRemoveBlock, + canMoveBlock, + + getSettings, + __unstableGetTemporarilyEditingAsBlocks, + getBlockEditingMode, + getBlockName, + isFirstMultiSelectedBlock, + getMultiSelectedBlockClientIds, + hasSelectedInnerBlock, + + getBlockIndex, + isTyping, + getGlobalBlockCount, + isBlockMultiSelected, + isAncestorMultiSelected, + isBlockSubtreeDisabled, + isBlockHighlighted, + __unstableIsFullySelected, + __unstableSelectionHasUnmergeableBlock, + isBlockBeingDragged, + hasBlockMovingClientId, + canInsertBlockType, + getBlockRootClientId, + __unstableHasActiveBlockOverlayActive, + __unstableGetEditorMode, + getSelectedBlocksInitialCaretPosition, + } = unlock( select( blockEditorStore ) ); + const block = __unstableGetBlockWithoutInnerBlocks( clientId ); + + // This is a temporary fix. + // This function should never be called when a block is not + // present in the state. It happens now because the order in + // withSelect rendering is not correct. + if ( ! block ) { + return; + } + + const { + hasBlockSupport: _hasBlockSupport, + getActiveBlockVariation, + } = select( blocksStore ); + const _isSelected = isBlockSelected( clientId ); + const templateLock = getTemplateLock( rootClientId ); + const canRemove = canRemoveBlock( clientId, rootClientId ); + const canMove = canMoveBlock( clientId, rootClientId ); + const { name: blockName, attributes, isValid } = block; + const isPartOfMultiSelection = + isBlockMultiSelected( clientId ) || + isAncestorMultiSelected( clientId ); + const blockType = getBlockType( blockName ); + const match = getActiveBlockVariation( blockName, attributes ); + const { outlineMode, supportsLayout } = getSettings(); + const isMultiSelected = isBlockMultiSelected( clientId ); + const checkDeep = true; + const isAncestorOfSelectedBlock = hasSelectedInnerBlock( + clientId, + checkDeep + ); + const typing = isTyping(); + const hasLightBlockWrapper = blockType?.apiVersion > 1; + const movingClientId = hasBlockMovingClientId(); + + return { + mode: getBlockMode( clientId ), + isSelectionEnabled: isSelectionEnabled(), + isLocked: !! templateLock, + canRemove, + canMove, + // Users of the editor.BlockListBlock filter used to be able to + // access the block prop. + // Ideally these blocks would rely on the clientId prop only. + // This is kept for backward compatibility reasons. + block, + name: blockName, + attributes, + isValid, + isSelected: _isSelected, + themeSupportsLayout: supportsLayout, + isTemporarilyEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks() === clientId, + blockEditingMode: getBlockEditingMode( clientId ), + mayDisplayControls: + _isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === blockName + ) ), + mayDisplayParentControls: + _hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && hasSelectedInnerBlock( clientId ), + index: getBlockIndex( clientId ), + blockApiVersion: blockType?.apiVersion || 1, + blockTitle: match?.title || blockType?.title, + isPartOfSelection: _isSelected || isPartOfMultiSelection, + adjustScrolling: + _isSelected || isFirstMultiSelectedBlock( clientId ), + enableAnimation: + ! typing && + getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, + isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), + isOutlineEnabled: outlineMode, + hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), + initialPosition: + _isSelected && __unstableGetEditorMode() === 'edit' + ? getSelectedBlocksInitialCaretPosition() + : undefined, + isHighlighted: isBlockHighlighted( clientId ), + isMultiSelected, + isPartiallySelected: + isMultiSelected && + ! __unstableIsFullySelected() && + ! __unstableSelectionHasUnmergeableBlock(), + isReusable: isReusableBlock( blockType ), + isDragging: isBlockBeingDragged( clientId ), + hasChildSelected: isAncestorOfSelectedBlock, + removeOutline: _isSelected && outlineMode && typing, + isBlockMovingMode: !! movingClientId, + canInsertMovingBlock: + movingClientId && + canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + isEditingDisabled: + getBlockEditingMode( clientId ) === 'disabled', + className: hasLightBlockWrapper + ? attributes.className + : undefined, + defaultClassName: hasLightBlockWrapper + ? getBlockDefaultClassName( blockName ) + : undefined, + }; + }, + [ clientId, rootClientId ] + ); + + const { + mode, + isSelectionEnabled, + isLocked, + canRemove, + canMove, + block, + name, + attributes, + isValid, + isSelected, + themeSupportsLayout, + isTemporarilyEditingAsBlocks, + blockEditingMode, + mayDisplayControls, + mayDisplayParentControls, + index, + blockApiVersion, + blockTitle, + isPartOfSelection, + adjustScrolling, + enableAnimation, + isSubtreeDisabled, + isOutlineEnabled, + hasOverlay, + initialPosition, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + className, + defaultClassName, + } = selectedProps; + + // Block is sometimes not mounted at the right time, causing it be + // undefined see issue for more info + // https://github.com/WordPress/gutenberg/issues/17013 + if ( ! selectedProps ) { + return null; + } + + const privateContext = { + clientId, + className, + index, + mode, + name, + blockApiVersion, + blockTitle, + isSelected, + isPartOfSelection, + adjustScrolling, + enableAnimation, + isSubtreeDisabled, + isOutlineEnabled, + hasOverlay, + initialPosition, + blockEditingMode, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + isTemporarilyEditingAsBlocks, + defaultClassName, + mayDisplayControls, + mayDisplayParentControls, + themeSupportsLayout, + }; + + // Here we separate between the props passed to BlockListBlock and any other + // information we selected for internal use. BlockListBlock is a filtered + // component and thus ALL the props are PUBLIC API. + + // Note that the context value doesn't have to be memoized in this case + // because when it changes, this component will be re-rendered anyway, and + // none of the consumers (BlockListBlock and useBlockProps) are memoized or + // "pure". This is different from the public BlockEditContext, where + // consumers might be memoized or "pure". + return ( + + + + ); +} + +export default pure( BlockListBlockProvider ); diff --git a/packages/block-editor/src/components/block-list/block-list-block-context.js b/packages/block-editor/src/components/block-list/private-block-context.js similarity index 59% rename from packages/block-editor/src/components/block-list/block-list-block-context.js rename to packages/block-editor/src/components/block-list/private-block-context.js index 6fa09c6969ec5..1498105896501 100644 --- a/packages/block-editor/src/components/block-list/block-list-block-context.js +++ b/packages/block-editor/src/components/block-list/private-block-context.js @@ -3,4 +3,4 @@ */ import { createContext } from '@wordpress/element'; -export const BlockListBlockContext = createContext( null ); +export const PrivateBlockContext = createContext( null ); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 593beafa06d83..fea20506c28a1 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -8,22 +8,15 @@ import classnames from 'classnames'; */ import { useContext } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { - __unstableGetBlockProps as getBlockProps, - getBlockType, - isReusableBlock, - getBlockDefaultClassName, - store as blocksStore, -} from '@wordpress/blocks'; +import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; import { useMergeRefs, useDisabled } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; import warning from '@wordpress/warning'; /** * Internal dependencies */ import useMovingAnimation from '../../use-moving-animation'; -import { BlockListBlockContext } from '../block-list-block-context'; +import { PrivateBlockContext } from '../private-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; import { useBlockEditContext } from '../../block-edit/context'; @@ -32,14 +25,6 @@ import { useEventHandlers } from './use-selected-block-event-handlers'; import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; -import { store as blockEditorStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; - -/** - * If the block count exceeds the threshold, we disable the reordering animation - * to avoid laginess. - */ -const BLOCK_ANIMATION_THRESHOLD = 200; /** * This hook is used to lightly mark an element as a block element. The element @@ -89,8 +74,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { className, wrapperProps = {}, isAligned, - } = useContext( BlockListBlockContext ); - const { index, mode, name, @@ -104,104 +87,20 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isOutlineEnabled, hasOverlay, initialPosition, - classNames, - } = useSelect( - ( select ) => { - const { - getBlockAttributes, - getBlockIndex, - getBlockMode, - getBlockName, - isTyping, - getGlobalBlockCount, - isBlockSelected, - isBlockMultiSelected, - isAncestorMultiSelected, - isFirstMultiSelectedBlock, - isBlockSubtreeDisabled, - getSettings, - isBlockHighlighted, - __unstableIsFullySelected, - __unstableSelectionHasUnmergeableBlock, - isBlockBeingDragged, - hasSelectedInnerBlock, - hasBlockMovingClientId, - canInsertBlockType, - getBlockRootClientId, - __unstableHasActiveBlockOverlayActive, - __unstableGetEditorMode, - getSelectedBlocksInitialCaretPosition, - } = unlock( select( blockEditorStore ) ); - const { getActiveBlockVariation } = select( blocksStore ); - const _isSelected = isBlockSelected( clientId ); - const isPartOfMultiSelection = - isBlockMultiSelected( clientId ) || - isAncestorMultiSelected( clientId ); - const blockName = getBlockName( clientId ); - const blockType = getBlockType( blockName ); - const attributes = getBlockAttributes( clientId ); - const match = getActiveBlockVariation( blockName, attributes ); - const { outlineMode } = getSettings(); - const isMultiSelected = isBlockMultiSelected( clientId ); - const checkDeep = true; - const isAncestorOfSelectedBlock = hasSelectedInnerBlock( - clientId, - checkDeep - ); - const typing = isTyping(); - const hasLightBlockWrapper = blockType?.apiVersion > 1; - const movingClientId = hasBlockMovingClientId(); - - return { - index: getBlockIndex( clientId ), - mode: getBlockMode( clientId ), - name: blockName, - blockApiVersion: blockType?.apiVersion || 1, - blockTitle: match?.title || blockType?.title, - isSelected: _isSelected, - isPartOfSelection: _isSelected || isPartOfMultiSelection, - adjustScrolling: - _isSelected || isFirstMultiSelectedBlock( clientId ), - enableAnimation: - ! typing && - getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, - isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), - isOutlineEnabled: outlineMode, - hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), - initialPosition: - _isSelected && __unstableGetEditorMode() === 'edit' - ? getSelectedBlocksInitialCaretPosition() - : undefined, - classNames: classnames( - { - 'is-selected': _isSelected, - 'is-highlighted': isBlockHighlighted( clientId ), - 'is-multi-selected': isMultiSelected, - 'is-partially-selected': - isMultiSelected && - ! __unstableIsFullySelected() && - ! __unstableSelectionHasUnmergeableBlock(), - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': isBlockBeingDragged( clientId ), - 'has-child-selected': isAncestorOfSelectedBlock, - 'remove-outline': _isSelected && outlineMode && typing, - 'is-block-moving-mode': !! movingClientId, - 'can-insert-moving-block': - movingClientId && - canInsertBlockType( - getBlockName( movingClientId ), - getBlockRootClientId( clientId ) - ), - }, - hasLightBlockWrapper ? attributes.className : undefined, - hasLightBlockWrapper - ? getBlockDefaultClassName( blockName ) - : undefined - ), - }; - }, - [ clientId ] - ); + blockEditingMode, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + isTemporarilyEditingAsBlocks, + defaultClassName, + } = useContext( PrivateBlockContext ); // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); @@ -233,7 +132,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { } return { - tabIndex: 0, + tabIndex: blockEditingMode === 'disabled' ? -1 : 0, ...wrapperProps, ...props, ref: mergedRefs, @@ -250,11 +149,24 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { // The wp-block className is important for editor styles. 'wp-block': ! isAligned, 'has-block-overlay': hasOverlay, + 'is-selected': isSelected, + 'is-highlighted': isHighlighted, + 'is-multi-selected': isMultiSelected, + 'is-partially-selected': isPartiallySelected, + 'is-reusable': isReusable, + 'is-dragging': isDragging, + 'has-child-selected': hasChildSelected, + 'remove-outline': removeOutline, + 'is-block-moving-mode': isBlockMovingMode, + 'can-insert-moving-block': canInsertMovingBlock, + 'is-editing-disabled': isEditingDisabled, + 'is-content-locked-temporarily-editing-as-blocks': + isTemporarilyEditingAsBlocks, }, className, props.className, wrapperProps.className, - classNames + defaultClassName ), style: { ...wrapperProps.style, ...props.style }, };