UNPKG

@loke/ui

Version:
215 lines (177 loc) 6.97 kB
--- name: alert-dialog type: core domain: overlays requires: [loke-ui, dialog] description: > Confirmation dialogs for destructive actions. AlertDialog, AlertDialogTrigger, AlertDialogPortal, AlertDialogOverlay, AlertDialogContent, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel. Always modal, always prevents outside dismiss, auto-focuses cancel button. --- # Alert Dialog This skill builds on dialog. Read it first: [dialog](../dialog/SKILL.md). `AlertDialog` is a strict subset of `Dialog` for destructive confirmations. It hardcodes `modal={true}`, prevents `onPointerDownOutside` and `onInteractOutside` dismissal, and auto-focuses `AlertDialogCancel` on open. Use it whenever the action cannot be undone. ## Setup Minimum working confirmation dialog. ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, } from "@loke/ui/alert-dialog"; function DeleteButton() { return ( <AlertDialog> <AlertDialogTrigger>Delete item</AlertDialogTrigger> <AlertDialogPortal> <AlertDialogOverlay /> <AlertDialogContent> <AlertDialogTitle>Delete item?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. The item will be permanently removed. </AlertDialogDescription> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Delete</AlertDialogAction> </AlertDialogContent> </AlertDialogPortal> </AlertDialog> ); } ``` On open, focus moves to `AlertDialogCancel` automatically. Clicking outside does nothing. Escape closes the dialog and returns focus to the trigger. ## Core Patterns ### Controlled open state ```tsx import { useState } from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, } from "@loke/ui/alert-dialog"; function ConfirmDiscard({ onDiscard }: { onDiscard: () => void }) { const [open, setOpen] = useState(false); return ( <> <button type="button" onClick={() => setOpen(true)}> Discard changes </button> <AlertDialog open={open} onOpenChange={setOpen}> <AlertDialogPortal> <AlertDialogOverlay /> <AlertDialogContent> <AlertDialogTitle>Discard changes?</AlertDialogTitle> <AlertDialogDescription> Unsaved changes will be lost. </AlertDialogDescription> <AlertDialogCancel>Keep editing</AlertDialogCancel> <AlertDialogAction onClick={onDiscard}>Discard</AlertDialogAction> </AlertDialogContent> </AlertDialogPortal> </AlertDialog> </> ); } ``` `AlertDialogTrigger` is optional. You can control open state directly and drive the dialog from any button. ### Custom action handling `AlertDialogAction` closes the dialog automatically (it wraps `DialogClose`). To run async work before closing, prevent default and manage state yourself. ```tsx import { useState } from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, } from "@loke/ui/alert-dialog"; function AsyncDeleteButton({ onDelete }: { onDelete: () => Promise<void> }) { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); async function handleDelete(event: React.MouseEvent) { event.preventDefault(); // prevent auto-close setLoading(true); await onDelete(); setLoading(false); setOpen(false); } return ( <AlertDialog open={open} onOpenChange={setOpen}> <AlertDialogTrigger>Delete</AlertDialogTrigger> <AlertDialogPortal> <AlertDialogOverlay /> <AlertDialogContent> <AlertDialogTitle>Delete?</AlertDialogTitle> <AlertDialogDescription>This cannot be undone.</AlertDialogDescription> <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel> <AlertDialogAction onClick={handleDelete} disabled={loading}> {loading ? "Deleting..." : "Delete"} </AlertDialogAction> </AlertDialogContent> </AlertDialogPortal> </AlertDialog> ); } ``` ## Common Mistakes ### Missing AlertDialogDescription `AlertDialogContent` warns in dev if `aria-describedby` resolves to nothing. Screen readers announce the description when the dialog opens, giving users the context to confirm or cancel. ```tsx // Wrong — no description <AlertDialogContent> <AlertDialogTitle>Delete item?</AlertDialogTitle> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Delete</AlertDialogAction> </AlertDialogContent> // Correct <AlertDialogContent> <AlertDialogTitle>Delete item?</AlertDialogTitle> <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Delete</AlertDialogAction> </AlertDialogContent> ``` Source: `src/components/alert-dialog/alert-dialog.tsx` — `DescriptionWarning` dev check. ### Missing AlertDialogCancel — nothing auto-focuses `AlertDialogContent` calls `cancelRef.current?.focus()` inside `onOpenAutoFocus`. Without `AlertDialogCancel`, `cancelRef` is null and focus falls to the first tabbable element, which may be the destructive `AlertDialogAction` button. ```tsx // Wrong — focus lands on the destructive button <AlertDialogContent> <AlertDialogTitle>Delete?</AlertDialogTitle> <AlertDialogDescription>Cannot be undone.</AlertDialogDescription> <AlertDialogAction>Delete</AlertDialogAction> </AlertDialogContent> // Correct — cancel gets focus, safer default <AlertDialogContent> <AlertDialogTitle>Delete?</AlertDialogTitle> <AlertDialogDescription>Cannot be undone.</AlertDialogDescription> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Delete</AlertDialogAction> </AlertDialogContent> ``` Source: `src/components/alert-dialog/alert-dialog.tsx` — `onOpenAutoFocus` focuses `cancelRef`. ### Setting modal={false} on AlertDialog `AlertDialog` internally passes `modal={true}` to the underlying `Dialog` unconditionally. The `modal` prop is omitted from `AlertDialogProps`. Passing `modal={false}` has no effect. ```tsx // This does nothing — AlertDialog is always modal <AlertDialog modal={false}> ``` Source: `src/components/alert-dialog/alert-dialog.tsx` — `modal={true}` hardcoded in root component. ## Cross-references - See also: [dialog](../dialog/SKILL.md) — base dialog pattern; read before this skill - See also: [choosing-the-right-component](../choosing-the-right-component/SKILL.md) — Dialog vs AlertDialog decision