Skip to content

Commit 7bba622

Browse files
arun279meta-codesync[bot]
authored andcommitted
Report a disabled Switch as disabled to iOS accessibility (#59333)
Summary: On iOS, `<Switch disabled />` is drawn greyed out and ignores taps, but VoiceOver and XCUITest still report it as enabled: the `UIAccessibilityTraitNotEnabled` trait is missing. Since #50796, the inner `UISwitch` is the Switch's accessibility element, so `RCTViewComponentView` writes the traits it derives from the role onto the `UISwitch`, replacing the ones UIKit sets, including `NotEnabled`. Since #57516, `accessibilityState.disabled` adds `NotEnabled` to those traits, but the iOS branch of `Switch.js` never sets it from `disabled`. The Android branch already does. This moves the existing Android logic above the platform check so iOS gets the same `accessibilityState`. As on Android, `disabled` wins over `accessibilityState.disabled` when both are set. It is done in `Switch.js` rather than `RCTSwitchComponentView` because the Android branch already does it here, and a Switch-specific native writer for the traits would bring back the second trait writer that #57516 removed. `disabled` now behaves exactly like `accessibilityState={{disabled: true}}` already does. That includes a Switch whose role maps to no trait, such as `accessibilityRole="none"`: it gets `NotEnabled` alone, without the switch trait. ## Changelog: [IOS] [FIXED] - Report a disabled `Switch` as disabled to VoiceOver and XCUITest Pull Request resolved: #59333 Test Plan: Local regression validation: ``` js1 test "(CosmoCollapsibleSelector|CosmoConsentSection|CosmoReportBugForm|TWUISwitch|TWUIListCell|OCDevModeToggleRow|CosmoGeneralAppPreferencesSection|CosmoImportWizard|CosmoSettingsRow|TWAUGroupedMenuListCell|CosmoStudioSettingsBox|TWUIPaymentListCell|TWUIPeopleListCell)-test\.js$" --maxWorkers=4 # 13 suites passed; 66 tests passed; 30 snapshots passed; exit 0 arc lint -a # No lint issues ``` The same targeted run before the snapshot update failed all 13 suites (16 snapshot failures). Updated only the expected disabled accessibility metadata in 16 snapshots. Removed one obsolete `TWUIPeopleListCell renders testSupportingText 2` entry: the helper invokes one snapshot assertion per exported test, so that second entry has no live producer; both retained snapshots are unchanged. Independent review found no blockers. Added two cases to `Switch-itest.js`: `disabled` overriding `accessibilityState.disabled`, and no `accessibilityState` when `disabled` is not set. Fantom renders the Android branch, so these cover the logic both platforms now share; the iOS side is covered by the manual check below. Each new case fails when its part of the logic is changed. ``` yarn fantom packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js # 6 passed yarn flow-check # Found 0 errors yarn eslint --max-warnings 0 packages/react-native/Libraries/Components/Switch/Switch.js packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js # no warnings yarn prettier --check packages/react-native/Libraries/Components/Switch/Switch.js packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js # All matched files use Prettier code style! ``` Fantom was built locally on macOS with Xcode 27, whose clang needed `-Wno-deprecated-literal-operator` for the bundled `nlohmann/json.hpp`. Manual check: a new 0.88.0 app (prebuilt core; `Switch.js` is the same on 0.88.0 and `main`) on an iPhone 17 Pro simulator, iOS 27. For "After", this branch's `Switch.js` was copied into the app's `node_modules/react-native/Libraries/Components/Switch/`. The accessibility tree was read with [AXe](https://github.com/cameroncooke/AXe) `describe-ui`, which reports each element's `enabled` attribute. ```tsx <Switch testID="disabled-mount" value disabled /> <Switch testID="disabled-live" value disabled={liveDisabled} /> <Switch testID="disabled-state" value disabled accessibilityState={{disabled: true}} /> ``` | Switch | Before | After | | --- | --- | --- | | `disabled-mount` | enabled | not enabled | | `disabled-live`, after `disabled` is set to `true` | enabled | not enabled | | `disabled-live`, after `disabled` is set back to `false` | enabled | enabled | | `disabled-state` | not enabled | not enabled | The role (`switch`) and value (`1`) are unchanged. Not run: VoiceOver on a device. Reviewed By: rubennorte Differential Revision: D127141579 Pulled By: javache fbshipit-source-id: c68bc8353788bd850cc2e00a7e69a8a0cb10bc92
1 parent bd08daf commit 7bba622

2 files changed

Lines changed: 33 additions & 8 deletions

File tree

‎packages/react-native/Libraries/Components/Switch/Switch.js‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const Switch: component(
177177
...SwitchProps,
178178
}): React.Node {
179179
const {
180+
accessibilityState,
180181
disabled,
181182
ios_backgroundColor,
182183
onChange,
@@ -227,16 +228,15 @@ const Switch: component(
227228
}
228229
}, [value, native]);
229230

231+
const _disabled = disabled != null ? disabled : accessibilityState?.disabled;
232+
233+
const _accessibilityState: ?AccessibilityState =
234+
_disabled !== accessibilityState?.disabled
235+
? {...accessibilityState, disabled: _disabled}
236+
: accessibilityState;
237+
230238
if (Platform.OS === 'android') {
231239
const {onTintColor, tintColor, ...androidProps} = restProps;
232-
const {accessibilityState} = androidProps;
233-
const _disabled =
234-
disabled != null ? disabled : accessibilityState?.disabled;
235-
236-
const _accessibilityState: ?AccessibilityState =
237-
_disabled !== accessibilityState?.disabled
238-
? {...accessibilityState, disabled: _disabled}
239-
: accessibilityState;
240240

241241
const platformProps = {
242242
accessibilityState: _accessibilityState,
@@ -262,6 +262,7 @@ const Switch: component(
262262
);
263263
} else {
264264
const platformProps = {
265+
accessibilityState: _accessibilityState,
265266
disabled,
266267
onTintColor: trackColorForTrue,
267268
style: StyleSheet.compose(

‎packages/react-native/Libraries/Components/Switch/__tests__/Switch-itest.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ describe('Switch', () => {
4646
);
4747
});
4848

49+
it('lets disabled override accessibilityState.disabled', () => {
50+
const root = render(
51+
<Switch
52+
value={false}
53+
disabled={false}
54+
accessibilityState={{disabled: true, busy: true}}
55+
/>,
56+
);
57+
58+
expect(
59+
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
60+
).toEqual(
61+
<rn-androidSwitch accessibilityState="{disabled:false,selected:null,checked:None,busy:true,expanded:null}" />,
62+
);
63+
});
64+
65+
it('does not set accessibilityState when disabled is not set', () => {
66+
const root = render(<Switch value={false} />);
67+
68+
expect(
69+
root.getRenderedOutput({props: ['accessibilityState']}).toJSX(),
70+
).toEqual(<rn-androidSwitch />);
71+
});
72+
4973
it('renders with track and thumb color props applied', () => {
5074
// Exercises the color-resolution path; the mounted switch still renders as
5175
// the native switch host.

0 commit comments

Comments
 (0)