confirm-dialog.tsx
1 import { useEffect, useRef } from "react"; 2 import { createPortal } from "react-dom"; 3 import { AlertTriangle } from "lucide-react"; 4 import { Button } from "@nous-research/ui/ui/components/button"; 5 import { cn } from "@/lib/utils"; 6 7 export function ConfirmDialog({ 8 cancelLabel = "Cancel", 9 confirmLabel = "Confirm", 10 description, 11 destructive = false, 12 loading = false, 13 onCancel, 14 onConfirm, 15 open, 16 title, 17 }: ConfirmDialogProps) { 18 const dialogRef = useRef<HTMLDivElement>(null); 19 20 // Focus the confirm button when opened; trap ESC to cancel. 21 useEffect(() => { 22 if (!open) return; 23 24 const prevActive = document.activeElement as HTMLElement | null; 25 dialogRef.current 26 ?.querySelector<HTMLButtonElement>("[data-confirm]") 27 ?.focus(); 28 29 const onKey = (e: KeyboardEvent) => { 30 if (e.key === "Escape") { 31 e.preventDefault(); 32 onCancel(); 33 } 34 }; 35 36 document.addEventListener("keydown", onKey); 37 const prevOverflow = document.body.style.overflow; 38 document.body.style.overflow = "hidden"; 39 40 return () => { 41 document.removeEventListener("keydown", onKey); 42 document.body.style.overflow = prevOverflow; 43 prevActive?.focus?.(); 44 }; 45 }, [open, onCancel]); 46 47 if (!open) return null; 48 49 return createPortal( 50 <div 51 role="dialog" 52 aria-modal="true" 53 aria-labelledby="confirm-dialog-title" 54 aria-describedby={description ? "confirm-dialog-desc" : undefined} 55 onClick={(e) => { 56 if (e.target === e.currentTarget) onCancel(); 57 }} 58 className={cn( 59 "fixed inset-0 z-50 flex items-center justify-center", 60 "bg-black/60 backdrop-blur-sm", 61 "animate-[fade-in_150ms_ease-out]", 62 )} 63 > 64 <div 65 ref={dialogRef} 66 className={cn( 67 "relative w-full max-w-md mx-4", 68 "border border-border bg-card shadow-lg", 69 "animate-[dialog-in_180ms_ease-out]", 70 )} 71 > 72 <div className="flex items-start gap-3 p-4 border-b border-border"> 73 {destructive && ( 74 <div 75 aria-hidden 76 className="mt-0.5 shrink-0 text-destructive" 77 > 78 <AlertTriangle className="h-4 w-4" /> 79 </div> 80 )} 81 82 <div className="flex-1 min-w-0 flex flex-col gap-1"> 83 <h2 84 id="confirm-dialog-title" 85 className="font-expanded text-sm font-bold tracking-[0.08em] uppercase blend-lighter" 86 > 87 {title} 88 </h2> 89 90 {description && ( 91 <p 92 id="confirm-dialog-desc" 93 className="font-mondwest text-xs text-muted-foreground leading-relaxed" 94 > 95 {description} 96 </p> 97 )} 98 </div> 99 </div> 100 101 <div className="flex items-center justify-end gap-2 p-3"> 102 <Button 103 type="button" 104 outlined 105 onClick={onCancel} 106 disabled={loading} 107 > 108 {cancelLabel} 109 </Button> 110 <Button 111 data-confirm 112 type="button" 113 destructive={destructive} 114 onClick={onConfirm} 115 disabled={loading} 116 > 117 {loading ? "…" : confirmLabel} 118 </Button> 119 </div> 120 </div> 121 </div>, 122 document.body, 123 ); 124 } 125 126 interface ConfirmDialogProps { 127 cancelLabel?: string; 128 confirmLabel?: string; 129 description?: string; 130 destructive?: boolean; 131 loading?: boolean; 132 onCancel: () => void; 133 onConfirm: () => void; 134 open: boolean; 135 title: string; 136 }