mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: name the composer, quick reply, folder and template search fields for screen readers
This commit is contained in:
@@ -291,6 +291,7 @@ function NamePrompt({ title, message, initial, confirmLabel, onSubmit, onClose }
|
||||
<Text style={styles.sheetTitle}>{title}</Text>
|
||||
{message ? <Text style={styles.promptMessage}>{message}</Text> : null}
|
||||
<TextInput
|
||||
accessibilityLabel={t('mailbox_context_menu.placeholder_folder_name', 'Folder name')}
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
placeholder={t('mailbox_context_menu.placeholder_folder_name', 'Folder name')}
|
||||
|
||||
@@ -149,6 +149,7 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
||||
</View>
|
||||
{total > 4 && (
|
||||
<TextInput
|
||||
accessibilityLabel={t('templates.search_placeholder', 'Search templates...')}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder={t('templates.search_placeholder', 'Search templates...')}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Text fields need a name a screen reader can say (parity 11 "Form fields
|
||||
// have no name for screen readers"; field-names.test.ts covers Settings and
|
||||
// the sign-in steps). The composer, quick reply, folder prompt and template
|
||||
// search had fields named only by their placeholder, or by nothing.
|
||||
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 (ch === '/' && depth === 0 && 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` opening a JSX element, not `useRef<TextInput>`.
|
||||
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 COMPOSE = readdirSync(join(SRC, 'components', 'compose'))
|
||||
.filter((name) => name.endsWith('.tsx'))
|
||||
.map((name) => `components/compose/${name}`);
|
||||
|
||||
const FILES = [
|
||||
'screens/ComposeScreen.tsx',
|
||||
'components/email/QuickReplyBox.tsx',
|
||||
'components/SidebarDrawer.tsx',
|
||||
'components/TemplateSheet.tsx',
|
||||
...COMPOSE,
|
||||
];
|
||||
|
||||
describe('field names in mail and the composer', () => {
|
||||
it('every field has a name', () => {
|
||||
expect(FILES.flatMap(unnamedFields)).toEqual([]);
|
||||
});
|
||||
|
||||
it('finds the composer fields at all', () => {
|
||||
const source = readFileSync(join(SRC, 'screens/ComposeScreen.tsx'), 'utf8');
|
||||
expect([...source.matchAll(/<TextInput(?=[\s/])/g)].length).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
});
|
||||
@@ -267,6 +267,7 @@ export function QuickReplyBox({ email, jmapAccountId, onMoreOptions, onSent, onF
|
||||
// No full-screen keyboard in landscape: it would hide the suggestions
|
||||
// and the text around the field.
|
||||
disableFullscreenUI
|
||||
accessibilityLabel={t('email_viewer.quick_reply_placeholder', 'Write a quick reply...')}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
onFocus={onFocus}
|
||||
|
||||
@@ -2708,6 +2708,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
// No full-screen keyboard in landscape: it would hide the suggestions
|
||||
// and the text around the field.
|
||||
disableFullscreenUI
|
||||
accessibilityLabel={label}
|
||||
style={[styles.recipientInput, invalidInput && styles.recipientInputInvalid]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={c.textMuted}
|
||||
@@ -2900,6 +2901,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
<TextInput
|
||||
ref={subjectRef}
|
||||
disableFullscreenUI
|
||||
accessibilityLabel={t('email_composer.subject', 'Subject')}
|
||||
style={styles.subjectInput}
|
||||
placeholder={t('email_composer.subject_placeholder', 'Subject')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
@@ -2936,6 +2938,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
<TextInput
|
||||
ref={plainBodyRef}
|
||||
disableFullscreenUI
|
||||
accessibilityLabel={t('email_composer.body_label', 'Message')}
|
||||
style={styles.plainEditor}
|
||||
multiline
|
||||
value={plainBody}
|
||||
@@ -3098,6 +3101,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
{t('email_composer.link_url_prompt', 'Enter the URL')}
|
||||
</Text>
|
||||
<TextInput
|
||||
accessibilityLabel={t('email_composer.link_url_prompt', 'Enter the URL')}
|
||||
style={styles.modalInput}
|
||||
value={linkPromptValue}
|
||||
onChangeText={setLinkPromptValue}
|
||||
@@ -3345,6 +3349,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
{t(`templates.placeholders.${name}`, name.replace(/_/g, ' '))}
|
||||
</Text>
|
||||
<TextInput
|
||||
accessibilityLabel={t(`templates.placeholders.${name}`, name.replace(/_/g, ' '))}
|
||||
style={styles.modalInput}
|
||||
value={placeholderPrompt?.values[name] ?? ''}
|
||||
onChangeText={(v) => setPlaceholderPrompt((p) => (p ? { ...p, values: { ...p.values, [name]: v } } : p))}
|
||||
@@ -3392,6 +3397,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
<>
|
||||
<Text style={styles.modalLabel}>{t('identities.sub_address.popover_title', 'Add Sub-Address Tag')}</Text>
|
||||
<TextInput
|
||||
accessibilityLabel={t('identities.sub_address.popover_title', 'Add Sub-Address Tag')}
|
||||
style={styles.modalInput}
|
||||
value={tagDraft}
|
||||
onChangeText={setTagDraft}
|
||||
@@ -3437,6 +3443,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
<>
|
||||
<Text style={styles.modalLabel}>{t('email_composer.from_override.toggle_tooltip', 'Edit the From name and address freely. Mail is still sent through your identity - only the visible From header changes.')}</Text>
|
||||
<TextInput
|
||||
accessibilityLabel={t('email_composer.from_override.name_label', 'From name')}
|
||||
style={styles.modalInput}
|
||||
value={overrideName}
|
||||
onChangeText={setOverrideName}
|
||||
@@ -3444,6 +3451,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
placeholderTextColor={c.textMuted}
|
||||
/>
|
||||
<TextInput
|
||||
accessibilityLabel={t('email_composer.from_override.email_label', 'From email address')}
|
||||
style={styles.modalInput}
|
||||
value={overrideEmail}
|
||||
onChangeText={setOverrideEmail}
|
||||
|
||||
Reference in New Issue
Block a user