Skip to content

fix(ui): convert overlay modals to native dialog elements - #46

Merged
hyperpolymath merged 3 commits into
mainfrom
fix/native-dialog-modals
Sep 22, 2026
Merged

hyperpolymath merged 3 commits into
mainfrom
fix/native-dialog-modals

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Fixes #32.

What changed

frontend/src/components/NameDialog.tsx, frontend/src/components/AnnotationPanelControls.tsx (AddFuncdbModal)

  • Both overlays are now a native <dialog> opened via showModal(): implicit dialog role, aria-modal, focus trap and Escape-to-cancel come from the platform.
  • role="presentation" is gone from both; the hand-rolled document-level keydown Escape listener in AddFuncdbModal and the input-level Escape handler in NameDialog are deleted, not bypassed — the native cancel/close flow drives onClose.
  • Every dismissal routes through dialog.close() so the close event fires onClose exactly once (Escape, backdrop click, or Cancel).
  • Backdrop-click-to-dismiss is kept deliberately (criterion 6): the dialog is stretched transparent over the viewport, so a click outside the panel hits the dialog element itself, which the handler treats as backdrop.
  • Focus is restored to the invoking control on close. Subtlety found and fixed while e2e-testing: the input must be focused after showModal(), because showModal() records the currently focused element as the restoration target; React's autoFocus would move focus first and the dialog would restore focus to an element inside itself, dropping it on unmount.

frontend/src/styles/app.css

  • .mm-modal resets the UA default margin/border/padding, stretches the dialog, centres the panel; ::backdrop carries the dimmed overlay (replaces the hand-painted rgba(0,0,0,.45)). The UA dialog:not([open]){display:none} outranks the class, so centreing applies only while open.

frontend/package.json + frontend/bun.lock

  • @playwright/test@1.63.0 added to devDependencies — test:e2e was declared in scripts but Playwright was in neither dependency list (grep -c -i playwright bun.lock was 0; now 4). The lane is provisioned for real.

frontend/tests/e2e/dialogs.e2e.ts — new specs: implicit dialog role in the accessibility tree, Escape closes, Cancel closes + focus restored to the invoker, Tab never escapes the open dialog, backdrop click dismisses while panel click does not.

Acceptance criteria check

  1. ✅ both render native <dialog> via showModal(); no role="presentation"
  2. ✅ grep -rn 'role="presentation"' frontend/src/ → 0 matches
  3. ✅ Escape closes via native cancel/close; hand-rolled listeners deleted
  4. ✅ focus moves in on open (input) and restores to the invoking control on close; Tab contained — asserted by e2e
  5. ✅ ::backdrop styles the overlay; UA default chrome reset
  6. ✅ backdrop-click-to-dismiss kept, mechanism recorded here
  7. ✅ no S6819/S1082 shape remains (no click handler on non-interactive element; native element used)
  8. ✅ verified in a real browser — headless Chromium 153 on this branch: dialogs spec 5/5 pass, full e2e lane 6/6 pass

Verification summary

  • bun run typecheck clean (strict mode)
  • bun test 604 tests, 0 fail
  • bunx playwright test 6/6 pass (5 dialog specs + app-shell smoke)
  • Repo hygiene gates + source lint pass on this branch

NameDialog and AddFuncdbModal dismissed via a click handler on a
presentational div with no dialog semantics: nothing announced a
dialog, nothing trapped focus, and Escape was hand-rolled on one of
them (#32).

Both now render a native <dialog> opened with showModal(), which
supplies the implicit role, aria-modal, the focus trap and
Escape-to-cancel; the hand-rolled keydown listener and the
presentational role are deleted, not bypassed. Backdrop-click
dismissal is kept deliberately: the dialog is stretched transparent
over the viewport so a click outside the panel hits the dialog
element itself. The dimmed overlay moves to ::backdrop and the UA
default chrome is reset in app.css so the panels render as before.
Focus is restored to the invoking control on close; the input is
focused after showModal() so the browser records the invoker, not the
input, as the restoration target.

The test:e2e lane is provisioned for real: @playwright/test 1.63.0
joins devDependencies (it was declared in package.json scripts but in
neither dependency list), and tests/e2e/dialogs.e2e.ts covers role,
Escape, Cancel, focus containment, focus restoration and backdrop
dismissal. Verified in headless Chromium on this branch: 6/6 e2e
pass, typecheck clean, 604/604 unit tests pass, and the
presentational-role grep over frontend/src returns zero matches.
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5cf2a167-6b90-4985-8914-ddefbcff548c

📥 Commits

Reviewing files that changed from the base of the PR and between 4df7881 and d4b3310.

⛔ Files ignored due to path filters (1)
  • frontend/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • frontend/package.json
  • frontend/src/components/AnnotationPanelControls.tsx
  • frontend/src/components/NameDialog.tsx
  • frontend/src/styles/app.css
  • frontend/tests/e2e/dialogs.e2e.ts
 ____________________________________________________________________________________________________________________________________________
< Every great developer you know got there by solving problems they were unqualified to solve until they actually did it. - Patrick McKenzie >
 --------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SonarCloud S1082 flagged the onClick the native dialog carried for
backdrop dismissal -- the same whack-a-mole the issue describes, one
rule's cure minting the other rule's finding. Criterion 6 permits
dropping the behaviour deliberately with the reason recorded: these
are form dialogs, so a stray click outside the panel must not discard
typed input. Escape and Cancel remain as dismissals, both
keyboard-reachable, and the e2e lane now pins the decision with a
test asserting backdrop clicks do NOT dismiss, so it cannot be
silently restored. The dialog no longer stretches as a click target;
UA margin:auto centreing replaces the flex trick (#32).
@hyperpolymath
hyperpolymath merged commit 22d228f into main Sep 22, 2026
@hyperpolymath
hyperpolymath deleted the fix/native-dialog-modals branch September 22, 2026 08:05
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@sonarqubecloud

Copy link
Copy Markdown

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.

Two overlay modals have no dialog semantics: role="presentation" stands in for a native <dialog>

1 participant