fix: give the contact, event and files fields a name for screen readers

This commit is contained in:
Linus Rath
2026-10-31 05:51:52 +01:00
parent 01578ac2fb
commit 7ba00be60e
15 changed files with 111 additions and 0 deletions
@@ -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[] = [];
// `<TextInput>` as a type argument (useRef<TextInput>) is not an element.
for (const m of source.matchAll(/<TextInput(?=\s)/g)) {
const element = source.slice(m.index!, elementEnd(source, m.index! + m[0].length));
if (/\baccessibilityLabel=/.test(element)) continue;
out.push(`${file}:${source.slice(0, m.index!).split('\n').length}`);
}
return out;
}
const dir = (rel: string) => 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([]);
});
});
@@ -225,6 +225,7 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend
<ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={styles.body}>
<Text style={styles.label}>{t('calendar.management.name', 'Name')}</Text>
<TextInput
accessibilityLabel={t('calendar.management.name', 'Name')}
value={name}
onChangeText={setName}
placeholder={t('calendar.management.name_placeholder', 'Calendar name')}
@@ -308,6 +309,7 @@ export function CalendarEditSheet({ visible, calendar, onSave, onClose }: Calend
)}
<Text style={styles.label}>{t('calendar.management.description', 'Description')}</Text>
<TextInput
accessibilityLabel={t('calendar.management.description', 'Description')}
value={description}
onChangeText={setDescription}
placeholder={t('calendar.management.description_placeholder', 'Optional')}
+5
View File
@@ -558,6 +558,7 @@ export function EventModal({
automaticallyAdjustKeyboardInsets
>
<TextInput
accessibilityLabel={t('calendar.form.title', 'Title')}
value={title}
onChangeText={setTitle}
placeholder={t('calendar.form.title', 'Title')}
@@ -797,6 +798,7 @@ export function EventModal({
{customReminderOpen && (
<View style={styles.customReminderRow}>
<TextInput
accessibilityLabel={t('calendar.alerts.amount', 'Reminder amount')}
value={customAmount}
onChangeText={(v) => setCustomAmount(v.replace(/[^0-9]/g, ''))}
keyboardType="number-pad"
@@ -834,6 +836,7 @@ export function EventModal({
<Section icon={<MapPin size={16} color={c.textMuted} />} label={t('calendar.form.location', 'Location')}>
<TextInput
accessibilityLabel={t('calendar.form.location', 'Location')}
value={location}
onChangeText={setLocation}
placeholder={t('calendar.form.location_placeholder', 'Place or address')}
@@ -844,6 +847,7 @@ export function EventModal({
<Section icon={<Video size={16} color={c.textMuted} />} label={t('calendar.form.meeting_link', 'Meeting link')}>
<TextInput
accessibilityLabel={t('calendar.form.meeting_link', 'Meeting link')}
value={videoUrl}
onChangeText={setVideoUrl}
placeholder="https://meet.example.com/…"
@@ -860,6 +864,7 @@ export function EventModal({
label={t('calendar.form.description', 'Description')}
>
<TextInput
accessibilityLabel={t('calendar.form.description', 'Description')}
value={description}
onChangeText={setDescription}
placeholder={t('calendar.form.description_placeholder', 'Notes, agenda…')}
@@ -94,6 +94,7 @@ export function ParticipantInput({ attendees, onAdd, onRemove }: ParticipantInpu
)}
<TextInput
accessibilityLabel={t('calendar.event_modal.participant_placeholder', 'Add participant by email')}
// No full-screen keyboard in landscape: it would hide the suggestions
// and the text around the field.
disableFullscreenUI
+2
View File
@@ -282,6 +282,7 @@ export function TasksSheet({
<View style={styles.addRow}>
<TextInput
accessibilityLabel={t('calendar.tasks.title_placeholder', 'Task title')}
value={editor.title}
onChangeText={(v) => 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 && (
<View style={styles.editor}>
<TextInput
accessibilityLabel={t('calendar.tasks.description_placeholder', 'Notes')}
value={editor.description}
onChangeText={(v) => setEditor((e) => ({ ...e, description: v }))}
placeholder={t('calendar.tasks.description_placeholder', 'Notes')}
@@ -128,6 +128,7 @@ export default function AddressBookPickerSheet({
{creating ? (
<View style={styles.createRow}>
<TextInput
accessibilityLabel={t('contacts.address_books.name_label', 'Address book name')}
style={styles.createInput}
placeholder={t('contacts.address_books.name_label', 'Address book name')}
placeholderTextColor={c.textMuted}
@@ -140,6 +140,7 @@ export default function ContactPickerSheet({
<View style={styles.searchBar}>
<Search size={16} color={c.textMuted} />
<TextInput
accessibilityLabel={t('contacts.search_placeholder', 'Search contacts...')}
style={styles.searchInput}
placeholder={t('contacts.search_placeholder', 'Search contacts...')}
placeholderTextColor={c.textMuted}
@@ -282,6 +282,7 @@ export default function ContactsSidebarDrawer({ visible, onClose }: Props) {
<View key={kw.keyword} style={styles.renameRow}>
<Tag size={16} color={c.textSecondary} />
<TextInput
accessibilityLabel={t('contacts.rename_category', 'Rename category')}
style={styles.renameInput}
value={renameValue}
onChangeText={setRenameValue}
@@ -94,6 +94,7 @@ export default function TagAssignSheet({
<View style={styles.inputRow}>
<TextInput
accessibilityLabel={t('contacts.tag_name_placeholder', 'New tag name')}
style={styles.input}
placeholder={t('contacts.tag_name_placeholder', 'New tag name')}
placeholderTextColor={c.textMuted}
+1
View File
@@ -274,6 +274,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps
<View style={styles.section}>
<Text style={styles.sectionLabel}>{t('files.share_add_people', 'Add people')}</Text>
<TextInput
accessibilityLabel={t('sharing.add_person', 'Add person or group')}
value={search}
onChangeText={setSearch}
placeholder={t('files.share_search_placeholder', 'Search by name or email')}
@@ -331,6 +331,7 @@ export function VacationSettings() {
description={t('settings.vacation.date_range.start_description', 'Leave empty for no start limit')}
>
<TextInput
accessibilityLabel={t('settings.vacation.date_range.start', 'Start Date')}
value={fromDate}
onChangeText={setFromDate}
placeholder="YYYY-MM-DD HH:MM"
@@ -345,6 +346,7 @@ export function VacationSettings() {
description={t('settings.vacation.date_range.end_description', 'Leave empty for no end limit')}
>
<TextInput
accessibilityLabel={t('settings.vacation.date_range.end', 'End Date')}
value={toDate}
onChangeText={setToDate}
placeholder="YYYY-MM-DD HH:MM"
@@ -379,6 +381,7 @@ export function VacationSettings() {
<Text style={styles.bodyLabel}>{t('settings.vacation.message.body_label', 'Message Body')}</Text>
<Text style={styles.bodyDesc}>{t('settings.vacation.message.body_description', 'Plain text message content')}</Text>
<TextInput
accessibilityLabel={t('settings.vacation.message.body_label', 'Message Body')}
value={body}
onChangeText={setBody}
placeholder={t('settings.vacation.message.body_placeholder', 'Thank you for your email. I am currently out of the office and will respond when I return.')}
@@ -236,6 +236,7 @@ export function ShareCollectionSheet<R extends object>({
<View style={styles.section}>
<Text style={styles.sectionLabel}>{t('calendar.share.add_people', 'Add people')}</Text>
<TextInput
accessibilityLabel={t('sharing.add_person', 'Add person or group')}
value={search}
onChangeText={setSearch}
placeholder={t('calendar.share.search_placeholder', 'Search by name or email')}