From 885ec7c82e5149b848e9490db0df22f26477ea70 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Mon, 19 Oct 2026 15:35:59 -0400 Subject: [PATCH] fix(a11y): axe findings: dialog names, aria-pressed, labels, avatar alt, placeholder contrast (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modal500 takes a label: Room/Space Settings (" settings") and Settings dialogs have accessible names. - aria-selected on plain buttons (inbox filter chips, date/time picker chips, verification/usage/notification menu items) -> aria-pressed; the Permissions power chips open a menu -> aria-haspopup + aria-expanded. - Labels tied to the room pack name input and the Export From/To dates. - UserAvatar defaults to alt="" (decorative next to a name / in a labelled button) unless a caller passes one. - Composer placeholder: Slate's inline opacity 0.333 overrode the #222 AA-contrast class (2.66:1); it is dropped so the class (0.75) applies. - e2e/a11y.spec.ts also audits Room Settings → Permissions, Emojis & Stickers and Export, checks the dialog name, and audits the inbox. Fixes #278 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- e2e/a11y.spec.ts | 20 ++++++++++++++++++- src/app/components/ManualVerification.tsx | 4 ++-- src/app/components/Modal500.tsx | 5 ++++- src/app/components/editor/Editor.tsx | 9 ++++++++- .../image-pack-view/UsageSwitcher.tsx | 2 +- src/app/components/time-date/DatePicker.tsx | 6 +++--- src/app/components/time-date/TimePicker.tsx | 10 +++++----- src/app/components/user-avatar/UserAvatar.tsx | 4 +++- .../emojis-stickers/RoomPacks.tsx | 5 ++++- .../permissions/PermissionGroups.tsx | 6 ++++-- .../room-settings/ExportRoomHistory.tsx | 10 ++++++++-- .../room-settings/RoomSettingsRenderer.tsx | 2 +- .../NotificationModeSwitcher.tsx | 2 +- .../space-settings/SpaceSettingsRenderer.tsx | 2 +- src/app/pages/client/inbox/Invites.tsx | 6 +++--- src/app/pages/client/sidebar/SettingsTab.tsx | 2 +- .../pages/client/sidebar/UnverifiedTab.tsx | 2 +- 17 files changed, 69 insertions(+), 28 deletions(-) diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index 1c233f715..12bcfb869 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -118,8 +118,26 @@ test.describe('accessibility', () => { test('room settings', async ({ page }) => { await page.getByRole('button', { name: 'More Options' }).first().click(); await page.getByText('Room Settings', { exact: true }).click(); - await expect(page.getByRole('dialog').first()).toBeVisible(); + // [Gitea #278] The dialog has an accessible name. + await expect(page.getByRole('dialog', { name: 'A11y Room settings' })).toBeVisible(); await auditPage(page, 'room settings'); + // [Gitea #278] aria-selected on the power chips, unlabelled pack name and + // export date inputs were only on these pages. + // One page at a time, in order. + for (const name of ['Permissions', 'Emojis & Stickers', 'Export']) { + // eslint-disable-next-line no-await-in-loop + await page.getByRole('dialog').getByText(name, { exact: true }).first().click(); + // eslint-disable-next-line no-await-in-loop + await page.waitForTimeout(1000); + // eslint-disable-next-line no-await-in-loop + await auditPage(page, `room settings: ${name}`); + } + }); + + test('inbox invites', async ({ page }) => { + await page.goto('/inbox/invites/'); + await expect(page.getByRole('button', { name: /Known|Unknown|Spam/ }).first()).toBeVisible(); + await auditPage(page, 'inbox invites'); }); }); }); diff --git a/src/app/components/ManualVerification.tsx b/src/app/components/ManualVerification.tsx index e451aa450..158d6e233 100644 --- a/src/app/components/ManualVerification.tsx +++ b/src/app/components/ManualVerification.tsx @@ -82,7 +82,7 @@ export function ManualVerificationMethodSwitcher({ handleSelect(ManualVerificationMethod.RecoveryPassphrase)} > @@ -93,7 +93,7 @@ export function ManualVerificationMethodSwitcher({ handleSelect(ManualVerificationMethod.RecoveryKey)} > diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx index 7281a421d..fc9a3fbf9 100644 --- a/src/app/components/Modal500.tsx +++ b/src/app/components/Modal500.tsx @@ -7,8 +7,10 @@ import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize'; type Modal500Props = { requestClose: () => void; children: ReactNode; + /** [Gitea #278] The dialog's accessible name (role="dialog" needs one). */ + label: string; }; -export function Modal500({ requestClose, children }: Modal500Props) { +export function Modal500({ requestClose, children, label }: Modal500Props) { const isMobile = useScreenSizeContext() === ScreenSize.Mobile; return ( }> @@ -26,6 +28,7 @@ export function Modal500({ requestClose, children }: Modal500Props) { variant="Background" role="dialog" aria-modal="true" + aria-label={label} // On mobile expand to fill the viewport. On desktop fall back to the // folds `size="500"` width (~50rem) — overriding maxWidth here would // squish the two-pane settings layout. diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index e72605001..4f50a1a29 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -117,7 +117,14 @@ export const CustomEditor = forwardRef( const renderPlaceholder = useCallback( ({ attributes, children }: RenderPlaceholderProps) => ( - + {/* Inner component to style the actual text position and appearance */} {children} diff --git a/src/app/components/image-pack-view/UsageSwitcher.tsx b/src/app/components/image-pack-view/UsageSwitcher.tsx index 6823ae0ec..e4f148d95 100644 --- a/src/app/components/image-pack-view/UsageSwitcher.tsx +++ b/src/app/components/image-pack-view/UsageSwitcher.tsx @@ -39,7 +39,7 @@ export function UsageSelector({ selected, onChange }: UsageSelectorProps) { key={getUsageStr(usage)} size="300" variant={isSelected(usage) ? 'SurfaceVariant' : 'Surface'} - aria-selected={isSelected(usage)} + aria-pressed={isSelected(usage)} radii="300" onClick={() => onChange(usage)} > diff --git a/src/app/components/time-date/DatePicker.tsx b/src/app/components/time-date/DatePicker.tsx index d822a5596..0b52c9688 100644 --- a/src/app/components/time-date/DatePicker.tsx +++ b/src/app/components/time-date/DatePicker.tsx @@ -69,7 +69,7 @@ export const DatePicker = forwardRef( variant={selectedDay === day ? 'Primary' : 'SurfaceVariant'} fill="None" radii="300" - aria-selected={selectedDay === day} + aria-pressed={selectedDay === day} onClick={() => handleDay(day)} disabled={ (selectedYear === minYear && selectedMonth === minMonth && day < minDay) || @@ -90,7 +90,7 @@ export const DatePicker = forwardRef( variant={selectedMonth === month ? 'Primary' : 'SurfaceVariant'} fill="None" radii="300" - aria-selected={selectedMonth === month} + aria-pressed={selectedMonth === month} onClick={() => handleMonth(month)} disabled={ (selectedYear === minYear && month < minMonth) || @@ -115,7 +115,7 @@ export const DatePicker = forwardRef( variant={selectedYear === year ? 'Primary' : 'SurfaceVariant'} fill="None" radii="300" - aria-selected={selectedYear === year} + aria-pressed={selectedYear === year} onClick={() => handleYear(year)} > {year} diff --git a/src/app/components/time-date/TimePicker.tsx b/src/app/components/time-date/TimePicker.tsx index 399c996f4..7d11168ae 100644 --- a/src/app/components/time-date/TimePicker.tsx +++ b/src/app/components/time-date/TimePicker.tsx @@ -71,7 +71,7 @@ export const TimePicker = forwardRef( variant={hour === selectedHour ? 'Primary' : 'Background'} fill="None" radii="300" - aria-selected={hour === selectedHour} + aria-pressed={hour === selectedHour} onClick={() => handleHour(hour)} disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)} > @@ -90,7 +90,7 @@ export const TimePicker = forwardRef( variant={hour === selectedHour ? 'Primary' : 'Background'} fill="None" radii="300" - aria-selected={hour === selectedHour} + aria-pressed={hour === selectedHour} onClick={() => handleHour(hour)} disabled={ (minDay && hour12to24(hour, selectedPM) < minHour24) || @@ -109,7 +109,7 @@ export const TimePicker = forwardRef( variant={minute === selectedMinute ? 'Primary' : 'Background'} fill="None" radii="300" - aria-selected={minute === selectedMinute} + aria-pressed={minute === selectedMinute} onClick={() => handleMinute(minute)} disabled={ (minDay && hour24 === minHour24 && minute < minMinute) || @@ -127,7 +127,7 @@ export const TimePicker = forwardRef( variant={!selectedPM ? 'Primary' : 'SurfaceVariant'} fill="None" radii="300" - aria-selected={!selectedPM} + aria-pressed={!selectedPM} onClick={() => handlePeriod(false)} disabled={minDay && minPM} > @@ -138,7 +138,7 @@ export const TimePicker = forwardRef( variant={selectedPM ? 'Primary' : 'SurfaceVariant'} fill="None" radii="300" - aria-selected={selectedPM} + aria-pressed={selectedPM} onClick={() => handlePeriod(true)} disabled={maxDay && !maxPM} > diff --git a/src/app/components/user-avatar/UserAvatar.tsx b/src/app/components/user-avatar/UserAvatar.tsx index d9de9b79f..2bc1b9247 100644 --- a/src/app/components/user-avatar/UserAvatar.tsx +++ b/src/app/components/user-avatar/UserAvatar.tsx @@ -33,7 +33,9 @@ export function UserAvatar({ className, userId, src, alt, renderFallback }: User setError(true)} onLoad={handleLoad} draggable={false} diff --git a/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx b/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx index eb0b45503..1b3892a0b 100644 --- a/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx +++ b/src/app/features/common-settings/emojis-stickers/RoomPacks.tsx @@ -109,8 +109,11 @@ function CreatePackTile({ packs, roomId }: CreatePackTileProps) { alignItems="End" > - Name + + Name + - From + + From + - To + + To + + diff --git a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx index 8fd524341..52a26afc0 100644 --- a/src/app/features/settings/notifications/NotificationModeSwitcher.tsx +++ b/src/app/features/settings/notifications/NotificationModeSwitcher.tsx @@ -98,7 +98,7 @@ export function NotificationModeSwitcher({ pushRule, onChange }: NotificationMod key={mode} size="300" variant="Surface" - aria-selected={mode === selectedMode} + aria-pressed={mode === selectedMode} radii="300" onClick={() => handleSelect(mode)} > diff --git a/src/app/features/space-settings/SpaceSettingsRenderer.tsx b/src/app/features/space-settings/SpaceSettingsRenderer.tsx index 79947e0ca..a1c97a7b9 100644 --- a/src/app/features/space-settings/SpaceSettingsRenderer.tsx +++ b/src/app/features/space-settings/SpaceSettingsRenderer.tsx @@ -21,7 +21,7 @@ function RenderSettings({ state }: RenderSettingsProps) { if (!room) return null; return ( - + diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx index 0768f2aca..803bf5b2d 100644 --- a/src/app/pages/client/inbox/Invites.tsx +++ b/src/app/pages/client/inbox/Invites.tsx @@ -369,7 +369,7 @@ function InviteFilters({ onFilter(InviteFilter.Known)} before={isKnown && } @@ -385,7 +385,7 @@ function InviteFilters({ onFilter(InviteFilter.Unknown)} before={isUnknown && } @@ -401,7 +401,7 @@ function InviteFilters({ onFilter(InviteFilter.Spam)} before={isSpam && } diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx index cf063e0e3..081d87c95 100644 --- a/src/app/pages/client/sidebar/SettingsTab.tsx +++ b/src/app/pages/client/sidebar/SettingsTab.tsx @@ -217,7 +217,7 @@ export function SettingsTab() { {settingsOpen && ( - setSettingsOpen(false)}> + setSettingsOpen(false)} label="Settings"> setSettingsOpen(false)} /> )} diff --git a/src/app/pages/client/sidebar/UnverifiedTab.tsx b/src/app/pages/client/sidebar/UnverifiedTab.tsx index 1da7a30a5..a18684dd8 100644 --- a/src/app/pages/client/sidebar/UnverifiedTab.tsx +++ b/src/app/pages/client/sidebar/UnverifiedTab.tsx @@ -82,7 +82,7 @@ function UnverifiedIndicator() { )} {settings && ( - + )}