Signed-off-by: Nic Luckie <nicolasluckie@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
4.4 KiB
ConfirmDialog - Accessible Confirmation Dialog
Overview
ConfirmDialog standardizes confirmation flows across the app. It replaces ad‑hoc window.confirm usage with an accessible, themeable dialog that supports asynchronous operations.
Key Features
1. Accessibility & UX
- Uses shared
Dialogprimitives (focus trap, ARIA roles) - Blocks dismissal while async confirm is pending
- Clear separation of title (action) vs description (context)
2. Async-Aware
- Accepts sync or async
onConfirm - Auto-closes on resolve; remains open on error for retry / toast
3. Internationalization Ready
- All labels / text provided by caller through i18n hook
- Supports interpolation for dynamic context
4. Minimal Surface, Easy Extension
- Lightweight API (few required props)
- Style hook via
.containerclass (SCSS module)
Architecture
ConfirmDialog
├── State: loading (tracks pending confirm action)
├── Dialog primitives: Header (title + description), Footer (buttons)
└── External control: parent owns open state via onOpenChange
Usage
import { useTranslate } from "@/utils/i18n";
import ConfirmDialog from "@/components/ConfirmDialog";
const t = useTranslate();
<ConfirmDialog
open={open}
onOpenChange={setOpen}
title={t("memo.delete-confirm")}
description={t("memo.delete-confirm-description")}
confirmLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
onConfirm={handleDelete}
confirmVariant="destructive"
/>;
Props
| Prop | Type | Required | Acceptable Values |
|---|---|---|---|
open |
boolean |
Yes | true (visible) / false (hidden) |
onOpenChange |
(open: boolean) => void |
Yes | Callback receiving next state; should update parent state |
title |
React.ReactNode |
Yes | Short localized action summary (text / node) |
description |
React.ReactNode |
No | Optional contextual message |
confirmLabel |
string |
Yes | Non-empty localized action text (1–2 words) |
cancelLabel |
string |
Yes | Localized cancel label |
onConfirm |
`() => void | Promise` | Yes |
confirmVariant |
`"default" | "destructive"` | No |
Benefits vs Previous Implementation
Before (window.confirm / ad‑hoc dialogs)
- Blocking native prompt, inconsistent styling
- No async progress handling
- No rich formatting
- Hard to localize consistently
After (ConfirmDialog)
- Unified styling + accessibility semantics
- Async-safe with loading state shielding
- Plain description flexibility
- i18n-first via externalized labels
Technical Implementation Details
Async Handling
const handleConfirm = async () => {
setLoading(true);
try {
await onConfirm(); // resolve -> close
onOpenChange(false);
} catch (e) {
console.error(e); // remain open for retry
} finally {
setLoading(false);
}
};
Close Guard
<Dialog open={open} onOpenChange={(next) => !loading && onOpenChange(next)} />
Browser / Environment Support
- Works anywhere the existing
Dialogprimitives work (modern browsers) - No ResizeObserver / layout dependencies
Performance Considerations
- Minimal renders: loading state toggles once per confirm attempt
- No portal churn—relies on underlying dialog infra
Future Enhancements
- Severity icon / header accent
- Auto-focus destructive button toggle
- Secondary action (e.g. "Archive" vs "Delete")
- Built-in retry / error slot
- Optional checkbox confirmation ("I understand the consequences")
- Motion/animation tokens integration
Styling
The ConfirmDialog.module.scss file provides a .container hook. It currently only hosts a harmless custom property so the stylesheet is non-empty. Add real layout or variant tokens there instead of inline styles.
Internationalization
All visible strings must come from the translation system. Use useTranslate() and pass localized values into props. Separate keys for title/description.
Error Handling
Errors thrown in onConfirm are caught and logged. The dialog stays open so the caller can surface a toast or inline message and allow retry. (Consider routing serious errors to a higher-level handler.)
If you extend this component, update this README to keep usage discoverable.