mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: name the search and filter fields for screen readers
This commit is contained in:
@@ -393,10 +393,13 @@ const EmailListItem = React.memo(function EmailListItem({
|
||||
function DebouncedFilterInput({
|
||||
value,
|
||||
onCommit,
|
||||
accessibilityLabel,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof TextInput>, 'value' | 'onChangeText'> & {
|
||||
}: Omit<React.ComponentProps<typeof TextInput>, 'value' | 'onChangeText' | 'accessibilityLabel'> & {
|
||||
value: string;
|
||||
onCommit: (value: string) => void;
|
||||
/** The field's name for screen readers: its label sits beside it as separate Text. */
|
||||
accessibilityLabel: string;
|
||||
}) {
|
||||
const [text, setText] = React.useState(value);
|
||||
React.useEffect(() => { setText(value); }, [value]);
|
||||
@@ -410,6 +413,7 @@ function DebouncedFilterInput({
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
returnKeyType="search"
|
||||
@@ -1280,6 +1284,7 @@ export default function EmailListScreen({
|
||||
disableFullscreenUI
|
||||
ref={searchInputRef}
|
||||
style={styles.searchInput}
|
||||
accessibilityLabel={t('advanced_search.search', 'Search')}
|
||||
placeholder={t('email_list.search_placeholder', 'Search mail...')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
value={searchInput}
|
||||
@@ -1831,6 +1836,7 @@ export default function EmailListScreen({
|
||||
<DebouncedFilterInput
|
||||
value={filters.from ?? ''}
|
||||
onCommit={(v) => setFilterField('from', v)}
|
||||
accessibilityLabel={t('advanced_search.from', 'From')}
|
||||
placeholder={t('advanced_search.from_placeholder', 'Sender email or name')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
autoCapitalize="none"
|
||||
@@ -1843,6 +1849,7 @@ export default function EmailListScreen({
|
||||
<DebouncedFilterInput
|
||||
value={filters.to ?? ''}
|
||||
onCommit={(v) => setFilterField('to', v)}
|
||||
accessibilityLabel={t('advanced_search.to', 'To')}
|
||||
placeholder={t('advanced_search.to_placeholder', 'Recipient email or name')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
autoCapitalize="none"
|
||||
@@ -1857,6 +1864,7 @@ export default function EmailListScreen({
|
||||
<DebouncedFilterInput
|
||||
value={filters.subject ?? ''}
|
||||
onCommit={(v) => setFilterField('subject', v)}
|
||||
accessibilityLabel={t('advanced_search.subject', 'Subject')}
|
||||
placeholder={t('advanced_search.subject_placeholder', 'Subject contains...')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.filterFieldInput}
|
||||
@@ -1868,6 +1876,7 @@ export default function EmailListScreen({
|
||||
<DebouncedFilterInput
|
||||
value={filters.body ?? ''}
|
||||
onCommit={(v) => setFilterField('body', v)}
|
||||
accessibilityLabel={t('advanced_search.body', 'Body')}
|
||||
placeholder={t('advanced_search.body_placeholder', 'Body contains...')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
style={styles.filterFieldInput}
|
||||
|
||||
@@ -517,6 +517,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
||||
// and the text around the field.
|
||||
disableFullscreenUI
|
||||
style={styles.searchInput}
|
||||
accessibilityLabel={t('advanced_search.search', 'Search')}
|
||||
placeholder={t('email_list.search_placeholder', 'Search mail...')}
|
||||
placeholderTextColor={c.textMuted}
|
||||
value={searchInput}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Text fields need a name a screen reader can say (webmail ef13556a0,
|
||||
// dcc21acd0, 4e2affe40, f73643bbb). The search field of the folder list and
|
||||
// the unified views, and the From/To/Subject/Body fields of the filter
|
||||
// panel, had only a placeholder, which isn't read once the field holds
|
||||
// text; the filter labels are separate Text beside the field. Same scan as
|
||||
// components/__tests__/field-names.test.ts.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { 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, Input or DebouncedFilterInput element without a name. */
|
||||
function unnamedFields(file: string): string[] {
|
||||
const source = readFileSync(join(SRC, file), 'utf8');
|
||||
const out: string[] = [];
|
||||
for (const m of source.matchAll(/<(TextInput|Input|DebouncedFilterInput)\b/g)) {
|
||||
const element = source.slice(m.index!, elementEnd(source, m.index! + m[0].length));
|
||||
if (/\baccessibilityLabel=/.test(element)) continue;
|
||||
if (m[1] === 'Input' && /\blabel=/.test(element)) continue;
|
||||
out.push(`${file}:${source.slice(0, m.index!).split('\n').length}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe('search field names', () => {
|
||||
it('every field of the mail list, its filter panel and the unified views has a name', () => {
|
||||
expect(['screens/EmailListScreen.tsx', 'screens/UnifiedInboxScreen.tsx'].flatMap(unnamedFields)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user