Skip to content

Return a longer recommended timeout on iOS while Switch Control is running - #59453

Open
camilopires wants to merge 5 commits into
react:mainfrom
camilopires:a11y/ios-switch-control-timeout
Open

camilopires wants to merge 5 commits into
react:mainfrom
camilopires:a11y/ios-switch-control-timeout

Conversation

@camilopires

Copy link
Copy Markdown
Contributor

Summary:

Following the discussion in react-native-community/discussions-and-proposals#1026, this makes the existing AccessibilityInfo.getRecommendedTimeoutMillis(originalTimeout) useful on iOS. It was split out of #59318 (the accessibility preferences snapshot) so that the change to an existing API can be reviewed on its own.

Until now, getRecommendedTimeoutMillis read Android's "Time to take action" setting and resolved originalTimeout unchanged on iOS. iOS has no such setting, but it does have people who need more time: Switch Control users move between elements one switch press at a time, so a 4-second toast or an auto-dismissing banner is often gone before they reach it.

On iOS, getRecommendedTimeoutMillis now asks RCTAccessibilityManager, which returns three times originalTimeout while Switch Control is running (UIAccessibilityIsSwitchControlRunning()), and originalTimeout otherwise. This uses the method apps already call for timeouts, rather than a new isSwitchControlEnabled() flag that every app would have to turn into a timeout itself. It also replaces the separate getRecommendedTimeout I suggested on #1026.

Why 3×: Apple publishes no recommended figure, and Switch Control's own scanning speed is user-configurable, so there's no value to read. 3× is a judgement call: it turns a typical 3–5 second toast into 9–15 seconds, which gives a switch user time to scan to it without leaving the UI on screen indefinitely. I'm happy to change the multiplier, or to make it configurable, if reviewers prefer.

Effect on existing callers ([IOS] [CHANGED]): iOS code that already calls getRecommendedTimeoutMillis gets a longer value while Switch Control is running. Nothing changes when it isn't running, 0 stays 0, and Android is unchanged.

Older binaries: because this changes an existing API, new JS on a binary without the native method keeps the old behaviour. When NativeAccessibilityManagerIOS.getRecommendedTimeoutMillis, or the whole module, is missing, it still resolves originalTimeout rather than rejecting. (New APIs such as getPreferences() in #59318 reject instead, like isShowBordersEnabled.)

The docs now also say that Android follows "Time to take action" only from Android 10 (API 29); earlier versions resolve originalTimeout.

Overlaps: this edits several of the same files as #59318, so whichever lands second needs a small rebase. Five files conflict, each in a few lines: the iOS native-module mock in __tests__/AccessibilityInfo-test.js, RCTAccessibilityManager+Internal.h, the @implementation ivar block in RCTAccessibilityManager.mm, __typetests__/index.tsx and RNTester's Accessibility example. The C++ API snapshots and project.pbxproj merge cleanly, because the XCTests are in their own file, RCTAccessibilityManagerTimeoutTests.m.

Changelog:

[IOS] [CHANGED] - AccessibilityInfo.getRecommendedTimeoutMillis() returns three times originalTimeout while Switch Control is running

Test Plan:

  • yarn jest packages/react-native/Libraries/Components/AccessibilityInfo: 45 tests pass. New ones cover the iOS native value, and that iOS resolves originalTimeout when the native method is missing and when the whole native module is missing.
  • yarn flow-check: 0 errors
  • yarn lint: passes
  • yarn build-types --validate: API snapshot up to date (the public signature doesn't change)
  • yarn test-generated-typescript and yarn test-typescript-legacy: pass
  • yarn format-check-cpp, and Prettier on the changed JS and TS: pass
  • C++ API snapshots regenerated with Doxygen 1.16.1 and Python 3.12, and validated: the four Apple snapshots gain getRecommendedTimeoutMillis:onSuccess:.
  • New RCTAccessibilityManagerTimeoutTests in RNTesterUnitTests: 5 tests pass on an iPhone 18 Pro simulator (iOS 27.0, Xcode 27.1). Each module instance gets its own Switch Control reader (RCTAccessibilityManagerSetSwitchControlReader, in +Internal.h). They check 3× while Switch Control is running, the original value when it isn't, that 0 stays 0, that the reader is read on every call, that two instances keep their own readers, and that with no reader (or after resetting it to nil) the system value is used. Removing the * 3 makes 4 of them fail.
  • RNTester's Accessibility → "display options" gains a "Recommended timeout for 4000 ms" row, which reads the value again whenever RNTester becomes active, because there's no change event for it.

I haven't run the new RNTester row on a simulator or emulator yet. With Switch Control off it would show the original 4000 ms, which the unit tests already cover; I'll add screenshots once I've run it.

Not verified: the timeout with Switch Control running on a real device. The iOS simulator has no Switch Control, so that path is covered by the unit tests only. I'll add the device result here once I've run it.

camilopires and others added 5 commits October 24, 2026 15:00
getRecommendedTimeoutMillis(originalTimeout) read Android's "Time to
take action" setting and resolved originalTimeout unchanged on iOS. It
now asks NativeAccessibilityManagerIOS too, so iOS can allow a longer
timeout while Switch Control is running. The native side follows.

This changes an existing API, so new JS on an older binary keeps the old
behaviour: when the native method or the whole module is missing, it
still resolves originalTimeout rather than rejecting.

The docs now say that Android follows the setting from Android 10 (API
29), and that iOS allows three times originalTimeout while Switch
Control is running. Adds Jest tests for the native value and for both
fallbacks, and a type test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a row to Accessibility's display options that shows
getRecommendedTimeoutMillis(4000). AccessibilityInfo has no event for
the recommended timeout, so the row reads it again whenever RNTester
becomes active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nning

RCTAccessibilityManager implements getRecommendedTimeoutMillis: it
returns three times the original timeout while Switch Control is
running (UIAccessibilityIsSwitchControlRunning), and the original
timeout otherwise. iOS has no recommended timeout setting, and moving
between elements one switch press at a time is slow.

Tests can replace how one module instance reads Switch Control with
RCTAccessibilityManagerSetSwitchControlReader, in +Internal.h. Pass nil
to read the system again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add RCTAccessibilityManagerTimeoutTests to RNTesterUnitTests, in its own
file. With a per-instance Switch Control reader, the tests check three
times the timeout while Switch Control is running, the original timeout
when it isn't, that 0 stays 0, that the reader is read on every call,
that two instances keep their own readers, and that with no reader the
system is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS NativeAccessibilityManager spec gains getRecommendedTimeoutMillis,
so the generated spec method appears in the Apple snapshots. Generated
with Doxygen 1.16.1 and Python 3.12, as in validate-cxx-api-snapshots.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 24, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 24, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant