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
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:
co-authored by
Claude Opus 5.5
parent
ce8ed89fdc
commit
7f2e93d389
@@ -71,7 +71,15 @@ function UserRoomProfileContextMenu({ state }: { state: UserRoomProfileState })
|
|||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap focusTrapOptions={focusTrapOptions}>
|
<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
|
{/* Full-screen covers the backdrop (no tap-to-dismiss) and the
|
||||||
profile has no self-close, so provide an explicit close. */}
|
profile has no self-close, so provide an explicit close. */}
|
||||||
<Header size="600" style={{ flexShrink: 0, paddingRight: config.space.S200 }}>
|
<Header size="600" style={{ flexShrink: 0, paddingRight: config.space.S200 }}>
|
||||||
|
|||||||
@@ -105,13 +105,18 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#file-viewer-2-dialog',
|
||||||
onDeactivate: () => setTextViewer(false),
|
onDeactivate: () => setTextViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
id="file-viewer-2-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={`File: ${body}`}
|
||||||
|
tabIndex={-1}
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
|
||||||
@@ -194,13 +199,18 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#file-viewer-1-dialog',
|
||||||
onDeactivate: () => setPdfViewer(false),
|
onDeactivate: () => setPdfViewer(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
|
id="file-viewer-1-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={`File: ${body}`}
|
||||||
|
tabIndex={-1}
|
||||||
className={ModalWide}
|
className={ModalWide}
|
||||||
size="500"
|
size="500"
|
||||||
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
|
onContextMenu={(evt: React.MouseEvent) => evt.stopPropagation()}
|
||||||
|
|||||||
@@ -76,13 +76,22 @@ export function ReadReceiptAvatars({
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#readreceipt-seenby-dialog',
|
||||||
onDeactivate: () => setOpen(false),
|
onDeactivate: () => setOpen(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
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)} />
|
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|||||||
@@ -105,13 +105,20 @@ function ErrorDialog({
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#roomcard-error-dialog',
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
onDeactivate: closeError,
|
onDeactivate: closeError,
|
||||||
escapeDeactivates: stopPropagation,
|
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 style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
|
|||||||
@@ -51,13 +51,22 @@ export const RoomViewFollowing = as<'div', RoomViewFollowingProps>(
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#following-seenby-dialog',
|
||||||
onDeactivate: () => setOpen(false),
|
onDeactivate: () => setOpen(false),
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
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)} />
|
<EventReaders room={room} eventId={eventId} requestClose={() => setOpen(false)} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|||||||
@@ -290,13 +290,22 @@ export const MessageReadReceiptItem = as<
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#message-seenby-dialog',
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
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} />
|
<EventReaders room={room} eventId={eventId} requestClose={handleClose} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
@@ -368,13 +377,21 @@ export const MessageSourceCodeItem = as<
|
|||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
<FocusTrap
|
<FocusTrap
|
||||||
focusTrapOptions={{
|
focusTrapOptions={{
|
||||||
initialFocus: false,
|
fallbackFocus: '#message-source-dialog',
|
||||||
onDeactivate: handleClose,
|
onDeactivate: handleClose,
|
||||||
clickOutsideDeactivates: true,
|
clickOutsideDeactivates: true,
|
||||||
escapeDeactivates: stopPropagation,
|
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
|
<TextViewer
|
||||||
name="Source Code"
|
name="Source Code"
|
||||||
langName="json"
|
langName="json"
|
||||||
|
|||||||
Reference in New Issue
Block a user