Skip to content

Commit

Permalink
Refactor useAnchorRef and related components to work with the new P…
Browse files Browse the repository at this point in the history
…opover `anchor` prop (#43713)

* useAnchorRef: return a VirtualElement instead of a range

* Update useAnchorRef usage in FormatToolbarContainer, use anchor prop

* Update remaining `useAnchorRef` usages, switch to the `anchor` prop

* useAnchorRef: normalize `null` returns to `undefined` as it is not a valid `anchor` value

* Revert changes to native RichText component

* Update docs

* Allow useAnchorRef to return `null`
  • Loading branch information
ciampo committed Sep 14, 2022
1 parent 336a4b4 commit 5a31a1e
Show file tree
Hide file tree
Showing 8 changed files with 44 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,32 @@ import FormatToolbar from './format-toolbar';
import NavigableToolbar from '../navigable-toolbar';
import { store as blockEditorStore } from '../../store';

function InlineSelectionToolbar( { value, anchorRef, activeFormats } ) {
function InlineSelectionToolbar( {
value,
editableContentRef,
activeFormats,
} ) {
const lastFormat = activeFormats[ activeFormats.length - 1 ];
const lastFormatType = lastFormat?.type;
const settings = useSelect(
( select ) => select( richTextStore ).getFormatType( lastFormatType ),
[ lastFormatType ]
);
const selectionRef = useAnchorRef( {
ref: anchorRef,
const popoverAnchor = useAnchorRef( {
ref: editableContentRef,
value,
settings,
} );

return <InlineToolbar anchorRef={ selectionRef } />;
return <InlineToolbar popoverAnchor={ popoverAnchor } />;
}

function InlineToolbar( { anchorRef } ) {
function InlineToolbar( { popoverAnchor } ) {
return (
<Popover
position="top center"
focusOnMount={ false }
anchorRef={ anchorRef }
anchor={ popoverAnchor }
className="block-editor-rich-text__inline-format-toolbar"
__unstableSlotName="block-toolbar"
>
Expand All @@ -57,14 +61,14 @@ function InlineToolbar( { anchorRef } ) {
);
}

const FormatToolbarContainer = ( { inline, anchorRef, value } ) => {
const FormatToolbarContainer = ( { inline, editableContentRef, value } ) => {
const hasInlineToolbar = useSelect(
( select ) => select( blockEditorStore ).getSettings().hasInlineToolbar,
[]
);

if ( inline ) {
return <InlineToolbar anchorRef={ anchorRef } />;
return <InlineToolbar popoverAnchor={ editableContentRef.current } />;
}

if ( hasInlineToolbar ) {
Expand All @@ -76,7 +80,7 @@ const FormatToolbarContainer = ( { inline, anchorRef, value } ) => {

return (
<InlineSelectionToolbar
anchorRef={ anchorRef }
editableContentRef={ editableContentRef }
value={ value }
activeFormats={ activeFormats }
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ function RichTextWrapper(
{ isSelected && hasFormats && (
<FormatToolbarContainer
inline={ inlineToolbar }
anchorRef={ anchorRef }
editableContentRef={ anchorRef }
value={ value }
/>
) }
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/autocomplete/autocompleter-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function getAutoCompleterUI( autocompleter ) {
contentRef,
} ) {
const [ items ] = useItems( filterValue );
const anchorRef = useAnchorRef( { ref: contentRef, value } );
const popoverAnchor = useAnchorRef( { ref: contentRef, value } );

useLayoutEffect( () => {
onChangeOptions( items );
Expand All @@ -54,7 +54,7 @@ export function getAutoCompleterUI( autocompleter ) {
onClose={ onReset }
position="top right"
className="components-autocomplete__popover"
anchorRef={ anchorRef }
anchor={ popoverAnchor }
>
<div
id={ listBoxId }
Expand Down
4 changes: 2 additions & 2 deletions packages/format-library/src/image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const image = {
function InlineUI( { value, onChange, activeObjectAttributes, contentRef } ) {
const { style } = activeObjectAttributes;
const [ width, setWidth ] = useState( style?.replace( /\D/g, '' ) );
const anchorRef = useAnchorRef( {
const popoverAnchor = useAnchorRef( {
ref: contentRef,
value,
settings: image,
Expand All @@ -46,7 +46,7 @@ function InlineUI( { value, onChange, activeObjectAttributes, contentRef } ) {
<Popover
position="bottom center"
focusOnMount={ false }
anchorRef={ anchorRef }
anchor={ popoverAnchor }
className="block-editor-format-toolbar__image-popover"
>
<form
Expand Down
6 changes: 3 additions & 3 deletions packages/format-library/src/link/inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,13 @@ function InlineLinkUI( {
}
}

const anchorRef = useAnchorRef( { ref: contentRef, value, settings } );
const popoverAnchor = useAnchorRef( { ref: contentRef, value, settings } );

// Generate a string based key that is unique to this anchor reference.
// This is used to force re-mount the LinkControl component to avoid
// potential stale state bugs caused by the component not being remounted
// See https://github.com/WordPress/gutenberg/pull/34742.
const forceRemountKey = useLinkInstanceKey( anchorRef );
const forceRemountKey = useLinkInstanceKey( popoverAnchor );

// The focusOnMount prop shouldn't evolve during render of a Popover
// otherwise it causes a render of the content.
Expand Down Expand Up @@ -223,7 +223,7 @@ function InlineLinkUI( {

return (
<Popover
anchorRef={ anchorRef }
anchor={ popoverAnchor }
focusOnMount={ focusOnMount.current }
onClose={ stopAddingLink }
position="bottom center"
Expand Down
6 changes: 3 additions & 3 deletions packages/format-library/src/text-color/inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,22 @@ export default function InlineColorUI( {
onClose,
contentRef,
} ) {
/*
/*
As you change the text color by typing a HEX value into a field,
the return value of document.getSelection jumps to the field you're editing,
not the highlighted text. Given that useAnchorRef uses document.getSelection,
it will return null, since it can't find the <mark> element within the HEX input.
This caches the last truthy value of the selection anchor reference.
*/
const anchorRef = useCachedTruthy(
const popoverAnchor = useCachedTruthy(
useAnchorRef( { ref: contentRef, value, settings } )
);

return (
<Popover
onClose={ onClose }
className="components-inline-color-popover"
anchorRef={ anchorRef }
anchor={ popoverAnchor }
>
<TabPanel
tabs={ [
Expand Down
8 changes: 4 additions & 4 deletions packages/rich-text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ _Returns_
### useAnchorRef

This hook, to be used in a format type's Edit component, returns the active
element that is formatted, or the selection range if no format is active.
The returned value is meant to be used for positioning UI, e.g. by passing it
to the `Popover` component.
element that is formatted, or a virtual element for the selection range if
no format is active. The returned value is meant to be used for positioning
UI, e.g. by passing it to the `Popover` component.

_Parameters_

Expand All @@ -490,7 +490,7 @@ _Parameters_

_Returns_

- `Element|Range`: The active element or selection range.
- `Element|VirtualAnchorElement|undefined|null`: The active element or selection range.

<!-- END TOKEN(Autogenerated API docs) -->

Expand Down
21 changes: 16 additions & 5 deletions packages/rich-text/src/component/use-anchor-ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@ import { getActiveFormat } from '../get-active-format';
/** @typedef {import('../register-format-type').RichTextFormatType} RichTextFormatType */
/** @typedef {import('../create').RichTextValue} RichTextValue */

/**
* @typedef {Object} VirtualAnchorElement
* @property {Function} getBoundingClientRect A function returning a DOMRect
* @property {Document} ownerDocument The element's ownerDocument
*/

/**
* This hook, to be used in a format type's Edit component, returns the active
* element that is formatted, or the selection range if no format is active.
* The returned value is meant to be used for positioning UI, e.g. by passing it
* to the `Popover` component.
* element that is formatted, or a virtual element for the selection range if
* no format is active. The returned value is meant to be used for positioning
* UI, e.g. by passing it to the `Popover` component.
*
* @param {Object} $1 Named parameters.
* @param {RefObject<HTMLElement>} $1.ref React ref of the element
* containing the editable content.
* @param {RichTextValue} $1.value Value to check for selection.
* @param {RichTextFormatType} $1.settings The format type's settings.
*
* @return {Element|Range} The active element or selection range.
* @return {Element|VirtualAnchorElement|undefined|null} The active element or selection range.
*/
export function useAnchorRef( { ref, value, settings = {} } ) {
const { tagName, className, name } = settings;
Expand All @@ -44,7 +50,12 @@ export function useAnchorRef( { ref, value, settings = {} } ) {
const range = selection.getRangeAt( 0 );

if ( ! activeFormat ) {
return range;
return {
ownerDocument: range.startContainer.ownerDocument,
getBoundingClientRect() {
return range.getBoundingClientRect();
},
};
}

let element = range.startContainer;
Expand Down

0 comments on commit 5a31a1e

Please sign in to comment.