mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 16:07:32 +00:00
fix: give the contact, event and files fields a name for screen readers
This commit is contained in:
@@ -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')}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user