From 7ba00be60e9e5f01fb4e0e9eea8f9b2f70da302c Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 05:51:52 +0100 Subject: [PATCH] fix: give the contact, event and files fields a name for screen readers --- .../__tests__/field-names-pim.test.ts | 58 +++++++++++++++++++ src/components/calendar/CalendarEditSheet.tsx | 2 + src/components/calendar/EventModal.tsx | 5 ++ src/components/calendar/ParticipantInput.tsx | 1 + src/components/calendar/TasksSheet.tsx | 2 + .../contacts/AddressBookPickerSheet.tsx | 1 + .../contacts/ContactPickerSheet.tsx | 1 + .../contacts/ContactsSidebarDrawer.tsx | 1 + src/components/contacts/TagAssignSheet.tsx | 1 + src/components/files/ShareSheet.tsx | 1 + src/components/settings/VacationSettings.tsx | 3 + .../sharing/ShareCollectionSheet.tsx | 1 + src/screens/ContactFormScreen.tsx | 31 ++++++++++ src/screens/ContactsScreen.tsx | 1 + src/screens/FilesScreen.tsx | 2 + 15 files changed, 111 insertions(+) create mode 100644 src/components/__tests__/field-names-pim.test.ts diff --git a/src/components/__tests__/field-names-pim.test.ts b/src/components/__tests__/field-names-pim.test.ts new file mode 100644 index 00000000..9469f65a --- /dev/null +++ b/src/components/__tests__/field-names-pim.test.ts @@ -0,0 +1,58 @@ +// Text fields need a name a screen reader can say (file 11 "Form fields +// have no name for screen readers"; field-names.test.ts covers Settings and +// sign-in). The contact form, the event editor and the calendar, contacts +// and files sheets had fields whose only name was their placeholder, or +// their value (the custom reminder's "30"). Scanned so a bare field doesn't +// creep back in. +import { describe, expect, it } from 'vitest'; +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const SRC = join(__dirname, '..', '..'); + +function elementEnd(source: string, from: number): number { + let depth = 0; + for (let i = from; i < source.length; i++) { + const ch = source[i]; + if (ch === '{') depth += 1; + else if (ch === '}') depth -= 1; + else if (depth === 0 && (ch === '>' || (ch === '/' && source[i + 1] === '>'))) return i; + } + return source.length; +} + +/** `file:line` of every TextInput element without an accessibilityLabel. */ +function unnamedFields(file: string): string[] { + const source = readFileSync(join(SRC, file), 'utf8'); + const out: string[] = []; + // `` as a type argument (useRef) is not an element. + for (const m of source.matchAll(/ readdirSync(join(SRC, rel)) + .filter((name) => name.endsWith('.tsx')) + .map((name) => `${rel}/${name}`); + +const FILES = [ + ...dir('components/calendar'), + ...dir('components/contacts'), + ...dir('components/files'), + ...dir('components/sharing'), + 'components/settings/VacationSettings.tsx', + 'screens/CalendarScreen.tsx', + 'screens/ContactFormScreen.tsx', + 'screens/ContactsScreen.tsx', + 'screens/ContactDetailScreen.tsx', + 'screens/FilesScreen.tsx', +]; + +describe('field names in calendar, contacts and files', () => { + it('every field has a name', () => { + expect(FILES.flatMap(unnamedFields)).toEqual([]); + }); +}); diff --git a/src/components/calendar/CalendarEditSheet.tsx b/src/components/calendar/CalendarEditSheet.tsx index b6130c0b..67d9f5c3 100644 --- a/src/components/calendar/CalendarEditSheet.tsx +++ b/src/components/calendar/CalendarEditSheet.tsx @@ -225,6 +225,7 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend {t('calendar.management.name', 'Name')} {t('calendar.management.description', 'Description')} setCustomAmount(v.replace(/[^0-9]/g, ''))} keyboardType="number-pad" @@ -834,6 +836,7 @@ export function EventModal({
} label={t('calendar.form.location', 'Location')}> } label={t('calendar.form.meeting_link', 'Meeting link')}> setEditor((e) => ({ ...e, title: v }))} placeholder={editor.id ? t('calendar.tasks.title_placeholder', 'Task title') : t('calendar.tasks.add_placeholder', 'Add a task…')} @@ -305,6 +306,7 @@ export function TasksSheet({ {expanded && ( setEditor((e) => ({ ...e, description: v }))} placeholder={t('calendar.tasks.description_placeholder', 'Notes')} diff --git a/src/components/contacts/AddressBookPickerSheet.tsx b/src/components/contacts/AddressBookPickerSheet.tsx index d8958d63..ef6ce5f2 100644 --- a/src/components/contacts/AddressBookPickerSheet.tsx +++ b/src/components/contacts/AddressBookPickerSheet.tsx @@ -128,6 +128,7 @@ export default function AddressBookPickerSheet({ {creating ? ( {t('files.share_add_people', 'Add people')} {t('settings.vacation.message.body_label', 'Message Body')} {t('settings.vacation.message.body_description', 'Plain text message content')} ({ {t('calendar.share.add_people', 'Add people')} updateForm('notes', form.notes.filter((_, idx) => idx !== i))} > )} } label={t('contacts.group', 'Group')}> updateForm('emails', form.emails.filter((_, idx) => idx !== i))} > updateForm('phones', form.phones.filter((_, idx) => idx !== i))} > updateForm('addresses', form.addresses.filter((_, idx) => idx !== i))} > updateForm('personalInfo', form.personalInfo.filter((_, idx) => idx !== i))} > {searchOpen ? ( {props.title}