Skip to content

Commit 109e97b

Browse files
Merge pull request #519 from AbdulSnk/feat/frontend-confirm-dialog-component
Feat/frontend confirm dialog component
2 parents 0cf29cc + bf7a392 commit 109e97b

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client";
2+
3+
import React, { useEffect, useRef } from "react";
4+
import { X, Loader2 } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
7+
interface ConfirmDialogProps {
8+
title?: string;
9+
message?: React.ReactNode;
10+
onConfirm: () => void;
11+
onCancel: () => void;
12+
loading?: boolean;
13+
}
14+
15+
export function ConfirmDialog({
16+
title = "Confirm",
17+
message = "Are you sure?",
18+
onConfirm,
19+
onCancel,
20+
loading = false,
21+
}: ConfirmDialogProps) {
22+
const dialogRef = useRef<HTMLDivElement | null>(null);
23+
const previouslyFocused = useRef<HTMLElement | null>(null);
24+
25+
useEffect(() => {
26+
// save previously focused element to restore later
27+
previouslyFocused.current = document.activeElement as HTMLElement | null;
28+
29+
// lock scroll
30+
const prevOverflow = document.body.style.overflow;
31+
document.body.style.overflow = "hidden";
32+
33+
// focus first focusable element in dialog
34+
const node = dialogRef.current;
35+
const focusable = node?.querySelector<HTMLElement>(
36+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
37+
);
38+
if (focusable) focusable.focus();
39+
40+
const onKeyDown = (e: KeyboardEvent) => {
41+
if (e.key === "Escape") {
42+
e.preventDefault();
43+
onCancel();
44+
}
45+
46+
if (e.key === "Tab") {
47+
// simple focus trap
48+
const container = dialogRef.current;
49+
if (!container) return;
50+
const focusableEls = Array.from(
51+
container.querySelectorAll<HTMLElement>(
52+
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex]:not([tabindex="-1"]), [contenteditable]'
53+
)
54+
).filter((el) => el.offsetWidth || el.offsetHeight || el.getClientRects().length);
55+
56+
if (focusableEls.length === 0) {
57+
e.preventDefault();
58+
return;
59+
}
60+
61+
const first = focusableEls[0];
62+
const last = focusableEls[focusableEls.length - 1];
63+
64+
if (!e.shiftKey && document.activeElement === last) {
65+
e.preventDefault();
66+
first.focus();
67+
}
68+
69+
if (e.shiftKey && document.activeElement === first) {
70+
e.preventDefault();
71+
last.focus();
72+
}
73+
}
74+
};
75+
76+
document.addEventListener("keydown", onKeyDown);
77+
78+
return () => {
79+
document.removeEventListener("keydown", onKeyDown);
80+
document.body.style.overflow = prevOverflow;
81+
// restore focus
82+
try {
83+
previouslyFocused.current?.focus();
84+
} catch (err) {
85+
// ignore
86+
}
87+
};
88+
}, [onCancel]);
89+
90+
return (
91+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
92+
{/* Backdrop */}
93+
<div
94+
aria-hidden
95+
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
96+
onClick={onCancel}
97+
/>
98+
99+
{/* Dialog */}
100+
<div
101+
ref={dialogRef}
102+
role="dialog"
103+
aria-modal="true"
104+
aria-labelledby="confirm-dialog-title"
105+
aria-describedby="confirm-dialog-description"
106+
tabIndex={-1}
107+
className="relative bg-white rounded-2xl shadow-xl w-full max-w-xl mx-auto p-6"
108+
>
109+
<div className="flex items-start justify-between gap-4">
110+
<div className="min-w-0">
111+
<h2 id="confirm-dialog-title" className="text-lg font-semibold text-gray-900">
112+
{title}
113+
</h2>
114+
<p id="confirm-dialog-description" className="mt-2 text-sm text-gray-600">
115+
{message}
116+
</p>
117+
</div>
118+
119+
<button
120+
type="button"
121+
aria-label="Close dialog"
122+
onClick={onCancel}
123+
className="text-gray-400 hover:text-gray-600 p-1 rounded-full"
124+
>
125+
<X size={18} />
126+
</button>
127+
</div>
128+
129+
<div className="mt-6 flex items-center justify-end gap-3">
130+
<Button variant="outline" onClick={onCancel} className="px-4 py-2">
131+
Cancel
132+
</Button>
133+
134+
<Button
135+
onClick={onConfirm}
136+
disabled={loading}
137+
className="px-4 py-2 flex items-center"
138+
variant="destructive"
139+
>
140+
{loading && <Loader2 size={16} className="animate-spin mr-2" />}
141+
Confirm
142+
</Button>
143+
</div>
144+
</div>
145+
</div>
146+
);
147+
}
148+
149+
export default ConfirmDialog;
150+

0 commit comments

Comments
 (0)