mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: give the text fields in settings and sign-in a name for screen readers
This commit is contained in:
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { TextInput as RNTextInput, View, Text, StyleSheet, ViewStyle, TextInputProps } from 'react-native';
|
||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens';
|
||||
import { useColors } from '../theme/colors';
|
||||
import { inputAccessibility } from './input-a11y';
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
label?: string;
|
||||
@@ -48,6 +49,7 @@ export default function Input({
|
||||
<RNTextInput
|
||||
ref={ref}
|
||||
{...props}
|
||||
{...inputAccessibility({ label, error, placeholder: props.placeholder, accessibilityLabel: props.accessibilityLabel, accessibilityHint: props.accessibilityHint })}
|
||||
style={[styles.input, style]}
|
||||
placeholderTextColor={c.mutedForeground}
|
||||
onFocus={(e) => { setFocused(true); props.onFocus?.(e); }}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Text fields need a name a screen reader can say (webmail ef13556a0,
|
||||
// dcc21acd0, 4e2affe40, f73643bbb). The password, code and new-password
|
||||
// fields under Security had none: no label prop, no placeholder, no
|
||||
// accessibilityLabel. Settings and the sign-in steps are scanned so that a
|
||||
// bare field doesn't creep back in; Input itself names its field from its
|
||||
// label, then its placeholder.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { inputAccessibility } from '../input-a11y';
|
||||
|
||||
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 or Input 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)\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;
|
||||
}
|
||||
|
||||
const SETTINGS = readdirSync(join(SRC, 'components', 'settings'))
|
||||
.filter((name) => name.endsWith('.tsx'))
|
||||
// The calendar pass owns the auto-reply editor.
|
||||
.filter((name) => name !== 'VacationSettings.tsx')
|
||||
.map((name) => `components/settings/${name}`);
|
||||
|
||||
const FILES = [
|
||||
...SETTINGS,
|
||||
'screens/SettingsScreen.tsx',
|
||||
'screens/login/EmailStep.tsx',
|
||||
'screens/login/PasswordStep.tsx',
|
||||
'screens/login/ServerStep.tsx',
|
||||
];
|
||||
|
||||
describe('field names', () => {
|
||||
it('every field in Settings and the sign-in steps has a name', () => {
|
||||
expect(FILES.flatMap(unnamedFields)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inputAccessibility', () => {
|
||||
it('names the field after its label', () => {
|
||||
expect(inputAccessibility({ label: 'Folder name', placeholder: 'Receipts' })).toEqual({ accessibilityLabel: 'Folder name' });
|
||||
});
|
||||
|
||||
it('falls back to the placeholder, and lets an explicit name win', () => {
|
||||
expect(inputAccessibility({ placeholder: 'Search' })).toEqual({ accessibilityLabel: 'Search' });
|
||||
expect(inputAccessibility({ label: 'Name', accessibilityLabel: 'Display name' })).toEqual({ accessibilityLabel: 'Display name' });
|
||||
});
|
||||
|
||||
it('reads the error as the hint', () => {
|
||||
expect(inputAccessibility({ label: 'Email', error: 'Not an address' })).toEqual({
|
||||
accessibilityLabel: 'Email',
|
||||
accessibilityHint: 'Not an address',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// The name a screen reader gives a text field (webmail components/ui/field.tsx,
|
||||
// ef13556a0, dcc21acd0, 4e2affe40). Input drew its label as a Text of its
|
||||
// own, not tied to the field, so TalkBack and VoiceOver announced an
|
||||
// unnamed "edit box"; a placeholder alone stops being read once the field
|
||||
// has a value. The label names the field, then the placeholder; an error
|
||||
// is read as the hint so it is heard on focus.
|
||||
|
||||
export interface InputAccessibilityProps {
|
||||
label?: string;
|
||||
error?: string;
|
||||
placeholder?: string;
|
||||
accessibilityLabel?: string;
|
||||
accessibilityHint?: string;
|
||||
}
|
||||
|
||||
export function inputAccessibility(props: InputAccessibilityProps): {
|
||||
accessibilityLabel?: string;
|
||||
accessibilityHint?: string;
|
||||
} {
|
||||
const name = props.accessibilityLabel || props.label || props.placeholder || undefined;
|
||||
const hint = props.error || props.accessibilityHint || undefined;
|
||||
return {
|
||||
...(name ? { accessibilityLabel: name } : {}),
|
||||
...(hint ? { accessibilityHint: hint } : {}),
|
||||
};
|
||||
}
|
||||
@@ -178,7 +178,7 @@ function PasswordChangeSection({ otpEnabled }: { otpEnabled: boolean }) {
|
||||
<View style={styles.pwField}>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.password.current', "Current password")}</Text>
|
||||
<View style={styles.pwInputRow}>
|
||||
<Input value={currentPwd} onChangeText={setCurrentPwd} secureTextEntry={!showCurrent} autoCapitalize="none" containerStyle={{ flex: 1 }} />
|
||||
<Input accessibilityLabel={t('settings.security.password.current', "Current password")} value={currentPwd} onChangeText={setCurrentPwd} secureTextEntry={!showCurrent} autoCapitalize="none" containerStyle={{ flex: 1 }} />
|
||||
<Pressable style={styles.eyeBtn} onPress={() => setShowCurrent((v) => !v)} accessibilityRole="button" accessibilityLabel={t('settings.security.password.toggle_visibility', "Show or hide password")}>
|
||||
{showCurrent ? <EyeOff size={16} color={c.mutedForeground} /> : <Eye size={16} color={c.mutedForeground} />}
|
||||
</Pressable>
|
||||
@@ -188,14 +188,14 @@ function PasswordChangeSection({ otpEnabled }: { otpEnabled: boolean }) {
|
||||
{otpEnabled && (
|
||||
<View style={styles.pwField}>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.two_factor.code', "Verification code")}</Text>
|
||||
<Input value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} textContentType="oneTimeCode" autoComplete="one-time-code" />
|
||||
<Input accessibilityLabel={t('settings.security.two_factor.code', "Verification code")} value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} textContentType="oneTimeCode" autoComplete="one-time-code" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.pwField}>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.password.new', "New password")}</Text>
|
||||
<View style={styles.pwInputRow}>
|
||||
<Input value={newPwd} onChangeText={setNewPwd} secureTextEntry={!showNew} autoCapitalize="none" containerStyle={{ flex: 1 }} />
|
||||
<Input accessibilityLabel={t('settings.security.password.new', "New password")} value={newPwd} onChangeText={setNewPwd} secureTextEntry={!showNew} autoCapitalize="none" containerStyle={{ flex: 1 }} />
|
||||
<Pressable style={styles.eyeBtn} onPress={() => setShowNew((v) => !v)} accessibilityRole="button" accessibilityLabel={t('settings.security.password.toggle_visibility', "Show or hide password")}>
|
||||
{showNew ? <EyeOff size={16} color={c.mutedForeground} /> : <Eye size={16} color={c.mutedForeground} />}
|
||||
</Pressable>
|
||||
@@ -204,7 +204,7 @@ function PasswordChangeSection({ otpEnabled }: { otpEnabled: boolean }) {
|
||||
|
||||
<View style={styles.pwField}>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.password.confirm', "Confirm password")}</Text>
|
||||
<Input value={confirmPwd} onChangeText={setConfirmPwd} secureTextEntry={!showNew} autoCapitalize="none" />
|
||||
<Input accessibilityLabel={t('settings.security.password.confirm', "Confirm password")} value={confirmPwd} onChangeText={setConfirmPwd} secureTextEntry={!showNew} autoCapitalize="none" />
|
||||
</View>
|
||||
|
||||
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||
@@ -252,7 +252,7 @@ function DisplayNameSection({ initial, onSaved }: { initial: string; onSaved: (n
|
||||
return (
|
||||
<SettingItem label={t('settings.security.display_name.label', "Display name")} description={t('settings.security.display_name.description', "The name shown on this account.")}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
|
||||
<Input value={name} onChangeText={setName} placeholder={t('settings.security.display_name.label', "Display name")} containerStyle={{ width: 150 }} />
|
||||
<Input accessibilityLabel={t('settings.security.display_name.label', "Display name")} value={name} onChangeText={setName} placeholder={t('settings.security.display_name.label', "Display name")} containerStyle={{ width: 150 }} />
|
||||
<Button size="sm" loading={saving} disabled={saving || name === initial} onPress={() => { void save(); }}>
|
||||
{saved ? <Check size={14} color={c.primaryForeground} /> : t('common.save', "Save")}
|
||||
</Button>
|
||||
@@ -356,11 +356,11 @@ function TotpSection({ enabled, onChanged }: { enabled: boolean; onChanged: () =
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.password.current', "Current password")}</Text>
|
||||
<Input value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none" />
|
||||
<Input accessibilityLabel={t('settings.security.password.current', "Current password")} value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none" />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.two_factor.code', "Verification code")}</Text>
|
||||
<Input value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} />
|
||||
<Input accessibilityLabel={t('settings.security.two_factor.code', "Verification code")} value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} />
|
||||
</View>
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
<View style={styles.rowGap}>
|
||||
@@ -375,8 +375,8 @@ function TotpSection({ enabled, onChanged }: { enabled: boolean; onChanged: () =
|
||||
{disableOpen && (
|
||||
<View style={styles.panel}>
|
||||
<Text style={styles.panelHint}>{t('settings.security.two_factor.disable_hint_code', "Enter your password and a code from your authenticator app to turn off two-factor authentication.")}</Text>
|
||||
<Input value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none" placeholder={t('settings.security.password.current', "Current password")} />
|
||||
<Input value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} textContentType="oneTimeCode" autoComplete="one-time-code" placeholder={t('settings.security.two_factor.code', "Verification code")} />
|
||||
<Input accessibilityLabel={t('settings.security.password.current', "Current password")} value={password} onChangeText={setPassword} secureTextEntry autoCapitalize="none" placeholder={t('settings.security.password.current', "Current password")} />
|
||||
<Input accessibilityLabel={t('settings.security.two_factor.code', "Verification code")} value={otpCode} onChangeText={setOtpCode} keyboardType="number-pad" maxLength={6} textContentType="oneTimeCode" autoComplete="one-time-code" placeholder={t('settings.security.two_factor.code', "Verification code")} />
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
<View style={styles.rowGap}>
|
||||
<Button variant="destructive" size="sm" loading={saving} disabled={saving || !password || !otpCode.trim()} onPress={() => { void confirmDisable(); }}>
|
||||
@@ -488,7 +488,7 @@ function CredentialSection({ icon: Icon, title, description, nameLabel, namePlac
|
||||
<View style={styles.panel}>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{nameLabel}</Text>
|
||||
<Input value={name} onChangeText={setName} placeholder={namePlaceholder} />
|
||||
<Input accessibilityLabel={nameLabel} value={name} onChangeText={setName} placeholder={namePlaceholder} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.credentials.expires', "Expires (optional)")}</Text>
|
||||
@@ -508,7 +508,7 @@ function CredentialSection({ icon: Icon, title, description, nameLabel, namePlac
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.credentials.allowed_ips', "Allowed IPs (optional)")}</Text>
|
||||
<Input value={allowedIpsRaw} onChangeText={setAllowedIpsRaw} placeholder="1.2.3.4, 10.0.0.0/8" autoCapitalize="none" />
|
||||
<Input accessibilityLabel={t('settings.security.credentials.allowed_ips', "Allowed IPs (optional)")} value={allowedIpsRaw} onChangeText={setAllowedIpsRaw} placeholder="1.2.3.4, 10.0.0.0/8" autoCapitalize="none" />
|
||||
<Text style={styles.fieldHint}>{t('settings.security.credentials.allowed_ips_hint', "Comma or space separated. Leave empty to allow any IP.")}</Text>
|
||||
</View>
|
||||
<View style={styles.rowGap}>
|
||||
@@ -656,11 +656,11 @@ function PublicKeysSection({
|
||||
<View style={styles.panel}>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.public_keys.name_label', 'Public Key name')}</Text>
|
||||
<Input value={name} onChangeText={setName} placeholder={t('settings.security.public_keys.name_placeholder', 'e.g. My Key')} />
|
||||
<Input accessibilityLabel={t('settings.security.public_keys.name_label', 'Public Key name')} value={name} onChangeText={setName} placeholder={t('settings.security.public_keys.name_placeholder', 'e.g. My Key')} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.pwLabel}>{t('settings.security.public_keys.key_label', 'Public Key (S/MIME or PGP ASCII-armored)')}</Text>
|
||||
<Input
|
||||
<Input accessibilityLabel={t('settings.security.public_keys.key_label', 'Public Key (S/MIME or PGP ASCII-armored)')}
|
||||
value={keyText}
|
||||
onChangeText={setKeyText}
|
||||
placeholder={t('settings.security.public_keys.key_placeholder', 'Paste your public key here')}
|
||||
|
||||
@@ -226,7 +226,7 @@ export function ComposingSettings() {
|
||||
</View>
|
||||
|
||||
<View style={styles.addRow}>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
value={newKeyword}
|
||||
onChangeText={setNewKeyword}
|
||||
placeholder={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
|
||||
@@ -224,6 +224,7 @@ export function ContactsSettings() {
|
||||
{editingId === book.id ? (
|
||||
<>
|
||||
<TextInput
|
||||
accessibilityLabel={t('contacts.address_books.name_label', 'Address book name')}
|
||||
style={styles.bookInput}
|
||||
value={editName}
|
||||
onChangeText={setEditName}
|
||||
@@ -311,7 +312,7 @@ export function ContactsSettings() {
|
||||
{adding ? (
|
||||
<View style={styles.bookRow}>
|
||||
<BookUser size={16} color={c.textSecondary} />
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('contacts.address_books.name_label', 'Address book name')}
|
||||
style={styles.bookInput}
|
||||
value={newName}
|
||||
onChangeText={setNewName}
|
||||
|
||||
@@ -211,7 +211,7 @@ export function ContentSendersSettings() {
|
||||
</View>
|
||||
|
||||
<View style={styles.addRow}>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.email_behavior.trusted_senders.add_placeholder', 'Enter email address')}
|
||||
value={newSender}
|
||||
onChangeText={setNewSender}
|
||||
placeholder={t('settings.email_behavior.trusted_senders.add_placeholder', 'Enter email address')}
|
||||
|
||||
@@ -52,6 +52,7 @@ export function DownloadsSettings() {
|
||||
description={t('settings.downloads.email_template.description_mobile', "Template used when exporting a message as an .eml file.")}
|
||||
>
|
||||
<Input
|
||||
accessibilityLabel={t('settings.downloads.email_template.label', "Email (.eml) filename")}
|
||||
value={emailTemplate}
|
||||
onChangeText={(v) => update('emailExportTemplate', v)}
|
||||
autoCapitalize="none"
|
||||
@@ -76,6 +77,7 @@ export function DownloadsSettings() {
|
||||
description={t('settings.downloads.attachment_template.description_mobile', "Template used when saving or sharing an attachment.")}
|
||||
>
|
||||
<Input
|
||||
accessibilityLabel={t('settings.downloads.attachment_template.label', "Attachment filename")}
|
||||
value={attachmentTemplate}
|
||||
onChangeText={(v) => update('attachmentExportTemplate', v)}
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -318,7 +318,7 @@ export function FolderSettings() {
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalBody} keyboardShouldPersistTaps="handled">
|
||||
<Text style={styles.fieldLabel}>{t('settings.folders.folder_name', 'Folder name')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.folders.folder_name', 'Folder name')}
|
||||
value={draftName}
|
||||
onChangeText={setDraftName}
|
||||
placeholder={t('settings.folders.name_placeholder', 'Receipts')}
|
||||
|
||||
@@ -314,7 +314,7 @@ export function IdentitySettings() {
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalBody} keyboardShouldPersistTaps="handled">
|
||||
<Text style={styles.fieldLabel}>{t('settings.identities.display_name', "Display name")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.identities.display_name', "Display name")}
|
||||
value={editing?.name ?? ''}
|
||||
onChangeText={(name) => setEditing((d) => (d ? { ...d, name } : d))}
|
||||
placeholder={t('settings.identities.display_name_placeholder', 'Jane Doe')}
|
||||
@@ -322,7 +322,7 @@ export function IdentitySettings() {
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{t('settings.identities.email_address', "Email address")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.identities.email_address', "Email address")}
|
||||
value={editing?.email ?? ''}
|
||||
onChangeText={(email) => setEditing((d) => (d ? { ...d, email } : d))}
|
||||
placeholder={t('identities.form.email_placeholder', 'your.email@example.com')}
|
||||
@@ -337,7 +337,7 @@ export function IdentitySettings() {
|
||||
<Text style={styles.hint}>{t('settings.identities.email_locked', "JMAP does not allow changing an identity's email; create a new one instead.")}</Text>
|
||||
)}
|
||||
<Text style={styles.fieldLabel}>{t('identities.form.reply_to_label', 'Reply-To (optional)')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('identities.form.reply_to_label', 'Reply-To (optional)')}
|
||||
value={editing?.replyTo ?? ''}
|
||||
onChangeText={(replyTo) => setEditing((d) => (d ? { ...d, replyTo } : d))}
|
||||
placeholder={t('identities.form.reply_to_placeholder', 'different@example.com')}
|
||||
@@ -348,7 +348,7 @@ export function IdentitySettings() {
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{t('identities.form.bcc_label', 'Auto BCC (optional)')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('identities.form.bcc_label', 'Auto BCC (optional)')}
|
||||
value={editing?.bcc ?? ''}
|
||||
onChangeText={(bcc) => setEditing((d) => (d ? { ...d, bcc } : d))}
|
||||
placeholder={t('identities.form.bcc_placeholder', 'archive@example.com')}
|
||||
@@ -359,7 +359,7 @@ export function IdentitySettings() {
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{t('identities.form.text_signature_label', "Text Signature")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('identities.form.text_signature_label', "Text Signature")}
|
||||
value={editing?.textSignature ?? ''}
|
||||
onChangeText={(textSignature) => setEditing((d) => (d ? { ...d, textSignature } : d))}
|
||||
placeholder="-- Jane Doe Bulwark Mail"
|
||||
@@ -371,7 +371,7 @@ export function IdentitySettings() {
|
||||
{t('identities.form.signature_byte_counter', '{bytes} / {max} bytes', { bytes: textBytes, max: SIGNATURE_MAX_BYTES })}
|
||||
</Text>
|
||||
<Text style={styles.fieldLabel}>{t('identities.form.html_signature_label', "HTML Signature")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('identities.form.html_signature_label', "HTML Signature")}
|
||||
value={editing?.htmlSignature ?? ''}
|
||||
onChangeText={(htmlSignature) => setEditing((d) => (d ? { ...d, htmlSignature } : d))}
|
||||
placeholder="<p><b>Jane Doe</b><br>Bulwark Mail</p>"
|
||||
|
||||
@@ -304,7 +304,7 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
|
||||
<View style={styles.form}>
|
||||
<View>
|
||||
<Text style={styles.formLabel}>{t('settings.keywords.label_field', "Display Name")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.keywords.label_field', "Display Name")}
|
||||
value={label}
|
||||
onChangeText={setLabel}
|
||||
placeholder={t('settings.keywords.label_placeholder', "e.g. Work, Personal, Urgent")}
|
||||
|
||||
@@ -246,7 +246,7 @@ export function ReadingSettings() {
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.addKeywordRow}>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
value={newKeyword}
|
||||
onChangeText={setNewKeyword}
|
||||
placeholder={t('settings.email_behavior.attachment_reminder.add_placeholder', "Add keyword...")}
|
||||
|
||||
@@ -133,7 +133,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
<View style={formStyles.form}>
|
||||
<View>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.name', "Name")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.sidebar_apps.form.name', "Name")}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder={t('settings.sidebar_apps.form.name_placeholder', "App name")}
|
||||
@@ -144,7 +144,7 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
|
||||
<View>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.url', "URL")}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={t('settings.sidebar_apps.form.url', "URL")}
|
||||
value={url}
|
||||
onChangeText={setUrl}
|
||||
placeholder="https://example.com"
|
||||
|
||||
@@ -224,7 +224,7 @@ export function TemplateSettings() {
|
||||
</View>
|
||||
<ScrollView contentContainerStyle={styles.modalBody}>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.name', 'Template Name')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={tr('settings.templates.name', 'Template Name')}
|
||||
value={draftName}
|
||||
onChangeText={setDraftName}
|
||||
placeholder={tr('settings.templates.name_placeholder', 'e.g., Follow-up email')}
|
||||
@@ -232,7 +232,7 @@ export function TemplateSettings() {
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.subject', 'Subject')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={tr('settings.templates.subject', 'Subject')}
|
||||
value={draftSubject}
|
||||
onChangeText={setDraftSubject}
|
||||
placeholder={tr('settings.templates.subject_placeholder', 'Email subject line')}
|
||||
@@ -240,7 +240,7 @@ export function TemplateSettings() {
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.category', 'Category')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={tr('settings.templates.category', 'Category')}
|
||||
value={draftCategory}
|
||||
onChangeText={setDraftCategory}
|
||||
placeholder={tr('settings.templates.category_placeholder', 'e.g., Work, Personal')}
|
||||
@@ -248,7 +248,7 @@ export function TemplateSettings() {
|
||||
style={styles.input}
|
||||
/>
|
||||
<Text style={styles.fieldLabel}>{tr('settings.templates.body', 'Body')}</Text>
|
||||
<TextInput
|
||||
<TextInput accessibilityLabel={tr('settings.templates.body', 'Body')}
|
||||
value={draftBody}
|
||||
onChangeText={setDraftBody}
|
||||
placeholder={tr('settings.templates.body_placeholder', 'Email body content...')}
|
||||
@@ -297,6 +297,7 @@ export function TemplateSettings() {
|
||||
<ScrollView contentContainerStyle={styles.modalBody}>
|
||||
<Text style={styles.fieldLabel}>JSON</Text>
|
||||
<TextInput
|
||||
accessibilityLabel={tr('settings.templates.import_json_label', 'Templates as JSON')}
|
||||
value={importText}
|
||||
onChangeText={setImportText}
|
||||
placeholder='{"templates": [...]}'
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function EmailStep({
|
||||
</View>
|
||||
|
||||
<Input
|
||||
accessibilityLabel={t('login.username_label', 'Email')}
|
||||
placeholder={t('login.username_placeholder', 'user@example.com')}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
|
||||
Reference in New Issue
Block a user