UNPKG

@coin-voyage/paykit

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

109 lines (108 loc) 4.96 kB
import { useCallback, useEffect, useRef, useState } from "react"; import { useTransition } from "react-transition-state"; import useLockBodyScroll from "../../../hooks/useLockBodyScroll"; /** * Manage modal mount/unmount transitions and provide measured content dimensions. * * - Controls mount state via react-transition-state (timeout: 160ms). * - Locks body scroll when the modal is mounted and rendered outside the trigger. * - Measures content size (width/height) and exposes a CSS-friendly object for use by the modal. * - `resizeDependency` may be changed by callers (e.g. triggerResize()) to force a re-measure. * * The hook attempts to avoid transient zero-size measurements and keeps a short * "inTransition" window after content is attached so layout can settle before consumers * react to the new dimensions. */ export function useModalTransition({ open, positionInside, onClose, resizeDependency = 0, }) { const [state, setOpen] = useTransition({ timeout: 160, preEnter: true, mountOnEnter: true, unmountOnExit: true }); const mounted = !(state === "exited" || state === "unmounted"); const rendered = state === "preEnter" || state !== "exiting"; useLockBodyScroll(!positionInside && mounted); const [dimensions, setDimensions] = useState({}); const [inTransition, setInTransition] = useState(false); const ref = useRef(null); const inTransitionTimeoutRef = useRef(null); const resizeObserverRef = useRef(null); const updateBounds = useCallback((node) => { if (!node) return; const w = node.offsetWidth; const h = node.offsetHeight; // Avoid applying zero dimensions during page transitions where the element hasn't been // laid out yet. ResizeObserver or a subsequent contentRef attachment will re-measure // once the node reports a real size. if (w > 0 && h > 0) { setDimensions({ width: `${w}px`, height: `${h}px` }); } }, []); // Re-measure when callers update `resizeDependency` (used by pages to force a re-measure // after async content loads or other layout changes). useEffect(() => { if (resizeDependency > 0 && ref.current) { const node = ref.current; const id = requestAnimationFrame(() => { updateBounds(node); }); return () => cancelAnimationFrame(id); } }, [resizeDependency, updateBounds]); // Observe the content node so the modal can resize when the content changes // (for example: placeholder/loading → full content). useEffect(() => { const node = ref.current; if (!node || typeof ResizeObserver === "undefined") return; resizeObserverRef.current?.disconnect(); const ro = new ResizeObserver(() => { updateBounds(node); }); resizeObserverRef.current = ro; ro.observe(node); return () => { ro.disconnect(); resizeObserverRef.current = null; }; }, [updateBounds, resizeDependency]); const contentRef = useCallback((node) => { ref.current = node; if (!node) return; // Mark content as "inTransition" to suppress transient layout reactions while new // content is being attached. The timeout is intentionally a bit longer than the // visual transition window so consumers have time to measure and apply stable layout. setInTransition(true); if (inTransitionTimeoutRef.current) { clearTimeout(inTransitionTimeoutRef.current); } inTransitionTimeoutRef.current = window.setTimeout(() => { setInTransition(false); inTransitionTimeoutRef.current = null; }, 360); // Immediately measure when a new content node is attached. updateBounds(node); // If the ResizeObserver effect ran before the ref was attached, create it here so // we still observe subsequent content size changes. if (typeof ResizeObserver !== "undefined") { resizeObserverRef.current?.disconnect(); const ro = new ResizeObserver(() => { updateBounds(node); }); resizeObserverRef.current = ro; ro.observe(node); } }, [updateBounds]); useEffect(() => { setOpen(open); }, [open, setOpen]); useEffect(() => { if (!mounted) { requestAnimationFrame(() => setDimensions({})); return; } const listener = (e) => e.key === "Escape" && onClose?.(); document.addEventListener("keydown", listener); return () => document.removeEventListener("keydown", listener); }, [mounted, onClose]); const dimensionsCSS = { "--height": dimensions.height, "--width": dimensions.width }; return { state, mounted, rendered, inTransition, contentRef, dimensionsCSS }; }