Skip to content

Commit cdbcfa6

Browse files
camilopiresclaude
andcommitted
Add AccessibilityInfo.isDifferentiateWithoutColorEnabled on iOS
Reads UIAccessibilityShouldDifferentiateWithoutColor() and emits differentiateWithoutColorChanged when it changes. Android resolves to false. Adds Jest tests for the getter and the event wiring, and an RNTester row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 27e7721 commit cdbcfa6

11 files changed

Lines changed: 241 additions & 3 deletions

File tree

‎packages/jest-preset/jest/mocks/AccessibilityInfo.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const AccessibilityInfo = {
2020
isAccessibilityServiceEnabled: jest.fn(() =>
2121
Promise.resolve(false),
2222
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
23+
isDifferentiateWithoutColorEnabled: jest.fn(() =>
24+
Promise.resolve(false),
25+
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
2326
isBoldTextEnabled: jest.fn(() => Promise.resolve(false)) as JestMockFn<
2427
$FlowFixMe,
2528
$FlowFixMe,

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type AccessibilityEventDefinitionsIOS = {
3030
boldTextChanged: [boolean],
3131
darkerSystemColorsChanged: [boolean],
3232
reduceTransparencyChanged: [boolean],
33+
differentiateWithoutColorChanged: [boolean],
3334
};
3435

3536
type AccessibilityEventDefinitions = {
@@ -67,6 +68,10 @@ const EventNames: Map<keyof AccessibilityEventDefinitions, string> =
6768
['reduceTransparencyChanged', 'reduceTransparencyChanged'],
6869
['screenReaderChanged', 'screenReaderChanged'],
6970
['darkerSystemColorsChanged', 'darkerSystemColorsChanged'],
71+
[
72+
'differentiateWithoutColorChanged',
73+
'differentiateWithoutColorChanged',
74+
],
7075
]);
7176

7277
/**
@@ -287,6 +292,38 @@ const AccessibilityInfo = {
287292
}
288293
},
289294

295+
/**
296+
* Query whether differentiate without color is currently enabled.
297+
*
298+
* Returns a promise which resolves to a boolean.
299+
* The result is `true` when differentiate without color is enabled and `false` otherwise.
300+
*
301+
* @platform ios
302+
*/
303+
isDifferentiateWithoutColorEnabled(): Promise<boolean> {
304+
if (Platform.OS === 'android') {
305+
return Promise.resolve(false);
306+
} else {
307+
return new Promise((resolve, reject) => {
308+
if (
309+
NativeAccessibilityManagerIOS?.getCurrentDifferentiateWithoutColorState !=
310+
null
311+
) {
312+
NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState(
313+
resolve,
314+
reject,
315+
);
316+
} else {
317+
reject(
318+
new Error(
319+
'NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState is not available',
320+
),
321+
);
322+
}
323+
});
324+
}
325+
},
326+
290327
/**
291328
* Query whether reduced transparency is currently enabled.
292329
*
@@ -407,6 +444,9 @@ const AccessibilityInfo = {
407444
* toggle changes. The argument to the event handler is a boolean. The boolean is `true` when
408445
* dark system colors is enabled and `false` otherwise.
409446
*
447+
* - `differentiateWithoutColorChanged`: iOS-only event. Fires when the state of the differentiate without color toggle changes.
448+
* The argument to the event handler is a boolean.
449+
*
410450
* These events are only supported on Android:
411451
*
412452
* - `highTextContrastChanged`: Android-only event. Fires when the state of the high text contrast
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import typeof RCTDeviceEventEmitterType from '../../../EventEmitter/RCTDeviceEventEmitter';
12+
import typeof AccessibilityInfoType from '../AccessibilityInfo';
13+
14+
const invariant = require('invariant');
15+
16+
jest.unmock('../AccessibilityInfo');
17+
18+
type LoadedModules = {
19+
AccessibilityInfo: AccessibilityInfoType,
20+
RCTDeviceEventEmitter: RCTDeviceEventEmitterType,
21+
};
22+
23+
// `AccessibilityInfo` picks its native event names when it is first loaded,
24+
// so each platform needs a fresh module registry.
25+
function loadForPlatform(os: 'android' | 'ios'): LoadedModules {
26+
let modules: ?LoadedModules = null;
27+
jest.isolateModules(() => {
28+
const Platform = require('../../../Utilities/Platform').default;
29+
/* $FlowFixMe[incompatible-type] */
30+
Platform.OS = os;
31+
modules = {
32+
AccessibilityInfo: require('../AccessibilityInfo').default,
33+
RCTDeviceEventEmitter:
34+
require('../../../EventEmitter/RCTDeviceEventEmitter').default,
35+
};
36+
});
37+
invariant(modules != null, 'Expected modules to load');
38+
return modules;
39+
}
40+
41+
describe('AccessibilityInfo differentiateWithoutColorChanged', () => {
42+
it('delivers differentiateWithoutColorChanged on iOS', () => {
43+
const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios');
44+
const handler = jest.fn();
45+
46+
const subscription = AccessibilityInfo.addEventListener(
47+
'differentiateWithoutColorChanged',
48+
handler,
49+
);
50+
RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', true);
51+
subscription.remove();
52+
RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', false);
53+
54+
expect(handler).toHaveBeenCalledTimes(1);
55+
expect(handler).toHaveBeenCalledWith(true);
56+
});
57+
58+
it('does not subscribe on Android', () => {
59+
const {AccessibilityInfo, RCTDeviceEventEmitter} =
60+
loadForPlatform('android');
61+
const handler = jest.fn();
62+
63+
const subscription = AccessibilityInfo.addEventListener(
64+
'differentiateWithoutColorChanged',
65+
handler,
66+
);
67+
RCTDeviceEventEmitter.emit('differentiateWithoutColorChanged', true);
68+
subscription.remove();
69+
70+
expect(handler).not.toHaveBeenCalled();
71+
});
72+
});

‎packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const mockGetCurrentPrefersCrossFadeTransitionsState = jest.fn(
1616
const mockGetCurrentDarkerSystemColorsState = jest.fn((onSuccess, onError) =>
1717
onSuccess(true),
1818
);
19+
const mockGetCurrentDifferentiateWithoutColorState = jest.fn(
20+
(onSuccess, onError) => onSuccess(true),
21+
);
1922
const mockGetCurrentBoldTextState = jest.fn((onSuccess, onError) =>
2023
onSuccess(true),
2124
);
@@ -35,6 +38,13 @@ const mockGetCurrentVoiceOverState = jest.fn((onSuccess, onError) =>
3538
onSuccess(true),
3639
);
3740
let mockNativeAccessibilityManagerIOS: {
41+
getCurrentDifferentiateWithoutColorState: JestMockFn<
42+
[
43+
onSuccess: (isDifferentiateWithoutColorEnabled: boolean) => void,
44+
onError: (error: Error) => void,
45+
],
46+
void,
47+
> | null,
3848
getCurrentPrefersCrossFadeTransitionsState: JestMockFn<
3949
[
4050
onSuccess: (prefersCrossFadeTransitions: boolean) => void,
@@ -92,6 +102,8 @@ let mockNativeAccessibilityManagerIOS: {
92102
void,
93103
> | null,
94104
} = {
105+
getCurrentDifferentiateWithoutColorState:
106+
mockGetCurrentDifferentiateWithoutColorState,
95107
getCurrentPrefersCrossFadeTransitionsState:
96108
mockGetCurrentPrefersCrossFadeTransitionsState,
97109
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,
@@ -660,6 +672,54 @@ describe('AccessibilityInfo', () => {
660672
});
661673
});
662674

675+
describe('isDifferentiateWithoutColorEnabled', () => {
676+
describe('Android', () => {
677+
it('should return immediately', async () => {
678+
/* $FlowFixMe[incompatible-type] */
679+
Platform.OS = 'android';
680+
681+
const result =
682+
await AccessibilityInfo.isDifferentiateWithoutColorEnabled();
683+
684+
expect(result).toBe(false);
685+
});
686+
});
687+
688+
describe('iOS', () => {
689+
it('should call getCurrentDifferentiateWithoutColorState if available', async () => {
690+
/* $FlowFixMe[incompatible-type] */
691+
Platform.OS = 'ios';
692+
693+
const result =
694+
await AccessibilityInfo.isDifferentiateWithoutColorEnabled();
695+
696+
expect(mockGetCurrentDifferentiateWithoutColorState).toHaveBeenCalled();
697+
expect(result).toBe(true);
698+
});
699+
700+
it('should throw error if getCurrentDifferentiateWithoutColorState is not available', async () => {
701+
/* $FlowFixMe[incompatible-type] */
702+
Platform.OS = 'ios';
703+
704+
mockNativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState =
705+
null;
706+
707+
const result: unknown =
708+
await AccessibilityInfo.isDifferentiateWithoutColorEnabled().catch(
709+
e => e,
710+
);
711+
712+
invariant(
713+
result instanceof Error,
714+
'Expected isDifferentiateWithoutColorEnabled to reject',
715+
);
716+
expect(result.message).toEqual(
717+
'NativeAccessibilityManagerIOS.getCurrentDifferentiateWithoutColorState is not available',
718+
);
719+
});
720+
});
721+
});
722+
663723
describe('isDarkerSystemColorsEnabled', () => {
664724
describe('Android', () => {
665725
it('should return immediately', async () => {
@@ -791,6 +851,8 @@ describe('AccessibilityInfo', () => {
791851

792852
afterEach(() => {
793853
mockNativeAccessibilityManagerIOS = {
854+
getCurrentDifferentiateWithoutColorState:
855+
mockGetCurrentDifferentiateWithoutColorState,
794856
getCurrentPrefersCrossFadeTransitionsState:
795857
mockGetCurrentPrefersCrossFadeTransitionsState,
796858
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,

‎packages/react-native/React/CoreModules/RCTAccessibilityManager.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; /
2727
@property (nonatomic, assign) BOOL isDarkerSystemColorsEnabled;
2828
@property (nonatomic, assign) BOOL prefersCrossFadeTransitions;
2929
@property (nonatomic, assign) BOOL isReduceTransparencyEnabled;
30+
@property (nonatomic, assign) BOOL isDifferentiateWithoutColorEnabled;
3031
@property (nonatomic, assign) BOOL isVoiceOverEnabled;
3132

3233
@end

‎packages/react-native/React/CoreModules/RCTAccessibilityManager.mm‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ - (instancetype)init
9191
name:UIAccessibilityReduceTransparencyStatusDidChangeNotification
9292
object:nil];
9393

94+
[[NSNotificationCenter defaultCenter]
95+
addObserver:self
96+
selector:@selector(differentiateWithoutColorStatusDidChange:)
97+
name:UIAccessibilityShouldDifferentiateWithoutColorDidChangeNotification
98+
object:nil];
99+
94100
[[NSNotificationCenter defaultCenter] addObserver:self
95101
selector:@selector(voiceVoiceOverStatusDidChange:)
96102
name:UIAccessibilityVoiceOverStatusDidChangeNotification
@@ -103,6 +109,7 @@ - (instancetype)init
103109
_isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
104110
_isDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled();
105111
_isReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled();
112+
_isDifferentiateWithoutColorEnabled = UIAccessibilityShouldDifferentiateWithoutColor();
106113
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
107114
}
108115
return self;
@@ -207,6 +214,19 @@ - (void)reduceTransparencyStatusDidChange:(__unused NSNotification *)notificatio
207214
}
208215
}
209216

217+
- (void)differentiateWithoutColorStatusDidChange:(__unused NSNotification *)notification
218+
{
219+
BOOL newValue = UIAccessibilityShouldDifferentiateWithoutColor();
220+
if (_isDifferentiateWithoutColorEnabled != newValue) {
221+
_isDifferentiateWithoutColorEnabled = newValue;
222+
#pragma clang diagnostic push
223+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
224+
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"differentiateWithoutColorChanged"
225+
body:@(_isDifferentiateWithoutColorEnabled)];
226+
#pragma clang diagnostic pop
227+
}
228+
}
229+
210230
- (void)voiceVoiceOverStatusDidChange:(__unused NSNotification *)notification
211231
{
212232
BOOL isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
@@ -411,6 +431,12 @@ - (void)getCurrentReduceTransparencyState:(RCTResponseSenderBlock)onSuccess
411431
onSuccess(@[ @(_isReduceTransparencyEnabled) ]);
412432
}
413433

434+
- (void)getCurrentDifferentiateWithoutColorState:(RCTResponseSenderBlock)onSuccess
435+
onError:(__unused RCTResponseSenderBlock)onError
436+
{
437+
onSuccess(@[ @(_isDifferentiateWithoutColorEnabled) ]);
438+
}
439+
414440
- (void)getCurrentVoiceOverState:(RCTResponseSenderBlock)onSuccess onError:(__unused RCTResponseSenderBlock)onError
415441
{
416442
onSuccess(@[ @(_isVoiceOverEnabled) ]);

‎packages/react-native/ReactNativeApi.d.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<b0c8c41e65758fde472ddf1c5b106a27>>
7+
* @generated SignedSource<<4a4f2da4bdd6b014a0b1cf24dc1318e9>>
88
*
99
* This file was generated by scripts/js-api/build-types/index.js.
1010
*/
@@ -95,6 +95,7 @@ declare const AccessibilityInfo_default: {
9595
isAccessibilityServiceEnabled(): Promise<boolean>
9696
isBoldTextEnabled(): Promise<boolean>
9797
isDarkerSystemColorsEnabled(): Promise<boolean>
98+
isDifferentiateWithoutColorEnabled(): Promise<boolean>
9899
isGrayscaleEnabled(): Promise<boolean>
99100
isHighTextContrastEnabled(): Promise<boolean>
100101
isInvertColorsEnabled(): Promise<boolean>
@@ -973,6 +974,7 @@ declare type AccessibilityEventDefinitionsIOS = {
973974
]
974975
boldTextChanged: [boolean]
975976
darkerSystemColorsChanged: [boolean]
977+
differentiateWithoutColorChanged: [boolean]
976978
reduceTransparencyChanged: [boolean]
977979
}
978980
declare type AccessibilityEventTypes =
@@ -5770,7 +5772,7 @@ declare type WrapperComponentProvider = (
57705772
export {
57715773
AccessibilityActionEvent, // a276ec6e
57725774
AccessibilityActionInfo, // c94545a7
5773-
AccessibilityInfo, // d92c5eae
5775+
AccessibilityInfo, // c3f5c938
57745776
AccessibilityProps, // 95dbb1d8
57755777
AccessibilityRole, // f2f2e066
57765778
AccessibilityState, // b0c2b3f7

‎packages/react-native/__typetests__/index.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,6 +1464,18 @@ class AccessibilityTest extends React.Component {
14641464
}
14651465
}
14661466

1467+
AccessibilityInfo.isDifferentiateWithoutColorEnabled().then(isEnabled =>
1468+
console.log(
1469+
`AccessibilityInfo.isDifferentiateWithoutColorEnabled => ${isEnabled}`,
1470+
),
1471+
);
1472+
AccessibilityInfo.addEventListener(
1473+
'differentiateWithoutColorChanged',
1474+
isEnabled =>
1475+
console.log(
1476+
`AccessibilityInfo.differentiateWithoutColorChanged => ${isEnabled}`,
1477+
),
1478+
);
14671479
AccessibilityInfo.isBoldTextEnabled().then(isEnabled =>
14681480
console.log(`AccessibilityInfo.isBoldTextEnabled => ${isEnabled}`),
14691481
);

‎packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityManager.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface Spec extends TurboModule {
3838
onSuccess: (prefersCrossFadeTransitions: boolean) => void,
3939
onError: (error: UnsafeObject) => void,
4040
) => void;
41+
readonly getCurrentDifferentiateWithoutColorState?: (
42+
onSuccess: (isDifferentiateWithoutColorEnabled: boolean) => void,
43+
onError: (error: UnsafeObject) => void,
44+
) => void;
4145
readonly getCurrentReduceTransparencyState: (
4246
onSuccess: (isReduceTransparencyEnabled: boolean) => void,
4347
onError: (error: UnsafeObject) => void,

‎packages/react-native/types_DEPRECATED/Libraries/Components/AccessibilityInfo/AccessibilityInfo.d.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ type AccessibilityChangeEventName =
2626
| 'reduceMotionChanged'
2727
/** iOS only */
2828
| 'reduceTransparencyChanged'
29-
| 'screenReaderChanged';
29+
| 'screenReaderChanged'
30+
/** iOS only */
31+
| 'differentiateWithoutColorChanged';
3032

3133
type AccessibilityChangeEvent = boolean;
3234

@@ -86,6 +88,13 @@ export interface AccessibilityInfoStatic {
8688
*/
8789
isHighTextContrastEnabled: () => Promise<boolean>;
8890

91+
/**
92+
* Query whether differentiate without color is currently enabled.
93+
*
94+
* @platform ios
95+
*/
96+
isDifferentiateWithoutColorEnabled: () => Promise<boolean>;
97+
8998
/**
9099
* Query whether darker system colors is currently enabled.
91100
*

0 commit comments

Comments
 (0)