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 91f44d2df09f89..84cab43330773b 100644
--- a/packages/block-editor/src/components/block-list/block-popover.js
+++ b/packages/block-editor/src/components/block-list/block-popover.js
@@ -7,13 +7,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import {
- useState,
- useCallback,
- useContext,
- useRef,
- useEffect,
-} from '@wordpress/element';
+import { useState, useCallback, useRef, useEffect } from '@wordpress/element';
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks';
import { Popover } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
@@ -27,9 +21,8 @@ import { getScrollContainer } from '@wordpress/dom';
import BlockSelectionButton from './block-selection-button';
import BlockContextualToolbar from './block-contextual-toolbar';
import Inserter from '../inserter';
-import { BlockNodes } from './';
-import { getBlockDOMNode } from '../../utils/dom';
import { store as blockEditorStore } from '../../store';
+import { __unstableUseBlockElement as useBlockElement } from './use-block-props/use-block-refs';
function selector( select ) {
const {
@@ -71,7 +64,6 @@ function BlockPopover( {
const isLargeViewport = useViewportMatch( 'medium' );
const [ isToolbarForced, setIsToolbarForced ] = useState( false );
const [ isInserterShown, setIsInserterShown ] = useState( false );
- const blockNodes = useContext( BlockNodes );
const { stopTyping } = useDispatch( blockEditorStore );
// Controls when the side inserter on empty lines should
@@ -115,11 +107,9 @@ function BlockPopover( {
// 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 selectedElement = useBlockElement( clientId );
+ const lastSelectedElement = useBlockElement( lastClientId );
+ const capturingElement = useBlockElement( capturingClientId );
if (
! shouldShowBreadcrumb &&
@@ -130,32 +120,28 @@ function BlockPopover( {
return null;
}
- let node = blockNodes[ clientId ];
+ let node = selectedElement;
if ( ! node ) {
return null;
}
- const { ownerDocument } = node;
-
if ( capturingClientId ) {
- node = getBlockDOMNode( capturingClientId, ownerDocument );
+ node = capturingElement;
}
let anchorRef = node;
if ( hasMultiSelection ) {
- const bottomNode = blockNodes[ lastClientId ];
-
// Wait to render the popover until the bottom reference is available
// as well.
- if ( ! bottomNode ) {
+ if ( ! lastSelectedElement ) {
return null;
}
anchorRef = {
top: node,
- bottom: bottomNode,
+ bottom: lastSelectedElement,
};
}
@@ -174,6 +160,7 @@ function BlockPopover( {
const popoverPosition = showEmptyBlockSideInserter
? 'top left right'
: 'top right left';
+ const { ownerDocument } = node;
const stickyBoundaryElement = showEmptyBlockSideInserter
? undefined
: // The sticky boundary element should be the boundary at which the
@@ -235,6 +222,9 @@ function BlockPopover( {
__experimentalOnIndexChange={ ( index ) => {
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 && (
diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js
index 352df1fd8a4f33..f19c46e9d8d600 100644
--- a/packages/block-editor/src/components/block-list/index.js
+++ b/packages/block-editor/src/components/block-list/index.js
@@ -7,7 +7,7 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { AsyncModeProvider, useSelect } from '@wordpress/data';
-import { useRef, createContext, useState } from '@wordpress/element';
+import { useRef } from '@wordpress/element';
import { useViewportMatch, useMergeRefs } from '@wordpress/compose';
/**
@@ -22,12 +22,8 @@ import { store as blockEditorStore } from '../../store';
import { usePreParsePatterns } from '../../utils/pre-parse-patterns';
import { LayoutProvider, defaultLayout } from './layout';
-export const BlockNodes = createContext();
-export const SetBlockNodes = createContext();
-
export default function BlockList( { className, __experimentalLayout } ) {
const ref = useRef();
- const [ blockNodes, setBlockNodes ] = useState( {} );
const insertionPoint = useInsertionPoint( ref );
usePreParsePatterns();
@@ -53,7 +49,7 @@ export default function BlockList( { className, __experimentalLayout } ) {
}, [] );
return (
-
+ <>
{ insertionPoint }
-
-
-
+
-
+ >
);
}
diff --git a/packages/block-editor/src/components/block-list/insertion-point.js b/packages/block-editor/src/components/block-list/insertion-point.js
index c209c48f513a69..6aec1ab40e720c 100644
--- a/packages/block-editor/src/components/block-list/insertion-point.js
+++ b/packages/block-editor/src/components/block-list/insertion-point.js
@@ -21,8 +21,8 @@ import { isRTL } from '@wordpress/i18n';
* Internal dependencies
*/
import Inserter from '../inserter';
-import { getBlockDOMNode } from '../../utils/dom';
import { store as blockEditorStore } from '../../store';
+import { __unstableUseBlockElement as useBlockElement } from './use-block-props/use-block-refs';
function InsertionPointInserter( {
clientId,
@@ -53,17 +53,14 @@ function InsertionPointPopover( {
isInserterShown,
isInserterForced,
setIsInserterForced,
- containerRef,
showInsertionPoint,
} ) {
const { selectBlock } = useDispatch( blockEditorStore );
const ref = useRef();
-
const {
- previousElement,
- nextElement,
orientation,
isHidden,
+ previousClientId,
nextClientId,
rootClientId,
} = useSelect(
@@ -77,7 +74,6 @@ function InsertionPointPopover( {
hasMultiSelection,
getSettings,
} = select( blockEditorStore );
- const { ownerDocument } = containerRef.current;
const targetRootClientId = clientId
? getBlockRootClientId( clientId )
: selectedRootClientId;
@@ -100,8 +96,7 @@ function InsertionPointPopover( {
'vertical';
return {
- previousElement: getBlockDOMNode( previous, ownerDocument ),
- nextElement: getBlockDOMNode( next, ownerDocument ),
+ previousClientId: previous,
nextClientId: next,
isHidden:
hasReducedUI ||
@@ -116,7 +111,8 @@ function InsertionPointPopover( {
},
[ clientId, selectedRootClientId ]
);
-
+ const previousElement = useBlockElement( previousClientId );
+ const nextElement = useBlockElement( nextClientId );
const style = useMemo( () => {
if ( ! previousElement ) {
return {};
@@ -411,7 +407,6 @@ export default function useInsertionPoint( ref ) {
setIsInserterShown( value );
}
} }
- containerRef={ ref }
showInsertionPoint={ isInserterVisible }
/>
)
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 627f848ebf8410..4331a9251d5635 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
@@ -29,8 +29,8 @@ import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-name
import { useFocusHandler } from './use-focus-handler';
import { useEventHandlers } from './use-selected-block-event-handlers';
import { useNavModeExit } from './use-nav-mode-exit';
-import { useBlockNodes } from './use-block-nodes';
import { useScrollIntoView } from './use-scroll-into-view';
+import { useBlockRefProvider } from './use-block-refs';
import { store as blockEditorStore } from '../../../store';
/**
@@ -106,7 +106,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
useFocusFirstElement( clientId ),
// Must happen after focus because we check for focus in the block.
useScrollIntoView( clientId ),
- useBlockNodes( clientId ),
+ useBlockRefProvider( clientId ),
useFocusHandler( clientId ),
useEventHandlers( clientId ),
useNavModeExit( clientId ),
diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-nodes.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-nodes.js
deleted file mode 100644
index 49542364e2cfe5..00000000000000
--- a/packages/block-editor/src/components/block-list/use-block-props/use-block-nodes.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * External dependencies
- */
-import { omit } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { useContext } from '@wordpress/element';
-import { useRefEffect } from '@wordpress/compose';
-import { useSelect } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import { SetBlockNodes } from '../';
-import { store as blockEditorStore } from '../../../store';
-
-export function useBlockNodes( clientId ) {
- const setBlockNodes = useContext( SetBlockNodes );
- // 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.
- const isNodeNeeded = useSelect(
- ( select ) => {
- const {
- isBlockSelected,
- isFirstMultiSelectedBlock,
- getLastMultiSelectedBlockClientId,
- } = select( blockEditorStore );
- return (
- isBlockSelected( clientId ) ||
- isFirstMultiSelectedBlock( clientId ) ||
- getLastMultiSelectedBlockClientId() === clientId
- );
- }
- // To do: figure out why tests are failing when dependencies are added.
- // This data was originally retrieved with `withSelect` in `block.js`.
- // For some reason, adding `clientId` as a dependency results in
- // `toolbar-roving-tabindex.test.js` e2e test failures.
- );
-
- return useRefEffect(
- ( node ) => {
- if ( ! isNodeNeeded ) {
- return;
- }
-
- setBlockNodes( ( nodes ) => ( {
- ...nodes,
- [ clientId ]: node,
- } ) );
-
- return () => {
- setBlockNodes( ( nodes ) => omit( nodes, clientId ) );
- };
- },
- [ isNodeNeeded, clientId, setBlockNodes ]
- );
-}
diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js
new file mode 100644
index 00000000000000..50794c611f1f68
--- /dev/null
+++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js
@@ -0,0 +1,101 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ useCallback,
+ useContext,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { BlockRefs } from '../../provider/block-refs-provider';
+
+/** @typedef {import('@wordpress/element').RefCallback} RefCallback */
+/** @typedef {import('@wordpress/element').RefObject} RefObject */
+
+/**
+ * Provides a ref to the BlockRefs context.
+ *
+ * @param {string} clientId The client ID of the element ref.
+ *
+ * @return {RefCallback} Ref callback.
+ */
+export function useBlockRefProvider( clientId ) {
+ const { refs, callbacks } = useContext( BlockRefs );
+ const ref = useRef();
+ useLayoutEffect( () => {
+ refs.set( clientId, ref );
+ return () => {
+ refs.delete( clientId );
+ };
+ }, [] );
+ return useCallback( ( element ) => {
+ // Update the ref in the provider.
+ ref.current = element;
+ // Call any update functions.
+ callbacks.forEach( ( id, setElement ) => {
+ if ( clientId === id ) {
+ setElement( element );
+ }
+ } );
+ }, [] );
+}
+
+/**
+ * Gets a ref pointing to the current block element. Continues to return a
+ * stable ref even if the block client ID changes.
+ *
+ * @param {string} clientId The client ID to get a ref for.
+ *
+ * @return {RefObject} A ref containing the element.
+ */
+function useBlockRef( clientId ) {
+ const { refs } = useContext( BlockRefs );
+ const freshClientId = useRef();
+ freshClientId.current = clientId;
+ // Always return an object, even if no ref exists for a given client ID, so
+ // that `current` works at a later point.
+ return useMemo(
+ () => ( {
+ get current() {
+ return refs.get( freshClientId.current )?.current || null;
+ },
+ } ),
+ []
+ );
+}
+
+/**
+ * Return the element for a given client ID. Updates whenever the element
+ * changes, becomes available, or disappears.
+ *
+ * @param {string} clientId The client ID to an element for.
+ *
+ * @return {Element|null} The block's wrapper element.
+ */
+function useBlockElement( clientId ) {
+ const { callbacks } = useContext( BlockRefs );
+ const ref = useBlockRef( clientId );
+ const [ element, setElement ] = useState( null );
+
+ useLayoutEffect( () => {
+ if ( ! clientId ) {
+ return;
+ }
+
+ callbacks.set( setElement, clientId );
+ return () => {
+ callbacks.delete( setElement );
+ };
+ }, [ clientId ] );
+
+ return ref.current || element;
+}
+
+export { useBlockRef as __unstableUseBlockRef };
+export { useBlockElement as __unstableUseBlockElement };
diff --git a/packages/block-editor/src/components/provider/block-refs-provider.js b/packages/block-editor/src/components/provider/block-refs-provider.js
new file mode 100644
index 00000000000000..e98e0f4d25c9d4
--- /dev/null
+++ b/packages/block-editor/src/components/provider/block-refs-provider.js
@@ -0,0 +1,16 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext, useMemo } from '@wordpress/element';
+
+export const BlockRefs = createContext();
+
+export function BlockRefsProvider( { children } ) {
+ const value = useMemo(
+ () => ( { refs: new Map(), callbacks: new Map() } ),
+ []
+ );
+ return (
+ { children }
+ );
+}
diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js
index 157cd72d1f237b..8851a2b9b47a15 100644
--- a/packages/block-editor/src/components/provider/index.js
+++ b/packages/block-editor/src/components/provider/index.js
@@ -10,6 +10,7 @@ import { useEffect } from '@wordpress/element';
import withRegistryProvider from './with-registry-provider';
import useBlockSync from './use-block-sync';
import { store as blockEditorStore } from '../../store';
+import { BlockRefsProvider } from './block-refs-provider';
/** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */
@@ -24,7 +25,7 @@ function BlockEditorProvider( props ) {
// Syncs the entity provider with changes in the block-editor store.
useBlockSync( props );
- return children;
+ return { children };
}
export default withRegistryProvider( BlockEditorProvider );
diff --git a/packages/block-editor/src/components/skip-to-selected-block/index.js b/packages/block-editor/src/components/skip-to-selected-block/index.js
index e5c54524dc46e6..61e7e3f8fd05df 100644
--- a/packages/block-editor/src/components/skip-to-selected-block/index.js
+++ b/packages/block-editor/src/components/skip-to-selected-block/index.js
@@ -8,16 +8,13 @@ import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/
-import { getBlockDOMNode } from '../../utils/dom';
import { store as blockEditorStore } from '../../store';
+import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs';
const SkipToSelectedBlock = ( { selectedBlockClientId } ) => {
+ const ref = useBlockRef( selectedBlockClientId );
const onClick = () => {
- const selectedBlockElement = getBlockDOMNode(
- selectedBlockClientId,
- document
- );
- selectedBlockElement.focus();
+ ref.current.focus();
};
return selectedBlockClientId ? (
diff --git a/packages/block-editor/src/components/writing-flow/use-multi-selection.js b/packages/block-editor/src/components/writing-flow/use-multi-selection.js
index a6aee58da331f3..71ded141fce5d4 100644
--- a/packages/block-editor/src/components/writing-flow/use-multi-selection.js
+++ b/packages/block-editor/src/components/writing-flow/use-multi-selection.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { first, last } from 'lodash';
+
/**
* WordPress dependencies
*/
@@ -7,8 +12,9 @@ import { useSelect, useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
-import { getBlockClientId, getBlockDOMNode } from '../../utils/dom';
+import { getBlockClientId } from '../../utils/dom';
import { store as blockEditorStore } from '../../store';
+import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs';
/**
* Returns for the deepest node at the start or end of a container node. Ignores
@@ -84,6 +90,10 @@ export default function useMultiSelection( ref ) {
const rafId = useRef();
const startClientId = useRef();
const anchorElement = useRef();
+ const selectedRef = useBlockRef( selectedBlockClientId );
+ // These must be in the right DOM order.
+ const startRef = useBlockRef( first( multiSelectedBlockClientIds ) );
+ const endRef = useBlockRef( last( multiSelectedBlockClientIds ) );
/**
* When the component updates, and there is multi selection, we need to
@@ -101,10 +111,7 @@ export default function useMultiSelection( ref ) {
const selection = defaultView.getSelection();
if ( selection.rangeCount && ! selection.isCollapsed ) {
- const blockNode = getBlockDOMNode(
- selectedBlockClientId,
- ownerDocument
- );
+ const blockNode = selectedRef.current;
const { startContainer, endContainer } = selection.getRangeAt(
0
);
@@ -128,19 +135,14 @@ export default function useMultiSelection( ref ) {
}
// These must be in the right DOM order.
- const start = multiSelectedBlockClientIds[ 0 ];
- const end = multiSelectedBlockClientIds[ length - 1 ];
-
- let startNode = getBlockDOMNode( start, ownerDocument );
- let endNode = getBlockDOMNode( end, ownerDocument );
const selection = defaultView.getSelection();
const range = ownerDocument.createRange();
// The most stable way to select the whole block contents is to start
// and end at the deepest points.
- startNode = getDeepestNode( startNode, 'start' );
- endNode = getDeepestNode( endNode, 'end' );
+ const startNode = getDeepestNode( startRef.current, 'start' );
+ const endNode = getDeepestNode( endRef.current, 'end' );
range.setStartBefore( startNode );
range.setEndAfter( endNode );
diff --git a/packages/block-editor/src/hooks/color-panel.js b/packages/block-editor/src/hooks/color-panel.js
index 9b93203b3d265a..eacc2bbbc413bf 100644
--- a/packages/block-editor/src/hooks/color-panel.js
+++ b/packages/block-editor/src/hooks/color-panel.js
@@ -10,7 +10,7 @@ import { useState, useEffect } from '@wordpress/element';
import PanelColorGradientSettings from '../components/colors-gradients/panel-color-gradient-settings';
import ContrastChecker from '../components/contrast-checker';
import InspectorControls from '../components/inspector-controls';
-import { getBlockDOMNode } from '../utils/dom';
+import { __unstableUseBlockRef as useBlockRef } from '../components/block-list/use-block-props/use-block-refs';
function getComputedStyle( node ) {
return node.ownerDocument.defaultView.getComputedStyle( node );
@@ -23,19 +23,19 @@ export default function ColorPanel( {
} ) {
const [ detectedBackgroundColor, setDetectedBackgroundColor ] = useState();
const [ detectedColor, setDetectedColor ] = useState();
+ const ref = useBlockRef( clientId );
useEffect( () => {
if ( ! enableContrastChecking ) {
return;
}
- const colorsDetectionElement = getBlockDOMNode( clientId, document );
- if ( ! colorsDetectionElement ) {
+ if ( ! ref.current ) {
return;
}
- setDetectedColor( getComputedStyle( colorsDetectionElement ).color );
+ setDetectedColor( getComputedStyle( ref.current ).color );
- let backgroundColorNode = colorsDetectionElement;
+ let backgroundColorNode = ref.current;
let backgroundColor = getComputedStyle( backgroundColorNode )
.backgroundColor;
while (
diff --git a/packages/block-editor/src/utils/dom.js b/packages/block-editor/src/utils/dom.js
index 248587311d1d2b..f99468a7e751c7 100644
--- a/packages/block-editor/src/utils/dom.js
+++ b/packages/block-editor/src/utils/dom.js
@@ -1,36 +1,3 @@
-/**
- * Given a block client ID, returns the corresponding DOM node for the block,
- * if exists. As much as possible, this helper should be avoided, and used only
- * in cases where isolated behaviors need remote access to a block node.
- *
- * @param {string} clientId Block client ID.
- * @param {Document} doc Document to search.
- *
- * @return {Element?} Block DOM node.
- */
-export function getBlockDOMNode( clientId, doc ) {
- return doc.getElementById( 'block-' + clientId );
-}
-
-/**
- * Returns the preview container DOM node for a given block client ID, or
- * undefined if the container cannot be determined.
- *
- * @param {string} clientId Block client ID.
- * @param {Document} doc Document to search.
- *
- * @return {Node|undefined} Preview container DOM node.
- */
-export function getBlockPreviewContainerDOMNode( clientId, doc ) {
- const domNode = getBlockDOMNode( clientId, doc );
-
- if ( ! domNode ) {
- return;
- }
-
- return domNode.firstChild || domNode;
-}
-
/**
* Returns true if two elements are contained within the same block.
*