[Bug Fix] AlertDialog: render a native <dialog> and play the exit animation
TV
2mo ago+215+215+012345678901234567890123456789-45-45-01234567890123456789
Problem
AlertDialogContentrendered a<template>that the controller cloned to the end of<body>and removed on dismiss: closing was a hard cut, no Escape, no focus trap/restore, the page behind stayed interactive.- Backdrop was
bg-black/80while every other overlay usesbg-background/80.
Change
- Native
<dialog role="alertdialog">rendered in place, opened withshowModal()— top layer, inert background, focus trap and focus return from the platform. No template/clone, no nested controller. - shadcn/Radix parity: Escape closes (
cancelintercepted so the exit animates), backdrop click does not close, focus lands on Cancel (autofocus), Action dismisses too (a link/form submit on it still navigates). - Enter/exit animations on panel and
::backdropviadata-state+ tw-animate-css.dismiss()setsdata-state="closed", waits for the panel's and the backdrop's exit animations (getAnimations({subtree: true})on the<dialog>filtered to its owneffect.target, theirfinishedpromises, a token per run so a superseded run cannot settle a later one), then callsclose(). This differs from theanimationendblock of the other overlays (#506):getAnimations()withoutsubtreenever lists the::backdrop, and its events land on the<dialog>under the same keyframe names as the panel's, so that block closed on whichever exit ended first and a staleanimationcancelfrom a previous run could close a new run early. Same block as Dialog (#517). - The backdrop gets
backdrop:duration-200so it fades in step with the panel by default (--tw-durationis registeredinherits: false, so::backdropdoes not pick the panel's duration up). The controller does not depend on the two matching: aduration-*override closes after the longer of the two. - A second Escape mid-exit is left to the browser (non-cancelable in Chrome, closes at once where it stays cancelable). A close the controller did not start settles the pending run and lifts the body scroll lock, so the next open starts clean;
disconnect()lifts it even if the<dialog>is already gone. - Backdrop aligned to
bg-background/80 backdrop-blur-sm. Public API and the docs example unchanged. mcp/data/registry.jsonrebuilt (separate commit).
Dialog and Combobox get the same treatment in sibling PRs.
Test
cd gem && bundle exec rake— 13 new tests inalert_dialog_test.rb.- Manual, on the AlertDialog docs page:
- Open → panel and backdrop fade/zoom in, focus is on Cancel, page scroll is locked.
- Cancel / Escape / Continue → exit animation plays, then the dialog closes, focus returns to the trigger, scroll is restored.
- Click the backdrop → stays open.
- Escape twice quickly → closes hard; opening again works normally.
AlertDialog(open: true)→ open on page load.
- Timing cases checked in headless Chrome (CDP), before/after: normal close, Escape once/twice, reopen mid-exit, dismiss mid-enter, disconnect mid-exit and an external
close()all close after the 200 ms exit (or at once where the browser closes natively); reopen + re-dismiss within the first frame now runs the full 200 ms (was ~30 ms); a longerduration-*on either panel or backdrop closes after the longer one;requestClose()mid-exit closes at once. No console errors.
🤖 Generated with Claude Code