Skip to content

refactor(ui): shared modal component + ESC consistency fix (#93 review)#188

Merged
gerchowl merged 1 commit into
mainfrom
refactor/modal-component
May 31, 2026
Merged

refactor(ui): shared modal component + ESC consistency fix (#93 review)#188
gerchowl merged 1 commit into
mainfrom
refactor/modal-component

Conversation

@gerchowl

Copy link
Copy Markdown
Contributor

Summary

Follow-up to the architecture review (the filter-bar misalignment turned out to be a symptom of a missing component layer). This extracts the first shared component: modal.ts.

The review found 7 hand-rolled modal/overlay scaffolds that diverged — auth & import modals closed on Escape, but the lookup detail/assembly modals and the recovery dialog did not. A user who learns "ESC dismisses" got inconsistent behavior. That's a real bug, not just duplication.

What changed

  • web/src/ui/components/modal.tsopenModal({ body, overlayClass, cardClass, ariaLabel, dismissable, closeButton, onClose }) owns the overlay + card + ✕ + ESC + backdrop-click + focus-on-open / focus-restore-on-close contract once. role="dialog"/aria-modal for a11y. Call sites keep their feature class (so existing CSS + e2e selectors are preserved). body may be a (close) => HTMLElement builder for forms. dismissable:false forces a choice (crash recovery) — no ESC/backdrop/✕.
  • Migrated: lookup detail + assembly modals (now close on ESC — the fix), and main.ts recovery dialog (non-dismissable, structure shared).
  • CSS: base .modal-overlay / .modal-card / .modal-card__close.

Tests

  • modal.test.ts (8): mount + classes, a11y attrs, ESC closes, backdrop-vs-card click, ✕, listener cleanup / no double-close, non-dismissable mode, function body.
  • Full unit suite passes; lookup + smoke e2e (which exercise the detail modal) pass locally.

Deliberately out of scope (follow-ups worth their own issues)

  • Migrate auth / import / scan overlays onto modal.ts (they already behave correctly — pure dedup).
  • Extract a shared dropdown helper (3 re-implementations; one leaks a document listener).
  • Add control-height / shadow / z-index design tokens (the styling-SSoT review — root cause of controls misaligning by default).

🤖 Generated with Claude Code

The architecture review found 7 hand-rolled modal/overlay scaffolds that
diverged: auth/import modals closed on Escape, but the lookup detail and
assembly modals and the recovery dialog did not. A user who learns "ESC
dismisses" got inconsistent behavior.

- web/src/ui/components/modal.ts: openModal({ body, overlayClass,
  cardClass, ariaLabel, dismissable, closeButton, onClose }) owning the
  overlay + card + ✕ + ESC + backdrop + focus(open)/restore(close)
  contract once. role=dialog/aria-modal for a11y. Call sites keep their
  feature class (so CSS + e2e selectors are preserved). `body` may be a
  builder (close) => HTMLElement for forms. dismissable:false forces a
  choice (crash recovery) — no ESC/backdrop/✕.
- Migrate: lookup detail + assembly modals (now close on ESC — the fix),
  and main.ts recovery dialog (dismissable:false, structure shared).
- CSS: base .modal-overlay/.modal-card/.modal-card__close.
- modal.test.ts (8): mount, a11y, ESC, backdrop-vs-card, ✕, listener
  cleanup/no-double-close, non-dismissable, function body.

Follow-ups (own issues): migrate auth/import/scan overlays onto modal.ts;
extract a shared dropdown helper; add control-height/shadow/z-index
design tokens (the styling-SSoT review).
@gerchowl gerchowl merged commit 8f1c240 into main May 31, 2026
2 checks passed
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