Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block editor: create useBlockRef and useBlockElement for better access #29573

Merged
merged 9 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 13 additions & 23 deletions packages/block-editor/src/components/block-list/block-popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand All @@ -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,
};
}

Expand All @@ -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
Expand Down Expand Up @@ -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 }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remounting the component is better because the block element may change immediately from one to the other. Previously the block node would be undefined between client ID changes.

/>
) }
{ shouldShowBreadcrumb && (
Expand Down
16 changes: 4 additions & 12 deletions packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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();

Expand All @@ -53,7 +49,7 @@ export default function BlockList( { className, __experimentalLayout } ) {
}, [] );

return (
<BlockNodes.Provider value={ blockNodes }>
<>
{ insertionPoint }
<BlockPopover />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this and #31134, we can move the popover and insertion point out of the BlockList component.

<div
Expand All @@ -69,13 +65,9 @@ export default function BlockList( { className, __experimentalLayout } ) {
}
) }
>
<SetBlockNodes.Provider value={ setBlockNodes }>
<BlockListItems
__experimentalLayout={ __experimentalLayout }
/>
</SetBlockNodes.Provider>
<BlockListItems __experimentalLayout={ __experimentalLayout } />
</div>
</BlockNodes.Provider>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -77,7 +74,6 @@ function InsertionPointPopover( {
hasMultiSelection,
getSettings,
} = select( blockEditorStore );
const { ownerDocument } = containerRef.current;
const targetRootClientId = clientId
? getBlockRootClientId( clientId )
: selectedRootClientId;
Expand All @@ -100,8 +96,7 @@ function InsertionPointPopover( {
'vertical';

return {
previousElement: getBlockDOMNode( previous, ownerDocument ),
nextElement: getBlockDOMNode( next, ownerDocument ),
previousClientId: previous,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that previously the element wouldn't update if it changed because it's memoized by client ID.

nextClientId: next,
isHidden:
hasReducedUI ||
Expand All @@ -116,7 +111,8 @@ function InsertionPointPopover( {
},
[ clientId, selectedRootClientId ]
);

const previousElement = useBlockElement( previousClientId );
const nextElement = useBlockElement( nextClientId );
const style = useMemo( () => {
if ( ! previousElement ) {
return {};
Expand Down Expand Up @@ -411,7 +407,6 @@ export default function useInsertionPoint( ref ) {
setIsInserterShown( value );
}
} }
containerRef={ ref }
showInsertionPoint={ isInserterVisible }
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 ),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could just set the element in the context here instead of using an intermediary "ref". I thought the "ref" was useful for useBlockRef, but it doesn't seem to be the case anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could through useMergeRefs, but we'd need to callback anyway as well, so this seems a bit simpler.

// 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 };
Loading