<commit_before>import { RefObject, useLayoutEffect, useState } from 'react';
import { debounce } from '@proton/shared/lib/helpers/function';

// Can't loop over DOMRect keys with getOwnPropertyNames.
const keys = ['bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'];
const isEquivalent = (aRect?: DOMRect, bRect?: DOMRect) => {
    if (!aRect && bRect) {
        return false;
    }
    if (aRect && !bRect) {
        return false;
    }
    for (const key of keys) {
        if (aRect?.[key as keyof DOMRect] !== bRect?.[key as keyof DOMRect]) {
            return false;
        }
    }
    return true;
};

const getElementRect = (target?: HTMLElement | null) => {
    if (!target) {
        return;
    }
    return target.getBoundingClientRect();
};

type RateLimiter = <A extends any[]>(func: (...args: A) => void, wait: number) => (...args: A) => void;

const useElementRect = <E extends HTMLElement>(ref: RefObject<E>, rateLimiter: RateLimiter = debounce) => {
    const [elementRect, setElementRect] = useState(() => getElementRect(ref.current));

    useLayoutEffect(() => {
        const target = ref.current;
        if (!target) {
            return;
        }
        const reducer = (old?: DOMRect, newRect?: DOMRect) => (isEquivalent(old, newRect) ? old : newRect);

        if (typeof ResizeObserver === 'function') {
            const resizeObserver = new ResizeObserver(
                // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
                rateLimiter(([{ contentRect }]) => {
                    // The contentRect does not give correct global positions?
                    // setElementRect((old) => reducer(old, contentRect));
                    setElementRect((old) => reducer(old, getElementRect(target)));
                }, 100)
            );
            resizeObserver.observe(target, { box: 'border-box' });
            setElementRect((old) => reducer(old, getElementRect(target)));
            return () => {
                resizeObserver.disconnect();
            };
        }

        const onResize = rateLimiter(() => {
            setElementRect((old) => reducer(old, getElementRect(target)));
        }, 100);
        window.addEventListener('resize', onResize);
        setElementRect((old) => reducer(old, getElementRect(target)));
        return () => {
            window.removeEventListener('resize', onResize);
        };
    }, [ref.current]);

    return elementRect;
};<commit_msg>Settle the element rect computation<commit_after>import { RefObject, useLayoutEffect, useState } from 'react';
import { debounce } from '@proton/shared/lib/helpers/function';
import useInstance from './useInstance';

// Can't loop over DOMRect keys with getOwnPropertyNames.
const keys = ['bottom', 'height', 'left', 'right', 'top', 'width', 'x', 'y'];
const isEquivalent = (aRect?: DOMRect, bRect?: DOMRect) => {
    if (!aRect && bRect) {
        return false;
    }
    if (aRect && !bRect) {
        return false;
    }
    for (const key of keys) {
        if (aRect?.[key as keyof DOMRect] !== bRect?.[key as keyof DOMRect]) {
            return false;
        }
    }
    return true;
};

const getElementRect = (target?: HTMLElement | null) => {
    if (!target) {
        return;
    }
    return target.getBoundingClientRect();
};

type RateLimiter = <A extends any[]>(func: (...args: A) => void, wait: number) => (...args: A) => void;

const equivalentReducer = (oldRect?: DOMRect, newRect?: DOMRect) => {
    return isEquivalent(oldRect, newRect) ? oldRect : newRect;
}
const defaultReducer = (oldRect: DOMRect | undefined, newRect: DOMRect | undefined, sizeCache: (DOMRect | undefined)[]) => {
    const [prevOld, prevNew] = sizeCache;
    // If it's flipping back and forth, settle on the previous value to prevent jiggling effects
    if (isEquivalent(prevOld, newRect) && isEquivalent(prevNew, oldRect)) {
        return oldRect;
    }
    sizeCache[0] = oldRect;
    sizeCache[1] = newRect;
    return equivalentReducer(oldRect, newRect);
}
type Reducer = typeof defaultReducer;

const useElementRect = <E extends HTMLElement>(ref: RefObject<E>, rateLimiter: RateLimiter = debounce, reducer: Reducer = defaultReducer) => {
    const sizeCache = useInstance((): (DOMRect | undefined)[] => [])
    const [elementRect, setElementRect] = useState(() => getElementRect(ref.current));

    useLayoutEffect(() => {
        const target = ref.current;
        if (!target) {
            return;
        }

        if (typeof ResizeObserver === 'function') {
            const resizeObserver = new ResizeObserver(
                // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
                rateLimiter(([{ contentRect }]) => {
                    // The contentRect does not give correct global positions?
                    // setElementRect((old) => reducer(old, contentRect));
                    setElementRect((old) => reducer(old, getElementRect(target), sizeCache));
                }, 100)
            );
            resizeObserver.observe(target, { box: 'border-box' });
            setElementRect((old) => reducer(old, getElementRect(target), sizeCache));
            return () => {
                resizeObserver.disconnect();
            };
        }

        const onResize = rateLimiter(() => {
            setElementRect((old) => reducer(old, getElementRect(target), sizeCache));
        }, 100);
        window.addEventListener('resize', onResize);
        setElementRect((old) => reducer(old, getElementRect(target), sizeCache));
        return () => {
            window.removeEventListener('resize', onResize);
        };
    }, [ref.current]);

    return elementRect;
};
{'fraction_non_alphanumeric': 0.14663167104111985, 'fraction_numerical': 0.0024496937882764656, 'mean_word_length': 2.5836990595611287, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5541272', 'n_tokens_mistral': 1664, 'n_tokens_neox': 1632, 'n_words': 407}