diff --git a/src/components/__tests__/radio-options.test.ts b/src/components/__tests__/radio-options.test.ts new file mode 100644 index 00000000..e928627f --- /dev/null +++ b/src/components/__tests__/radio-options.test.ts @@ -0,0 +1,37 @@ +// The undo-send delay chips (and the other Composer choices), the Files sort +// sheet and the language list drew the picked option in another colour only: +// a screen reader heard every option alike and couldn't tell which was set. +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { radioOptionA11y, RADIO_GROUP_A11Y } from '../radio-a11y'; + +const SRC = path.resolve(__dirname, '../..'); +const read = (rel: string) => fs.readFileSync(path.join(SRC, rel), 'utf8').split('\r\n').join('\n'); + +describe('radioOptionA11y', () => { + it('gives the picked option the radio role and the selected and checked state', () => { + expect(radioOptionA11y(true)).toEqual({ accessibilityRole: 'radio', accessibilityState: { selected: true, checked: true } }); + expect(radioOptionA11y(false).accessibilityState).toEqual({ selected: false, checked: false }); + expect(RADIO_GROUP_A11Y.accessibilityRole).toBe('radiogroup'); + }); +}); + +describe('option groups', () => { + it('Composer chips (undo-send delay, autosave, signature) say which one is set', () => { + const source = read('components/settings/ComposingSettings.tsx'); + const segmented = source.slice(source.indexOf('const segmented'), source.indexOf('return (\n { + const source = read('screens/FilesScreen.tsx'); + expect(source).toContain('{...radioOptionA11y(sortKey === k)}'); + expect(source).toContain('{...radioOptionA11y(sortDir === d)}'); + }); + + it('the language list says which language is set', () => { + expect(read('components/settings/LanguageSettings.tsx')).toContain('{...radioOptionA11y(active)}'); + }); +}); diff --git a/src/components/radio-a11y.ts b/src/components/radio-a11y.ts new file mode 100644 index 00000000..06a6b0b1 --- /dev/null +++ b/src/components/radio-a11y.ts @@ -0,0 +1,17 @@ +// One choice of a set of options (the undo-send delay, the Files sort order, +// the app language). Drawn as a styled Pressable, an option told a screen +// reader nothing about which one was picked: TalkBack read "5 seconds" for +// every chip alike. A radio role with the selected and checked state reads +// "selected" (Android) or "checked" (iOS) on the current one. + +export interface RadioOptionA11y { + accessibilityRole: 'radio'; + accessibilityState: { selected: boolean; checked: boolean }; +} + +export function radioOptionA11y(selected: boolean): RadioOptionA11y { + return { accessibilityRole: 'radio', accessibilityState: { selected, checked: selected } }; +} + +/** For the View around the options. */ +export const RADIO_GROUP_A11Y = { accessibilityRole: 'radiogroup' } as const; diff --git a/src/components/settings/ComposingSettings.tsx b/src/components/settings/ComposingSettings.tsx index 50d49c30..abd83843 100644 --- a/src/components/settings/ComposingSettings.tsx +++ b/src/components/settings/ComposingSettings.tsx @@ -8,6 +8,7 @@ import { useColors } from '../../theme/colors'; import { useSettingsStore, type ReplyIdentityMatch, type SignaturePosition } from '../../stores/settings-store'; import { useLocaleStore } from '../../stores/locale-store'; import { SUPPORTED_SUB_ADDRESS_DELIMITERS } from '../../lib/sub-addressing'; +import { RADIO_GROUP_A11Y, radioOptionA11y } from '../radio-a11y'; export function ComposingSettings() { const c = useColors(); @@ -82,12 +83,13 @@ export function ComposingSettings() { current: T, onPick: (value: T) => void, ) => ( - + {options.map((opt) => { const active = current === opt.value; return ( onPick(opt.value)} style={[styles.segment, active && styles.segmentActive]} > diff --git a/src/components/settings/LanguageSettings.tsx b/src/components/settings/LanguageSettings.tsx index 320da97d..0a43cf9f 100644 --- a/src/components/settings/LanguageSettings.tsx +++ b/src/components/settings/LanguageSettings.tsx @@ -8,6 +8,7 @@ import { useLocaleStore } from '../../stores/locale-store'; import { useSettingsStore, type DateFormat, type TimeFormat } from '../../stores/settings-store'; import { formatListDate } from '../../lib/date-format'; import { SUPPORTED_LOCALES, detectDeviceLocale, type LocaleCode } from '../../i18n'; +import { radioOptionA11y } from '../radio-a11y'; export function LanguageSettings() { const c = useColors(); @@ -43,6 +44,7 @@ export function LanguageSettings() { return ( setOverride(code === 'system' ? null : (code as LocaleCode))} style={({ pressed }) => [styles.row, pressed && styles.rowPressed]} > diff --git a/src/screens/FilesScreen.tsx b/src/screens/FilesScreen.tsx index d9c040d0..8aafeb73 100644 --- a/src/screens/FilesScreen.tsx +++ b/src/screens/FilesScreen.tsx @@ -72,6 +72,7 @@ import { formatListDate } from '../lib/date-format'; import { useLayout, useSheetInsets } from '../lib/use-layout'; import { useSheetBottomInset } from '../lib/sheet-bottom-inset'; import { filesListWidth } from '../lib/files-split'; +import { radioOptionA11y } from '../components/radio-a11y'; import { useBackWhileFocused } from '../lib/use-back-while-focused'; import { filesBackStep } from '../lib/files-back'; import { driveChange, type DriveScope } from '../lib/files-drive'; @@ -2148,7 +2149,7 @@ function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChang {t('files.settings_default_sort', 'Sort')} {keys.map((k) => ( - { onChangeKey(k); onClose(); }}> + { onChangeKey(k); onClose(); }}> {t(`files.${k}`, k)} @@ -2157,7 +2158,7 @@ function SortSheet({ visible, sortKey, sortDir, t, onClose, onChangeKey, onChang ))} {(['asc', 'desc'] as FilesSortDir[]).map((d) => ( - { onChangeDir(d); onClose(); }}> + { onChangeDir(d); onClose(); }}> {d === 'asc' ? : } {d === 'asc'