import { useEffect, useId, useLayoutEffect, useRef, type PointerEvent, type ReactNode } from "react"; type Props = { open: boolean; title: string; className?: string; onClose: () => void; cancelGuard?: () => boolean; children: ReactNode }; // Native dialog provides focus containment, inert background, Escape and focus return. // Keeping children mounted preserves drafts and pending import results when closed. export function DraggableDialog({ open, title, className = "", onClose, cancelGuard, children }: Props) { const ref = useRef(null); const titleId = useId(); const drag = useRef<{ id: number; x: number; y: number; left: number; top: number } | null>(null); const position = (left: number, top: number) => { const dialog = ref.current; if (!dialog) return; const bounds = dialog.getBoundingClientRect(); const width = document.documentElement.clientWidth, height = window.innerHeight; dialog.style.left = `${Math.max(8, Math.min(left, width - bounds.width - 8))}px`; dialog.style.top = `${Math.max(8, Math.min(top, height - bounds.height - 8))}px`; }; useLayoutEffect(() => { const dialog = ref.current!; if (!open) { if (dialog.open) dialog.close(); return; } if (!dialog.open) dialog.showModal(); const bounds = dialog.getBoundingClientRect(); position((document.documentElement.clientWidth - bounds.width) / 2, (window.innerHeight - bounds.height) / 2); const keepVisible = () => { const rect = dialog.getBoundingClientRect(); position(rect.left, rect.top); }; const observer = new ResizeObserver(keepVisible); observer.observe(dialog); window.addEventListener("resize", keepVisible); return () => { observer.disconnect(); window.removeEventListener("resize", keepVisible); drag.current = null; }; }, [open]); useEffect(() => () => { if (ref.current?.open) ref.current.close(); }, []); const start = (event: PointerEvent) => { if (event.button !== 0 || (event.target as HTMLElement).closest("button")) return; const bounds = ref.current!.getBoundingClientRect(); drag.current = { id: event.pointerId, x: event.clientX, y: event.clientY, left: bounds.left, top: bounds.top }; event.currentTarget.setPointerCapture(event.pointerId); }; const move = (event: PointerEvent) => { const from = drag.current; if (from?.id !== event.pointerId) return; position(from.left + event.clientX - from.x, from.top + event.clientY - from.y); }; const stop = () => { drag.current = null; }; return { event.preventDefault(); if (!cancelGuard?.()) onClose(); }} onKeyDown={(event) => { if (event.key !== "Tab") return; const elements = [...event.currentTarget.querySelectorAll('button:not(:disabled), a[href], input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), summary, [tabindex="0"]')].filter((element) => element.getClientRects().length > 0); const first = elements[0], last = elements.at(-1); if (event.shiftKey && (document.activeElement === first || !elements.includes(document.activeElement as HTMLElement))) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && (document.activeElement === last || !elements.includes(document.activeElement as HTMLElement))) { event.preventDefault(); first?.focus(); } }}>
{title}
{children}
; }