Skip to content

feat(react): add the AlertDialog primitive#20

Open
ivanbanov wants to merge 2 commits into
feat/floatingfrom
feat/alert-dialog
Open

feat(react): add the AlertDialog primitive#20
ivanbanov wants to merge 2 commits into
feat/floatingfrom
feat/alert-dialog

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

Summary

Adds the AlertDialog primitive as two packages:

  • @dunky.dev/alert-dialog — the framework-free state machine (core), built on @dunky.dev/state-machine.
  • @dunky.dev/react-alert-dialog — the thin React binding.

An alert dialog is a modal window that interrupts the user with an urgent question and requires a response, per the WAI-ARIA APG alertdialog pattern. Where a plain dialog can be waved away, an alert dialog cannot: it is always modal, an outside press never dismisses it, and the only ways out are the Cancel/Action parts and Escape.

Stacked PR: this branch is based on feat/floating (the shared overlay utilities extraction). It targets feat/floating for now and will retarget to main when that PR merges.

Anatomy

<AlertDialog>          - root; owns open/close state, renders nothing of its own
  |_ <Trigger>         - opens the alert dialog; focus returns here on close
  |_ <Portal>          - moves the layer to the document body
     |_ <Backdrop>     - the layer behind the window; never dismisses
     |_ <Viewport>     - positioning and scroll layer; never dismisses
        |_ <Content>       - the alert dialog window itself
           |_ <Title>       - names the alert dialog
           |_ <Description> - states the situation that demands a response
           |_ <Cancel>      - the least destructive answer; closes; takes initial focus
           |_ <Action>      - the confirming answer; closes

Behavior

  • Always modal: role="alertdialog" + aria-modal="true", focus is trapped inside, background scroll is locked, and outside presses never dismiss.
  • Initial focus lands on Cancel — the least destructive answer; focus returns to the trigger on close.
  • Escape closes, vetoable via onEscapeKeyDown (event.preventDefault()); backdrop/viewport presses are not a dismissal.
  • Controlled open is truly controlled: every open/close intent (trigger press, Escape, Cancel/Action) is reported through onOpenChange, but the alert dialog only moves when the prop does — ignoring a report is a working veto (e.g. hold it open while an async destructive operation resolves). Prop-driven transitions are not echoed back.
  • aria-labelledby/aria-describedby follow the rendered Title/Description — an omitted part never leaves a dangling reference.
  • data-state="open|closed" on the stateful parts is the styling hook; no positioning engine — the Viewport is the consumer's layout layer.

Test plan

  • pnpm test:ci — 145 tests, 13 files, all passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 23:41
Adds @dunky.dev/alert-dialog (framework-free state machine) and
@dunky.dev/react-alert-dialog (React binding). Anatomy: root, Trigger,
Portal, Backdrop, Viewport, Content, Title, Description, Cancel, Action.

A modal confirmation dialog per the APG alertdialog pattern: always
modal, outside presses never dismiss, Escape closes (vetoable), initial
focus lands on Cancel, and controlled open only moves with the prop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: add the AlertDialog primitive feat(react): add the AlertDialog primitive Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant