mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: name the settings switches for screen readers
A settings switch had a role and a checked state but no label, so TalkBack only announced "switch, off". A switch inside a SettingItem now takes that row's label, and the switches outside one (filter rules, expanded view, template and sidebar app editors, filter rule options) get their visible label passed explicitly.
This commit is contained in:
@@ -7,6 +7,8 @@ interface ToggleSwitchProps {
|
||||
value: boolean;
|
||||
onValueChange: (val: boolean) => void;
|
||||
disabled?: boolean;
|
||||
/** What the switch turns on or off, read out by screen readers. */
|
||||
accessibilityLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,7 +18,7 @@ interface ToggleSwitchProps {
|
||||
* - thumb: h-4 w-4 rounded-full bg-background
|
||||
* - translate-x-6 (checked) / translate-x-1 (unchecked)
|
||||
*/
|
||||
export default function ToggleSwitch({ value, onValueChange, disabled = false }: ToggleSwitchProps) {
|
||||
export default function ToggleSwitch({ value, onValueChange, disabled = false, accessibilityLabel }: ToggleSwitchProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
return (
|
||||
@@ -28,6 +30,7 @@ export default function ToggleSwitch({ value, onValueChange, disabled = false }:
|
||||
]}
|
||||
onPress={() => !disabled && onValueChange(!value)}
|
||||
accessibilityRole="switch"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityState={{ checked: value, disabled }}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -400,6 +400,7 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
|
||||
<ToggleSwitch
|
||||
checked={!!action.keepCopy}
|
||||
onChange={(v) => updateAction(index, { keepCopy: v || undefined })}
|
||||
accessibilityLabel={t('settings.filters.forward_keep_copy', 'Keep a copy')}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
@@ -446,7 +447,11 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
|
||||
<Text style={styles.stopLabel}>
|
||||
{t('settings.filters.stop_processing', 'Stop processing subsequent rules')}
|
||||
</Text>
|
||||
<ToggleSwitch checked={stopProcessing} onChange={setStopProcessing} />
|
||||
<ToggleSwitch
|
||||
checked={stopProcessing}
|
||||
onChange={setStopProcessing}
|
||||
accessibilityLabel={t('settings.filters.stop_processing', 'Stop processing subsequent rules')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Folder rules skip spam unless the rule opts in (webmail 6241ed61). */}
|
||||
@@ -455,7 +460,11 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F
|
||||
<Text style={styles.stopLabel}>
|
||||
{t('settings.filters.include_spam', 'Also move messages marked as spam')}
|
||||
</Text>
|
||||
<ToggleSwitch checked={includeSpam} onChange={setIncludeSpam} />
|
||||
<ToggleSwitch
|
||||
checked={includeSpam}
|
||||
onChange={setIncludeSpam}
|
||||
accessibilityLabel={t('settings.filters.include_spam', 'Also move messages marked as spam')}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
@@ -385,7 +385,7 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
|
||||
return (
|
||||
<View key={rule.id} style={[styles.ruleRow, !rule.enabled && styles.ruleDisabled]}>
|
||||
<View style={{ paddingTop: 2 }}>
|
||||
<ToggleSwitch checked={rule.enabled} onChange={() => handleToggle(rule.id)} />
|
||||
<ToggleSwitch checked={rule.enabled} onChange={() => handleToggle(rule.id)} accessibilityLabel={rule.name} />
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
@@ -458,7 +458,11 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) {
|
||||
{!isOpaque && rules.length > 0 && (
|
||||
<View style={styles.expandedToggle}>
|
||||
<Text style={styles.expandedLabel}>{t('settings.filters.expanded_view', 'Expanded view')}</Text>
|
||||
<ToggleSwitch checked={expandedView} onChange={(v) => updateSetting('filtersExpandedView', v)} />
|
||||
<ToggleSwitch
|
||||
checked={expandedView}
|
||||
onChange={(v) => updateSetting('filtersExpandedView', v)}
|
||||
accessibilityLabel={t('settings.filters.expanded_view', 'Expanded view')}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -188,7 +188,11 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) {
|
||||
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={formStyles.label}>{t('settings.sidebar_apps.form.show_on_mobile', "Show on mobile")}</Text>
|
||||
<ToggleSwitch checked={showOnMobile} onChange={setShowOnMobile} />
|
||||
<ToggleSwitch
|
||||
checked={showOnMobile}
|
||||
onChange={setShowOnMobile}
|
||||
accessibilityLabel={t('settings.sidebar_apps.form.show_on_mobile', "Show on mobile")}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm, justifyContent: 'flex-end' }}>
|
||||
|
||||
@@ -246,11 +246,19 @@ export function TemplateSettings() {
|
||||
</Text>
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>{tr('settings.templates.favorite', 'Favorite')}</Text>
|
||||
<ToggleSwitch checked={draftFavorite} onChange={setDraftFavorite} />
|
||||
<ToggleSwitch
|
||||
checked={draftFavorite}
|
||||
onChange={setDraftFavorite}
|
||||
accessibilityLabel={tr('settings.templates.favorite', 'Favorite')}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.toggleRow}>
|
||||
<Text style={styles.toggleLabel}>{tr('settings.templates.is_html', 'Body is HTML')}</Text>
|
||||
<ToggleSwitch checked={draftIsHtml} onChange={setDraftIsHtml} />
|
||||
<ToggleSwitch
|
||||
checked={draftIsHtml}
|
||||
onChange={setDraftIsHtml}
|
||||
accessibilityLabel={tr('settings.templates.is_html', 'Body is HTML')}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.modalActions}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -65,6 +65,10 @@ export function SettingsSection({
|
||||
);
|
||||
}
|
||||
|
||||
// The label of the SettingItem a control sits in. A switch has no text of
|
||||
// its own, so without it a screen reader only announces "switch, off".
|
||||
const SettingLabelContext = React.createContext<string | undefined>(undefined);
|
||||
|
||||
interface SettingItemProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
@@ -96,7 +100,11 @@ export function SettingItem({
|
||||
</View>
|
||||
{description && <Text style={styles.settingDescription}>{description}</Text>}
|
||||
</View>
|
||||
{children && <View style={styles.settingRight}>{children}</View>}
|
||||
{children && (
|
||||
<SettingLabelContext.Provider value={label}>
|
||||
<View style={styles.settingRight}>{children}</View>
|
||||
</SettingLabelContext.Provider>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -105,14 +113,18 @@ interface ToggleSwitchProps {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
disabled?: boolean;
|
||||
/** Defaults to the label of the enclosing SettingItem. */
|
||||
accessibilityLabel?: string;
|
||||
}
|
||||
|
||||
export function ToggleSwitch({ checked, onChange, disabled }: ToggleSwitchProps) {
|
||||
export function ToggleSwitch({ checked, onChange, disabled, accessibilityLabel }: ToggleSwitchProps) {
|
||||
const settingLabel = useContext(SettingLabelContext);
|
||||
return (
|
||||
<ToggleSwitchComponent
|
||||
value={checked}
|
||||
onValueChange={onChange}
|
||||
disabled={disabled}
|
||||
accessibilityLabel={accessibilityLabel ?? settingLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user