Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / hooks / useElementRect.ts
blob1945a59270655c42722ffe327e45e3ac3e59324a
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) {
11         return false;
12     }
13     if (aRect && !bRect) {
14         return false;
15     }
16     for (const key of keys) {
17         if (aRect?.[key as keyof DOMRect] !== bRect?.[key as keyof DOMRect]) {
18             return false;
19         }
20     }
21     return true;
24 function getElementRect(target: HTMLElement): DOMRect;
25 function getElementRect(target?: HTMLElement | null) {
26     if (!target) {
27         return;
28     }
29     return target.getBoundingClientRect();
32 type RateLimiter = <A extends any[]>(func: (...args: A) => void) => ((...args: A) => void) & { cancel: () => void };
34 /**
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.`
37  *
38  * Simply toss it as the second argument:
39  *
40  * ```typescript
41  *
42  * const elementRect = useElementRect(ref, requestAnimationFrameRateLimiter);
43  *
44  * ```
45  */
46 export const requestAnimationFrameRateLimiter: RateLimiter = <A extends any[]>(func: (...args: A) => void) => {
47     const cb = (...args: A) => requestAnimationFrame(() => func(...args));
48     cb.cancel = noop;
49     return cb;
52 export const equivalentReducer = (oldRect?: DOMRect, newRect?: DOMRect) => {
53     return isEquivalent(oldRect, newRect) ? oldRect : newRect;
56 export const createObserver = (
57     target: HTMLElement,
58     onResize: (rect: DOMRect) => void,
59     maybeRateLimiter?: RateLimiter | null
60 ) => {
61     if (maybeRateLimiter === undefined) {
62         maybeRateLimiter = (cb) => throttle(cb, 16, { leading: true, trailing: true });
63     }
65     let cache = {} as DOMRect;
66     const handleResizeCallback = (rect: DOMRect) => {
67         if (isEquivalent(cache, rect)) {
68             return;
69         }
70         cache = rect;
71         onResize(rect);
72     };
73     handleResizeCallback.cancel = noop;
75     const handleResize = maybeRateLimiter ? maybeRateLimiter(handleResizeCallback) : handleResizeCallback;
77     const handleResizeObserver = () => {
78         handleResize(getElementRect(target));
79     };
80     const resizeObserver = new ResizeObserver(handleResizeObserver);
81     resizeObserver.observe(target, { box: 'border-box' });
82     handleResizeObserver();
83     return () => {
84         handleResize?.cancel?.();
85         resizeObserver.disconnect();
86     };
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;
94         if (!target) {
95             return;
96         }
97         return createObserver(
98             target,
99             (rect: DOMRect) => {
100                 setElementRect(rect);
101             },
102             maybeRateLimiter
103         );
104     }, [ref, ref?.current]);
106     return elementRect;
109 export default useElementRect;