Dialog and Drawer each install their own document keydown listener (Dialog.stx onDocumentKey, line ~105; Drawer.stx line ~146) and act on it whenever they are open. Nothing tells one instance that another is open on top of it.
That is fine for one dialog. It breaks the moment two are open, which is ordinary: a picker over a form, a confirmation over a drawer.
What happens with one open over another
- One Escape closes both. Every open instance's listener sees the same event and calls its own
close(). - Tab is contested. Each instance's trap checks whether focus is inside its panel and pulls it back to its own first or last element when it is not. With an inner dialog rendered as a sibling of the outer one (the usual shape, since each is its own component), the outer trap sees focus in the inner panel as "outside" and drags it back. Which one wins depends on listener order.
- No way to opt out. There is no
closeOnEscape/closeOnBackdropprop, so a dialog that must be answered (a destructive confirmation, a form with unsaved input) cannot refuse Escape or a backdrop click.
Why it matters for adoption
hq.training nests modals deliberately (an exercise picker at one layer over a plan editor at another) and keeps its own dialog.ts for exactly this reason: one stack, and only the innermost dialog owns the keyboard. Its modal.stx was written so that swapping in the library's Dialog/DialogPanel/DialogBackdrop would be a change inside one file once stacksjs/stx#1977 and #1978 landed. Both have, and Dialog now traps focus, locks scroll and handles Escape and the backdrop. This is the remaining blocker: adopting it would turn every nested pair into "Escape closes everything".
Suggested shape
- A module-level stack shared by
DialogandDrawer(andstxConfirm/stxAlert, which #1998 fixed for the same "one Escape answers every open dialog" problem in isolation). Push on open, remove on close; only the top entry handles Escape and Tab. - Scroll lock taken by the first open and released by the last close, rather than each instance saving and restoring
body.stylearound the others. closeOnEscapeandcloseOnBackdropprops, defaulting totrue.
A regression test: two open, one Escape, the inner one closed and the outer one still open with focus back on whatever opened the inner one.