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/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( { { + if ( + ! selectedElement || + ( bottomClientId && ! lastSelectedElement ) + ) { + return undefined; + } + + return { + 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 ?? Infinity + ); + const top = Math.min( + selectedBCR.top, + lastSelectedBCR?.top ?? Infinity + ); + const right = Math.max( + selectedBCR.right, + lastSelectedBCR.right ?? -Infinity + ); + const bottom = Math.max( + selectedBCR.bottom, + lastSelectedBCR.bottom ?? -Infinity + ); + const width = right - left; + const height = bottom - top; + + return new window.DOMRect( left, top, width, height ); + }, + ownerDocument: selectedElement.ownerDocument, + }; + }, [ bottomClientId, lastSelectedElement, selectedElement ] ); + if ( ! selectedElement || ( bottomClientId && ! lastSelectedElement ) ) { return null; } - const anchorRef = { - top: selectedElement, - bottom: lastSelectedElement, - }; - return ( { - 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 ( 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..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'; @@ -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, + editableContentElement, + 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 = useAnchor( { + editableContentElement, value, settings, } ); - return ; + return ; } -function InlineToolbar( { anchorRef } ) { +function InlineToolbar( { popoverAnchor } ) { return ( @@ -57,14 +61,18 @@ function InlineToolbar( { anchorRef } ) { ); } -const FormatToolbarContainer = ( { inline, anchorRef, value } ) => { +const FormatToolbarContainer = ( { + inline, + editableContentElement, + value, +} ) => { const hasInlineToolbar = useSelect( ( select ) => select( blockEditorStore ).getSettings().hasInlineToolbar, [] ); if ( inline ) { - return ; + return ; } if ( hasInlineToolbar ) { @@ -76,7 +84,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..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/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..99a0d27bb91fd 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 @@ -51,7 +51,9 @@ const ImageURLInputUI = ( { rel, } ) => { const [ isOpen, setIsOpen ] = useState( false ); - const buttonRef = useRef( null ); + // Use internal state instead of a ref to make sure that the component + // re-renders when the popover's anchor updates. + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); const openLinkUI = useCallback( () => { setIsOpen( true ); } ); @@ -246,11 +248,11 @@ const ImageURLInputUI = ( { label={ url ? __( 'Edit link' ) : __( 'Insert link' ) } aria-expanded={ isOpen } onClick={ openLinkUI } - ref={ buttonRef } + ref={ setPopoverAnchor } /> { isOpen && ( advancedOptions } diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 97775bc0bcd10..55b095f9e848b 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -30,6 +30,7 @@ import { import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; import { createBlock } from '@wordpress/blocks'; +import { useMergeRefs } from '@wordpress/compose'; const NEW_TAB_REL = 'noreferrer noopener'; @@ -114,12 +115,19 @@ function ButtonEdit( props ) { } } + // Use internal state instead of a ref to make sure that the component + // re-renders when the popover's anchor updates. + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + const borderProps = useBorderProps( attributes ); const colorProps = useColorProps( attributes ); const spacingProps = useSpacingProps( attributes ); const ref = useRef(); const richTextRef = useRef(); - const blockProps = useBlockProps( { ref, onKeyDown } ); + const blockProps = useBlockProps( { + ref: useMergeRefs( [ setPopoverAnchor, ref ] ), + onKeyDown, + } ); const [ isEditingURL, setIsEditingURL ] = useState( false ); const isURLSet = !! url; @@ -218,7 +226,7 @@ function ButtonEdit( props ) { setIsEditingURL( false ); richTextRef.current?.focus(); } } - anchorRef={ ref?.current } + anchor={ popoverAnchor } 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 c208d83c1a45e..466f032a9e86e 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -47,6 +47,7 @@ import { useResourcePermissions, } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -461,6 +462,9 @@ export default function NavigationLinkEdit( { const { replaceBlock, __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); const [ isLinkOpen, setIsLinkOpen ] = useState( false ); + // Use internal state instead of a ref to make sure that the component + // re-renders when the popover's anchor updates. + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); const listItemRef = useRef( null ); const isDraggingWithin = useIsDraggingWithin( listItemRef ); const itemLabelPlaceholder = __( 'Add link…' ); @@ -651,7 +655,7 @@ export default function NavigationLinkEdit( { } const blockProps = useBlockProps( { - ref: listItemRef, + ref: useMergeRefs( [ setPopoverAnchor, listItemRef ] ), className: classnames( 'wp-block-navigation-item', { 'is-editing': isSelected || isParentOfSelectedBlock, 'is-dragging-within': isDraggingWithin, @@ -844,7 +848,7 @@ export default function NavigationLinkEdit( { setIsLinkOpen( false ) } - anchorRef={ listItemRef.current } + anchor={ popoverAnchor } shift > setIsLinkOpen( false ) } - anchorRef={ listItemRef.current } + anchor={ popoverAnchor } shift > ( { anchor: popoverAnchor } ), + [ popoverAnchor ] + ); + const isDescendentOfQueryLoop = Number.isFinite( queryId ); const dateSettings = getDateSettings(); const [ siteFormat = dateSettings.formats.date ] = useEntityProp( @@ -68,7 +77,7 @@ export default function PostDateEdit( { ); let postDate = date ? ( -