Context
Apps that put an interactive overlay (modal/dialog) over a shortcut-driven UI
need the overlay to stop background keyboard shortcuts from firing while it
is open — e.g. Delete/Ctrl+Z should act on the dialog's inputs, not on the
app behind it. Today consumers do this by hand with a keydown guard
(ev.stopPropagation()) on the overlay, alongside an otherwise standard
FocusTrap.create(...) setup.
This came up as the only genuinely reusable, generic piece of behavior when a
consumer (Rasterwerk) deduplicated two modal shells. The rest of a "modal"
(backdrop/dialog DOM, ARIA structure, body mount, app-owned CSS) is deliberately
out of scope for this library — see Non-goals. Keydown isolation, however,
is a clean focus-management primitive that fits FocusTrap.
Proposal
Add an opt-in isolateKeydown option to FocusTrapOptions
(src/focus/FocusTrap.ts). When enabled, the trap's existing keydown handling
also calls stopPropagation() so keydown events do not bubble past the trapped
container to app-level shortcut handlers. Escape/Tab handling is unchanged.
const trap = FocusTrap.create(dialog, {
initialFocus,
returnFocus: true,
escapeDeactivates: true,
onEscapeDeactivate: close,
isolateKeydown: true, // NEW — keep app shortcuts from firing behind the trap
});
Design notes
- Opt-in, default
false — must not change behavior for existing consumers.
- Reuse the trap's existing
keydown listener (around FocusTrap.ts:119); do
not add a second listener.
stopPropagation() only — not preventDefault() (typing/Tab/Esc inside
the dialog must still work normally).
- Keep it modern/zero-dep/a11y-correct, consistent with the module.
Acceptance criteria
Non-goals
- No modal-shell wrapper /
createModal, no backdrop or dialog DOM creation, no
ARIA structure factory, no styling/CSS class hooks. The library provides the
focus/keydown behavior; the app owns the modal DOM and its styles.
Context
Apps that put an interactive overlay (modal/dialog) over a shortcut-driven UI
need the overlay to stop background keyboard shortcuts from firing while it
is open — e.g.
Delete/Ctrl+Zshould act on the dialog's inputs, not on theapp behind it. Today consumers do this by hand with a
keydownguard(
ev.stopPropagation()) on the overlay, alongside an otherwise standardFocusTrap.create(...)setup.This came up as the only genuinely reusable, generic piece of behavior when a
consumer (Rasterwerk) deduplicated two modal shells. The rest of a "modal"
(backdrop/dialog DOM, ARIA structure, body mount, app-owned CSS) is deliberately
out of scope for this library — see Non-goals. Keydown isolation, however,
is a clean focus-management primitive that fits
FocusTrap.Proposal
Add an opt-in
isolateKeydownoption toFocusTrapOptions(
src/focus/FocusTrap.ts). When enabled, the trap's existing keydown handlingalso calls
stopPropagation()so keydown events do not bubble past the trappedcontainer to app-level shortcut handlers.
Escape/Tabhandling is unchanged.Design notes
false— must not change behavior for existing consumers.keydownlistener (aroundFocusTrap.ts:119); donot add a second listener.
stopPropagation()only — notpreventDefault()(typing/Tab/Esc insidethe dialog must still work normally).
Acceptance criteria
isolateKeydown?: booleanonFocusTrapOptions, documented (TSDoc).true, a keydown inside the trapped container does not reach alistener bound higher up (e.g.
document); whenfalse/unset, it does(regression guard for current behavior).
Escapedeactivation,Tabcycling,initialFocus,returnFocusunaffected in both modes.
Non-goals
createModal, no backdrop or dialog DOM creation, noARIA structure factory, no styling/CSS class hooks. The library provides the
focus/keydown behavior; the app owns the modal DOM and its styles.