Skip to content

feat(focus): optional keydown isolation for FocusTrap #156

Description

@marcstraube

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

  • isolateKeydown?: boolean on FocusTrapOptions, documented (TSDoc).
  • When true, a keydown inside the trapped container does not reach a
    listener bound higher up (e.g. document); when false/unset, it does
    (regression guard for current behavior).
  • Escape deactivation, Tab cycling, initialFocus, returnFocus
    unaffected in both modes.
  • Tests cover both modes; 100% branch coverage on the new option.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort:sSmall (15-30 min)enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions