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

Refactor useAnchorRef and related components to work with the new Popover anchor prop #43713

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,32 @@ import BlockControls from '../block-controls';
import FormatToolbar from './format-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 @@ -51,14 +55,14 @@ function InlineToolbar( { anchorRef } ) {
);
}

const FormatToolbarContainer = ( { inline, anchorRef, value } ) => {
const FormatToolbarContainer = ( { inline, editableContentRef, value } ) => {
Copy link
Contributor Author

@ciampo ciampo Aug 30, 2022

Choose a reason for hiding this comment

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

Other notable change in this PR: I decided to rename anchorRef to editableContentRef in this file, as it is better representing what that variable is for. editableContentRef is expected to be a "standard" React ref, ie. an object with the current property.

It looks like the FormatToolbarContainer is not exported from the package anyway, so I think this change is ok.

const hasInlineToolbar = useSelect(
( select ) => select( blockEditorStore ).getSettings().hasInlineToolbar,
[]
);

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

Choose a reason for hiding this comment

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

Other change: because InlineToolbar now uses the Popover's anchor prop, we need to pass the actual element as the popoverAnchor prop (and not the reference)

}

if ( hasInlineToolbar ) {
Expand All @@ -70,7 +74,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 @@ -142,22 +142,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();
},
};
Comment on lines +53 to +58
Copy link
Contributor Author

@ciampo ciampo Aug 30, 2022

Choose a reason for hiding this comment

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

This is the main change of the PR: this hook now returns a VirtualElement instead of a Range.

A VirtualElement is an element with the ownerDocument and getBoundingClientRect() properties, and is used by floating-ui to determine an anchor when there's not an actual DOM Element available.

Couple of notes:

}

let element = range.startContainer;
Expand Down