import classNames from "classnames"; import FocusTrap from "focus-trap-react"; import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Transition } from "@/components/utils/Transition"; import { useInternalOverlayRouter, useRouterAnchorUpdate, } from "@/hooks/useOverlayRouter"; export interface OverlayProps { id: string; children?: ReactNode; darken?: boolean; } export function OverlayDisplay(props: { children: ReactNode }) { const router = useInternalOverlayRouter("hello world :)"); const refRouter = useRef(router); // close router on first mount, we dont want persist routes for overlays useEffect(() => { const r = refRouter.current; r.close(); return () => { r.close(); }; }, []); return
{props.children}
; } export function OverlayPortal(props: { children?: ReactNode; darken?: boolean; show?: boolean; close?: () => void; durationClass?: string; zIndex?: number; }) { const [portalElement, setPortalElement] = useState(null); const [isReady, setIsReady] = useState(false); const ref = useRef(null); const close = props.close; const zIndex = props.zIndex ?? 999; useEffect(() => { const element = ref.current?.closest(".popout-location"); setPortalElement(element ?? document.body); // Ensure DOM is ready before enabling focus trap const timer = setTimeout(() => { setIsReady(true); }, 100); // Increased delay to ensure DOM is fully rendered return () => clearTimeout(timer); }, []); // Add global error handler for unhandled promise rejections useEffect(() => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => { if ( event.reason && typeof event.reason === "object" && "message" in event.reason ) { const message = event.reason.message; if ( message && typeof message === "string" && message.includes("matches.call") ) { console.warn( "Caught focus-trap matches.call error, preventing crash:", event.reason, ); event.preventDefault(); } } }; window.addEventListener("unhandledrejection", handleUnhandledRejection); return () => window.removeEventListener( "unhandledrejection", handleUnhandledRejection, ); }, []); return (
{portalElement ? createPortal( document.body, returnFocusOnDeactivate: true, escapeDeactivates: false, // Let our keyboard handler manage escape preventScroll: true, // Disable the problematic check that causes the matches.call error checkCanFocusTrap: () => Promise.resolve(), }} >
{/* a tabable index that does nothing - used so focus trap doesn't error when nothing is rendered yet */}
{props.children}
, portalElement, ) : null}
); } export function Overlay(props: OverlayProps) { const router = useInternalOverlayRouter(props.id); const realClose = router.close; // listen for anchor updates useRouterAnchorUpdate(props.id); const close = useCallback(() => { realClose(); }, [realClose]); return ( {props.children} ); }