fix(a11y): name the headless modals too — Seen by, source, file viewers (#185)
CI / Build & Quality Checks (push) Successful in 1m41s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 11s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 13m14s

Follow-up to ce8ed89f for the modals that have no visible heading to
point at: the three "Seen by" reader lists (read-receipt pill, "is
following", message menu), View source, the text/PDF file viewers, the
room-card join error and the user-profile modal get role="dialog",
aria-modal and an aria-label; their traps move focus in (fallbackFocus
on the dialog) where the trap is local.

Verified: the receipt pill opens a "Seen by" dialog with focus on
Close; Escape closes it and focus returns to the pill.

The remaining unnamed Modal/Dialog uses are startup/loading and error
screens (config, feature check, spec versions, client root, password
reset) and wrappers around components that carry their own role (image
viewer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-24 13:12:13 -04:00
co-authored by Claude Opus 5.5
parent ce8ed89fdc
commit 7f2e93d389
6 changed files with 73 additions and 13 deletions
@@ -71,7 +71,15 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState })
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap focusTrapOptions={focusTrapOptions}>
<Modal size="500" style={MOBILE_FULLSCREEN}>
<Modal
id="user-profile-dialog"
role="dialog"
aria-modal="true"
aria-label="User profile"
tabIndex={-1}
size="500"
style={MOBILE_FULLSCREEN}
>
{/* Full-screen covers the backdrop (no tap-to-dismiss) and the
profile has no self-close, so provide an explicit close. */}
<Header size="600" style={{ flexShrink: 0, paddingRight: config.space.S200 }}>
@@ -105,13 +105,18 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#file-viewer-2-dialog',
onDeactivate: () => setTextViewer(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
id="file-viewer-2-dialog"
role="dialog"
aria-modal="true"
aria-label={`File: ${body}`}
tabIndex={-1}
className={ModalWide}
size="500"
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
@@ -194,13 +199,18 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#file-viewer-1-dialog',
onDeactivate: () => setPdfViewer(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal
id="file-viewer-1-dialog"
role="dialog"
aria-modal="true"
aria-label={`File: ${body}`}
tabIndex={-1}
className={ModalWide}
size="500"
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
@@ -76,13 +76,22 @@ export function ReadReceiptAvatars({
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#readreceipt-seenby-dialog',
onDeactivate: () => setOpen(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal variant="Surface" size="300" style={modalStyle}>
<Modal
id="readreceipt-seenby-dialog"
role="dialog"
aria-modal="true"
aria-label="Seen by"
tabIndex={-1}
variant="Surface"
size="300"
style={modalStyle}
>
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
</Modal>
</FocusTrap>
+9 -2
View File
@@ -105,13 +105,20 @@ function ErrorDialog({
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#roomcard-error-dialog',
clickOutsideDeactivates: true,
onDeactivate: closeError,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Dialog
id="roomcard-error-dialog"
role="dialog"
aria-modal="true"
aria-label={title}
tabIndex={-1}
variant="Surface"
>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text>{title}</Text>
+11 -2
View File
@@ -51,13 +51,22 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#following-seenby-dialog',
onDeactivate: () => setOpen(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal variant="Surface" size="300" style={modalStyle}>
<Modal
id="following-seenby-dialog"
role="dialog"
aria-modal="true"
aria-label="Seen by"
tabIndex={-1}
variant="Surface"
size="300"
style={modalStyle}
>
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
</Modal>
</FocusTrap>
+21 -4
View File
@@ -290,13 +290,22 @@ export const MessageReadReceiptItem = as<
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#message-seenby-dialog',
onDeactivate: handleClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal variant="Surface" size="300" style={modalStyle}>
<Modal
id="message-seenby-dialog"
role="dialog"
aria-modal="true"
aria-label="Seen by"
tabIndex={-1}
variant="Surface"
size="300"
style={modalStyle}
>
<EventReaders room={room} eventId={eventId} requestClose={handleClose} />
</Modal>
</FocusTrap>
@@ -368,13 +377,21 @@ export const MessageSourceCodeItem = as<
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
fallbackFocus: '#message-source-dialog',
onDeactivate: handleClose,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal variant="Surface" size="500">
<Modal
id="message-source-dialog"
role="dialog"
aria-modal="true"
aria-label="View source"
tabIndex={-1}
variant="Surface"
size="500"
>
<TextViewer
name="Source Code"
langName="json"