From 3165b40127a6abaca09dd8ee749a1be3e14bc797 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:29:26 +0100 Subject: [PATCH] fix: toggle a settings switch by tapping anywhere on its row --- .../settings/__tests__/switch-row.test.ts | 40 +++++++++++++++++++ src/components/settings/settings-section.tsx | 36 +++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/components/settings/__tests__/switch-row.test.ts diff --git a/src/components/settings/__tests__/switch-row.test.ts b/src/components/settings/__tests__/switch-row.test.ts new file mode 100644 index 00000000..10d7c34c --- /dev/null +++ b/src/components/settings/__tests__/switch-row.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +vi.mock('../../ToggleSwitch', () => ({ default: () => null })); +vi.mock('../../icons', () => ({})); +vi.mock('../../../stores/locale-store', () => ({ useLocaleStore: () => undefined })); +vi.mock('../../../theme/colors', () => ({ useColors: () => ({}) })); +vi.mock('../search-highlight', () => ({ useSearchHighlight: () => ({}) })); +vi.mock('../../../lib/modal-orientations', () => ({ MODAL_ORIENTATIONS: [] })); +vi.mock('react-native', () => ({ StyleSheet: { create: (s: unknown) => s } })); + +import { switchRowPress } from '../settings-section'; + +// Tapping the "Colour-blind-safe colours" label did nothing: only the switch +// itself flipped the setting. + +describe('a settings row with a switch', () => { + it('flips the switch when the row is tapped', () => { + const onChange = vi.fn(); + switchRowPress(false, onChange)!(); + expect(onChange).toHaveBeenLastCalledWith(true); + switchRowPress(true, onChange)!(); + expect(onChange).toHaveBeenLastCalledWith(false); + }); + + it('leaves a disabled switch alone', () => { + expect(switchRowPress(false, vi.fn(), true)).toBeNull(); + }); + + it('hands the switch to its row, which presses it unless the setting is locked', () => { + const src = fs.readFileSync(path.resolve(__dirname, '..', 'settings-section.tsx'), 'utf8'); + expect(src).toMatch(/rowToggle\.current = switchRowPress\(checked, onChange, disabled\);/); + expect(src).toMatch(/onPress=\{\(\) => \{ if \(!locked\) rowToggle\.current\?\.\(\); \}\}/); + expect(src).toMatch(//); + // The colour-blind row is such a row. + const a11y = fs.readFileSync(path.resolve(__dirname, '..', 'AccessibilitySettings.tsx'), 'utf8'); + expect(a11y).toMatch(/label=\{t\('settings\.accessibility\.cvd_palette\.label'[\s\S]{0,300}(undefined); +// What a tap on the row does when its control is a switch: flip it, so the +// label and the description are as good a target as the 44 x 24 switch +// (tapping "Colour-blind-safe colours" did nothing). +const SettingToggleContext = React.createContext void) | null> | null>(null); + +/** The row tap for a switch: flips it; null while it is disabled. */ +export function switchRowPress(checked: boolean, onChange: (v: boolean) => void, disabled?: boolean): (() => void) | null { + return disabled ? null : () => onChange(!checked); +} + interface SettingItemProps { label: string; description?: string; @@ -100,6 +110,8 @@ export function SettingItem({ const styles = React.useMemo(() => makeStyles(c), [c]); const t = useLocaleStore((s) => s.t); const highlight = useSearchHighlight(label); + // Set by a ToggleSwitch in the row (switchRowPress). + const rowToggle = React.useRef<(() => void) | null>(null); // The control sits on the right. When that leaves the label column narrower // than STACK_BELOW (a long translation, two buttons), the control moves // under the label, as the webmail stacks it on a phone. A new row width @@ -130,8 +142,10 @@ export function SettingItem({ {children && ( - {/* A locked setting shows its value but can't be changed. */} - {children} + + {/* A locked setting shows its value but can't be changed. */} + {children} + )} {onPress && (link === 'external' @@ -152,10 +166,18 @@ export function SettingItem({ ); } + // A tap anywhere on the row flips its switch, if it has one. Not a + // control of its own for a screen reader: the switch carries the label. return ( - + { if (!locked) rowToggle.current?.(); }} + accessible={false} + style={rowStyle} + > {body} - + ); } @@ -219,6 +241,12 @@ interface ToggleSwitchProps { export function ToggleSwitch({ checked, onChange, disabled, accessibilityLabel }: ToggleSwitchProps) { const settingLabel = useContext(SettingLabelContext); + const rowToggle = useContext(SettingToggleContext); + React.useEffect(() => { + if (!rowToggle) return undefined; + rowToggle.current = switchRowPress(checked, onChange, disabled); + return () => { rowToggle.current = null; }; + }, [rowToggle, checked, onChange, disabled]); return (