diff --git a/packages/jest-preset/jest/mocks/AccessibilityInfo.js b/packages/jest-preset/jest/mocks/AccessibilityInfo.js index 4dc215bcabd7..32c968b1a4e0 100644 --- a/packages/jest-preset/jest/mocks/AccessibilityInfo.js +++ b/packages/jest-preset/jest/mocks/AccessibilityInfo.js @@ -20,6 +20,9 @@ const AccessibilityInfo = { isAccessibilityServiceEnabled: jest.fn(() => Promise.resolve(false), ) as JestMockFn<$FlowFixMe, $FlowFixMe>, + isDifferentiateWithoutColorEnabled: jest.fn(() => + Promise.resolve(false), + ) as JestMockFn<$FlowFixMe, $FlowFixMe>, isBoldTextEnabled: jest.fn(() => Promise.resolve(false)) as JestMockFn< $FlowFixMe, $FlowFixMe, diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index b922c77f0626..89973b1bede8 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -30,6 +30,7 @@ type AccessibilityEventDefinitionsIOS = { boldTextChanged: [boolean], darkerSystemColorsChanged: [boolean], reduceTransparencyChanged: [boolean], + differentiateWithoutColorChanged: [boolean], }; type AccessibilityEventDefinitions = { @@ -67,6 +68,10 @@ const EventNames: Map = ['reduceTransparencyChanged', 'reduceTransparencyChanged'], ['screenReaderChanged', 'screenReaderChanged'], ['darkerSystemColorsChanged', 'darkerSystemColorsChanged'], + [ + 'differentiateWithoutColorChanged', + 'differentiateWithoutColorChanged', + ], ]); /** @@ -287,6 +292,38 @@ const AccessibilityInfo = { } }, + /** + * Query whether differentiate without color is currently enabled. + * + * Returns a promise which resolves to a boolean. + * The result is `true` when differentiate without color is enabled and `false` otherwise. + * + * @platform ios + */ + isDifferentiateWithoutColorEnabled(): Promise { + if (Platform.OS === 'android') { + return Promise.resolve(false); + } else { + return new Promise((resolve, reject) => { + if ( + NativeAccessibilityManagerIOS?.getCurrentDifferentiateWithoutColorState != + null + ) { + NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState( + resolve, + reject, + ); + } else { + reject( + new Error( + 'NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState is not available', + ), + ); + } + }); + } + }, + /** * Query whether reduced transparency is currently enabled. * @@ -407,6 +444,9 @@ const AccessibilityInfo = { * toggle changes. The argument to the event handler is a boolean. The boolean is `true` when * dark system colors is enabled and `false` otherwise. * + * - `differentiateWithoutColorChanged`: iOS-only event. Fires when the state of the differentiate without color toggle changes. + * The argument to the event handler is a boolean. + * * These events are only supported on Android: * * - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-differentiateWithoutColorChanged-test.js b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-differentiateWithoutColorChanged-test.js new file mode 100644 index 000000000000..c02d3963d7b1 --- /dev/null +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-differentiateWithoutColorChanged-test.js @@ -0,0 +1,72 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import typeof RCTDeviceEventEmitterType from '../../../EventEmitter/RCTDeviceEventEmitter'; +import typeof AccessibilityInfoType from '../AccessibilityInfo'; + +const invariant = require('invariant'); + +jest.unmock('../AccessibilityInfo'); + +type LoadedModules = { + AccessibilityInfo: AccessibilityInfoType, + RCTDeviceEventEmitter: RCTDeviceEventEmitterType, +}; + +// `AccessibilityInfo` picks its native event names when it is first loaded, +// so each platform needs a fresh module registry. +function loadForPlatform(os: 'android' | 'ios'): LoadedModules { + let modules: ?LoadedModules = null; + jest.isolateModules(() => { + const Platform = require('../../../Utilities/Platform').default; + /* $FlowFixMe[incompatible-type] */ + Platform.OS = os; + modules = { + AccessibilityInfo: require('../AccessibilityInfo').default, + RCTDeviceEventEmitter: + require('../../../EventEmitter/RCTDeviceEventEmitter').default, + }; + }); + invariant(modules != null, 'Expected modules to load'); + return modules; +} + +describe('AccessibilityInfo differentiateWithoutColorChanged', () => { + it('delivers differentiateWithoutColorChanged on iOS', () => { + const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios'); + const handler = jest.fn(); + + const subscription = AccessibilityInfo.addEventListener( + 'differentiateWithoutColorChanged', + handler, + ); + RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', true); + subscription.remove(); + RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', false); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(true); + }); + + it('does not subscribe on Android', () => { + const {AccessibilityInfo, RCTDeviceEventEmitter} = + loadForPlatform('android'); + const handler = jest.fn(); + + const subscription = AccessibilityInfo.addEventListener( + 'differentiateWithoutColorChanged', + handler, + ); + RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', true); + subscription.remove(); + + expect(handler).not.toHaveBeenCalled(); + }); +}); 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 9e3d63ca330d..868840112389 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js @@ -16,6 +16,9 @@ const mockGetCurrentPrefersCrossFadeTransitionsState = jest.fn( const mockGetCurrentDarkerSystemColorsState = jest.fn((onSuccess, onError) => onSuccess(true), ); +const mockGetCurrentDifferentiateWithoutColorState = jest.fn( + (onSuccess, onError) => onSuccess(true), +); const mockGetCurrentBoldTextState = jest.fn((onSuccess, onError) => onSuccess(true), ); @@ -35,6 +38,13 @@ const mockGetCurrentVoiceOverState = jest.fn((onSuccess, onError) => onSuccess(true), ); let mockNativeAccessibilityManagerIOS: { + getCurrentDifferentiateWithoutColorState: JestMockFn< + [ + onSuccess: (isDifferentiateWithoutColorEnabled: boolean) => void, + onError: (error: Error) => void, + ], + void, + > | null, getCurrentPrefersCrossFadeTransitionsState: JestMockFn< [ onSuccess: (prefersCrossFadeTransitions: boolean) => void, @@ -92,6 +102,8 @@ let mockNativeAccessibilityManagerIOS: { void, > | null, } = { + getCurrentDifferentiateWithoutColorState: + mockGetCurrentDifferentiateWithoutColorState, getCurrentPrefersCrossFadeTransitionsState: mockGetCurrentPrefersCrossFadeTransitionsState, getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState, @@ -660,6 +672,54 @@ describe('AccessibilityInfo', () => { }); }); + describe('isDifferentiateWithoutColorEnabled', () => { + describe('Android', () => { + it('should return immediately', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'android'; + + const result = + await AccessibilityInfo.isDifferentiateWithoutColorEnabled(); + + expect(result).toBe(false); + }); + }); + + describe('iOS', () => { + it('should call getCurrentDifferentiateWithoutColorState if available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'ios'; + + const result = + await AccessibilityInfo.isDifferentiateWithoutColorEnabled(); + + expect(mockGetCurrentDifferentiateWithoutColorState).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should throw error if getCurrentDifferentiateWithoutColorState is not available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'ios'; + + mockNativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState = + null; + + const result: unknown = + await AccessibilityInfo.isDifferentiateWithoutColorEnabled().catch( + e => e, + ); + + invariant( + result instanceof Error, + 'Expected isDifferentiateWithoutColorEnabled to reject', + ); + expect(result.message).toEqual( + 'NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState is not available', + ); + }); + }); + }); + describe('isDarkerSystemColorsEnabled', () => { describe('Android', () => { it('should return immediately', async () => { @@ -791,6 +851,8 @@ describe('AccessibilityInfo', () => { afterEach(() => { mockNativeAccessibilityManagerIOS = { + getCurrentDifferentiateWithoutColorState: + mockGetCurrentDifferentiateWithoutColorState, getCurrentPrefersCrossFadeTransitionsState: mockGetCurrentPrefersCrossFadeTransitionsState, getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState, diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager.h b/packages/react-native/React/CoreModules/RCTAccessibilityManager.h index e8ea831b931b..c5b8a339cadd 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager.h +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager.h @@ -27,6 +27,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; / @property (nonatomic, assign) BOOL isDarkerSystemColorsEnabled; @property (nonatomic, assign) BOOL prefersCrossFadeTransitions; @property (nonatomic, assign) BOOL isReduceTransparencyEnabled; +@property (nonatomic, assign) BOOL isDifferentiateWithoutColorEnabled; @property (nonatomic, assign) BOOL isVoiceOverEnabled; @end diff --git a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm index 7ff290fdce80..250ccb62a39f 100644 --- a/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm +++ b/packages/react-native/React/CoreModules/RCTAccessibilityManager.mm @@ -91,6 +91,12 @@ - (instancetype)init name:UIAccessibilityReduceTransparencyStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(differentiateWithoutColorStatusDidChange:) + name:UIAccessibilityShouldDifferentiateWithoutColorDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(voiceVoiceOverStatusDidChange:) name:UIAccessibilityVoiceOverStatusDidChangeNotification @@ -103,6 +109,7 @@ - (instancetype)init _isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled(); _isDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled(); _isReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled(); + _isDifferentiateWithoutColorEnabled = UIAccessibilityShouldDifferentiateWithoutColor(); _isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); } return self; @@ -207,6 +214,19 @@ - (void)reduceTransparencyStatusDidChange:(__unused NSNotification *)notificatio } } +- (void)differentiateWithoutColorStatusDidChange:(__unused NSNotification *)notification +{ + BOOL newValue = UIAccessibilityShouldDifferentiateWithoutColor(); + if (_isDifferentiateWithoutColorEnabled != newValue) { + _isDifferentiateWithoutColorEnabled = newValue; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"differentiateWithoutColorChanged" + body:@(_isDifferentiateWithoutColorEnabled)]; +#pragma clang diagnostic pop + } +} + - (void)voiceVoiceOverStatusDidChange:(__unused NSNotification *)notification { BOOL isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning(); @@ -411,6 +431,12 @@ - (void)getCurrentReduceTransparencyState:(RCTResponseSenderBlock)onSuccess onSuccess(@[ @(_isReduceTransparencyEnabled) ]); } +- (void)getCurrentDifferentiateWithoutColorState:(RCTResponseSenderBlock)onSuccess + onError:(__unused RCTResponseSenderBlock)onError +{ + onSuccess(@[ @(_isDifferentiateWithoutColorEnabled) ]); +} + - (void)getCurrentVoiceOverState:(RCTResponseSenderBlock)onSuccess onError:(__unused RCTResponseSenderBlock)onError { onSuccess(@[ @(_isVoiceOverEnabled) ]); diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 74c407136ff1..df3d91e8dd63 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4a4f2da4bdd6b014a0b1cf24dc1318e9>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -95,6 +95,7 @@ declare const AccessibilityInfo_default: { isAccessibilityServiceEnabled(): Promise isBoldTextEnabled(): Promise isDarkerSystemColorsEnabled(): Promise + isDifferentiateWithoutColorEnabled(): Promise isGrayscaleEnabled(): Promise isHighTextContrastEnabled(): Promise isInvertColorsEnabled(): Promise @@ -973,6 +974,7 @@ declare type AccessibilityEventDefinitionsIOS = { ] boldTextChanged: [boolean] darkerSystemColorsChanged: [boolean] + differentiateWithoutColorChanged: [boolean] reduceTransparencyChanged: [boolean] } declare type AccessibilityEventTypes = @@ -5770,7 +5772,7 @@ declare type WrapperComponentProvider = ( export { AccessibilityActionEvent, // a276ec6e AccessibilityActionInfo, // c94545a7 - AccessibilityInfo, // d92c5eae + AccessibilityInfo, // c3f5c938 AccessibilityProps, // 95dbb1d8 AccessibilityRole, // f2f2e066 AccessibilityState, // b0c2b3f7 diff --git a/packages/react-native/__typetests__/index.tsx b/packages/react-native/__typetests__/index.tsx index 54ea8b7694ba..587a0e6edbe5 100644 --- a/packages/react-native/__typetests__/index.tsx +++ b/packages/react-native/__typetests__/index.tsx @@ -1464,6 +1464,18 @@ class AccessibilityTest extends React.Component { } } +AccessibilityInfo.isDifferentiateWithoutColorEnabled().then(isEnabled => + console.log( + `AccessibilityInfo.isDifferentiateWithoutColorEnabled => ${isEnabled}`, + ), +); +AccessibilityInfo.addEventListener( + 'differentiateWithoutColorChanged', + isEnabled => + console.log( + `AccessibilityInfo.differentiateWithoutColorChanged => ${isEnabled}`, + ), +); 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 f30e52bc0b2f..c03b2c7d8292 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js @@ -38,6 +38,10 @@ export interface Spec extends TurboModule { onSuccess: (prefersCrossFadeTransitions: boolean) => void, onError: (error: UnsafeObject) => void, ) => void; + readonly getCurrentDifferentiateWithoutColorState?: ( + onSuccess: (isDifferentiateWithoutColorEnabled: boolean) => void, + onError: (error: UnsafeObject) => void, + ) => void; readonly getCurrentReduceTransparencyState: ( onSuccess: (isReduceTransparencyEnabled: boolean) => void, onError: (error: UnsafeObject) => void, 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 80e5fe6a3295..ea09b4c8aebb 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 @@ -26,7 +26,9 @@ type AccessibilityChangeEventName = | 'reduceMotionChanged' /** iOS only */ | 'reduceTransparencyChanged' - | 'screenReaderChanged'; + | 'screenReaderChanged' + /** iOS only */ + | 'differentiateWithoutColorChanged'; type AccessibilityChangeEvent = boolean; @@ -86,6 +88,13 @@ export interface AccessibilityInfoStatic { */ isHighTextContrastEnabled: () => Promise; + /** + * Query whether differentiate without color is currently enabled. + * + * @platform ios + */ + isDifferentiateWithoutColorEnabled: () => Promise; + /** * Query whether darker system colors is currently enabled. * diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 2ac5879c9458..d1c0e630c361 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -1594,6 +1594,13 @@ class DisplayOptionsStatusExample extends React.Component<{}> { optionChecker={AccessibilityInfo.isInvertColorsEnabled} notification={'invertColorsChanged'} /> +