1 import type { RefObject } from 'react';
2 import { useLayoutEffect, useState } from 'react';
4 import noop from '@proton/utils/noop';
5 import throttle from '@proton/utils/throttle';
7 // Can't loop over DOMRect keys with getOwnPropertyNames.
8 const keys = ['bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'];
9 const isEquivalent = (aRect?: DOMRect, bRect?: DOMRect) => {
10 if (!aRect && bRect) {
13 if (aRect && !bRect) {
16 for (const key of keys) {
17 if (aRect?.[key as keyof DOMRect] !== bRect?.[key as keyof DOMRect]) {
24 function getElementRect(target: HTMLElement): DOMRect;
25 function getElementRect(target?: HTMLElement | null) {
29 return target.getBoundingClientRect();
32 type RateLimiter = <A extends any[]>(func: (...args: A) => void) => ((...args: A) => void) & { cancel: () => void };
35 * It might be helpful if you have `ResizeObserver loop limit exceeded` error.
36 * The error can also manifest itself as `ResizeObserver loop completed with undelivered notifications.`
38 * Simply toss it as the second argument:
42 * const elementRect = useElementRect(ref, requestAnimationFrameRateLimiter);
46 export const requestAnimationFrameRateLimiter: RateLimiter = <A extends any[]>(func: (...args: A) => void) => {
47 const cb = (...args: A) => requestAnimationFrame(() => func(...args));
52 export const equivalentReducer = (oldRect?: DOMRect, newRect?: DOMRect) => {
53 return isEquivalent(oldRect, newRect) ? oldRect : newRect;
56 export const createObserver = (
58 onResize: (rect: DOMRect) => void,
59 maybeRateLimiter?: RateLimiter | null
61 if (maybeRateLimiter === undefined) {
62 maybeRateLimiter = (cb) => throttle(cb, 16, { leading: true, trailing: true });
65 let cache = {} as DOMRect;
66 const handleResizeCallback = (rect: DOMRect) => {
67 if (isEquivalent(cache, rect)) {
73 handleResizeCallback.cancel = noop;
75 const handleResize = maybeRateLimiter ? maybeRateLimiter(handleResizeCallback) : handleResizeCallback;
77 const handleResizeObserver = () => {
78 handleResize(getElementRect(target));
80 const resizeObserver = new ResizeObserver(handleResizeObserver);
81 resizeObserver.observe(target, { box: 'border-box' });
82 handleResizeObserver();
84 handleResize?.cancel?.();
85 resizeObserver.disconnect();
89 const useElementRect = <E extends HTMLElement>(ref: RefObject<E> | null, maybeRateLimiter?: RateLimiter | null) => {
90 const [elementRect, setElementRect] = useState<DOMRect | null>(null);
92 useLayoutEffect(() => {
93 const target = ref?.current;
97 return createObserver(
100 setElementRect(rect);
104 }, [ref, ref?.current]);
109 export default useElementRect;