From 81c54604feff5286043630205ce74ddf931373a7 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:00:58 +0100 Subject: [PATCH 1/5] Ask iOS for the recommended timeout in getRecommendedTimeoutMillis 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 --- .../AccessibilityInfo/AccessibilityInfo.js | 26 +++++++-- .../__tests__/AccessibilityInfo-test.js | 54 ++++++++++++++++++- packages/react-native/__typetests__/index.tsx | 3 ++ .../modules/NativeAccessibilityManager.js | 4 ++ .../AccessibilityInfo/AccessibilityInfo.d.ts | 10 ++-- 5 files changed, 89 insertions(+), 8 deletions(-) diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index 017c1d3c842e..2845263e8f8a 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -528,10 +528,15 @@ const AccessibilityInfo = { }, /** - * Get the recommended timeout in milliseconds the user needs, as specified - * in the device's Accessibility settings. + * Get the recommended timeout in milliseconds the user needs, given the + * timeout the app would otherwise use. * - * @platform android + * On Android 10 (API 29) and later, this follows the "Time to take action" + * accessibility setting; earlier versions resolve to `originalTimeout`. + * iOS has no such setting, so React Native allows three times + * `originalTimeout` while Switch Control is running, because moving between + * elements one switch press at a time is slow, and `originalTimeout` + * otherwise. */ getRecommendedTimeoutMillis(originalTimeout: number): Promise { if (Platform.OS === 'android') { @@ -548,7 +553,20 @@ const AccessibilityInfo = { } }); } else { - return Promise.resolve(originalTimeout); + return new Promise(resolve => { + if ( + NativeAccessibilityManagerIOS?.getRecommendedTimeoutMillis != null + ) { + NativeAccessibilityManagerIOS.getRecommendedTimeoutMillis( + originalTimeout, + resolve, + ); + } else { + // An existing API, so new JS on an older binary keeps resolving the + // value it always did on iOS rather than rejecting. + resolve(originalTimeout); + } + }); } }, }; diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js index 380be9fdd394..02e52643ebf9 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js @@ -22,6 +22,10 @@ const mockGetCurrentShowBordersState = jest.fn((onSuccess, onError) => const mockGetCurrentBoldTextState = jest.fn((onSuccess, onError) => onSuccess(true), ); +const IOS_RECOMMENDED_TIMEOUT = 15000; +const mockGetRecommendedTimeoutMillisIOS = jest.fn( + (originalTimeout, onSuccess) => onSuccess(IOS_RECOMMENDED_TIMEOUT), +); const mockGetCurrentGrayscaleState = jest.fn((onSuccess, onError) => onSuccess(true), ); @@ -38,6 +42,10 @@ const mockGetCurrentVoiceOverState = jest.fn((onSuccess, onError) => onSuccess(true), ); let mockNativeAccessibilityManagerIOS: { + getRecommendedTimeoutMillis: JestMockFn< + [originalTimeout: number, onSuccess: (timeout: number) => void], + void, + > | null, getCurrentShowBordersState: JestMockFn< [ onSuccess: (isShowBordersEnabled: boolean) => void, @@ -102,6 +110,7 @@ let mockNativeAccessibilityManagerIOS: { void, > | null, } = { + getRecommendedTimeoutMillis: mockGetRecommendedTimeoutMillisIOS, getCurrentShowBordersState: mockGetCurrentShowBordersState, getCurrentPrefersCrossFadeTransitionsState: mockGetCurrentPrefersCrossFadeTransitionsState, @@ -831,20 +840,63 @@ describe('AccessibilityInfo', () => { }); describe('iOS', () => { - it('should return originalTimeout', async () => { + it('should return the native recommended timeout', async () => { /* $FlowFixMe[incompatible-type] */ Platform.OS = 'ios'; + const recommendedTimeoutMillis = + await AccessibilityInfo.getRecommendedTimeoutMillis(5000); + + expect(mockGetRecommendedTimeoutMillisIOS).toHaveBeenCalledWith( + 5000, + expect.any(Function), + ); + expect(recommendedTimeoutMillis).toBe(IOS_RECOMMENDED_TIMEOUT); + }); + + it('should return originalTimeout if getRecommendedTimeoutMillis is not available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'ios'; + + mockNativeAccessibilityManagerIOS.getRecommendedTimeoutMillis = null; + const recommendedTimeoutMillis = await AccessibilityInfo.getRecommendedTimeoutMillis(5000); expect(recommendedTimeoutMillis).toBe(5000); }); + + it('should return originalTimeout if NativeAccessibilityManagerIOS is not available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'ios'; + + const nativeAccessibilityManagerModule = jest.requireMock( + '../NativeAccessibilityManager', + ); + const originalModule = nativeAccessibilityManagerModule.default; + // The native method is present, so only the missing module can make + // getRecommendedTimeoutMillis skip it. + expect(originalModule.getRecommendedTimeoutMillis).toBe( + mockGetRecommendedTimeoutMillisIOS, + ); + nativeAccessibilityManagerModule.default = null; + + try { + const recommendedTimeoutMillis = + await AccessibilityInfo.getRecommendedTimeoutMillis(5000); + + expect(mockGetRecommendedTimeoutMillisIOS).not.toHaveBeenCalled(); + expect(recommendedTimeoutMillis).toBe(5000); + } finally { + nativeAccessibilityManagerModule.default = originalModule; + } + }); }); }); afterEach(() => { mockNativeAccessibilityManagerIOS = { + getRecommendedTimeoutMillis: mockGetRecommendedTimeoutMillisIOS, getCurrentShowBordersState: mockGetCurrentShowBordersState, getCurrentPrefersCrossFadeTransitionsState: mockGetCurrentPrefersCrossFadeTransitionsState, diff --git a/packages/react-native/__typetests__/index.tsx b/packages/react-native/__typetests__/index.tsx index 75e5dce1634c..4593da40d959 100644 --- a/packages/react-native/__typetests__/index.tsx +++ b/packages/react-native/__typetests__/index.tsx @@ -1470,6 +1470,9 @@ AccessibilityInfo.isShowBordersEnabled().then(isEnabled => AccessibilityInfo.addEventListener('showBordersChanged', isEnabled => console.log(`AccessibilityInfo.showBordersChanged => ${isEnabled}`), ); +AccessibilityInfo.getRecommendedTimeoutMillis(4000).then((timeout: number) => + console.log(`AccessibilityInfo.getRecommendedTimeoutMillis => ${timeout}`), +); AccessibilityInfo.isBoldTextEnabled().then(isEnabled => console.log(`AccessibilityInfo.isBoldTextEnabled => ${isEnabled}`), ); diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js index 0dfb462fe674..2629b1d1c068 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js @@ -50,6 +50,10 @@ export interface Spec extends TurboModule { onSuccess: (isScreenReaderEnabled: boolean) => void, onError: (error: UnsafeObject) => void, ) => void; + readonly getRecommendedTimeoutMillis?: ( + mSec: number, + onSuccess: (recommendedTimeoutMillis: number) => void, + ) => void; readonly setAccessibilityContentSizeMultipliers: (JSMultipliers: { readonly extraSmall?: ?number, readonly small?: ?number, diff --git a/packages/react-native/types_DEPRECATED/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts b/packages/react-native/types_DEPRECATED/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts index 6f2ccb478328..db1c0a49ed29 100644 --- a/packages/react-native/types_DEPRECATED/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts +++ b/packages/react-native/types_DEPRECATED/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts @@ -182,10 +182,14 @@ export interface AccessibilityInfoStatic { ): void; /** - * Gets the timeout in millisecond that the user needs. - * This value is set in "Time to take action (Accessibility timeout)" of "Accessibility" settings. + * Gets the timeout in milliseconds that the user needs, given the timeout + * the app would otherwise use. * - * @platform android + * On Android 10 (API 29) and later, this value is set in "Time to take + * action (Accessibility timeout)" of "Accessibility" settings; earlier + * versions resolve to `originalTimeout`. iOS has no such setting, so React + * Native allows three times `originalTimeout` while Switch Control is + * running, and `originalTimeout` otherwise. */ getRecommendedTimeoutMillis: (originalTimeout: number) => Promise; sendAccessibilityEvent: ( From 378a640866eb444089af843f1974a152bfeb3145 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:01:00 +0100 Subject: [PATCH 2/5] Show the recommended timeout in RNTester 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 --- .../Accessibility/AccessibilityExample.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index ff2891104139..476a919d7447 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -23,6 +23,7 @@ import {createRef, useEffect, useRef, useState} from 'react'; import { AccessibilityInfo, Alert, + AppState, Button, Image, ImageBackground, @@ -1606,6 +1607,7 @@ class DisplayOptionsStatusExample extends React.Component<{}> { /> )} + ); } @@ -1647,6 +1649,41 @@ function DisplayOptionStatusExample({ ); } +function RecommendedTimeoutStatusExample(): React.Node { + const [recommendedTimeout, setRecommendedTimeout] = useState(null); + useEffect(() => { + let isMounted = true; + // AccessibilityInfo has no event for the recommended timeout, so read it + // again whenever the app becomes active. + const read = () => { + // $FlowFixMe[unused-promise] + AccessibilityInfo.getRecommendedTimeoutMillis(4000).then(timeout => { + if (isMounted) { + setRecommendedTimeout(timeout); + } + }); + }; + read(); + const appStateListener = AppState.addEventListener('change', state => { + if (state === 'active') { + read(); + } + }); + return function cleanup() { + isMounted = false; + appStateListener.remove(); + }; + }, []); + return ( + + + {'Recommended timeout for 4000 ms: '} + {recommendedTimeout == null ? 'loading' : `${recommendedTimeout} ms`}. + + + ); +} + function AccessibilityExpandedExample(): React.Node { const [expand, setExpanded] = useState(false); const expandAction = {name: 'expand'}; From 5dbad16df951e5f3c08058c06b40d13241db8d35 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:19:06 +0100 Subject: [PATCH 3/5] Return a longer recommended timeout on iOS while Switch Control is running 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 --- .../RCTAccessibilityManager+Internal.h | 9 ++++++++ .../CoreModules/RCTAccessibilityManager.mm | 22 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager+Internal.h b/packages/react-native/React/CoreModules/RCTAccessibilityManager+Internal.h index 6e180007fdca..d5c0efd4ed96 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager+Internal.h +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager+Internal.h @@ -19,6 +19,15 @@ void RCTAccessibilityManagerSetIsVoiceOverEnabled( RCTAccessibilityManager *accessibilityManager, BOOL isVoiceOverEnabled); +// Returns whether Switch Control is running. +typedef BOOL (^RCTSwitchControlReader)(void); + +// Replaces how this module reads whether Switch Control is running, for +// getRecommendedTimeoutMillis. Pass nil to read the system again. +void RCTAccessibilityManagerSetSwitchControlReader( + RCTAccessibilityManager *accessibilityManager, + RCTSwitchControlReader _Nullable switchControlReader); + RCT_EXTERN_C_END NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm index dee5e643c758..1038d5d6ba6b 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm @@ -56,7 +56,10 @@ static NSNotificationName RCTShowBordersStatusDidChangeNotification() #pragma clang diagnostic pop } -@implementation RCTAccessibilityManager +@implementation RCTAccessibilityManager { + // Replaced in tests through RCTAccessibilityManagerSetSwitchControlReader. + RCTSwitchControlReader _switchControlReader; +} @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; @synthesize moduleRegistry = _moduleRegistry; @@ -469,6 +472,16 @@ - (void)getCurrentVoiceOverState:(RCTResponseSenderBlock)onSuccess onError:(__un onSuccess(@[ @(_isVoiceOverEnabled) ]); } +- (void)getRecommendedTimeoutMillis:(double)mSec onSuccess:(RCTResponseSenderBlock)onSuccess +{ + BOOL isSwitchControlRunning = + _switchControlReader != nil ? _switchControlReader() : UIAccessibilityIsSwitchControlRunning(); + // iOS has no recommended timeout setting. Moving between elements one switch + // press at a time is slow, so allow three times as long while Switch Control + // is running. + onSuccess(@[ @(isSwitchControlRunning ? mSec * 3 : mSec) ]); +} + - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { @@ -484,6 +497,13 @@ void RCTAccessibilityManagerSetIsVoiceOverEnabled( [accessibilityManager _setIsVoiceOverEnabled:isVoiceOverEnabled]; } +void RCTAccessibilityManagerSetSwitchControlReader( + RCTAccessibilityManager *accessibilityManager, + RCTSwitchControlReader _Nullable switchControlReader) +{ + accessibilityManager->_switchControlReader = [switchControlReader copy]; +} + @end @implementation RCTBridgeProxy (RCTAccessibilityManager) From e1584a309bf2398656e1fbe7441852d2a3e30d50 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:19:08 +0100 Subject: [PATCH 4/5] Test the Switch Control timeout in RCTAccessibilityManager 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 --- .../RNTesterPods.xcodeproj/project.pbxproj | 4 + .../RCTAccessibilityManagerTimeoutTests.m | 110 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/rn-tester/RNTesterUnitTests/RCTAccessibilityManagerTimeoutTests.m diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj index 1d336b424136..8dab68e46d66 100644 --- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj +++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 8145AE06241172D900A3F8DA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8145AE05241172D900A3F8DA /* LaunchScreen.storyboard */; }; 832F45BB2A8A6E1F0097B4E6 /* SwiftTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */; }; A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; }; + A11E0F1D2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A11E0F1C2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m */; }; C175B6D9ED9336FB66637943 /* libPods-RNTester.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */; }; CD10C7A5290BD4EB0033E1ED /* RCTEventEmitterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */; }; E62F11832A5C6580000BF1C8 /* FlexibleSizeExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.mm */; }; @@ -100,6 +101,7 @@ 832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftTest.swift; path = RNTester/SwiftTest.swift; sourceTree = ""; }; 93A243F0D4D5C54911E811C4 /* libPods-RNTesterIntegrationTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTesterIntegrationTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkTaskTests.m; sourceTree = ""; }; + A11E0F1C2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManagerTimeoutTests.m; sourceTree = ""; }; AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = ""; }; B0E70A8A05E03E868F8703FE /* Pods-RNTesterIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-RNTesterIntegrationTests/Pods-RNTesterIntegrationTests.release.xcconfig"; sourceTree = ""; }; CA59C9994B1822826D8983F0 /* Pods-RNTester.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.debug.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.debug.xcconfig"; sourceTree = ""; }; @@ -320,6 +322,7 @@ children = ( CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */, E7DB20A322B2BA84005AC45F /* Info.plist */, + A11E0F1C2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m */, E7DB20C622B2BAA5005AC45F /* RCTAllocationTests.m */, E7DB20B222B2BAA4005AC45F /* RCTAnimationUtilsTests.m */, E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */, @@ -753,6 +756,7 @@ E7DB20DF22B2BAA6005AC45F /* RCTImageLoaderTests.m in Sources */, E7DB20D522B2BAA6005AC45F /* RCTPerformanceLoggerTests.m in Sources */, E7DB20D922B2BAA6005AC45F /* RCTAnimationUtilsTests.m in Sources */, + A11E0F1D2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m in Sources */, E7DB20D122B2BAA6005AC45F /* RCTBundleURLProviderTests.m in Sources */, E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */, E7DB20E022B2BAA6005AC45F /* RCTMethodArgumentTests.m in Sources */, diff --git a/packages/rn-tester/RNTesterUnitTests/RCTAccessibilityManagerTimeoutTests.m b/packages/rn-tester/RNTesterUnitTests/RCTAccessibilityManagerTimeoutTests.m new file mode 100644 index 000000000000..3d17e9984d69 --- /dev/null +++ b/packages/rn-tester/RNTesterUnitTests/RCTAccessibilityManagerTimeoutTests.m @@ -0,0 +1,110 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#import +#import +#import + +// The native module method JS calls, from the generated spec. +@interface RCTAccessibilityManager (RCTAccessibilityManagerTimeoutTests) + +- (void)getRecommendedTimeoutMillis:(double)mSec onSuccess:(RCTResponseSenderBlock)onSuccess; + +@end + +@interface RCTAccessibilityManagerTimeoutTests : XCTestCase + +@end + +@implementation RCTAccessibilityManagerTimeoutTests + +- (NSNumber *)recommendedTimeoutOf:(RCTAccessibilityManager *)manager for:(double)mSec +{ + __block NSNumber *timeout = nil; + [manager getRecommendedTimeoutMillis:mSec + onSuccess:^(NSArray *response) { + timeout = response.firstObject; + }]; + return timeout; +} + +- (void)testTimeoutIsThreeTimesLongerWhileSwitchControlIsRunning +{ + RCTAccessibilityManager *manager = [RCTAccessibilityManager new]; + XCTAssertNotNil(manager); + RCTAccessibilityManagerSetSwitchControlReader(manager, ^BOOL { + return YES; + }); + + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], @12000); + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:0], @0); +} + +- (void)testTimeoutIsUnchangedWhileSwitchControlIsNotRunning +{ + RCTAccessibilityManager *manager = [RCTAccessibilityManager new]; + XCTAssertNotNil(manager); + RCTAccessibilityManagerSetSwitchControlReader(manager, ^BOOL { + return NO; + }); + + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], @4000); + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:0], @0); +} + +- (void)testReaderIsReadOnEveryCall +{ + RCTAccessibilityManager *manager = [RCTAccessibilityManager new]; + XCTAssertNotNil(manager); + __block BOOL isSwitchControlRunning = NO; + RCTAccessibilityManagerSetSwitchControlReader(manager, ^BOOL { + return isSwitchControlRunning; + }); + + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], @4000); + isSwitchControlRunning = YES; + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], @12000); +} + +- (void)testSeparateInstancesUseTheirOwnReaders +{ + RCTAccessibilityManager *managerA = [RCTAccessibilityManager new]; + RCTAccessibilityManager *managerB = [RCTAccessibilityManager new]; + XCTAssertNotNil(managerA); + XCTAssertNotNil(managerB); + RCTAccessibilityManagerSetSwitchControlReader(managerA, ^BOOL { + return YES; + }); + RCTAccessibilityManagerSetSwitchControlReader(managerB, ^BOOL { + return NO; + }); + + XCTAssertEqualObjects([self recommendedTimeoutOf:managerA for:4000], @12000); + XCTAssertEqualObjects([self recommendedTimeoutOf:managerB for:4000], @4000); +} + +- (void)testWithoutAReaderTheSystemIsRead +{ + RCTAccessibilityManager *manager = [RCTAccessibilityManager new]; + XCTAssertNotNil(manager); + NSNumber *expected = UIAccessibilityIsSwitchControlRunning() ? @12000 : @4000; + + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], expected); + + RCTAccessibilityManagerSetSwitchControlReader(manager, ^BOOL { + return !UIAccessibilityIsSwitchControlRunning(); + }); + XCTAssertNotEqualObjects([self recommendedTimeoutOf:manager for:4000], expected); + + RCTAccessibilityManagerSetSwitchControlReader(manager, nil); + XCTAssertEqualObjects([self recommendedTimeoutOf:manager for:4000], expected); +} + +@end From 817626686df929f630b0ff0dd9e9b203108d9bc4 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:19:11 +0100 Subject: [PATCH 5/5] Update C++ API snapshots for getRecommendedTimeoutMillis 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 --- scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 1 + 4 files changed, 4 insertions(+) diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 8de2c49759a6..f465bfe67531 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2152,6 +2152,7 @@ protocol NativeAccessibilityManagerSpec : public NSObjectRCTBridgeModule, public public virtual void getCurrentReduceTransparencyState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentShowBordersState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentVoiceOverState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); + public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void setAccessibilityContentSizeMultipliers:(JS::NativeAccessibilityManager::SpecSetAccessibilityContentSizeMultipliersJSMultipliers& JSMultipliers); public virtual void setAccessibilityFocus:(double reactTag); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index 6b5e45141f07..060b6be388d1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -2210,6 +2210,7 @@ protocol NativeAccessibilityManagerSpec : public NSObjectRCTBridgeModule, public public virtual void getCurrentReduceTransparencyState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentShowBordersState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentVoiceOverState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); + public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void setAccessibilityContentSizeMultipliers:(JS::NativeAccessibilityManager::SpecSetAccessibilityContentSizeMultipliersJSMultipliers& JSMultipliers); public virtual void setAccessibilityFocus:(double reactTag); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index e8201541b2e1..d133ed1a1ac8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -2145,6 +2145,7 @@ protocol NativeAccessibilityManagerSpec : public NSObjectRCTBridgeModule, public public virtual void getCurrentReduceTransparencyState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentShowBordersState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentVoiceOverState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); + public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void setAccessibilityContentSizeMultipliers:(JS::NativeAccessibilityManager::SpecSetAccessibilityContentSizeMultipliersJSMultipliers& JSMultipliers); public virtual void setAccessibilityFocus:(double reactTag); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index d3ff97cc59c6..5f7444a233dc 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2152,6 +2152,7 @@ protocol NativeAccessibilityManagerSpec : public NSObjectRCTBridgeModule, public public virtual void getCurrentReduceTransparencyState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentShowBordersState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); public virtual void getCurrentVoiceOverState:onError:(RCTResponseSenderBlock onSuccess, RCTResponseSenderBlock onError); + public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void setAccessibilityContentSizeMultipliers:(JS::NativeAccessibilityManager::SpecSetAccessibilityContentSizeMultipliersJSMultipliers& JSMultipliers); public virtual void setAccessibilityFocus:(double reactTag); }