Skip to content
File

Blob: src/client/components/ui/dialog.tsx

typescript72 lines
1import { useCallback, useEffect, useRef, type ReactNode } from "react";
2import { X } from "lucide-react";
3 
4interface DialogProps {
5 open: boolean;
6 onClose: () => void;
7 title: string;
8 children: ReactNode;
9}
10 
11export function Dialog({ open, onClose, title, children }: DialogProps) {
12 const dialogRef = useRef<HTMLDialogElement>(null);
13 const previousFocus = useRef<HTMLElement | null>(null);
14 
15 const close = useCallback(() => {
16 dialogRef.current?.close();
17 previousFocus.current?.focus();
18 onClose();
19 }, [onClose]);
20 
21 useEffect(() => {
22 const dialog = dialogRef.current;
23 if (!dialog) return;
24 
25 if (open && !dialog.open) {
26 previousFocus.current = document.activeElement as HTMLElement;
27 dialog.showModal();
28 } else if (!open && dialog.open) {
29 dialog.close();
30 previousFocus.current?.focus();
31 }
32 }, [open]);
33 
34 useEffect(() => {
35 const dialog = dialogRef.current;
36 if (!dialog) return;
37 
38 const handleCancel = (e: Event) => {
39 e.preventDefault();
40 close();
41 };
42 
43 dialog.addEventListener("cancel", handleCancel);
44 return () => dialog.removeEventListener("cancel", handleCancel);
45 }, [close]);
46 
47 return (
48 <dialog
49 ref={dialogRef}
50 className="m-auto max-w-lg rounded-xl border border-zinc-800/60 bg-zinc-900 p-0 shadow-2xl backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm"
51 onClick={(e) => {
52 if (e.target === dialogRef.current) close();
53 }}
54 >
55 <div className="p-5 sm:p-6">
56 <div className="mb-4 flex items-center justify-between">
57 <h2 className="text-lg font-semibold text-zinc-100">{title}</h2>
58 <button
59 type="button"
60 onClick={close}
61 className="rounded-lg p-1 text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
62 aria-label="Close"
63 >
64 <X className="h-4 w-4" />
65 </button>
66 </div>
67 {children}
68 </div>
69 </dialog>
70 );
71}