Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
if (Platform.OS === 'android') {
Expand All @@ -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);
}
});
}
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand All @@ -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,
Expand Down Expand Up @@ -102,6 +110,7 @@ let mockNativeAccessibilityManagerIOS: {
void,
> | null,
} = {
getRecommendedTimeoutMillis: mockGetRecommendedTimeoutMillisIOS,
getCurrentShowBordersState: mockGetCurrentShowBordersState,
getCurrentPrefersCrossFadeTransitionsState:
mockGetCurrentPrefersCrossFadeTransitionsState,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
Expand All @@ -484,6 +497,13 @@ void RCTAccessibilityManagerSetIsVoiceOverEnabled(
[accessibilityManager _setIsVoiceOverEnabled:isVoiceOverEnabled];
}

void RCTAccessibilityManagerSetSwitchControlReader(
RCTAccessibilityManager *accessibilityManager,
RCTSwitchControlReader _Nullable switchControlReader)
{
accessibilityManager->_switchControlReader = [switchControlReader copy];
}

@end

@implementation RCTBridgeProxy (RCTAccessibilityManager)
Expand Down
3 changes: 3 additions & 0 deletions packages/react-native/__typetests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;
sendAccessibilityEvent: (
Expand Down
4 changes: 4 additions & 0 deletions packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -100,6 +101,7 @@
832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftTest.swift; path = RNTester/SwiftTest.swift; sourceTree = "<group>"; };
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 = "<group>"; };
A11E0F1C2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManagerTimeoutTests.m; sourceTree = "<group>"; };
AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
Expand Down Expand Up @@ -320,6 +322,7 @@
children = (
CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */,
E7DB20A322B2BA84005AC45F /* Info.plist */,
A11E0F1C2EA6000100A11E0F /* RCTAccessibilityManagerTimeoutTests.m */,
E7DB20C622B2BAA5005AC45F /* RCTAllocationTests.m */,
E7DB20B222B2BAA4005AC45F /* RCTAnimationUtilsTests.m */,
E7DB20AB22B2BAA3005AC45F /* RCTBlobManagerTests.m */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

#import <React/RCTAccessibilityManager+Internal.h>
#import <React/RCTAccessibilityManager.h>
#import <React/RCTBridgeModule.h>

// 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
Loading
Loading