From cb5c152cb3cfde7540248b40e7554c092b213763 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:05:57 +0200 Subject: [PATCH] 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. --- src/components/ToggleSwitch.tsx | 5 ++++- src/components/filters/FilterRuleModal.tsx | 13 +++++++++++-- src/components/settings/FilterSettings.tsx | 8 ++++++-- .../settings/SidebarAppsSettings.tsx | 6 +++++- src/components/settings/TemplateSettings.tsx | 12 ++++++++++-- src/components/settings/settings-section.tsx | 18 +++++++++++++++--- 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx index ce0944ee..81165b7e 100644 --- a/src/components/ToggleSwitch.tsx +++ b/src/components/ToggleSwitch.tsx @@ -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 }} > updateAction(index, { keepCopy: v || undefined })} + accessibilityLabel={t('settings.filters.forward_keep_copy', 'Keep a copy')} /> @@ -446,7 +447,11 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F {t('settings.filters.stop_processing', 'Stop processing subsequent rules')} - + {/* Folder rules skip spam unless the rule opts in (webmail 6241ed61). */} @@ -455,7 +460,11 @@ export function FilterRuleModal({ visible, rule, mailboxes, onSave, onClose }: F {t('settings.filters.include_spam', 'Also move messages marked as spam')} - + )} diff --git a/src/components/settings/FilterSettings.tsx b/src/components/settings/FilterSettings.tsx index 73893a2e..9f972b8b 100644 --- a/src/components/settings/FilterSettings.tsx +++ b/src/components/settings/FilterSettings.tsx @@ -385,7 +385,7 @@ export function FilterSettings({ onOpenVacation }: FilterSettingsProps = {}) { return ( - handleToggle(rule.id)} /> + handleToggle(rule.id)} accessibilityLabel={rule.name} /> 0 && ( {t('settings.filters.expanded_view', 'Expanded view')} - updateSetting('filtersExpandedView', v)} /> + updateSetting('filtersExpandedView', v)} + accessibilityLabel={t('settings.filters.expanded_view', 'Expanded view')} + /> )} diff --git a/src/components/settings/SidebarAppsSettings.tsx b/src/components/settings/SidebarAppsSettings.tsx index 7a57eb18..438a5b9b 100644 --- a/src/components/settings/SidebarAppsSettings.tsx +++ b/src/components/settings/SidebarAppsSettings.tsx @@ -188,7 +188,11 @@ function AppForm({ initial, onSave, onCancel }: AppFormProps) { {t('settings.sidebar_apps.form.show_on_mobile', "Show on mobile")} - + diff --git a/src/components/settings/TemplateSettings.tsx b/src/components/settings/TemplateSettings.tsx index b6628d21..b8ca34fc 100644 --- a/src/components/settings/TemplateSettings.tsx +++ b/src/components/settings/TemplateSettings.tsx @@ -246,11 +246,19 @@ export function TemplateSettings() { {tr('settings.templates.favorite', 'Favorite')} - + {tr('settings.templates.is_html', 'Body is HTML')} - + diff --git a/src/components/settings/settings-section.tsx b/src/components/settings/settings-section.tsx index c998cd79..22eb3140 100644 --- a/src/components/settings/settings-section.tsx +++ b/src/components/settings/settings-section.tsx @@ -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(undefined); + interface SettingItemProps { label: string; description?: string; @@ -96,7 +100,11 @@ export function SettingItem({ {description && {description}} - {children && {children}} + {children && ( + + {children} + + )} ); } @@ -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 ( ); }