import { MutableRefObject, useEffect } from "react"; import ResizeObserverPolyfill from "resize-observer-polyfill"; /** * A function that will return the resize observer target element. This should * return an HTMLElement or null. */ type GetTarget = () => E | null; type RefTarget< E extends HTMLElement = HTMLElement > = MutableRefObject; /** * The target element for the resize obsever. This can be one of: * * - null * - HTMLElement * - a document.querySelector string * - a ref with { current: null | HTMLElement } * - a function that returns * - null * - HTMLElement * * Whenever the target is resolved as `null`, the observer will be disabled. */ export type ResizeObserverTarget = | null | HTMLElement | string | RefTarget | GetTarget; /** * @private */ const isRefTarget = ( target: ResizeObserverTarget ): target is MutableRefObject => !!target && typeof (target as MutableRefObject).current !== "undefined"; /** * @private */ const isFunctionTarget = (target: ResizeObserverTarget): target is GetTarget => typeof target === "function"; /** * A utility function to get the current resize observer element. * * @private */ export function getResizeObserverTarget( target: ResizeObserverTarget ): HTMLElement | null { if (isRefTarget(target)) { return target.current; } if (isFunctionTarget(target)) { return target(); } if (typeof target === "string") { return document.querySelector(target); } return target; } /** * * @private */ export function isHeightChange( prevSize: ElementSize | undefined, nextSize: ElementSize ): boolean { return ( !prevSize || prevSize.height !== nextSize.height || prevSize.scrollHeight !== nextSize.scrollHeight ); } /** * * @private */ export function isWidthChange( prevSize: ElementSize | undefined, nextSize: ElementSize ): boolean { return ( !prevSize || prevSize.width !== nextSize.width || prevSize.scrollWidth !== nextSize.scrollWidth ); } interface ElementSize { /** * The height for the element that was changed. */ height: number; /** * The width for the element that was changed. */ width: number; /** * The scroll height for the element that was changed. */ scrollHeight: number; /** * The scroll height for the element that was changed. */ scrollWidth: number; } /** * The data that is provided whenever an observed element changes size. */ export interface ObservedResizeData extends ElementSize { /** * The element that was changed due to an observered resize event. */ element: HTMLElement; } /** * A type that can be used to strongly type a callback function for a resize * observe onResize function. It's really just a wrapper for the main * `ObserverableResizeEvent` */ export type ObservedResizeEventHandler = (event: ObservedResizeData) => void; export interface ResizeObserverOptions { target: ResizeObserverTarget; onResize: ObservedResizeEventHandler; disableHeight?: boolean; disableWidth?: boolean; } /** * A hook that is used to trigger esize events when a target element is resized * via CSS or other changes. * * @param options The resize observer options. */ export default function useResizeObserver({ disableHeight = false, disableWidth = false, onResize, target, }: ResizeObserverOptions): void { useEffect(() => { if (disableHeight && disableWidth) { return; } const resizeTarget = getResizeObserverTarget(target); if (!resizeTarget) { return; } let prevSize: ElementSize | undefined; const observer = new ResizeObserverPolyfill((entries) => { for (let i = 0; i < entries.length; i += 1) { const entry = entries[i]; const target = entry.target as HTMLElement; const { height, width } = entry.contentRect; const { scrollHeight, scrollWidth } = target; const nextSize: ElementSize = { height, width, scrollHeight, scrollWidth, }; const isNewHeight = isHeightChange(prevSize, nextSize); const isNewWidth = isWidthChange(prevSize, nextSize); prevSize = nextSize; if ((isNewHeight && !disableHeight) || (isNewWidth && !disableWidth)) { onResize({ ...nextSize, element: target, }); } } }); observer.observe(resizeTarget); return () => { observer.disconnect(); }; }, [target, onResize, disableHeight, disableWidth]); }