Dialogs are not dialogs: focus is not moved in, kept in, or given back #306

Closed
opened 2026-10-19 12:49:02 -07:00 by rays · 1 comment
Owner

Every dialog goes through openModal() (web/src/dialogs.ts:3), which fills <div id="modal"> (web/index.html:81) and shows it. Nothing marks it as a dialog or manages focus. Checked in Playwright on all eight (Add a feed, Settings, a feed's Settings, Download latest, Unsubscribe, OPML, Keyboard shortcuts, New signal):

  • No role="dialog", aria-modal or label on any of them, so a screen reader is not told a dialog opened, or what it is.
  • Six of the eight leave focus where it was, behind the overlay: Settings, a feed's Settings, Download latest, Unsubscribe, OPML, Keyboard shortcuts. On Unsubscribe, focus stays on the toolbar button behind it.
  • In all eight, Tab walks out of the dialog into the page behind it (the toolbar, the sidebar), which stays live (#shell is not inert).
  • Escape closes it (web/src/player.ts:220), but focus then goes to <body> or to wherever Tab had wandered, not back to the button that opened it.

Found in an interface review (2026-10-19). WCAG 2.4.3, 4.1.2, 1.3.1.

Fix: make #modal a <dialog> and open it with showModal(). That makes the page behind it inert, keeps Tab inside, handles Escape, and gives focus back to the opener when it closes. Label it with its heading (aria-labelledby on the <h3>), and focus the first field, or the dialog itself, when it opens.

Every dialog goes through `openModal()` (`web/src/dialogs.ts:3`), which fills `<div id="modal">` (`web/index.html:81`) and shows it. Nothing marks it as a dialog or manages focus. Checked in Playwright on all eight (Add a feed, Settings, a feed's Settings, Download latest, Unsubscribe, OPML, Keyboard shortcuts, New signal): - No `role="dialog"`, `aria-modal` or label on any of them, so a screen reader is not told a dialog opened, or what it is. - Six of the eight leave focus where it was, behind the overlay: Settings, a feed's Settings, Download latest, Unsubscribe, OPML, Keyboard shortcuts. On Unsubscribe, focus stays on the toolbar button behind it. - In all eight, Tab walks out of the dialog into the page behind it (the toolbar, the sidebar), which stays live (`#shell` is not inert). - Escape closes it (`web/src/player.ts:220`), but focus then goes to `<body>` or to wherever Tab had wandered, not back to the button that opened it. Found in an interface review (2026-10-19). WCAG 2.4.3, 4.1.2, 1.3.1. Fix: make `#modal` a `<dialog>` and open it with `showModal()`. That makes the page behind it inert, keeps Tab inside, handles Escape, and gives focus back to the opener when it closes. Label it with its heading (`aria-labelledby` on the `<h3>`), and focus the first field, or the dialog itself, when it opens.
rays added the bug label 2026-10-19 12:49:02 -07:00
Author
Owner

Fixed in 6ec2c35: the dialog card is role=dialog aria-modal, labelled by its heading, and takes focus as it opens; the rest of the page is inert until it closes, when focus goes back to what opened it. Not a

: its top layer would have covered the toasts and their Dismiss. A browser test checks Tab never reaches the page behind and Escape gives focus back. Deployed in ipx-web (1.2.1-dev).

Fixed in 6ec2c35: the dialog card is role=dialog aria-modal, labelled by its heading, and takes focus as it opens; the rest of the page is inert until it closes, when focus goes back to what opened it. Not a <dialog>: its top layer would have covered the toasts and their Dismiss. A browser test checks Tab never reaches the page behind and Escape gives focus back. Deployed in ipx-web (1.2.1-dev).
rays closed this issue 2026-10-19 13:13:48 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#306