Merge pull request 'fix(a11y): dialog names, aria-pressed, labels, avatar alt, placeholder contrast (#278)' (#436) from fix/a11y-axe-findings into lotus
CI / Build & Quality Checks (push) Successful in 2m29s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 10s
CI / Playwright smoke (e2e) (push) Successful in 16m49s
CI / Build & Quality Checks (push) Successful in 2m29s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 10s
CI / Playwright smoke (e2e) (push) Successful in 16m49s
This commit was merged in pull request #436.
This commit is contained in:
+19
-1
@@ -118,8 +118,26 @@ test.describe('accessibility', () => {
|
|||||||
test('room settings', async ({ page }) => {
|
test('room settings', async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'More Options' }).first().click();
|
await page.getByRole('button', { name: 'More Options' }).first().click();
|
||||||
await page.getByText('Room Settings', { exact: true }).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');
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function ManualVerificationMethodSwitcher({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
size="300"
|
size="300"
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
aria-selected={value === ManualVerificationMethod.RecoveryPassphrase}
|
aria-pressed={value === ManualVerificationMethod.RecoveryPassphrase}
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={() => handleSelect(ManualVerificationMethod.RecoveryPassphrase)}
|
onClick={() => handleSelect(ManualVerificationMethod.RecoveryPassphrase)}
|
||||||
>
|
>
|
||||||
@@ -93,7 +93,7 @@ export function ManualVerificationMethodSwitcher({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
size="300"
|
size="300"
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
aria-selected={value === ManualVerificationMethod.RecoveryKey}
|
aria-pressed={value === ManualVerificationMethod.RecoveryKey}
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={() => handleSelect(ManualVerificationMethod.RecoveryKey)}
|
onClick={() => handleSelect(ManualVerificationMethod.RecoveryKey)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
|||||||
type Modal500Props = {
|
type Modal500Props = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
children: ReactNode;
|
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;
|
const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
@@ -26,6 +28,7 @@ export function Modal500({ requestClose, children }: Modal500Props) {
|
|||||||
variant="Background"
|
variant="Background"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
aria-label={label}
|
||||||
// On mobile expand to fill the viewport. On desktop fall back to the
|
// On mobile expand to fill the viewport. On desktop fall back to the
|
||||||
// folds `size="500"` width (~50rem) — overriding maxWidth here would
|
// folds `size="500"` width (~50rem) — overriding maxWidth here would
|
||||||
// squish the two-pane settings layout.
|
// squish the two-pane settings layout.
|
||||||
|
|||||||
@@ -117,7 +117,14 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||||||
|
|
||||||
const renderPlaceholder = useCallback(
|
const renderPlaceholder = useCallback(
|
||||||
({ attributes, children }: RenderPlaceholderProps) => (
|
({ attributes, children }: RenderPlaceholderProps) => (
|
||||||
<span {...attributes} className={css.EditorPlaceholderContainer}>
|
<span
|
||||||
|
{...attributes}
|
||||||
|
// [Gitea #278] Slate puts an inline `opacity: 0.333` on the placeholder,
|
||||||
|
// which beat EditorPlaceholderContainer's AA-contrast opacity (#222):
|
||||||
|
// measured 2.66:1. Drop it so the class decides.
|
||||||
|
style={{ ...attributes.style, opacity: undefined }}
|
||||||
|
className={css.EditorPlaceholderContainer}
|
||||||
|
>
|
||||||
{/* Inner component to style the actual text position and appearance */}
|
{/* Inner component to style the actual text position and appearance */}
|
||||||
<Text as="span" className={css.EditorPlaceholderTextVisual} truncate>
|
<Text as="span" className={css.EditorPlaceholderTextVisual} truncate>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function UsageSelector({ selected, onChange }: UsageSelectorProps) {
|
|||||||
key={getUsageStr(usage)}
|
key={getUsageStr(usage)}
|
||||||
size="300"
|
size="300"
|
||||||
variant={isSelected(usage) ? 'SurfaceVariant' : 'Surface'}
|
variant={isSelected(usage) ? 'SurfaceVariant' : 'Surface'}
|
||||||
aria-selected={isSelected(usage)}
|
aria-pressed={isSelected(usage)}
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={() => onChange(usage)}
|
onClick={() => onChange(usage)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
|||||||
variant={selectedDay === day ? 'Primary' : 'SurfaceVariant'}
|
variant={selectedDay === day ? 'Primary' : 'SurfaceVariant'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={selectedDay === day}
|
aria-pressed={selectedDay === day}
|
||||||
onClick={() => handleDay(day)}
|
onClick={() => handleDay(day)}
|
||||||
disabled={
|
disabled={
|
||||||
(selectedYear === minYear && selectedMonth === minMonth && day < minDay) ||
|
(selectedYear === minYear && selectedMonth === minMonth && day < minDay) ||
|
||||||
@@ -90,7 +90,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
|||||||
variant={selectedMonth === month ? 'Primary' : 'SurfaceVariant'}
|
variant={selectedMonth === month ? 'Primary' : 'SurfaceVariant'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={selectedMonth === month}
|
aria-pressed={selectedMonth === month}
|
||||||
onClick={() => handleMonth(month)}
|
onClick={() => handleMonth(month)}
|
||||||
disabled={
|
disabled={
|
||||||
(selectedYear === minYear && month < minMonth) ||
|
(selectedYear === minYear && month < minMonth) ||
|
||||||
@@ -115,7 +115,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
|||||||
variant={selectedYear === year ? 'Primary' : 'SurfaceVariant'}
|
variant={selectedYear === year ? 'Primary' : 'SurfaceVariant'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={selectedYear === year}
|
aria-pressed={selectedYear === year}
|
||||||
onClick={() => handleYear(year)}
|
onClick={() => handleYear(year)}
|
||||||
>
|
>
|
||||||
<Text size="T300">{year}</Text>
|
<Text size="T300">{year}</Text>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||||||
variant={hour === selectedHour ? 'Primary' : 'Background'}
|
variant={hour === selectedHour ? 'Primary' : 'Background'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={hour === selectedHour}
|
aria-pressed={hour === selectedHour}
|
||||||
onClick={() => handleHour(hour)}
|
onClick={() => handleHour(hour)}
|
||||||
disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
|
disabled={(minDay && hour < minHour24) || (maxDay && hour > maxHour24)}
|
||||||
>
|
>
|
||||||
@@ -90,7 +90,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||||||
variant={hour === selectedHour ? 'Primary' : 'Background'}
|
variant={hour === selectedHour ? 'Primary' : 'Background'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={hour === selectedHour}
|
aria-pressed={hour === selectedHour}
|
||||||
onClick={() => handleHour(hour)}
|
onClick={() => handleHour(hour)}
|
||||||
disabled={
|
disabled={
|
||||||
(minDay && hour12to24(hour, selectedPM) < minHour24) ||
|
(minDay && hour12to24(hour, selectedPM) < minHour24) ||
|
||||||
@@ -109,7 +109,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||||||
variant={minute === selectedMinute ? 'Primary' : 'Background'}
|
variant={minute === selectedMinute ? 'Primary' : 'Background'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={minute === selectedMinute}
|
aria-pressed={minute === selectedMinute}
|
||||||
onClick={() => handleMinute(minute)}
|
onClick={() => handleMinute(minute)}
|
||||||
disabled={
|
disabled={
|
||||||
(minDay && hour24 === minHour24 && minute < minMinute) ||
|
(minDay && hour24 === minHour24 && minute < minMinute) ||
|
||||||
@@ -127,7 +127,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||||||
variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
|
variant={!selectedPM ? 'Primary' : 'SurfaceVariant'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={!selectedPM}
|
aria-pressed={!selectedPM}
|
||||||
onClick={() => handlePeriod(false)}
|
onClick={() => handlePeriod(false)}
|
||||||
disabled={minDay && minPM}
|
disabled={minDay && minPM}
|
||||||
>
|
>
|
||||||
@@ -138,7 +138,7 @@ export const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
|
|||||||
variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
|
variant={selectedPM ? 'Primary' : 'SurfaceVariant'}
|
||||||
fill="None"
|
fill="None"
|
||||||
radii="300"
|
radii="300"
|
||||||
aria-selected={selectedPM}
|
aria-pressed={selectedPM}
|
||||||
onClick={() => handlePeriod(true)}
|
onClick={() => handlePeriod(true)}
|
||||||
disabled={maxDay && !maxPM}
|
disabled={maxDay && !maxPM}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function UserAvatar({ className, userId, src, alt, renderFallback }: User
|
|||||||
<AvatarImage
|
<AvatarImage
|
||||||
className={classNames(css.UserAvatar, className)}
|
className={classNames(css.UserAvatar, className)}
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt}
|
// [Gitea #278] An avatar sits next to the person's name or inside a
|
||||||
|
// labelled button, so without an explicit alt it is decorative.
|
||||||
|
alt={alt ?? ''}
|
||||||
onError={() => setError(true)}
|
onError={() => setError(true)}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
|
|||||||
@@ -109,8 +109,11 @@ function CreatePackTile({ packs, roomId }: CreatePackTileProps) {
|
|||||||
alignItems="End"
|
alignItems="End"
|
||||||
>
|
>
|
||||||
<Box direction="Column" gap="100" grow="Yes">
|
<Box direction="Column" gap="100" grow="Yes">
|
||||||
<Text size="L400">Name</Text>
|
<Text as="label" htmlFor="room-pack-name" size="L400">
|
||||||
|
Name
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
id="room-pack-name"
|
||||||
name="nameInput"
|
name="nameInput"
|
||||||
required
|
required
|
||||||
size="400"
|
size="400"
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ export function PermissionGroups({
|
|||||||
outlined={powerChanges}
|
outlined={powerChanges}
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
aria-selected={opened}
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={opened}
|
||||||
disabled={!canEdit || applyingChanges}
|
disabled={!canEdit || applyingChanges}
|
||||||
after={
|
after={
|
||||||
powerChanges && (
|
powerChanges && (
|
||||||
@@ -200,7 +201,8 @@ export function PermissionGroups({
|
|||||||
outlined={powerChanges}
|
outlined={powerChanges}
|
||||||
fill="Soft"
|
fill="Soft"
|
||||||
radii="Pill"
|
radii="Pill"
|
||||||
aria-selected={opened}
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={opened}
|
||||||
disabled={!canEdit || applyingChanges}
|
disabled={!canEdit || applyingChanges}
|
||||||
after={
|
after={
|
||||||
powerChanges && (
|
powerChanges && (
|
||||||
|
|||||||
@@ -353,8 +353,11 @@ ${msgRows}
|
|||||||
>
|
>
|
||||||
<Box gap="400" wrap="Wrap">
|
<Box gap="400" wrap="Wrap">
|
||||||
<Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
|
<Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
|
||||||
<Text size="T300">From</Text>
|
<Text as="label" htmlFor="export-from-date" size="T300">
|
||||||
|
From
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
id="export-from-date"
|
||||||
type="date"
|
type="date"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
size="400"
|
size="400"
|
||||||
@@ -364,8 +367,11 @@ ${msgRows}
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
|
<Box direction="Column" gap="100" style={{ flex: 1, minWidth: 160 }}>
|
||||||
<Text size="T300">To</Text>
|
<Text as="label" htmlFor="export-to-date" size="T300">
|
||||||
|
To
|
||||||
|
</Text>
|
||||||
<Input
|
<Input
|
||||||
|
id="export-to-date"
|
||||||
type="date"
|
type="date"
|
||||||
variant="Secondary"
|
variant="Secondary"
|
||||||
size="400"
|
size="400"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function RenderSettings({ state }: RenderSettingsProps) {
|
|||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal500 requestClose={closeSettings}>
|
<Modal500 requestClose={closeSettings} label={`${room.name} settings`}>
|
||||||
<SpaceProvider value={space ?? null}>
|
<SpaceProvider value={space ?? null}>
|
||||||
<RoomProvider value={room}>
|
<RoomProvider value={room}>
|
||||||
<RoomSettings initialPage={page} requestClose={closeSettings} />
|
<RoomSettings initialPage={page} requestClose={closeSettings} />
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function NotificationModeSwitcher({ pushRule, onChange }: NotificationMod
|
|||||||
key={mode}
|
key={mode}
|
||||||
size="300"
|
size="300"
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
aria-selected={mode === selectedMode}
|
aria-pressed={mode === selectedMode}
|
||||||
radii="300"
|
radii="300"
|
||||||
onClick={() => handleSelect(mode)}
|
onClick={() => handleSelect(mode)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function RenderSettings({ state }: RenderSettingsProps) {
|
|||||||
if (!room) return null;
|
if (!room) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal500 requestClose={closeSettings}>
|
<Modal500 requestClose={closeSettings} label={`${room.name} space settings`}>
|
||||||
<SpaceProvider value={space ?? null}>
|
<SpaceProvider value={space ?? null}>
|
||||||
<RoomProvider value={room}>
|
<RoomProvider value={room}>
|
||||||
<SpaceSettings initialPage={page} requestClose={closeSettings} />
|
<SpaceSettings initialPage={page} requestClose={closeSettings} />
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ function InviteFilters({
|
|||||||
<Box gap="200">
|
<Box gap="200">
|
||||||
<Chip
|
<Chip
|
||||||
variant={isKnown ? 'Success' : 'Surface'}
|
variant={isKnown ? 'Success' : 'Surface'}
|
||||||
aria-selected={isKnown}
|
aria-pressed={isKnown}
|
||||||
outlined={!isKnown}
|
outlined={!isKnown}
|
||||||
onClick={() => onFilter(InviteFilter.Known)}
|
onClick={() => onFilter(InviteFilter.Known)}
|
||||||
before={isKnown && <Icon size="100" src={Icons.Check} />}
|
before={isKnown && <Icon size="100" src={Icons.Check} />}
|
||||||
@@ -385,7 +385,7 @@ function InviteFilters({
|
|||||||
</Chip>
|
</Chip>
|
||||||
<Chip
|
<Chip
|
||||||
variant={isUnknown ? 'Warning' : 'Surface'}
|
variant={isUnknown ? 'Warning' : 'Surface'}
|
||||||
aria-selected={isUnknown}
|
aria-pressed={isUnknown}
|
||||||
outlined={!isUnknown}
|
outlined={!isUnknown}
|
||||||
onClick={() => onFilter(InviteFilter.Unknown)}
|
onClick={() => onFilter(InviteFilter.Unknown)}
|
||||||
before={isUnknown && <Icon size="100" src={Icons.Check} />}
|
before={isUnknown && <Icon size="100" src={Icons.Check} />}
|
||||||
@@ -401,7 +401,7 @@ function InviteFilters({
|
|||||||
</Chip>
|
</Chip>
|
||||||
<Chip
|
<Chip
|
||||||
variant={isSpam ? 'Critical' : 'Surface'}
|
variant={isSpam ? 'Critical' : 'Surface'}
|
||||||
aria-selected={isSpam}
|
aria-pressed={isSpam}
|
||||||
outlined={!isSpam}
|
outlined={!isSpam}
|
||||||
onClick={() => onFilter(InviteFilter.Spam)}
|
onClick={() => onFilter(InviteFilter.Spam)}
|
||||||
before={isSpam && <Icon size="100" src={Icons.Check} />}
|
before={isSpam && <Icon size="100" src={Icons.Check} />}
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export function SettingsTab() {
|
|||||||
</SidebarItemTooltip>
|
</SidebarItemTooltip>
|
||||||
<PresencePicker />
|
<PresencePicker />
|
||||||
{settingsOpen && (
|
{settingsOpen && (
|
||||||
<Modal500 requestClose={() => setSettingsOpen(false)}>
|
<Modal500 requestClose={() => setSettingsOpen(false)} label="Settings">
|
||||||
<Settings initialPage={initialPage} requestClose={() => setSettingsOpen(false)} />
|
<Settings initialPage={initialPage} requestClose={() => setSettingsOpen(false)} />
|
||||||
</Modal500>
|
</Modal500>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function UnverifiedIndicator() {
|
|||||||
</SidebarItem>
|
</SidebarItem>
|
||||||
)}
|
)}
|
||||||
{settings && (
|
{settings && (
|
||||||
<Modal500 requestClose={closeSettings}>
|
<Modal500 requestClose={closeSettings} label="Settings">
|
||||||
<Settings initialPage={SettingsPages.DevicesPage} requestClose={closeSettings} />
|
<Settings initialPage={SettingsPages.DevicesPage} requestClose={closeSettings} />
|
||||||
</Modal500>
|
</Modal500>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user