From 157f8e7d0db20222ef2d6efd78fe9a89da07e5df Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 30 Aug 2022 08:31:29 +0200 Subject: [PATCH 01/36] Popover: add new anchor prop, mark other anchor props as deprecated --- packages/components/src/popover/README.md | 12 +++++ packages/components/src/popover/index.tsx | 42 ++++++++++++++-- packages/components/src/popover/types.ts | 60 +++++++++++++++-------- packages/components/src/popover/utils.ts | 20 ++++++-- 4 files changed, 104 insertions(+), 30 deletions(-) diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index 1778690a08b14..e35cd32c8de46 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -51,14 +51,24 @@ render( The component accepts the following props. Props not included in this set will be applied to the element wrapping Popover content. +### `anchor`: `Element | VirtualElement` + +The element that should be used by the `Popover` as its anchor. It can either be an `Element` or, alternatively, a `VirtualElement` — ie. an object with the `getBoundingClientRect()` and the `ownerDocument` properties defined. + +- Required: No + ### `anchorRect`: `DomRectWithOwnerDocument` +_Note: this prop is deprecated. Please use the `anchor` prop instead._ + An object extending a `DOMRect` with an additional optional `ownerDocument` property, used to specify a fixed popover position. - Required: No ### `anchorRef`: `Element | PopoverAnchorRefReference | PopoverAnchorRefTopBottom | Range` +_Note: this prop is deprecated. Please use the `anchor` prop instead._ + Used to specify a fixed popover position. It can be an `Element`, a React reference to an `element`, an object with a `top` and a `bottom` properties (both pointing to elements), or a `range`. - Required: No @@ -114,6 +124,8 @@ When not provided, the `onClose` callback will be called instead. ### `getAnchorRect`: `( fallbackReferenceElement: Element | null ) => DomRectWithOwnerDocument` +_Note: this prop is deprecated. Please use the `anchor` prop instead._ + A function returning the same value as the one expected by the `anchorRect` prop, used to specify a dynamic popover position. - Required: No diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 905943fc7cba2..6d7d945238714 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -163,7 +163,6 @@ const UnforwardedPopover = ( forwardedRef: ForwardedRef< any > ) => { const { - range, animate = true, headerTitle, onClose, @@ -175,17 +174,23 @@ const UnforwardedPopover = ( placement: placementProp = 'bottom-start', offset: offsetProp = 0, focusOnMount = 'firstElement', - anchorRef, - anchorRect, - getAnchorRect, + anchor, expandOnMobile, onFocusOutside, __unstableSlotName = SLOT_NAME, flip = true, resize = true, shift = false, - __unstableShift, + + // Deprecated props __unstableForcePosition, + __unstableShift, + anchorRef, + anchorRect, + getAnchorRect, + range, + + // Rest ...contentProps } = props; @@ -223,6 +228,30 @@ const UnforwardedPopover = ( shouldShift = __unstableShift; } + if ( anchorRef !== undefined ) { + deprecated( '`anchorRef` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); + } + + if ( anchorRect !== undefined ) { + deprecated( '`anchorRect` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); + } + + if ( getAnchorRect !== undefined ) { + deprecated( '`getAnchorRect` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); + } + const arrowRef = useRef( null ); const [ fallbackReferenceElement, setFallbackReferenceElement ] = @@ -383,6 +412,7 @@ const UnforwardedPopover = ( // recompute the reference element (real or virtual) and its owner document. useLayoutEffect( () => { const resultingReferenceOwnerDoc = getReferenceOwnerDocument( { + anchor, anchorRef, anchorRect, getAnchorRect, @@ -390,6 +420,7 @@ const UnforwardedPopover = ( fallbackDocument: document, } ); const resultingReferenceElement = getReferenceElement( { + anchor, anchorRef, anchorRect, getAnchorRect, @@ -400,6 +431,7 @@ const UnforwardedPopover = ( setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ + anchor, anchorRef as Element | undefined, ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top, ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.bottom, diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index e9b9a83bdcd17..ea00169f7d722 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -8,7 +8,9 @@ type PositionYAxis = 'top' | 'middle' | 'bottom'; type PositionXAxis = 'left' | 'center' | 'right'; type PositionCorner = 'top' | 'right' | 'bottom' | 'left'; -type DomRectWithOwnerDocument = DOMRect & { +type DomRectWithOwnerDocument = ReturnType< + Element[ 'getBoundingClientRect' ] +> & { ownerDocument?: Document; }; @@ -22,6 +24,10 @@ export type PopoverAnchorRefReference = MutableRefObject< >; export type PopoverAnchorRefTopBottom = { top: Element; bottom: Element }; +export type VirtualElement = Pick< Element, 'getBoundingClientRect' > & { + ownerDocument?: Document; +}; + export type PopoverProps = { /** * The name of the Slot in which the popover should be rendered. It should @@ -31,20 +37,11 @@ export type PopoverProps = { */ __unstableSlotName?: string; /** - * An object extending a `DOMRect` with an additional optional `ownerDocument` - * property, used to specify a fixed popover position. + * The element that should be used by the popover as its anchor. It can either + * be an `Element` or, alternatively, a `VirtualElement` — ie. an object with + * the `getBoundingClientRect()` and the `ownerDocument` properties defined. */ - anchorRect?: DomRectWithOwnerDocument; - /** - * Used to specify a fixed popover position. It can be an `Element`, a React - * reference to an `element`, an object with a `top` and a `bottom` properties - * (both pointing to elements), or a `range`. - */ - anchorRef?: - | Element - | PopoverAnchorRefReference - | PopoverAnchorRefTopBottom - | Range; + anchor: Element | VirtualElement; /** * Whether the popover should animate when opening. * @@ -89,13 +86,6 @@ export type PopoverProps = { * When not provided, the `onClose` callback will be called instead. */ onFocusOutside?: ( event: SyntheticEvent ) => void; - /** - * A function returning the same value as the one expected by the `anchorRect` - * prop, used to specify a dynamic popover position. - */ - getAnchorRect?: ( - fallbackReferenceElement: Element | null - ) => DomRectWithOwnerDocument; /** * Used to customize the header text shown when the popover is toggled to * fullscreen on mobile viewports (see the `expandOnMobile` prop). @@ -164,6 +154,34 @@ export type PopoverProps = { * @deprecated */ __unstableShift?: boolean; + /** + * An object extending a `DOMRect` with an additional optional `ownerDocument` + * property, used to specify a fixed popover position. + * + * @deprecated + */ + anchorRect?: DomRectWithOwnerDocument; + /** + * Used to specify a fixed popover position. It can be an `Element`, a React + * reference to an `element`, an object with a `top` and a `bottom` properties + * (both pointing to elements), or a `range`. + * + * @deprecated + */ + anchorRef?: + | Element + | PopoverAnchorRefReference + | PopoverAnchorRefTopBottom + | Range; + /** + * A function returning the same value as the one expected by the `anchorRect` + * prop, used to specify a dynamic popover position. + * + * @deprecated + */ + getAnchorRect?: ( + fallbackReferenceElement: Element | null + ) => DomRectWithOwnerDocument; /** * _Note: this prop is deprecated and has no effect on the component._ * diff --git a/packages/components/src/popover/utils.ts b/packages/components/src/popover/utils.ts index 6f838cb14d9c0..c5b15a076114f 100644 --- a/packages/components/src/popover/utils.ts +++ b/packages/components/src/popover/utils.ts @@ -116,12 +116,16 @@ export const getFrameOffset = ( }; export const getReferenceOwnerDocument = ( { + anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement, fallbackDocument, -}: Pick< PopoverProps, 'anchorRef' | 'anchorRect' | 'getAnchorRect' > & { +}: Pick< + PopoverProps, + 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' +> & { fallbackReferenceElement: Element | null; fallbackDocument: Document; } ): Document => { @@ -133,7 +137,9 @@ export const getReferenceOwnerDocument = ( { // with the `getBoundingClientRect()` function (like real elements). // See https://floating-ui.com/docs/virtual-elements for more info. let resultingReferenceOwnerDoc; - if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { + if ( anchor ) { + resultingReferenceOwnerDoc = anchor.ownerDocument; + } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { resultingReferenceOwnerDoc = ( anchorRef as PopoverAnchorRefTopBottom ) ?.top.ownerDocument; } else if ( ( anchorRef as Range | undefined )?.startContainer ) { @@ -160,16 +166,22 @@ export const getReferenceOwnerDocument = ( { }; export const getReferenceElement = ( { + anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement, -}: Pick< PopoverProps, 'anchorRef' | 'anchorRect' | 'getAnchorRect' > & { +}: Pick< + PopoverProps, + 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' +> & { fallbackReferenceElement: Element | null; } ): ReferenceType | null => { let referenceElement = null; - if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { + if ( anchor ) { + referenceElement = anchor; + } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { // Create a virtual element for the ref. The expectation is that // if anchorRef.top is defined, then anchorRef.bottom is defined too. // Seems to be used by the block toolbar, when multiple blocks are selected From c185f244405534a4f2fb221a2eea479e29b52a72 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 30 Aug 2022 14:05:43 +0200 Subject: [PATCH 02/36] Add `anchor` prop to Storybook --- packages/components/src/popover/stories/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/popover/stories/index.tsx b/packages/components/src/popover/stories/index.tsx index 60743c885bd2a..95284909b0a95 100644 --- a/packages/components/src/popover/stories/index.tsx +++ b/packages/components/src/popover/stories/index.tsx @@ -37,6 +37,7 @@ const meta: ComponentMeta< typeof Popover > = { title: 'Components/Popover', component: Popover, argTypes: { + anchor: { control: { type: null } }, anchorRef: { control: { type: null } }, anchorRect: { control: { type: null } }, children: { control: { type: null } }, From 76fd3f25ae96be3ec38331f296f59d46fdefba90 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 30 Aug 2022 14:07:11 +0200 Subject: [PATCH 03/36] Add WP version for deprecated props removal --- packages/components/src/popover/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index e35cd32c8de46..4e0242a7d6d4e 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -59,7 +59,7 @@ The element that should be used by the `Popover` as its anchor. It can either be ### `anchorRect`: `DomRectWithOwnerDocument` -_Note: this prop is deprecated. Please use the `anchor` prop instead._ +_Note: this prop is deprecated and will be removed in WordPress 6.3. Please use the `anchor` prop instead._ An object extending a `DOMRect` with an additional optional `ownerDocument` property, used to specify a fixed popover position. @@ -67,7 +67,7 @@ An object extending a `DOMRect` with an additional optional `ownerDocument` prop ### `anchorRef`: `Element | PopoverAnchorRefReference | PopoverAnchorRefTopBottom | Range` -_Note: this prop is deprecated. Please use the `anchor` prop instead._ +_Note: this prop is deprecated and will be removed in WordPress 6.3. Please use the `anchor` prop instead._ Used to specify a fixed popover position. It can be an `Element`, a React reference to an `element`, an object with a `top` and a `bottom` properties (both pointing to elements), or a `range`. @@ -124,7 +124,7 @@ When not provided, the `onClose` callback will be called instead. ### `getAnchorRect`: `( fallbackReferenceElement: Element | null ) => DomRectWithOwnerDocument` -_Note: this prop is deprecated. Please use the `anchor` prop instead._ +_Note: this prop is deprecated and will be removed in WordPress 6.3. Please use the `anchor` prop instead._ A function returning the same value as the one expected by the `anchorRect` prop, used to specify a dynamic popover position. @@ -192,6 +192,6 @@ Adjusts the size of the popover to prevent its contents from going out of view w ### `range`: `unknown` -_Note: this prop is deprecated and has no effect on the component._ +_Note: this prop is deprecated and will be removed in WordPress 6.3. It has no effect on the component._ - Required: No From 28c7b18627518a82387d6ee63c226cf511dba29e Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 30 Aug 2022 19:53:09 +0200 Subject: [PATCH 04/36] Do not render fallback anchor if there is already a prop-derived anchor --- packages/components/src/popover/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 6d7d945238714..b0e28d7237246 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -254,6 +254,7 @@ const UnforwardedPopover = ( const arrowRef = useRef( null ); + const [ referenceElement, setReferenceElement ] = useState(); const [ fallbackReferenceElement, setFallbackReferenceElement ] = useState< HTMLSpanElement | null >( null ); const [ referenceOwnerDocument, setReferenceOwnerDocument ] = useState< @@ -429,6 +430,7 @@ const UnforwardedPopover = ( referenceCallbackRef( resultingReferenceElement ); + setReferenceElement( resultingReferenceElement ); setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ anchor, @@ -559,7 +561,7 @@ const UnforwardedPopover = ( content = { content }; } - if ( anchorRef || anchorRect ) { + if ( referenceElement && referenceElement !== fallbackReferenceElement ) { return content; } From 16e66d404a02fbc229157bd2303ecb13c84554c0 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 31 Aug 2022 19:21:24 +0200 Subject: [PATCH 05/36] Block inbetween inserter: use Popover's new anchor prop (#43693) * BlockPopoverInbetween: refactor to use `anchor` prop * Simplify logic, use DOMRect * Add missing hook deps --- .../src/components/block-popover/inbetween.js | 102 +++++++++--------- 1 file changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index cf76527f4910a..442ae06edffe0 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { useSelect } from '@wordpress/data'; import { - useCallback, useMemo, createContext, useReducer, @@ -107,66 +106,65 @@ function BlockPopoverInbetween( { isVisible, ] ); - const getAnchorRect = useCallback( () => { + const popoverAnchor = useMemo( () => { if ( ( ! previousElement && ! nextElement ) || ! isVisible ) { - return {}; + return undefined; } const { ownerDocument } = previousElement || nextElement; - const previousRect = previousElement - ? previousElement.getBoundingClientRect() - : null; - const nextRect = nextElement - ? nextElement.getBoundingClientRect() - : null; + return { + ownerDocument, + getBoundingClientRect() { + const previousRect = previousElement + ? previousElement.getBoundingClientRect() + : null; + const nextRect = nextElement + ? nextElement.getBoundingClientRect() + : null; - if ( isVertical ) { - if ( isRTL() ) { - return { - top: previousRect ? previousRect.bottom : nextRect.top, - left: previousRect ? previousRect.right : nextRect.right, - right: previousRect ? previousRect.right : nextRect.right, - bottom: previousRect ? previousRect.bottom : nextRect.top, - height: 0, - width: 0, - ownerDocument, - }; - } + let left = 0; + let top = 0; - return { - top: previousRect ? previousRect.bottom : nextRect.top, - left: previousRect ? previousRect.left : nextRect.left, - right: previousRect ? previousRect.left : nextRect.left, - bottom: previousRect ? previousRect.bottom : nextRect.top, - height: 0, - width: 0, - ownerDocument, - }; - } + if ( isVertical ) { + // vertical + top = previousRect ? previousRect.bottom : nextRect.top; - if ( isRTL() ) { - return { - top: previousRect ? previousRect.top : nextRect.top, - left: previousRect ? previousRect.left : nextRect.right, - right: previousRect ? previousRect.left : nextRect.right, - bottom: previousRect ? previousRect.top : nextRect.top, - height: 0, - width: 0, - ownerDocument, - }; - } + if ( isRTL() ) { + // vertical, rtl + left = previousRect + ? previousRect.right + : nextRect.right; + } else { + // vertical, ltr + left = previousRect ? previousRect.left : nextRect.left; + } + } else { + top = previousRect ? previousRect.top : nextRect.top; - return { - top: previousRect ? previousRect.top : nextRect.top, - left: previousRect ? previousRect.right : nextRect.left, - right: previousRect ? previousRect.right : nextRect.left, - bottom: previousRect ? previousRect.left : nextRect.right, - height: 0, - width: 0, - ownerDocument, + if ( isRTL() ) { + // non vertical, rtl + left = previousRect + ? previousRect.left + : nextRect.right; + } else { + // non vertical, ltr + left = previousRect + ? previousRect.right + : nextRect.left; + } + } + + return new window.DOMRect( left, top, 0, 0 ); + }, }; - }, [ previousElement, nextElement, positionRecompute, isVisible ] ); + }, [ + previousElement, + nextElement, + positionRecompute, + isVertical, + isVisible, + ] ); const popoverScrollRef = usePopoverScroll( __unstableContentRef ); @@ -229,7 +227,7 @@ function BlockPopoverInbetween( { Date: Wed, 31 Aug 2022 19:23:55 +0200 Subject: [PATCH 06/36] ListViewDropIndicator: use Popover s new anchor prop (#43694) --- .../components/list-view/drop-indicator.js | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/list-view/drop-indicator.js b/packages/block-editor/src/components/list-view/drop-indicator.js index 0661e1717f4b7..1500e2f887fad 100644 --- a/packages/block-editor/src/components/list-view/drop-indicator.js +++ b/packages/block-editor/src/components/list-view/drop-indicator.js @@ -68,40 +68,41 @@ export default function ListViewDropIndicator( { }; }, [ getDropIndicatorIndent, targetElement ] ); - const getAnchorRect = useCallback( () => { - if ( ! targetElement ) { - return {}; + const popoverAnchor = useMemo( () => { + const isValidDropPosition = + dropPosition === 'top' || + dropPosition === 'bottom' || + dropPosition === 'inside'; + if ( ! targetElement || ! isValidDropPosition ) { + return undefined; } - const ownerDocument = targetElement.ownerDocument; - const rect = targetElement.getBoundingClientRect(); - const indent = getDropIndicatorIndent(); - - const anchorRect = { - left: rect.left + indent, - right: rect.right, - width: 0, - height: 0, - ownerDocument, + return { + ownerDocument: targetElement.ownerDocument, + getBoundingClientRect() { + const rect = targetElement.getBoundingClientRect(); + const indent = getDropIndicatorIndent(); + + const left = rect.left + indent; + const right = rect.right; + let top = 0; + let bottom = 0; + + if ( dropPosition === 'top' ) { + top = rect.top; + bottom = rect.top; + } else { + // `dropPosition` is either `bottom` or `inside` + top = rect.bottom; + bottom = rect.bottom; + } + + const width = right - left; + const height = bottom - top; + + return new window.DOMRect( left, top, width, height ); + }, }; - - if ( dropPosition === 'top' ) { - return { - ...anchorRect, - top: rect.top, - bottom: rect.top, - }; - } - - if ( dropPosition === 'bottom' || dropPosition === 'inside' ) { - return { - ...anchorRect, - top: rect.bottom, - bottom: rect.bottom, - }; - } - - return {}; }, [ targetElement, dropPosition, getDropIndicatorIndent ] ); if ( ! targetElement ) { @@ -111,7 +112,7 @@ export default function ListViewDropIndicator( { return ( From eef3dc65f79463a9c8e15806fb7895c1528213af Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 31 Aug 2022 19:34:42 +0200 Subject: [PATCH 07/36] Temporarily disable derpecation warnings --- packages/components/src/popover/index.tsx | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index b0e28d7237246..aec7b4437a61a 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -229,27 +229,27 @@ const UnforwardedPopover = ( } if ( anchorRef !== undefined ) { - deprecated( '`anchorRef` prop in Popover component', { - since: '6.1', - version: '6.3', - alternative: '`anchor` prop', - } ); + // deprecated( '`anchorRef` prop in Popover component', { + // since: '6.1', + // version: '6.3', + // alternative: '`anchor` prop', + // } ); } if ( anchorRect !== undefined ) { - deprecated( '`anchorRect` prop in Popover component', { - since: '6.1', - version: '6.3', - alternative: '`anchor` prop', - } ); + // deprecated( '`anchorRect` prop in Popover component', { + // since: '6.1', + // version: '6.3', + // alternative: '`anchor` prop', + // } ); } if ( getAnchorRect !== undefined ) { - deprecated( '`getAnchorRect` prop in Popover component', { - since: '6.1', - version: '6.3', - alternative: '`anchor` prop', - } ); + // deprecated( '`getAnchorRect` prop in Popover component', { + // since: '6.1', + // version: '6.3', + // alternative: '`anchor` prop', + // } ); } const arrowRef = useRef( null ); From 1866f3fe35dc7387576ecd388c06dd71346079b5 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 31 Aug 2022 19:39:40 +0200 Subject: [PATCH 08/36] Block toolbar: use Popover's new anchor prop (#43692) * Block toolbar: use anchor prop instead of anchorRef.{top,bottom} * Update packages/block-editor/src/components/block-popover/index.js Co-authored-by: Lena Morita Co-authored-by: Lena Morita --- .../src/components/block-popover/index.js | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 2a66623e2fbd6..aff95b62cfcff 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -47,22 +47,48 @@ function BlockPopover( }; }, [ selectedElement, lastSelectedElement, __unstableRefreshSize ] ); + const popoverAnchor = useMemo( + () => ( { + getBoundingClientRect() { + const selectedBCR = selectedElement.getBoundingClientRect(); + const lastSelectedBCR = + lastSelectedElement.getBoundingClientRect(); + + // Get the biggest rectangle that encompasses completely the currently + // selected element and the last selected element: + // - for top/left coordinates, use the smaller numbers + // - for the bottom/right coordinates, use the largest numbers + const left = Math.min( selectedBCR.left, lastSelectedBCR.left ); + const top = Math.min( selectedBCR.top, lastSelectedBCR.top ); + const right = Math.max( + selectedBCR.right, + lastSelectedBCR.right + ); + const bottom = Math.max( + selectedBCR.bottom, + lastSelectedBCR.bottom + ); + const width = right - left; + const height = bottom - top; + + return new window.DOMRect( left, top, width, height ); + }, + ownerDocument: selectedElement.ownerDocument, + } ), + [ selectedElement, lastSelectedElement ] + ); + if ( ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) { return null; } - const anchorRef = { - top: selectedElement, - bottom: lastSelectedElement, - }; - return ( Date: Wed, 31 Aug 2022 19:47:48 +0200 Subject: [PATCH 09/36] Dropdown: use Popover s new anchor prop (#43698) --- packages/components/src/dropdown/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index c260d27ffee7d..2ec44fb5d907b 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -83,7 +83,10 @@ export default function Dropdown( props ) { } const args = { isOpen, onToggle: toggle, onClose: close }; - const hasAnchorRef = + const hasPopoverAnchor = + !! popoverProps?.anchor || + // Note: `anchorRef`, `getAnchorRect` and `anchorRect` are deprecated and + // be removed from `Popover` from WordPress 6.3 !! popoverProps?.anchorRef || !! popoverProps?.getAnchorRect || !! popoverProps?.anchorRect; @@ -110,7 +113,9 @@ export default function Dropdown( props ) { // This value is used to ensure that the dropdowns // align with the editor header by default. offset={ 13 } - anchorRef={ ! hasAnchorRef ? containerRef : undefined } + anchor={ + ! hasPopoverAnchor ? containerRef.current : undefined + } { ...popoverProps } className={ classnames( 'components-dropdown__content', From 3e59ec1f33327e7dbf68eb85640870edffbe9b9d Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 1 Sep 2022 15:18:24 +0200 Subject: [PATCH 10/36] BlockPopover: prevent error when `selectedElement` is not defined --- .../src/components/block-popover/index.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index aff95b62cfcff..e2b2f58d5d4e3 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -47,26 +47,39 @@ function BlockPopover( }; }, [ selectedElement, lastSelectedElement, __unstableRefreshSize ] ); - const popoverAnchor = useMemo( - () => ( { + const popoverAnchor = useMemo( () => { + if ( + ! selectedElement || + ( bottomClientId && ! lastSelectedElement ) + ) { + return undefined; + } + + return { getBoundingClientRect() { const selectedBCR = selectedElement.getBoundingClientRect(); const lastSelectedBCR = - lastSelectedElement.getBoundingClientRect(); + lastSelectedElement?.getBoundingClientRect(); // Get the biggest rectangle that encompasses completely the currently // selected element and the last selected element: // - for top/left coordinates, use the smaller numbers // - for the bottom/right coordinates, use the largest numbers - const left = Math.min( selectedBCR.left, lastSelectedBCR.left ); - const top = Math.min( selectedBCR.top, lastSelectedBCR.top ); + const left = Math.min( + selectedBCR.left, + lastSelectedBCR?.left ?? Infinity + ); + const top = Math.min( + selectedBCR.top, + lastSelectedBCR?.top ?? Infinity + ); const right = Math.max( selectedBCR.right, - lastSelectedBCR.right + lastSelectedBCR.right ?? -Infinity ); const bottom = Math.max( selectedBCR.bottom, - lastSelectedBCR.bottom + lastSelectedBCR.bottom ?? -Infinity ); const width = right - left; const height = bottom - top; @@ -74,9 +87,8 @@ function BlockPopover( return new window.DOMRect( left, top, width, height ); }, ownerDocument: selectedElement.ownerDocument, - } ), - [ selectedElement, lastSelectedElement ] - ); + }; + }, [ bottomClientId, lastSelectedElement, selectedElement ] ); if ( ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) { return null; From c19495c80f3f011aaa444d3f2f3b8e53b8f30362 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 1 Sep 2022 15:45:59 +0200 Subject: [PATCH 11/36] Try to avoid infinite loop --- packages/components/src/popover/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index aec7b4437a61a..817d8940b2232 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -254,7 +254,6 @@ const UnforwardedPopover = ( const arrowRef = useRef( null ); - const [ referenceElement, setReferenceElement ] = useState(); const [ fallbackReferenceElement, setFallbackReferenceElement ] = useState< HTMLSpanElement | null >( null ); const [ referenceOwnerDocument, setReferenceOwnerDocument ] = useState< @@ -430,7 +429,6 @@ const UnforwardedPopover = ( referenceCallbackRef( resultingReferenceElement ); - setReferenceElement( resultingReferenceElement ); setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ anchor, @@ -561,7 +559,7 @@ const UnforwardedPopover = ( content = { content }; } - if ( referenceElement && referenceElement !== fallbackReferenceElement ) { + if ( anchorRef || anchorRect || anchor ) { return content; } From c342d796edb22f06746d132a79deb8112ce26a01 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 2 Sep 2022 10:13:39 +0200 Subject: [PATCH 12/36] Update PanelRow docs --- packages/components/src/panel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/panel/README.md b/packages/components/src/panel/README.md index 83f7d66af4bd2..7cfc2eb8a5e6e 100644 --- a/packages/components/src/panel/README.md +++ b/packages/components/src/panel/README.md @@ -176,7 +176,7 @@ The class that will be added with `components-panel__row`. to the classes of the PanelRow accepts a forwarded ref that will be added to the wrapper div. Usage: -`` +`` --- From c3a3f61863203999804186d2139bbb175bef672a Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 2 Sep 2022 11:28:34 +0200 Subject: [PATCH 13/36] Edit navigation menu actions: use Popover s new anchor prop --- .../src/components/header/menu-actions.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/edit-navigation/src/components/header/menu-actions.js b/packages/edit-navigation/src/components/header/menu-actions.js index 84310a1c8941b..50a8fe0289ec6 100644 --- a/packages/edit-navigation/src/components/header/menu-actions.js +++ b/packages/edit-navigation/src/components/header/menu-actions.js @@ -7,7 +7,7 @@ import { __experimentalText as Text, } from '@wordpress/components'; import { chevronDown } from '@wordpress/icons'; -import { useRef } from '@wordpress/element'; +import { useCallback, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; /** @@ -19,11 +19,15 @@ import { useMenuEntityProp, useSelectedMenuId } from '../../hooks'; export default function MenuActions( { menus, isLoading } ) { const [ selectedMenuId, setSelectedMenuId ] = useSelectedMenuId(); const [ menuName ] = useMenuEntityProp( 'name', selectedMenuId ); + const [ popoverAnchor, setPopoverAnchor ] = useState(); // The title ref is passed to the popover as the anchorRef so that the // dropdown is centered over the whole title area rather than just one // part of it. - const titleRef = useRef(); + const titleCallbackRef = useCallback( ( node ) => { + // The popover `anchor` prop can not be `null`. + setPopoverAnchor( node ?? undefined ); + }, [] ); if ( isLoading ) { return ( @@ -36,7 +40,7 @@ export default function MenuActions( { menus, isLoading } ) { return (
{ ( { onClose } ) => ( From 03eb730f7839c496c1bd5386e23d86fdc571b5e3 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 5 Sep 2022 11:50:59 +0200 Subject: [PATCH 14/36] BorderBoxControl: use Popover's new anchor prop (#43789) * BorderBoxControl: use new `anchor` prop for `Popover` * Make sure anchor value is `undefined` instead of `null` --- .../component.tsx | 36 +++++++++------- .../border-box-control/component.tsx | 41 +++++++++++-------- .../src/border-box-control/stories/index.js | 1 + packages/components/src/popover/types.ts | 2 +- 4 files changed, 49 insertions(+), 31 deletions(-) diff --git a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx index d5cf6e2ca3370..da72c22a2868b 100644 --- a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx +++ b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx @@ -1,12 +1,8 @@ -/** - * External dependencies - */ -import type { ComponentProps } from 'react'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useRef } from '@wordpress/element'; +import { useCallback, useState, useEffect } from '@wordpress/element'; import { useMergeRefs } from '@wordpress/compose'; /** @@ -18,6 +14,7 @@ import { Grid } from '../../grid'; import { contextConnect, WordPressComponentProps } from '../../ui/context'; import { useBorderBoxControlSplitControls } from './hook'; +import type { BorderControlProps } from '../../border-control/types'; import type { SplitControlsProps } from '../types'; const BorderBoxControlSplitControls = ( @@ -40,18 +37,27 @@ const BorderBoxControlSplitControls = ( __next36pxDefaultSize, ...otherProps } = useBorderBoxControlSplitControls( props ); - const containerRef = useRef(); - const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); - const popoverProps: ComponentProps< - typeof BorderControl - >[ '__unstablePopoverProps' ] = popoverPlacement - ? { + + const [ popoverProps, setPopoverProps ] = + useState< BorderControlProps[ '__unstablePopoverProps' ] >(); + const [ popoverAnchor, setPopoverAnchor ] = useState< Element >(); + + const containerRef = useCallback( ( node ) => { + setPopoverAnchor( node ?? undefined ); + }, [] ); + + useEffect( () => { + if ( popoverPlacement ) { + setPopoverProps( { placement: popoverPlacement, offset: popoverOffset, - anchorRef: containerRef, + anchor: popoverAnchor, shift: true, - } - : undefined; + } ); + } else { + setPopoverProps( undefined ); + } + }, [ popoverPlacement, popoverOffset, popoverAnchor ] ); const sharedBorderControlProps = { colors, @@ -64,6 +70,8 @@ const BorderBoxControlSplitControls = ( __next36pxDefaultSize, }; + const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); + return ( { const { label, hideLabelFromVision } = props; @@ -67,18 +65,29 @@ const BorderBoxControl = ( __next36pxDefaultSize = false, ...otherProps } = useBorderBoxControl( props ); - const containerRef = useRef(); - const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); - const popoverProps: ComponentProps< - typeof BorderControl - >[ '__unstablePopoverProps' ] = popoverPlacement - ? { + + const [ popoverProps, setPopoverProps ] = + useState< BorderControlProps[ '__unstablePopoverProps' ] >(); + const [ popoverAnchor, setPopoverAnchor ] = useState< Element >(); + + const containerRef = useCallback( ( node ) => { + setPopoverAnchor( node ?? undefined ); + }, [] ); + + useEffect( () => { + if ( popoverPlacement ) { + setPopoverProps( { placement: popoverPlacement, offset: popoverOffset, - anchorRef: containerRef, + anchor: popoverAnchor, shift: true, - } - : undefined; + } ); + } else { + setPopoverProps( undefined ); + } + }, [ popoverPlacement, popoverOffset, popoverAnchor ] ); + + const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); return ( diff --git a/packages/components/src/border-box-control/stories/index.js b/packages/components/src/border-box-control/stories/index.js index 1f6413fa50ab0..469d6d4d3407f 100644 --- a/packages/components/src/border-box-control/stories/index.js +++ b/packages/components/src/border-box-control/stories/index.js @@ -85,6 +85,7 @@ Default.args = { width: '1px', }, __next36pxDefaultSize: false, + popoverPlacement: 'right-start', }; const WrapperView = styled.div` diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index ea00169f7d722..b4b6d8c24fc41 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -41,7 +41,7 @@ export type PopoverProps = { * be an `Element` or, alternatively, a `VirtualElement` — ie. an object with * the `getBoundingClientRect()` and the `ownerDocument` properties defined. */ - anchor: Element | VirtualElement; + anchor?: Element | VirtualElement; /** * Whether the popover should animate when opening. * From 33c87f17cd77abc06d3fa0e993caaa17a9b779fc Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 5 Sep 2022 12:02:03 +0200 Subject: [PATCH 15/36] Image URL Input: use new anchor prop for Popover (#43784) * Image URL Input: use new anchor prop for Popover * Prevent value from being `null` --- .../src/components/url-popover/image-url-input-ui.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index f3680358c436a..142c24ed60d23 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -250,7 +250,8 @@ const ImageURLInputUI = ( { /> { isOpen && ( advancedOptions } From d1890ed9b0d30dc10633ccc5a8e4810e89adecdc Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 5 Sep 2022 13:30:01 +0200 Subject: [PATCH 16/36] Edit site Actions: use new anchor prop for Popover (#43810) --- .../header/document-actions/index.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/edit-site/src/components/header/document-actions/index.js b/packages/edit-site/src/components/header/document-actions/index.js index 53ae4c395c5a8..1258b759ff6ab 100644 --- a/packages/edit-site/src/components/header/document-actions/index.js +++ b/packages/edit-site/src/components/header/document-actions/index.js @@ -19,7 +19,7 @@ import { __experimentalText as Text, } from '@wordpress/components'; import { chevronDown } from '@wordpress/icons'; -import { useRef } from '@wordpress/element'; +import { useCallback, useState } from '@wordpress/element'; import { store as blockEditorStore } from '@wordpress/block-editor'; function getBlockDisplayText( block ) { @@ -73,10 +73,15 @@ export default function DocumentActions( { } ) { const { label } = useSecondaryText(); - // The title ref is passed to the popover as the anchorRef so that the - // dropdown is centered over the whole title area rather than just one - // part of it. - const titleRef = useRef(); + // Use internal state instead of a ref to make sure that the component + // re-renders when then anchor's ref updates. + const [ popoverAnchor, setPopoverAnchor ] = useState(); + const titleWrapperCallbackRef = useCallback( ( node ) => { + // Use the title wrapper as the popover anchor so that the dropdown is + // centered over the whole title area rather than just one part of it. + // Fall back to `undefined` in case the ref is `null`. + setPopoverAnchor( node ?? undefined ); + }, [] ); // Return a simple loading indicator until we have information to show. if ( ! isLoaded ) { @@ -103,7 +108,7 @@ export default function DocumentActions( { } ) } >
( From 044dd4a169e2a3438bd757a7362599ce0682b46b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 5 Sep 2022 14:24:39 +0200 Subject: [PATCH 17/36] Buttons block: use new Popover anchor prop (#43785) * Buttons block: use new `anchor` prop for `Popover` * Prevent anchor value from being `null` --- packages/block-library/src/button/edit.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 97775bc0bcd10..82be2fd4289a9 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -218,7 +218,8 @@ function ButtonEdit( props ) { setIsEditingURL( false ); richTextRef.current?.focus(); } } - anchorRef={ ref?.current } + // `anchor` should never be `null` + anchor={ ref.current ?? undefined } focusOnMount={ isEditingURL ? 'firstElement' : false } __unstableSlotName={ '__unstable-block-tools-after' } shift From f9fbae5a3f6f3c0c8cf3855a0a4c6d8442c89270 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 5 Sep 2022 15:53:47 +0200 Subject: [PATCH 18/36] Navigation block: use new anchor prop for Popover (#43786) * Navigation block: use new `anchor` prop for `Popover` * Use anchor for the Navigation submenu block too * Prevent anchor value from being `null` --- packages/block-library/src/navigation-link/edit.js | 3 ++- packages/block-library/src/navigation-submenu/edit.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index c208d83c1a45e..1bddb6cb3800d 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -844,7 +844,8 @@ export default function NavigationLinkEdit( { setIsLinkOpen( false ) } - anchorRef={ listItemRef.current } + // `anchor` should never be `null` + anchor={ listItemRef.current ?? undefined } shift > setIsLinkOpen( false ) } - anchorRef={ listItemRef.current } + // `anchor` should never be `null` + anchor={ listItemRef.current ?? undefined } shift > Date: Mon, 5 Sep 2022 15:54:10 +0200 Subject: [PATCH 19/36] Post Date block: use new anchor prop for Popover (#43787) * Post Date block: use new `anchor` prop for `Popover` * Prevent anchor value from being `null` --- packages/block-library/src/post-date/edit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/post-date/edit.js b/packages/block-library/src/post-date/edit.js index e0b642e06c825..fd750c4a44e2e 100644 --- a/packages/block-library/src/post-date/edit.js +++ b/packages/block-library/src/post-date/edit.js @@ -98,7 +98,10 @@ export default function PostDateEdit( { { date && ! isDescendentOfQueryLoop && ( ( Date: Mon, 5 Sep 2022 21:28:43 +0200 Subject: [PATCH 20/36] Tooltip: refactor using Popover's new anchor prop (#43799) * Tooltip: use Popover s new anchor prop * Use internal state to force re-renders when the anchor ref changes * Simplify code --- packages/components/src/tooltip/index.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/components/src/tooltip/index.js b/packages/components/src/tooltip/index.js index 75508d7a78b78..6f48b33af16ac 100644 --- a/packages/components/src/tooltip/index.js +++ b/packages/components/src/tooltip/index.js @@ -9,7 +9,6 @@ import { concatChildren, useEffect, useState, - useRef, } from '@wordpress/element'; import { useDebounce, useMergeRefs } from '@wordpress/compose'; @@ -60,7 +59,7 @@ const getRegularElement = ( { }; const addPopoverToGrandchildren = ( { - anchorRef, + anchor, grandchildren, isOver, offset, @@ -78,7 +77,7 @@ const addPopoverToGrandchildren = ( { aria-hidden="true" animate={ false } offset={ offset } - anchorRef={ anchorRef } + anchor={ anchor } shift > { text } @@ -124,13 +123,18 @@ function Tooltip( props ) { const [ isMouseDown, setIsMouseDown ] = useState( false ); const [ isOver, setIsOver ] = useState( false ); const delayedSetIsOver = useDebounce( setIsOver, delay ); + // Using internal state (instead of a ref) for the popover anchor to make sure + // that the component re-renders when the anchor updates. + const [ popoverAnchor, setPopoverAnchor ] = useState(); // Create a reference to the Tooltip's child, to be passed to the Popover // so that the Tooltip can be correctly positioned. Also, merge with the // existing ref for the first child, so that its ref is preserved. - const childRef = useRef( null ); const existingChildRef = Children.toArray( children )[ 0 ]?.ref; - const mergedChildRefs = useMergeRefs( [ childRef, existingChildRef ] ); + const mergedChildRefs = useMergeRefs( [ + setPopoverAnchor, + existingChildRef, + ] ); const createMouseDown = ( event ) => { // In firefox, the mouse down event is also fired when the select @@ -253,7 +257,8 @@ function Tooltip( props ) { : getRegularElement; const popoverData = { - anchorRef: childRef, + // `anchor` should never be `null` + anchor: popoverAnchor ?? undefined, isOver, offset: 4, position, From 4db67fc713c61de2d60b8da987a63c62c3affc73 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 09:48:50 +0200 Subject: [PATCH 21/36] Improve docs around using state instead of refs for the anchor element --- packages/components/src/popover/README.md | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index 4e0242a7d6d4e..a1317fcfd5eef 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -6,7 +6,9 @@ The behavior of the popover when it exceeds the viewport's edges can be controll ## Usage -Render a Popover within the parent to which it should anchor: +Render a Popover within the parent to which it should anchor. + +If a Popover is returned by your component, it will be shown. To hide the popover, simply omit it from your component's render value. ```jsx import { Button, Popover } from '@wordpress/components'; @@ -27,7 +29,36 @@ const MyPopover = () => { }; ``` -If a Popover is returned by your component, it will be shown. To hide the popover, simply omit it from your component's render value. +In order to pass an explicit anchor, you can use the `anchor` prop. When doing so, the anchor element should be stored in state rather than a plain ref to ensure reactive updating when it changes. + +```jsx +import { Button, Popover } from '@wordpress/components'; +import { useState } from '@wordpress/element'; + +const MyPopover = () => { + // Use internal state instead of a ref to make sure that the component + // re-renders when the anchor's ref updates. + const [ popoverAnchor, setPopoverAnchor ] = useState(); + const [ isVisible, setIsVisible ] = useState( false ); + const toggleVisible = () => { + setIsVisible( ( state ) => ! state ); + }; + + return ( +

Popover s anchor

+ + { isVisible && ( + + Popover is toggled! + + ) } + ); +}; +``` If you want Popover elements to render to a specific location on the page to allow style cascade to take effect, you must render a `Popover.Slot` further up the element tree: @@ -55,6 +86,8 @@ The component accepts the following props. Props not included in this set will b The element that should be used by the `Popover` as its anchor. It can either be an `Element` or, alternatively, a `VirtualElement` — ie. an object with the `getBoundingClientRect()` and the `ownerDocument` properties defined. +The element should be stored in state rather than a plain ref to ensure reactive updating when it changes. + - Required: No ### `anchorRect`: `DomRectWithOwnerDocument` From aadd73014ca53cd7a223bee755b90ad991bf8ea9 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 09:49:59 +0200 Subject: [PATCH 22/36] Allow `anchor` to be `null` --- packages/components/src/popover/README.md | 2 +- packages/components/src/popover/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index a1317fcfd5eef..3ff77dd6c4f14 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -82,7 +82,7 @@ render( The component accepts the following props. Props not included in this set will be applied to the element wrapping Popover content. -### `anchor`: `Element | VirtualElement` +### `anchor`: `Element | VirtualElement | null` The element that should be used by the `Popover` as its anchor. It can either be an `Element` or, alternatively, a `VirtualElement` — ie. an object with the `getBoundingClientRect()` and the `ownerDocument` properties defined. diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index b4b6d8c24fc41..7ad654ff9c4b7 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -41,7 +41,7 @@ export type PopoverProps = { * be an `Element` or, alternatively, a `VirtualElement` — ie. an object with * the `getBoundingClientRect()` and the `ownerDocument` properties defined. */ - anchor?: Element | VirtualElement; + anchor?: Element | VirtualElement | null; /** * Whether the popover should animate when opening. * From c787c544da82602adcfe102ca07ab6b3b9c77a77 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 11:26:27 +0200 Subject: [PATCH 23/36] Edit Post: use Popover's new anchor prop (#43808) * Edit Post: use Popover s new anchor prop * Update comment * SImplify code * Update packages/edit-post/src/components/sidebar/post-schedule/index.js Co-authored-by: Daniel Richards * Allow passing a `null` anchor Co-authored-by: Daniel Richards --- .../src/components/sidebar/post-schedule/index.js | 14 ++++++++++---- .../components/sidebar/post-schedule/style.scss | 2 +- .../src/components/sidebar/post-template/index.js | 10 ++++++---- .../src/components/sidebar/post-url/index.js | 11 +++++++---- .../components/sidebar/post-visibility/index.js | 14 ++++++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/edit-post/src/components/sidebar/post-schedule/index.js b/packages/edit-post/src/components/sidebar/post-schedule/index.js index f8b96fe6669e7..cf1c64fa79289 100644 --- a/packages/edit-post/src/components/sidebar/post-schedule/index.js +++ b/packages/edit-post/src/components/sidebar/post-schedule/index.js @@ -3,7 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { PanelRow, Dropdown, Button } from '@wordpress/components'; -import { useRef } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { PostSchedule as PostScheduleForm, PostScheduleCheck, @@ -11,13 +11,19 @@ import { } from '@wordpress/editor'; export default function PostSchedule() { - const anchorRef = useRef(); + // Use internal state instead of a ref to make sure that the component + // re-renders when the anchor's ref updates. + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + return ( - + { __( 'Publish' ) } { const postTypeSlug = select( editorStore ).getCurrentPostType(); @@ -46,10 +48,10 @@ export default function PostTemplate() { } return ( - + { __( 'Template' ) } - + { __( 'URL' ) } ( - + { __( 'Visibility' ) } { ! canEdit && ( @@ -31,7 +37,7 @@ export function PostVisibility() { // Anchor the popover to the middle of the // entire row so that it doesn't move around // when the label changes. - anchorRef: rowRef.current, + anchor: popoverAnchor, } } focusOnMount renderToggle={ ( { isOpen, onToggle } ) => ( From 0ac5e798cc44a1d074e61f6cb6c40f847c58042f Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 14:51:53 +0200 Subject: [PATCH 24/36] Refactor `useAnchorRef` and related components to work with the new Popover `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` --- .../rich-text/format-toolbar-container.js | 22 +++++++++++-------- .../src/components/rich-text/index.js | 2 +- .../src/autocomplete/autocompleter-ui.js | 4 ++-- packages/format-library/src/image/index.js | 4 ++-- packages/format-library/src/link/inline.js | 6 ++--- .../format-library/src/text-color/inline.js | 6 ++--- packages/rich-text/README.md | 8 +++---- .../rich-text/src/component/use-anchor-ref.js | 21 +++++++++++++----- 8 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.js index a2fae8e1b9a8e..bbc7dbf58d374 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar-container.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.js @@ -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 ; + return ; } -function InlineToolbar( { anchorRef } ) { +function InlineToolbar( { popoverAnchor } ) { return ( @@ -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 ; + return ; } if ( hasInlineToolbar ) { @@ -76,7 +80,7 @@ const FormatToolbarContainer = ( { inline, anchorRef, value } ) => { return ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index a49286e3ce38a..b38c53f6bacd8 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -354,7 +354,7 @@ function RichTextWrapper( { isSelected && hasFormats && ( ) } diff --git a/packages/components/src/autocomplete/autocompleter-ui.js b/packages/components/src/autocomplete/autocompleter-ui.js index c909aa6a4a578..c25dcfb403018 100644 --- a/packages/components/src/autocomplete/autocompleter-ui.js +++ b/packages/components/src/autocomplete/autocompleter-ui.js @@ -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 ); @@ -54,7 +54,7 @@ export function getAutoCompleterUI( autocompleter ) { onClose={ onReset } position="top right" className="components-autocomplete__popover" - anchorRef={ anchorRef } + anchor={ popoverAnchor } >
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 } ) ); @@ -152,7 +152,7 @@ export default function InlineColorUI( { diff --git a/packages/rich-text/src/component/use-anchor-ref.js b/packages/rich-text/src/component/use-anchor-ref.js index 4be4fdd93096a..d601174ad379d 100644 --- a/packages/rich-text/src/component/use-anchor-ref.js +++ b/packages/rich-text/src/component/use-anchor-ref.js @@ -12,11 +12,17 @@ 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} $1.ref React ref of the element @@ -24,7 +30,7 @@ import { getActiveFormat } from '../get-active-format'; * @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; @@ -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; From f9a748aa0cd9f6054b4503b445b71ef5b43b05e0 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 15:01:14 +0200 Subject: [PATCH 25/36] Re-enable deprecation warnings --- packages/components/src/popover/index.tsx | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 817d8940b2232..baca3c071fb0e 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -229,27 +229,27 @@ const UnforwardedPopover = ( } if ( anchorRef !== undefined ) { - // deprecated( '`anchorRef` prop in Popover component', { - // since: '6.1', - // version: '6.3', - // alternative: '`anchor` prop', - // } ); + deprecated( '`anchorRef` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); } if ( anchorRect !== undefined ) { - // deprecated( '`anchorRect` prop in Popover component', { - // since: '6.1', - // version: '6.3', - // alternative: '`anchor` prop', - // } ); + deprecated( '`anchorRect` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); } if ( getAnchorRect !== undefined ) { - // deprecated( '`getAnchorRect` prop in Popover component', { - // since: '6.1', - // version: '6.3', - // alternative: '`anchor` prop', - // } ); + deprecated( '`getAnchorRect` prop in Popover component', { + since: '6.1', + version: '6.3', + alternative: '`anchor` prop', + } ); } const arrowRef = useRef( null ); From bc75d2f9a18fc1e7797a52b578ec46d2d14a2075 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 6 Sep 2022 15:20:53 +0200 Subject: [PATCH 26/36] Remove fall back to `undefined` from `null` --- .../src/components/url-popover/image-url-input-ui.js | 3 +-- packages/block-library/src/button/edit.js | 3 +-- packages/block-library/src/navigation-link/edit.js | 3 +-- packages/block-library/src/navigation-submenu/edit.js | 3 +-- packages/block-library/src/post-date/edit.js | 5 +---- .../border-box-control-split-controls/component.tsx | 6 ++++-- .../border-box-control/border-box-control/component.tsx | 6 ++++-- packages/components/src/tooltip/index.js | 5 ++--- .../src/components/header/menu-actions.js | 5 ++--- .../src/components/header/document-actions/index.js | 9 +++------ 10 files changed, 20 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index 142c24ed60d23..de2d679202e0c 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -250,8 +250,7 @@ const ImageURLInputUI = ( { /> { isOpen && ( advancedOptions } diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 82be2fd4289a9..4359436b0516e 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -218,8 +218,7 @@ function ButtonEdit( props ) { setIsEditingURL( false ); richTextRef.current?.focus(); } } - // `anchor` should never be `null` - anchor={ ref.current ?? undefined } + anchor={ ref.current } focusOnMount={ isEditingURL ? 'firstElement' : false } __unstableSlotName={ '__unstable-block-tools-after' } shift diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 1bddb6cb3800d..bcd4a6f49ec89 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -844,8 +844,7 @@ export default function NavigationLinkEdit( { setIsLinkOpen( false ) } - // `anchor` should never be `null` - anchor={ listItemRef.current ?? undefined } + anchor={ listItemRef.current } shift > setIsLinkOpen( false ) } - // `anchor` should never be `null` - anchor={ listItemRef.current ?? undefined } + anchor={ listItemRef.current } shift > ( (); - const [ popoverAnchor, setPopoverAnchor ] = useState< Element >(); + const [ popoverAnchor, setPopoverAnchor ] = useState< Element | null >( + null + ); const containerRef = useCallback( ( node ) => { - setPopoverAnchor( node ?? undefined ); + setPopoverAnchor( node ); }, [] ); useEffect( () => { diff --git a/packages/components/src/border-box-control/border-box-control/component.tsx b/packages/components/src/border-box-control/border-box-control/component.tsx index c346d41585778..ae6beae18b394 100644 --- a/packages/components/src/border-box-control/border-box-control/component.tsx +++ b/packages/components/src/border-box-control/border-box-control/component.tsx @@ -68,10 +68,12 @@ const BorderBoxControl = ( const [ popoverProps, setPopoverProps ] = useState< BorderControlProps[ '__unstablePopoverProps' ] >(); - const [ popoverAnchor, setPopoverAnchor ] = useState< Element >(); + const [ popoverAnchor, setPopoverAnchor ] = useState< Element | null >( + null + ); const containerRef = useCallback( ( node ) => { - setPopoverAnchor( node ?? undefined ); + setPopoverAnchor( node ); }, [] ); useEffect( () => { diff --git a/packages/components/src/tooltip/index.js b/packages/components/src/tooltip/index.js index 6f48b33af16ac..c4b8a5ea6c147 100644 --- a/packages/components/src/tooltip/index.js +++ b/packages/components/src/tooltip/index.js @@ -125,7 +125,7 @@ function Tooltip( props ) { const delayedSetIsOver = useDebounce( setIsOver, delay ); // Using internal state (instead of a ref) for the popover anchor to make sure // that the component re-renders when the anchor updates. - const [ popoverAnchor, setPopoverAnchor ] = useState(); + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); // Create a reference to the Tooltip's child, to be passed to the Popover // so that the Tooltip can be correctly positioned. Also, merge with the @@ -257,8 +257,7 @@ function Tooltip( props ) { : getRegularElement; const popoverData = { - // `anchor` should never be `null` - anchor: popoverAnchor ?? undefined, + anchor: popoverAnchor, isOver, offset: 4, position, diff --git a/packages/edit-navigation/src/components/header/menu-actions.js b/packages/edit-navigation/src/components/header/menu-actions.js index 50a8fe0289ec6..dad783823574b 100644 --- a/packages/edit-navigation/src/components/header/menu-actions.js +++ b/packages/edit-navigation/src/components/header/menu-actions.js @@ -19,14 +19,13 @@ import { useMenuEntityProp, useSelectedMenuId } from '../../hooks'; export default function MenuActions( { menus, isLoading } ) { const [ selectedMenuId, setSelectedMenuId ] = useSelectedMenuId(); const [ menuName ] = useMenuEntityProp( 'name', selectedMenuId ); - const [ popoverAnchor, setPopoverAnchor ] = useState(); + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); // The title ref is passed to the popover as the anchorRef so that the // dropdown is centered over the whole title area rather than just one // part of it. const titleCallbackRef = useCallback( ( node ) => { - // The popover `anchor` prop can not be `null`. - setPopoverAnchor( node ?? undefined ); + setPopoverAnchor( node ); }, [] ); if ( isLoading ) { diff --git a/packages/edit-site/src/components/header/document-actions/index.js b/packages/edit-site/src/components/header/document-actions/index.js index 1258b759ff6ab..e27fb2e6b9a49 100644 --- a/packages/edit-site/src/components/header/document-actions/index.js +++ b/packages/edit-site/src/components/header/document-actions/index.js @@ -75,12 +75,11 @@ export default function DocumentActions( { // Use internal state instead of a ref to make sure that the component // re-renders when then anchor's ref updates. - const [ popoverAnchor, setPopoverAnchor ] = useState(); + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); const titleWrapperCallbackRef = useCallback( ( node ) => { // Use the title wrapper as the popover anchor so that the dropdown is // centered over the whole title area rather than just one part of it. - // Fall back to `undefined` in case the ref is `null`. - setPopoverAnchor( node ?? undefined ); + setPopoverAnchor( node ); }, [] ); // Return a simple loading indicator until we have information to show. @@ -135,9 +134,7 @@ export default function DocumentActions( { { dropdownContent && ( ( From 42a72ac809e0fdf8471363ff8d14ae3d62cd73b3 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 7 Sep 2022 13:38:18 +0200 Subject: [PATCH 29/36] CHANGELOG --- packages/components/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index a90e7125b8f2d..c7438626278df 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- `Popover`: added new `anchor` prop, supposed to supersede all previous anchor-related props (`anchorRef`, `anchorRect`, `getAnchorRect`). These older anchor-related props are now marked as deprecated and are scheduled to be removed in WordPress 6.3 ([#43691](https://github.com/WordPress/gutenberg/pull/43691)). + ### Internal - `NavigationMenu` updated to ignore `react/exhaustive-deps` eslint rule ([#44090](https://github.com/WordPress/gutenberg/pull/44090)). From 8c76d7b6e098923505413b62c34ab15159f5e9fa Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 7 Sep 2022 15:08:54 +0200 Subject: [PATCH 30/36] Add new `useAnchor` hook instead of changing existing `useAnchorRef` hook --- package-lock.json | 1 + .../rich-text/format-toolbar-container.js | 18 +++-- .../src/components/rich-text/index.js | 4 +- .../src/autocomplete/autocompleter-ui.js | 7 +- packages/format-library/src/image/index.js | 6 +- packages/format-library/src/link/inline.js | 8 +- .../format-library/src/text-color/inline.js | 10 ++- packages/rich-text/CHANGELOG.md | 4 + packages/rich-text/README.md | 27 ++++++- packages/rich-text/package.json | 1 + .../rich-text/src/component/use-anchor-ref.js | 28 +++---- .../rich-text/src/component/use-anchor.js | 79 +++++++++++++++++++ packages/rich-text/src/index.js | 1 + 13 files changed, 157 insertions(+), 37 deletions(-) create mode 100644 packages/rich-text/src/component/use-anchor.js diff --git a/package-lock.json b/package-lock.json index 173dc2adfa1bd..86445e1897690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18090,6 +18090,7 @@ "@wordpress/a11y": "file:packages/a11y", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", + "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", "@wordpress/escape-html": "file:packages/escape-html", "@wordpress/i18n": "file:packages/i18n", diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.js index bbc7dbf58d374..18027af27a05f 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar-container.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.js @@ -7,7 +7,7 @@ import { useSelect } from '@wordpress/data'; import { isCollapsed, getActiveFormats, - useAnchorRef, + useAnchor, store as richTextStore, } from '@wordpress/rich-text'; @@ -21,7 +21,7 @@ import { store as blockEditorStore } from '../../store'; function InlineSelectionToolbar( { value, - editableContentRef, + editableContentElement, activeFormats, } ) { const lastFormat = activeFormats[ activeFormats.length - 1 ]; @@ -30,8 +30,8 @@ function InlineSelectionToolbar( { ( select ) => select( richTextStore ).getFormatType( lastFormatType ), [ lastFormatType ] ); - const popoverAnchor = useAnchorRef( { - ref: editableContentRef, + const popoverAnchor = useAnchor( { + editableContentElement, value, settings, } ); @@ -61,14 +61,18 @@ function InlineToolbar( { popoverAnchor } ) { ); } -const FormatToolbarContainer = ( { inline, editableContentRef, value } ) => { +const FormatToolbarContainer = ( { + inline, + editableContentElement, + value, +} ) => { const hasInlineToolbar = useSelect( ( select ) => select( blockEditorStore ).getSettings().hasInlineToolbar, [] ); if ( inline ) { - return ; + return ; } if ( hasInlineToolbar ) { @@ -80,7 +84,7 @@ const FormatToolbarContainer = ( { inline, editableContentRef, value } ) => { return ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index b38c53f6bacd8..c6340ff4f94bc 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -328,7 +328,7 @@ function RichTextWrapper( } function onFocus() { - anchorRef.current.focus(); + anchorRef.current?.focus(); } const TagName = tagName; @@ -354,7 +354,7 @@ function RichTextWrapper( { isSelected && hasFormats && ( ) } diff --git a/packages/components/src/autocomplete/autocompleter-ui.js b/packages/components/src/autocomplete/autocompleter-ui.js index c25dcfb403018..f8a9fd08eaf95 100644 --- a/packages/components/src/autocomplete/autocompleter-ui.js +++ b/packages/components/src/autocomplete/autocompleter-ui.js @@ -8,7 +8,7 @@ import { map } from 'lodash'; * WordPress dependencies */ import { useLayoutEffect } from '@wordpress/element'; -import { useAnchorRef } from '@wordpress/rich-text'; +import { useAnchor } from '@wordpress/rich-text'; /** * Internal dependencies @@ -35,7 +35,10 @@ export function getAutoCompleterUI( autocompleter ) { contentRef, } ) { const [ items ] = useItems( filterValue ); - const popoverAnchor = useAnchorRef( { ref: contentRef, value } ); + const popoverAnchor = useAnchor( { + editableContentElement: contentRef.current, + value, + } ); useLayoutEffect( () => { onChangeOptions( items ); diff --git a/packages/format-library/src/image/index.js b/packages/format-library/src/image/index.js index 1969b5c570604..d45296474ed2d 100644 --- a/packages/format-library/src/image/index.js +++ b/packages/format-library/src/image/index.js @@ -4,7 +4,7 @@ import { Path, SVG, TextControl, Popover, Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; -import { insertObject, useAnchorRef } from '@wordpress/rich-text'; +import { insertObject, useAnchor } from '@wordpress/rich-text'; import { MediaUpload, RichTextToolbarButton, @@ -36,8 +36,8 @@ export const image = { function InlineUI( { value, onChange, activeObjectAttributes, contentRef } ) { const { style } = activeObjectAttributes; const [ width, setWidth ] = useState( style?.replace( /\D/g, '' ) ); - const popoverAnchor = useAnchorRef( { - ref: contentRef, + const popoverAnchor = useAnchor( { + editableContentElement: contentRef.current, value, settings: image, } ); diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 74256b34f0cc6..c47733e6bf9e9 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -10,7 +10,7 @@ import { insert, isCollapsed, applyFormat, - useAnchorRef, + useAnchor, removeFormat, slice, replace, @@ -183,7 +183,11 @@ function InlineLinkUI( { } } - const popoverAnchor = useAnchorRef( { ref: contentRef, value, settings } ); + const popoverAnchor = useAnchor( { + editableContentElement: contentRef.current, + 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 diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index 29396a99f0054..f956afa7d323a 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -7,7 +7,7 @@ import { applyFormat, removeFormat, getActiveFormat, - useAnchorRef, + useAnchor, } from '@wordpress/rich-text'; import { ColorPalette, @@ -140,12 +140,16 @@ export default function InlineColorUI( { /* 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, + not the highlighted text. Given that useAnchor uses document.getSelection, it will return null, since it can't find the element within the HEX input. This caches the last truthy value of the selection anchor reference. */ const popoverAnchor = useCachedTruthy( - useAnchorRef( { ref: contentRef, value, settings } ) + useAnchor( { + editableContentElement: contentRef.current, + value, + settings, + } ) ); return ( diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index cb8a7713327df..3a94b3bb4ccc1 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New features + +- Introduced new `useAnchor` hook, which works better with the new `Popover` component APIs. The previous `useAnchorRef` hook is now marked as deprecated, and is scheduled to be removed in WordPress 6.3 ([#43691](https://github.com/WordPress/gutenberg/pull/43691)). + ## 5.15.0 (2022-09-13) ## 5.14.0 (2022-08-24) diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 4c87a5d4a1d86..9bf1a8ffbe326 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -476,15 +476,38 @@ _Returns_ ### useAnchorRef +_Note: this hook is deprecated and is scheduled to be removed in WordPress 6.3. Please use the `useAnchor` hook instead._ + +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. + +_Parameters_ + +- _$1_ `Object`: Named parameters. +- _$1.ref_ `RefObject`: React ref of the element containing the editable content. +- _$1.value_ `RichTextValue`: Value to check for selection. +- _$1.settings_ `RichTextFormatType`: The format type's settings. + +_Returns_ + +- `Element|Range`: The active element or selection range. + +### useAnchor + This hook, to be used in a format type's Edit component, returns the active 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. +UI, e.g. by passing it to the `Popover` component via the `anchor` prop. + +To ensure reactive updates to the `Popover`, the `editableContentElement` should +be stored in state rather than in a plain ref. _Parameters_ - _$1_ `Object`: Named parameters. -- _$1.ref_ `RefObject`: React ref of the element containing the editable content. +- _$1.editableContentElement_ `HTMLElement|null`: The element containing the editable content. - _$1.value_ `RichTextValue`: Value to check for selection. - _$1.settings_ `RichTextFormatType`: The format type's settings. diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index cb157c909ba04..b493df7d8276c 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -33,6 +33,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/escape-html": "file:../escape-html", "@wordpress/i18n": "file:../i18n", diff --git a/packages/rich-text/src/component/use-anchor-ref.js b/packages/rich-text/src/component/use-anchor-ref.js index d601174ad379d..a66a7bbbeb2de 100644 --- a/packages/rich-text/src/component/use-anchor-ref.js +++ b/packages/rich-text/src/component/use-anchor-ref.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { useMemo } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -12,17 +13,11 @@ 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 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. + * 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. * * @param {Object} $1 Named parameters. * @param {RefObject} $1.ref React ref of the element @@ -30,9 +25,15 @@ import { getActiveFormat } from '../get-active-format'; * @param {RichTextValue} $1.value Value to check for selection. * @param {RichTextFormatType} $1.settings The format type's settings. * - * @return {Element|VirtualAnchorElement|undefined|null} The active element or selection range. + * @return {Element|Range} The active element or selection range. */ export function useAnchorRef( { ref, value, settings = {} } ) { + deprecated( '`useAnchorRef` hook', { + since: '6.1', + version: '6.3', + alternative: '`useAnchor` hook', + } ); + const { tagName, className, name } = settings; const activeFormat = name ? getActiveFormat( value, name ) : undefined; @@ -50,12 +51,7 @@ export function useAnchorRef( { ref, value, settings = {} } ) { const range = selection.getRangeAt( 0 ); if ( ! activeFormat ) { - return { - ownerDocument: range.startContainer.ownerDocument, - getBoundingClientRect() { - return range.getBoundingClientRect(); - }, - }; + return range; } let element = range.startContainer; diff --git a/packages/rich-text/src/component/use-anchor.js b/packages/rich-text/src/component/use-anchor.js new file mode 100644 index 0000000000000..dd6172a1661db --- /dev/null +++ b/packages/rich-text/src/component/use-anchor.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +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 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 via the `anchor` prop. + * + * @param {Object} $1 Named parameters. + * @param {HTMLElement|null} $1.editableContentElement 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|VirtualAnchorElement|undefined|null} The active element or selection range. + */ +export function useAnchor( { editableContentElement, value, settings = {} } ) { + const { tagName, className, name } = settings; + const activeFormat = name ? getActiveFormat( value, name ) : undefined; + + return useMemo( () => { + if ( ! editableContentElement ) return; + const { + ownerDocument: { defaultView }, + } = editableContentElement; + const selection = defaultView.getSelection(); + + if ( ! selection.rangeCount ) { + return; + } + + const range = selection.getRangeAt( 0 ); + + if ( ! activeFormat ) { + return { + ownerDocument: range.startContainer.ownerDocument, + getBoundingClientRect() { + return range.getBoundingClientRect(); + }, + }; + } + + let element = range.startContainer; + + // If the caret is right before the element, select the next element. + element = element.nextElementSibling || element; + + while ( element.nodeType !== element.ELEMENT_NODE ) { + element = element.parentNode; + } + + return element.closest( + tagName + ( className ? '.' + className : '' ) + ); + }, [ + editableContentElement, + activeFormat, + value.start, + value.end, + tagName, + className, + ] ); +} diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js index b4d2b19e36285..33e6e054a68a6 100644 --- a/packages/rich-text/src/index.js +++ b/packages/rich-text/src/index.js @@ -26,6 +26,7 @@ export { unregisterFormatType } from './unregister-format-type'; export { createElement as __unstableCreateElement } from './create-element'; export { useAnchorRef } from './component/use-anchor-ref'; +export { useAnchor } from './component/use-anchor'; export { default as __experimentalRichText, From d09cbfe4bbbed8622da4165b5dd1735b898e644a Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 7 Sep 2022 15:42:23 +0200 Subject: [PATCH 31/36] Fix API docs --- packages/rich-text/README.md | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 9bf1a8ffbe326..6e5a01b355462 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -474,46 +474,41 @@ _Returns_ - `RichTextFormatType|undefined`: The previous format value, if it has been successfully unregistered; otherwise `undefined`. -### useAnchorRef - -_Note: this hook is deprecated and is scheduled to be removed in WordPress 6.3. Please use the `useAnchor` hook instead._ +### useAnchor 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 via the `anchor` prop. _Parameters_ - _$1_ `Object`: Named parameters. -- _$1.ref_ `RefObject`: React ref of the element containing the editable content. +- _$1.editableContentElement_ `HTMLElement|null`: The element containing the editable content. - _$1.value_ `RichTextValue`: Value to check for selection. - _$1.settings_ `RichTextFormatType`: The format type's settings. _Returns_ -- `Element|Range`: The active element or selection range. +- `Element|VirtualAnchorElement|undefined|null`: The active element or selection range. -### useAnchor +### useAnchorRef This hook, to be used in a format type's Edit component, returns the active -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 via the `anchor` prop. - -To ensure reactive updates to the `Popover`, the `editableContentElement` should -be stored in state rather than in a plain ref. +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. _Parameters_ - _$1_ `Object`: Named parameters. -- _$1.editableContentElement_ `HTMLElement|null`: The element containing the editable content. +- _$1.ref_ `RefObject`: React ref of the element containing the editable content. - _$1.value_ `RichTextValue`: Value to check for selection. - _$1.settings_ `RichTextFormatType`: The format type's settings. _Returns_ -- `Element|VirtualAnchorElement|undefined|null`: The active element or selection range. +- `Element|Range`: The active element or selection range. From dffc3118bb74166e5ddbd262741b4278a75ca72b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 7 Sep 2022 16:47:17 +0200 Subject: [PATCH 32/36] Update Popover unit tests --- packages/components/src/popover/test/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/components/src/popover/test/index.js b/packages/components/src/popover/test/index.js index 1551ec6586753..dff3391465969 100644 --- a/packages/components/src/popover/test/index.js +++ b/packages/components/src/popover/test/index.js @@ -6,7 +6,7 @@ import { act, render, screen } from '@testing-library/react'; /** * WordPress dependencies */ -import { useRef } from '@wordpress/element'; +import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -57,14 +57,16 @@ describe( 'Popover', () => { ).toMatchSnapshot(); } ); - it( 'should render correctly when anchorRef is provided', () => { + it( 'should render correctly when anchor is provided', () => { const PopoverWithAnchor = ( args ) => { - const anchorRef = useRef( null ); + // Use internal state instead of a ref to make sure that the component + // re-renders when the popover's anchor updates. + const [ anchor, setAnchor ] = useState( null ); return (
-

Anchor

- +

Anchor

+
); }; From dd1b77167cfa7de0009a46f5ea0091992a8372b3 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 7 Sep 2022 16:48:09 +0200 Subject: [PATCH 33/36] Remove unused import --- packages/components/src/dropdown/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index e91902cacbafe..0092256a61225 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useRef, useEffect, useState, useMemo } from '@wordpress/element'; +import { useRef, useEffect, useState } from '@wordpress/element'; import { useMergeRefs } from '@wordpress/compose'; /** From 24c099a30c0a69821fe0545dc8e7aa3a7299a24d Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 9 Sep 2022 18:25:26 +0200 Subject: [PATCH 34/36] Use DOMRect in the DomRectWithOwnerDocument type --- packages/components/src/popover/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index 7ad654ff9c4b7..efd7c8a16107e 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -8,9 +8,7 @@ type PositionYAxis = 'top' | 'middle' | 'bottom'; type PositionXAxis = 'left' | 'center' | 'right'; type PositionCorner = 'top' | 'right' | 'bottom' | 'left'; -type DomRectWithOwnerDocument = ReturnType< - Element[ 'getBoundingClientRect' ] -> & { +type DomRectWithOwnerDocument = DOMRect & { ownerDocument?: Document; }; From 1d11ed241e4bf431d5a7308ec51dc150070c85cf Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 14 Sep 2022 09:19:09 +0200 Subject: [PATCH 35/36] Improve the wording of deprecation warnings --- packages/components/src/popover/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index baca3c071fb0e..1e9060b9a4235 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -195,7 +195,7 @@ const UnforwardedPopover = ( } = props; if ( range ) { - deprecated( 'range prop in Popover component', { + deprecated( '`range` prop in wp.components.Popover', { since: '6.1', version: '6.3', } ); @@ -204,7 +204,7 @@ const UnforwardedPopover = ( let computedFlipProp = flip; let computedResizeProp = resize; if ( __unstableForcePosition !== undefined ) { - deprecated( '__unstableForcePosition prop in Popover component', { + deprecated( '`__unstableForcePosition` prop wp.components.Popover', { since: '6.1', version: '6.3', alternative: '`flip={ false }` and `resize={ false }`', @@ -218,7 +218,7 @@ const UnforwardedPopover = ( let shouldShift = shift; if ( __unstableShift !== undefined ) { - deprecated( '`__unstableShift` prop in Popover component', { + deprecated( '`__unstableShift` prop in wp.components.Popover', { since: '6.1', version: '6.3', alternative: '`shift` prop`', @@ -229,7 +229,7 @@ const UnforwardedPopover = ( } if ( anchorRef !== undefined ) { - deprecated( '`anchorRef` prop in Popover component', { + deprecated( '`anchorRef` prop in wp.components.Popover', { since: '6.1', version: '6.3', alternative: '`anchor` prop', @@ -237,7 +237,7 @@ const UnforwardedPopover = ( } if ( anchorRect !== undefined ) { - deprecated( '`anchorRect` prop in Popover component', { + deprecated( '`anchorRect` prop in wp.components.Popover', { since: '6.1', version: '6.3', alternative: '`anchor` prop', @@ -245,7 +245,7 @@ const UnforwardedPopover = ( } if ( getAnchorRect !== undefined ) { - deprecated( '`getAnchorRect` prop in Popover component', { + deprecated( '`getAnchorRect` prop in wp.components.Popover', { since: '6.1', version: '6.3', alternative: '`anchor` prop', From caacb0e48c7e6727c1b7431df3e94556464ca2b6 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 14 Sep 2022 09:22:18 +0200 Subject: [PATCH 36/36] Put more emphasis on storing anchor in local state --- packages/components/src/popover/README.md | 2 +- packages/components/src/popover/types.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index 47e12157f9c41..85ae701894616 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -29,7 +29,7 @@ const MyPopover = () => { }; ``` -In order to pass an explicit anchor, you can use the `anchor` prop. When doing so, the anchor element should be stored in state rather than a plain ref to ensure reactive updating when it changes. +In order to pass an explicit anchor, you can use the `anchor` prop. When doing so, **the anchor element should be stored in local state** rather than a plain React ref to ensure reactive updating when it changes. ```jsx import { Button, Popover } from '@wordpress/components'; diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index efd7c8a16107e..b80e266c1fe2c 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -38,6 +38,9 @@ export type PopoverProps = { * The element that should be used by the popover as its anchor. It can either * be an `Element` or, alternatively, a `VirtualElement` — ie. an object with * the `getBoundingClientRect()` and the `ownerDocument` properties defined. + * + * **The anchor element should be stored in local state** rather than a + * plain React ref to ensure reactive updating when it changes. */ anchor?: Element | VirtualElement | null; /**