diff --git a/package-lock.json b/package-lock.json
index acd5b5df134116..387c46b529e7d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17282,7 +17282,6 @@
"re-resizable": "^6.4.0",
"react-colorful": "^5.3.1",
"react-dates": "^21.8.0",
- "react-resize-aware": "^3.1.0",
"reakit": "^1.3.8",
"uuid": "^8.3.0"
},
@@ -17369,7 +17368,6 @@
"clipboard": "^2.0.8",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
- "react-resize-aware": "^3.1.0",
"use-memo-one": "^1.1.1"
}
},
@@ -51126,11 +51124,6 @@
"integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==",
"dev": true
},
- "react-resize-aware": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/react-resize-aware/-/react-resize-aware-3.1.0.tgz",
- "integrity": "sha512-bIhHlxVTX7xKUz14ksXMEHjzCZPTpQZKZISY3nbTD273pDKPABGFNFBP6Tr42KECxzC5YQiKpMchjTVJCqaxpA=="
- },
"react-router": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.1.1.tgz",
@@ -52494,6 +52487,12 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
+ "resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+ "dev": true
+ },
"resolve": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
diff --git a/package.json b/package.json
index 487cfdc6d11b59..19e19e5f303bab 100755
--- a/package.json
+++ b/package.json
@@ -211,6 +211,7 @@
"react-refresh": "0.10.0",
"react-test-renderer": "17.0.2",
"redux": "4.1.2",
+ "resize-observer-polyfill": "1.5.1",
"rimraf": "3.0.2",
"rtlcss": "2.6.2",
"sass": "1.35.2",
diff --git a/packages/components/package.json b/packages/components/package.json
index d587d1889e085b..15ea54baa7d6b2 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -65,7 +65,6 @@
"re-resizable": "^6.4.0",
"react-colorful": "^5.3.1",
"react-dates": "^21.8.0",
- "react-resize-aware": "^3.1.0",
"reakit": "^1.3.8",
"uuid": "^8.3.0"
},
diff --git a/packages/components/src/flyout/test/__snapshots__/index.js.snap b/packages/components/src/flyout/test/__snapshots__/index.js.snap
index 66893021274f94..60c6d3fe10500f 100644
--- a/packages/components/src/flyout/test/__snapshots__/index.js.snap
+++ b/packages/components/src/flyout/test/__snapshots__/index.js.snap
@@ -93,12 +93,9 @@ exports[`props should render correctly 1`] = `
-
+
+
any } } onResize
*/
export function useFlyoutResizeUpdater( { onResize } ) {
- const [ resizeListener, sizes ] = useResizeAware();
+ const [ resizeListener, sizes ] = useResizeObserver();
useIsomorphicLayoutEffect( () => {
onResize?.();
diff --git a/packages/components/src/placeholder/test/index.js b/packages/components/src/placeholder/test/index.js
index e2d787bc51b40c..e2a6090764cb2b 100644
--- a/packages/components/src/placeholder/test/index.js
+++ b/packages/components/src/placeholder/test/index.js
@@ -14,6 +14,13 @@ import { useResizeObserver } from '@wordpress/compose';
*/
import Placeholder from '../';
+jest.mock( '@wordpress/compose', () => {
+ return {
+ ...jest.requireActual( '@wordpress/compose' ),
+ useResizeObserver: jest.fn( () => [] ),
+ };
+} );
+
describe( 'Placeholder', () => {
beforeEach( () => {
useResizeObserver.mockReturnValue( [
diff --git a/packages/components/src/popover/test/__snapshots__/index.js.snap b/packages/components/src/popover/test/__snapshots__/index.js.snap
index e59d209c85b129..2c7663281a154e 100644
--- a/packages/components/src/popover/test/__snapshots__/index.js.snap
+++ b/packages/components/src/popover/test/__snapshots__/index.js.snap
@@ -16,6 +16,10 @@ exports[`Popover should pass additional props to portaled element 1`] = `
@@ -38,6 +42,10 @@ exports[`Popover should render content 1`] = `
diff --git a/packages/components/src/resizable-box/resize-tooltip/utils.ts b/packages/components/src/resizable-box/resize-tooltip/utils.ts
index b4ec4b19f19fcb..2e75f1e831e13d 100644
--- a/packages/components/src/resizable-box/resize-tooltip/utils.ts
+++ b/packages/components/src/resizable-box/resize-tooltip/utils.ts
@@ -2,12 +2,12 @@
* External dependencies
*/
import { noop } from 'lodash';
-import useResizeAware from 'react-resize-aware';
/**
* WordPress dependencies
*/
import { useEffect, useRef, useState } from '@wordpress/element';
+import { useResizeObserver } from '@wordpress/compose';
const { clearTimeout, setTimeout } = window;
@@ -56,11 +56,10 @@ export function useResizeLabel( {
showPx = false,
}: UseResizeLabelArgs ): UseResizeLabelProps {
/*
- * The width/height values derive from this special useResizeAware hook.
- * This custom hook uses injects an iFrame into the element, allowing it
- * to tap into the onResize (window) callback events.
+ * The width/height values derive from this special useResizeObserver hook.
+ * This custom hook uses the ResizeObserver API to listen for resize events.
*/
- const [ resizeListener, sizes ] = useResizeAware();
+ const [ resizeListener, sizes ] = useResizeObserver();
/*
* Indicates if the x/y axis is preferred.
diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.js.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.js.snap
index 0eac732eed09b9..25b38d19434425 100644
--- a/packages/components/src/toggle-group-control/test/__snapshots__/index.js.snap
+++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.js.snap
@@ -173,12 +173,9 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = `
id="toggle-group-control-1"
role="radiogroup"
>
-
-
-for more details.
-
-_Related_
-
--
+\_Note: `useResizeObserver` will report `null` until after first render.
_Usage_
diff --git a/packages/compose/package.json b/packages/compose/package.json
index 0bc48ac1f3f4c5..724d7f68fc2b10 100644
--- a/packages/compose/package.json
+++ b/packages/compose/package.json
@@ -41,7 +41,6 @@
"clipboard": "^2.0.8",
"lodash": "^4.17.21",
"mousetrap": "^1.6.5",
- "react-resize-aware": "^3.1.0",
"use-memo-one": "^1.1.1"
},
"peerDependencies": {
diff --git a/packages/compose/src/hooks/use-resize-observer/index.js b/packages/compose/src/hooks/use-resize-observer/index.js
deleted file mode 100644
index e992b57fa6b834..00000000000000
--- a/packages/compose/src/hooks/use-resize-observer/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * External dependencies
- */
-import useResizeAware from 'react-resize-aware';
-
-/**
- * Hook which allows to listen the resize event of any target element when it changes sizes.
- * _Note: `useResizeObserver` will report `null` until after first render_
- *
- * Simply a re-export of `react-resize-aware` so refer to its documentation
- * for more details.
- *
- * @see https://github.com/FezVrasta/react-resize-aware
- *
- * @example
- *
- * ```js
- * const App = () => {
- * const [ resizeListener, sizes ] = useResizeObserver();
- *
- * return (
- *
- * { resizeListener }
- * Your content here
- *
- * );
- * };
- * ```
- *
- */
-export default useResizeAware;
diff --git a/packages/compose/src/hooks/use-resize-observer/index.native.js b/packages/compose/src/hooks/use-resize-observer/index.native.js
index 981412b4f86e96..101213cf90ceaf 100644
--- a/packages/compose/src/hooks/use-resize-observer/index.native.js
+++ b/packages/compose/src/hooks/use-resize-observer/index.native.js
@@ -10,8 +10,6 @@ import { useState, useCallback } from '@wordpress/element';
/**
* Hook which allows to listen the resize event of any target element when it changes sizes.
*
- * @return {[JSX.Element, { width: number, height: number } | null]} An array of {Element} `resizeListener` and {?Object} `sizes` with properties `width` and `height`
- *
* @example
*
* ```js
diff --git a/packages/compose/src/hooks/use-resize-observer/index.tsx b/packages/compose/src/hooks/use-resize-observer/index.tsx
new file mode 100644
index 00000000000000..2e4b60e63a36b5
--- /dev/null
+++ b/packages/compose/src/hooks/use-resize-observer/index.tsx
@@ -0,0 +1,362 @@
+/**
+ * External dependencies
+ */
+import type { RefCallback, RefObject } from 'react';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ useMemo,
+ useRef,
+ useCallback,
+ useEffect,
+ useState,
+} from '@wordpress/element';
+import type { WPElement } from '@wordpress/element';
+
+type SubscriberCleanup = () => void;
+type SubscriberResponse = SubscriberCleanup | void;
+
+// This of course could've been more streamlined with internal state instead of
+// refs, but then host hooks / components could not opt out of renders.
+// This could've been exported to its own module, but the current build doesn't
+// seem to work with module imports and I had no more time to spend on this...
+function useResolvedElement< T extends HTMLElement >(
+ subscriber: ( element: T ) => SubscriberResponse,
+ refOrElement?: T | RefObject< T > | null
+): RefCallback< T > {
+ const callbackRefElement = useRef< T | null >( null );
+ const lastReportRef = useRef< {
+ reporter: () => void;
+ element: T | null;
+ } | null >( null );
+ const cleanupRef = useRef< SubscriberResponse | null >();
+
+ const callSubscriber = useCallback( () => {
+ let element = null;
+ if ( callbackRefElement.current ) {
+ element = callbackRefElement.current;
+ } else if ( refOrElement ) {
+ if ( refOrElement instanceof HTMLElement ) {
+ element = refOrElement;
+ } else {
+ element = refOrElement.current;
+ }
+ }
+
+ if (
+ lastReportRef.current &&
+ lastReportRef.current.element === element &&
+ lastReportRef.current.reporter === callSubscriber
+ ) {
+ return;
+ }
+
+ if ( cleanupRef.current ) {
+ cleanupRef.current();
+ // Making sure the cleanup is not called accidentally multiple times.
+ cleanupRef.current = null;
+ }
+ lastReportRef.current = {
+ reporter: callSubscriber,
+ element,
+ };
+
+ // Only calling the subscriber, if there's an actual element to report.
+ if ( element ) {
+ cleanupRef.current = subscriber( element );
+ }
+ }, [ refOrElement, subscriber ] );
+
+ // On each render, we check whether a ref changed, or if we got a new raw
+ // element.
+ useEffect( () => {
+ // With this we're *technically* supporting cases where ref objects' current value changes, but only if there's a
+ // render accompanying that change as well.
+ // To guarantee we always have the right element, one must use the ref callback provided instead, but we support
+ // RefObjects to make the hook API more convenient in certain cases.
+ callSubscriber();
+ }, [ callSubscriber ] );
+
+ return useCallback< RefCallback< T > >(
+ ( element ) => {
+ callbackRefElement.current = element;
+ callSubscriber();
+ },
+ [ callSubscriber ]
+ );
+}
+
+type ObservedSize = {
+ width: number | undefined;
+ height: number | undefined;
+};
+
+type ResizeHandler = ( size: ObservedSize ) => void;
+
+type HookResponse< T extends HTMLElement > = {
+ ref: RefCallback< T >;
+} & ObservedSize;
+
+// Declaring my own type here instead of using the one provided by TS (available since 4.2.2), because this way I'm not
+// forcing consumers to use a specific TS version.
+type ResizeObserverBoxOptions =
+ | 'border-box'
+ | 'content-box'
+ | 'device-pixel-content-box';
+
+declare global {
+ interface ResizeObserverEntry {
+ readonly devicePixelContentBoxSize: ReadonlyArray< ResizeObserverSize >;
+ }
+}
+
+// We're only using the first element of the size sequences, until future versions of the spec solidify on how
+// exactly it'll be used for fragments in multi-column scenarios:
+// From the spec:
+// > The box size properties are exposed as FrozenArray in order to support elements that have multiple fragments,
+// > which occur in multi-column scenarios. However the current definitions of content rect and border box do not
+// > mention how those boxes are affected by multi-column layout. In this spec, there will only be a single
+// > ResizeObserverSize returned in the FrozenArray, which will correspond to the dimensions of the first column.
+// > A future version of this spec will extend the returned FrozenArray to contain the per-fragment size information.
+// (https://drafts.csswg.org/resize-observer/#resize-observer-entry-interface)
+//
+// Also, testing these new box options revealed that in both Chrome and FF everything is returned in the callback,
+// regardless of the "box" option.
+// The spec states the following on this:
+// > This does not have any impact on which box dimensions are returned to the defined callback when the event
+// > is fired, it solely defines which box the author wishes to observe layout changes on.
+// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
+// I'm not exactly clear on what this means, especially when you consider a later section stating the following:
+// > This section is non-normative. An author may desire to observe more than one CSS box.
+// > In this case, author will need to use multiple ResizeObservers.
+// (https://drafts.csswg.org/resize-observer/#resize-observer-interface)
+// Which is clearly not how current browser implementations behave, and seems to contradict the previous quote.
+// For this reason I decided to only return the requested size,
+// even though it seems we have access to results for all box types.
+// This also means that we get to keep the current api, being able to return a simple { width, height } pair,
+// regardless of box option.
+const extractSize = (
+ entry: ResizeObserverEntry,
+ boxProp: 'borderBoxSize' | 'contentBoxSize' | 'devicePixelContentBoxSize',
+ sizeType: keyof ResizeObserverSize
+): number | undefined => {
+ if ( ! entry[ boxProp ] ) {
+ if ( boxProp === 'contentBoxSize' ) {
+ // The dimensions in `contentBoxSize` and `contentRect` are equivalent according to the spec.
+ // See the 6th step in the description for the RO algorithm:
+ // https://drafts.csswg.org/resize-observer/#create-and-populate-resizeobserverentry-h
+ // > Set this.contentRect to logical this.contentBoxSize given target and observedBox of "content-box".
+ // In real browser implementations of course these objects differ, but the width/height values should be equivalent.
+ return entry.contentRect[
+ sizeType === 'inlineSize' ? 'width' : 'height'
+ ];
+ }
+
+ return undefined;
+ }
+
+ // A couple bytes smaller than calling Array.isArray() and just as effective here.
+ return entry[ boxProp ][ 0 ]
+ ? entry[ boxProp ][ 0 ][ sizeType ]
+ : // TS complains about this, because the RO entry type follows the spec and does not reflect Firefox's current
+ // behaviour of returning objects instead of arrays for `borderBoxSize` and `contentBoxSize`.
+ // @ts-ignore
+ entry[ boxProp ][ sizeType ];
+};
+
+type RoundingFunction = ( n: number ) => number;
+
+function useResizeObserver< T extends HTMLElement >(
+ opts: {
+ ref?: RefObject< T > | T | null | undefined;
+ onResize?: ResizeHandler;
+ box?: ResizeObserverBoxOptions;
+ round?: RoundingFunction;
+ } = {}
+): HookResponse< T > {
+ // Saving the callback as a ref. With this, I don't need to put onResize in the
+ // effect dep array, and just passing in an anonymous function without memoising
+ // will not reinstantiate the hook's ResizeObserver.
+ const onResize = opts.onResize;
+ const onResizeRef = useRef< ResizeHandler | undefined >( undefined );
+ onResizeRef.current = onResize;
+ const round = opts.round || Math.round;
+
+ // Using a single instance throughout the hook's lifetime
+ const resizeObserverRef = useRef< {
+ box?: ResizeObserverBoxOptions;
+ round?: RoundingFunction;
+ instance: ResizeObserver;
+ } >();
+
+ const [ size, setSize ] = useState< {
+ width?: number;
+ height?: number;
+ } >( {
+ width: undefined,
+ height: undefined,
+ } );
+
+ // In certain edge cases the RO might want to report a size change just after
+ // the component unmounted.
+ const didUnmount = useRef( false );
+ useEffect( () => {
+ return () => {
+ didUnmount.current = true;
+ };
+ }, [] );
+
+ // Using a ref to track the previous width / height to avoid unnecessary renders.
+ const previous: {
+ current: {
+ width?: number;
+ height?: number;
+ };
+ } = useRef( {
+ width: undefined,
+ height: undefined,
+ } );
+
+ // This block is kinda like a useEffect, only it's called whenever a new
+ // element could be resolved based on the ref option. It also has a cleanup
+ // function.
+ const refCallback = useResolvedElement< T >(
+ useCallback(
+ ( element ) => {
+ // We only use a single Resize Observer instance, and we're instantiating it on demand, only once there's something to observe.
+ // This instance is also recreated when the `box` option changes, so that a new observation is fired if there was a previously observed element with a different box option.
+ if (
+ ! resizeObserverRef.current ||
+ resizeObserverRef.current.box !== opts.box ||
+ resizeObserverRef.current.round !== round
+ ) {
+ resizeObserverRef.current = {
+ box: opts.box,
+ round,
+ instance: new ResizeObserver( ( entries ) => {
+ const entry = entries[ 0 ];
+
+ let boxProp:
+ | 'borderBoxSize'
+ | 'contentBoxSize'
+ | 'devicePixelContentBoxSize' = 'borderBoxSize';
+ if ( opts.box === 'border-box' ) {
+ boxProp = 'borderBoxSize';
+ } else {
+ boxProp =
+ opts.box === 'device-pixel-content-box'
+ ? 'devicePixelContentBoxSize'
+ : 'contentBoxSize';
+ }
+
+ const reportedWidth = extractSize(
+ entry,
+ boxProp,
+ 'inlineSize'
+ );
+ const reportedHeight = extractSize(
+ entry,
+ boxProp,
+ 'blockSize'
+ );
+
+ const newWidth = reportedWidth
+ ? round( reportedWidth )
+ : undefined;
+ const newHeight = reportedHeight
+ ? round( reportedHeight )
+ : undefined;
+
+ if (
+ previous.current.width !== newWidth ||
+ previous.current.height !== newHeight
+ ) {
+ const newSize = {
+ width: newWidth,
+ height: newHeight,
+ };
+ previous.current.width = newWidth;
+ previous.current.height = newHeight;
+ if ( onResizeRef.current ) {
+ onResizeRef.current( newSize );
+ } else if ( ! didUnmount.current ) {
+ setSize( newSize );
+ }
+ }
+ } ),
+ };
+ }
+
+ resizeObserverRef.current.instance.observe( element, {
+ box: opts.box,
+ } );
+
+ return () => {
+ if ( resizeObserverRef.current ) {
+ resizeObserverRef.current.instance.unobserve( element );
+ }
+ };
+ },
+ [ opts.box, round ]
+ ),
+ opts.ref
+ );
+
+ return useMemo(
+ () => ( {
+ ref: refCallback,
+ width: size.width,
+ height: size.height,
+ } ),
+ [ refCallback, size ? size.width : null, size ? size.height : null ]
+ );
+}
+
+/**
+ * Hook which allows to listen the resize event of any target element when it changes sizes.
+ * _Note: `useResizeObserver` will report `null` until after first render.
+ *
+ * @example
+ *
+ * ```js
+ * const App = () => {
+ * const [ resizeListener, sizes ] = useResizeObserver();
+ *
+ * return (
+ *
+ * { resizeListener }
+ * Your content here
+ *
+ * );
+ * };
+ * ```
+ */
+export default function useResizeAware(): [
+ WPElement,
+ { width: number | null; height: number | null }
+] {
+ const { ref, width, height } = useResizeObserver();
+ const sizes = useMemo( () => {
+ return { width: width ?? null, height: height ?? null };
+ }, [ width, height ] );
+ const resizeListener = (
+
+ );
+ return [ resizeListener, sizes ];
+}
diff --git a/test/unit/config/global-mocks.js b/test/unit/config/global-mocks.js
index 29ca4ce94d4a6a..8fac04b2151ad6 100644
--- a/test/unit/config/global-mocks.js
+++ b/test/unit/config/global-mocks.js
@@ -1,12 +1,7 @@
jest.mock( '@wordpress/compose', () => {
- const App = () => null;
return {
...jest.requireActual( '@wordpress/compose' ),
useViewportMatch: jest.fn(),
- useResizeObserver: jest.fn( () => [
- ,
- { width: 700, height: 500 },
- ] ),
};
} );
@@ -22,3 +17,5 @@ jest.mock( '@wordpress/compose', () => {
if ( ! window.wp?.galleryBlockV2Enabled ) {
window.wp = { ...window.wp, galleryBlockV2Enabled: true };
}
+
+global.ResizeObserver = require( 'resize-observer-polyfill' );