From 40d56d74505ed9b9e0bdd81fe9e1cd4b1f7d261b Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Tue, 21 Jan 2020 14:10:34 +0100 Subject: [PATCH 1/3] Block: use context to provide selected element --- .../components/block-list/block-popover.js | 15 ++++++----- .../src/components/block-list/block.js | 13 ++++----- .../components/block-list/root-container.js | 27 ++++++++++--------- packages/block-editor/src/store/actions.js | 12 --------- packages/block-editor/src/store/reducer.js | 20 -------------- packages/block-editor/src/store/selectors.js | 11 -------- 6 files changed, 30 insertions(+), 68 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block-popover.js b/packages/block-editor/src/components/block-list/block-popover.js index cb563c3319f21e..d197e663964791 100644 --- a/packages/block-editor/src/components/block-list/block-popover.js +++ b/packages/block-editor/src/components/block-list/block-popover.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useState, useCallback } from '@wordpress/element'; +import { useState, useCallback, useContext } from '@wordpress/element'; import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; import { Popover } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -20,6 +20,7 @@ import { useViewportMatch } from '@wordpress/compose'; import BlockBreadcrumb from './breadcrumb'; import BlockContextualToolbar from './block-contextual-toolbar'; import Inserter from '../inserter'; +import { SelectedBlockNode } from './root-container'; function selector( select ) { const { @@ -61,6 +62,7 @@ function BlockPopover( { const isLargeViewport = useViewportMatch( 'medium' ); const [ isToolbarForced, setIsToolbarForced ] = useState( false ); const [ isInserterShown, setIsInserterShown ] = useState( false ); + let [ node ] = useContext( SelectedBlockNode ); const showEmptyBlockSideInserter = ! isNavigationMode && isEmptyDefaultBlock && isValid; const shouldShowBreadcrumb = isNavigationMode; @@ -92,7 +94,9 @@ function BlockPopover( { return null; } - let node = document.getElementById( 'block-' + capturingClientId ); + if ( capturingClientId ) { + node = document.getElementById( 'block-' + capturingClientId ); + } if ( ! node ) { return null; @@ -188,7 +192,6 @@ function wrapperSelector( select ) { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, getBlockRootClientId, - __unstableGetSelectedMountedBlock, __unstableGetBlockWithoutInnerBlocks, getBlockParents, getBlockListSettings, @@ -213,7 +216,7 @@ function wrapperSelector( select ) { // This will be the top most ancestor because getBlockParents() returns tree from top -> bottom const topmostAncestorWithCaptureDescendantsToolbarsIndex = findIndex( ancestorBlockListSettings, [ '__experimentalCaptureToolbars', true ] ); - let capturingClientId = clientId; + let capturingClientId; if ( topmostAncestorWithCaptureDescendantsToolbarsIndex !== -1 ) { capturingClientId = blockParentsClientIds[ topmostAncestorWithCaptureDescendantsToolbarsIndex ]; @@ -222,7 +225,6 @@ function wrapperSelector( select ) { return { clientId, rootClientId: getBlockRootClientId( clientId ), - isMounted: __unstableGetSelectedMountedBlock() === clientId, name, align: attributes.align, isValid, @@ -242,7 +244,6 @@ export default function WrappedBlockPopover() { const { clientId, rootClientId, - isMounted, name, align, isValid, @@ -251,7 +252,7 @@ export default function WrappedBlockPopover() { capturingClientId, } = selected; - if ( ! name || ! isMounted ) { + if ( ! name ) { return null; } diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 8902227bbd5610..65b418b243ea42 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -28,7 +28,6 @@ import { withDispatch, withSelect, useSelect, - useDispatch, } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { compose, pure, ifCondition } from '@wordpress/compose'; @@ -43,7 +42,7 @@ import BlockCrashBoundary from './block-crash-boundary'; import BlockHtml from './block-html'; import { isInsideRootBlock } from '../../utils/dom'; import useMovingAnimation from './moving-animation'; -import { Context } from './root-container'; +import { Context, SelectedBlockNode } from './root-container'; function BlockListBlock( { mode, @@ -78,6 +77,7 @@ function BlockListBlock( { hasSelectedUI = true, } ) { const onSelectionStart = useContext( Context ); + const [ , setSelectedBlockNode ] = useContext( SelectedBlockNode ); // In addition to withSelect, we should favor using useSelect in this component going forward // to avoid leaking new props to the public API (editor.BlockListBlock filter) const { isDraggingBlocks } = useSelect( ( select ) => { @@ -85,16 +85,17 @@ 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 ); + const node = wrapper.current; + setSelectedBlockNode( node ); + return () => { + setSelectedBlockNode( ( n ) => n === node ? null : n ); + }; } }, [ isSelected, isFirstMultiSelected ] ); 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 5542b3edfe80a9..aae8c476c418b5 100644 --- a/packages/block-editor/src/components/block-list/root-container.js +++ b/packages/block-editor/src/components/block-list/root-container.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, forwardRef } from '@wordpress/element'; +import { createContext, forwardRef, useState } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; /** @@ -15,6 +15,7 @@ import BlockPopover from './block-popover'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ export const Context = createContext(); +export const SelectedBlockNode = createContext(); function selector( select ) { const { @@ -80,17 +81,19 @@ function RootContainer( { children, className }, ref ) { selectedBlockClientId={ selectedBlockClientId } containerRef={ ref } > - -
- - { children } - -
+ + +
+ + { children } + +
+
); } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 07106b64597774..b6a6552e1f019b 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -930,15 +930,3 @@ export function * insertAfterBlock( clientId ) { const firstSelectedIndex = yield select( 'core/block-editor', 'getBlockIndex', clientId, rootClientId ); yield insertDefaultBlock( {}, rootClientId, firstSelectedIndex + 1 ); } - -/** - * Sets the client ID for the mounted and selected block. - * - * @param {Element} clientId The block's client ID. - */ -export function __unstableSetSelectedMountedBlock( clientId ) { - return { - type: 'SET_SELECTED_MOUNTED_BLOCK', - clientId, - }; -} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 2471be0a1e23ad..d7b3799a61a0d4 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1347,7 +1347,6 @@ export function automaticChangeStatus( state, action ) { return; // Undoing an automatic change should still be possible after mouse // move. - case 'SET_SELECTED_MOUNTED_BLOCK': case 'STOP_TYPING': return state; } @@ -1355,24 +1354,6 @@ export function automaticChangeStatus( state, action ) { // Reset the state by default (for any action not handled). } -/** - * Reducer returning selected and mounted block. This state is useful for - * components rendering and positioning controls around the block's node. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {boolean} Updated state. - */ -export function selectedMountedBlock( state, action ) { - switch ( action.type ) { - case 'SET_SELECTED_MOUNTED_BLOCK': - return action.clientId; - } - - return state; -} - export default combineReducers( { blocks, isTyping, @@ -1392,5 +1373,4 @@ export default combineReducers( { lastBlockAttributesChange, isNavigationMode, automaticChangeStatus, - selectedMountedBlock, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index ceff4cb9644369..8c4d6c6395e7dd 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1526,14 +1526,3 @@ export function isNavigationMode( state ) { export function didAutomaticChange( state ) { return !! state.automaticChangeStatus; } - -/** - * Gets the selected block's DOM node. - * - * @param {Object} state Global application state. - * - * @return {Element} The selected block's DOM node. - */ -export function __unstableGetSelectedMountedBlock( state ) { - return state.selectedMountedBlock; -} From e0d9ddc9e7004167ff11f226ba8d66759916675d Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Tue, 21 Jan 2020 20:23:53 +0100 Subject: [PATCH 2/3] Include multi selection nodes --- .../components/block-list/block-popover.js | 23 +++++++++++++++---- .../src/components/block-list/block.js | 17 ++++++++------ .../components/block-list/root-container.js | 6 ++--- packages/components/src/popover/index.js | 22 ++++++++++++++++-- packages/components/src/popover/utils.js | 20 +++------------- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block-popover.js b/packages/block-editor/src/components/block-list/block-popover.js index d197e663964791..2919019d849a9e 100644 --- a/packages/block-editor/src/components/block-list/block-popover.js +++ b/packages/block-editor/src/components/block-list/block-popover.js @@ -20,7 +20,7 @@ import { useViewportMatch } from '@wordpress/compose'; import BlockBreadcrumb from './breadcrumb'; import BlockContextualToolbar from './block-contextual-toolbar'; import Inserter from '../inserter'; -import { SelectedBlockNode } from './root-container'; +import { BlockNodes } from './root-container'; function selector( select ) { const { @@ -30,6 +30,7 @@ function selector( select ) { isTyping, isCaretWithinFormattedText, getSettings, + getLastMultiSelectedBlockClientId, } = select( 'core/block-editor' ); return { isNavigationMode: isNavigationMode(), @@ -38,6 +39,7 @@ function selector( select ) { isCaretWithinFormattedText: isCaretWithinFormattedText(), hasMultiSelection: hasMultiSelection(), hasFixedToolbar: getSettings().hasFixedToolbar, + lastClientId: getLastMultiSelectedBlockClientId(), }; } @@ -58,11 +60,12 @@ function BlockPopover( { isCaretWithinFormattedText, hasMultiSelection, hasFixedToolbar, + lastClientId, } = useSelect( selector, [] ); const isLargeViewport = useViewportMatch( 'medium' ); const [ isToolbarForced, setIsToolbarForced ] = useState( false ); const [ isInserterShown, setIsInserterShown ] = useState( false ); - let [ node ] = useContext( SelectedBlockNode ); + const [ blockNodes ] = useContext( BlockNodes ); const showEmptyBlockSideInserter = ! isNavigationMode && isEmptyDefaultBlock && isValid; const shouldShowBreadcrumb = isNavigationMode; @@ -94,6 +97,8 @@ function BlockPopover( { return null; } + let node = blockNodes[ clientId ]; + if ( capturingClientId ) { node = document.getElementById( 'block-' + capturingClientId ); } @@ -107,6 +112,15 @@ function BlockPopover( { node = node.querySelector( '.is-block-content' ) || node; } + let anchorRef = node; + + if ( hasMultiSelection ) { + anchorRef = { + top: blockNodes[ clientId ], + bottom: blockNodes[ lastClientId ], + }; + } + function onFocus() { setIsInserterShown( true ); } @@ -120,7 +134,6 @@ function BlockPopover( { // 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 ( { @@ -90,14 +91,14 @@ function BlockListBlock( { const wrapper = useRef( null ); useLayoutEffect( () => { - if ( isSelected || isFirstMultiSelected ) { + if ( isSelected || isFirstMultiSelected || isLastMultiSelected ) { const node = wrapper.current; - setSelectedBlockNode( node ); + setBlockNodes( ( nodes ) => ( { ...nodes, [ clientId ]: node } ) ); return () => { - setSelectedBlockNode( ( n ) => n === node ? null : n ); + setBlockNodes( ( nodes ) => omit( nodes, clientId ) ); }; } - }, [ isSelected, isFirstMultiSelected ] ); + }, [ isSelected, isFirstMultiSelected, isLastMultiSelected ] ); // Handling the error state const [ hasError, setErrorState ] = useState( false ); @@ -332,6 +333,7 @@ const applyWithSelect = withSelect( isAncestorMultiSelected, isBlockMultiSelected, isFirstMultiSelectedBlock, + getLastMultiSelectedBlockClientId, isTyping, getBlockMode, isSelectionEnabled, @@ -362,6 +364,7 @@ const applyWithSelect = withSelect( isPartOfMultiSelection: isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ), isFirstMultiSelected: isFirstMultiSelectedBlock( clientId ), + isLastMultiSelected: getLastMultiSelectedBlockClientId() === clientId, // We only care about this prop when the block is selected // Thus to avoid unnecessary rerenders we avoid updating the prop if the block is not selected. 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 aae8c476c418b5..84985f8fd7c072 100644 --- a/packages/block-editor/src/components/block-list/root-container.js +++ b/packages/block-editor/src/components/block-list/root-container.js @@ -15,7 +15,7 @@ import BlockPopover from './block-popover'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ export const Context = createContext(); -export const SelectedBlockNode = createContext(); +export const BlockNodes = createContext(); function selector( select ) { const { @@ -81,7 +81,7 @@ function RootContainer( { children, className }, ref ) { selectedBlockClientId={ selectedBlockClientId } containerRef={ ref } > - +
-
+ ); } diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 82cb3874db2fb3..a97c964e065972 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -62,7 +62,25 @@ function computeAnchorRect( return getRectangleFromRange( anchorRef ); } - const rect = anchorRef.getBoundingClientRect(); + if ( anchorRef instanceof window.Element ) { + const rect = anchorRef.getBoundingClientRect(); + + if ( shouldAnchorIncludePadding ) { + return rect; + } + + return withoutPadding( rect, anchorRef ); + } + + const { top, bottom } = anchorRef; + const topRect = top.getBoundingClientRect(); + const bottomRect = bottom.getBoundingClientRect(); + const rect = new window.DOMRect( + topRect.left, + topRect.top, + topRect.width, + bottomRect.bottom - topRect.top + ); if ( shouldAnchorIncludePadding ) { return rect; @@ -301,7 +319,7 @@ const Popover = ( { yAxis, contentHeight, contentWidth, - } = computePopoverPosition( anchor, contentRect.current, position, __unstableSticky, anchorRef, relativeOffsetTop ); + } = computePopoverPosition( anchor, contentRect.current, position, __unstableSticky, containerRef.current, relativeOffsetTop ); if ( typeof popoverTop === 'number' && typeof popoverLeft === 'number' ) { if ( subpixels && __unstableAllowVerticalSubpixelPosition ) { diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js index f729247c98d7f5..187f3c292a5642 100644 --- a/packages/components/src/popover/utils.js +++ b/packages/components/src/popover/utils.js @@ -128,27 +128,13 @@ export function computePopoverYAxisPosition( anchorRect, contentSize, yAxis, cor const { height } = contentSize; if ( sticky ) { - let topEl = anchorRef; - let bottomEl = anchorRef; - - if ( typeof sticky === 'string' ) { - const elements = document.querySelectorAll( sticky ); - - if ( elements.length ) { - topEl = elements[ 0 ]; - bottomEl = elements[ elements.length - 1 ]; - } - } - - const scrollContainerEl = getScrollContainer( topEl ) || document.body; + const scrollContainerEl = getScrollContainer( anchorRef ) || document.body; const scrollRect = scrollContainerEl.getBoundingClientRect(); - const topRect = topEl.getBoundingClientRect(); - const bottomRect = bottomEl.getBoundingClientRect(); - if ( topRect.top - height <= scrollRect.top ) { + if ( anchorRect.top - height <= scrollRect.top ) { return { yAxis, - popoverTop: Math.min( bottomRect.bottom - relativeOffsetTop, scrollRect.top + height - relativeOffsetTop ), + popoverTop: Math.min( anchorRect.bottom - relativeOffsetTop, scrollRect.top + height - relativeOffsetTop ), }; } } From 790dd644d172fd8c31ba378edf2a752e7ee80b6b Mon Sep 17 00:00:00 2001 From: Ella van Durpe Date: Wed, 22 Jan 2020 18:56:11 +0100 Subject: [PATCH 3/3] Add comment --- packages/block-editor/src/components/block-list/block.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index a78d7af1d5bf2a..0d674919bc4dfb 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -90,6 +90,10 @@ function BlockListBlock( { // Reference of the wrapper const wrapper = useRef( null ); + // Provide the selected node, or the first and last nodes of a multi- + // selection, so it can be used to position the contextual block toolbar. + // We only provide what is necessary, and remove the nodes again when they + // are no longer selected. useLayoutEffect( () => { if ( isSelected || isFirstMultiSelected || isLastMultiSelected ) { const node = wrapper.current;