@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
JavaScript
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 };
}