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}
);
}