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
3 changes: 3 additions & 0 deletions packages/jest-preset/jest/mocks/AccessibilityInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const AccessibilityInfo = {
isAccessibilityServiceEnabled: jest.fn(() =>
Promise.resolve(false),
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
isOnOffSwitchLabelsEnabled: jest.fn(() =>
Promise.resolve(false),
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
isBoldTextEnabled: jest.fn(() => Promise.resolve(false)) as JestMockFn<
$FlowFixMe,
$FlowFixMe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type AccessibilityEventDefinitionsIOS = {
boldTextChanged: [boolean],
darkerSystemColorsChanged: [boolean],
reduceTransparencyChanged: [boolean],
onOffSwitchLabelsChanged: [boolean],
};

type AccessibilityEventDefinitions = {
Expand Down Expand Up @@ -67,6 +68,7 @@ const EventNames: Map<keyof AccessibilityEventDefinitions, string> =
['reduceTransparencyChanged', 'reduceTransparencyChanged'],
['screenReaderChanged', 'screenReaderChanged'],
['darkerSystemColorsChanged', 'darkerSystemColorsChanged'],
['onOffSwitchLabelsChanged', 'onOffSwitchLabelsChanged'],
]);

/**
Expand Down Expand Up @@ -287,6 +289,38 @@ const AccessibilityInfo = {
}
},

/**
* Query whether on/off switch labels are currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when on/off labels is enabled and `false` otherwise.
*
* @platform ios
*/
isOnOffSwitchLabelsEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (
NativeAccessibilityManagerIOS?.getCurrentOnOffSwitchLabelsState !=
null
) {
NativeAccessibilityManagerIOS.getCurrentOnOffSwitchLabelsState(
resolve,
reject,
);
} else {
reject(
new Error(
'NativeAccessibilityManagerIOS.getCurrentOnOffSwitchLabelsState is not available',
),
);
}
});
}
},

/**
* Query whether reduced transparency is currently enabled.
*
Expand Down Expand Up @@ -407,6 +441,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.
*
* - `onOffSwitchLabelsChanged`: iOS-only event. Fires when the state of the on/off labels 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 onOffSwitchLabelsChanged', () => {
it('delivers onOffSwitchLabelsChanged on iOS', () => {
const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios');
const handler = jest.fn();

const subscription = AccessibilityInfo.addEventListener(
'onOffSwitchLabelsChanged',
handler,
);
RCTDeviceEventEmitter.emit('onOffSwitchLabelsChanged', true);
subscription.remove();
RCTDeviceEventEmitter.emit('onOffSwitchLabelsChanged', 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(
'onOffSwitchLabelsChanged',
handler,
);
RCTDeviceEventEmitter.emit('onOffSwitchLabelsChanged', true);
subscription.remove();

expect(handler).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const mockGetCurrentPrefersCrossFadeTransitionsState = jest.fn(
const mockGetCurrentDarkerSystemColorsState = jest.fn((onSuccess, onError) =>
onSuccess(true),
);
const mockGetCurrentOnOffSwitchLabelsState = jest.fn((onSuccess, onError) =>
onSuccess(true),
);
const mockGetCurrentBoldTextState = jest.fn((onSuccess, onError) =>
onSuccess(true),
);
Expand All @@ -35,6 +38,13 @@ const mockGetCurrentVoiceOverState = jest.fn((onSuccess, onError) =>
onSuccess(true),
);
let mockNativeAccessibilityManagerIOS: {
getCurrentOnOffSwitchLabelsState: JestMockFn<
[
onSuccess: (isOnOffSwitchLabelsEnabled: boolean) => void,
onError: (error: Error) => void,
],
void,
> | null,
getCurrentPrefersCrossFadeTransitionsState: JestMockFn<
[
onSuccess: (prefersCrossFadeTransitions: boolean) => void,
Expand Down Expand Up @@ -92,6 +102,7 @@ let mockNativeAccessibilityManagerIOS: {
void,
> | null,
} = {
getCurrentOnOffSwitchLabelsState: mockGetCurrentOnOffSwitchLabelsState,
getCurrentPrefersCrossFadeTransitionsState:
mockGetCurrentPrefersCrossFadeTransitionsState,
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,
Expand Down Expand Up @@ -660,6 +671,50 @@ describe('AccessibilityInfo', () => {
});
});

describe('isOnOffSwitchLabelsEnabled', () => {
describe('Android', () => {
it('should return immediately', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'android';

const result = await AccessibilityInfo.isOnOffSwitchLabelsEnabled();

expect(result).toBe(false);
});
});

describe('iOS', () => {
it('should call getCurrentOnOffSwitchLabelsState if available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'ios';

const result = await AccessibilityInfo.isOnOffSwitchLabelsEnabled();

expect(mockGetCurrentOnOffSwitchLabelsState).toHaveBeenCalled();
expect(result).toBe(true);
});

it('should throw error if getCurrentOnOffSwitchLabelsState is not available', async () => {
/* $FlowFixMe[incompatible-type] */
Platform.OS = 'ios';

mockNativeAccessibilityManagerIOS.getCurrentOnOffSwitchLabelsState =
null;

const result: unknown =
await AccessibilityInfo.isOnOffSwitchLabelsEnabled().catch(e => e);

invariant(
result instanceof Error,
'Expected isOnOffSwitchLabelsEnabled to reject',
);
expect(result.message).toEqual(
'NativeAccessibilityManagerIOS.getCurrentOnOffSwitchLabelsState is not available',
);
});
});
});

describe('isDarkerSystemColorsEnabled', () => {
describe('Android', () => {
it('should return immediately', async () => {
Expand Down Expand Up @@ -791,6 +846,7 @@ describe('AccessibilityInfo', () => {

afterEach(() => {
mockNativeAccessibilityManagerIOS = {
getCurrentOnOffSwitchLabelsState: mockGetCurrentOnOffSwitchLabelsState,
getCurrentPrefersCrossFadeTransitionsState:
mockGetCurrentPrefersCrossFadeTransitionsState,
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 isOnOffSwitchLabelsEnabled;
@property (nonatomic, assign) BOOL isVoiceOverEnabled;

@end
Expand Down
25 changes: 25 additions & 0 deletions packages/react-native/React/CoreModules/RCTAccessibilityManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ - (instancetype)init
name:UIAccessibilityReduceTransparencyStatusDidChangeNotification
object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onOffSwitchLabelsStatusDidChange:)
name:UIAccessibilityOnOffSwitchLabelsDidChangeNotification
object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(voiceVoiceOverStatusDidChange:)
name:UIAccessibilityVoiceOverStatusDidChangeNotification
Expand All @@ -103,6 +108,7 @@ - (instancetype)init
_isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
_isDarkerSystemColorsEnabled = UIAccessibilityDarkerSystemColorsEnabled();
_isReduceTransparencyEnabled = UIAccessibilityIsReduceTransparencyEnabled();
_isOnOffSwitchLabelsEnabled = UIAccessibilityIsOnOffSwitchLabelsEnabled();
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
}
return self;
Expand Down Expand Up @@ -207,6 +213,19 @@ - (void)reduceTransparencyStatusDidChange:(__unused NSNotification *)notificatio
}
}

- (void)onOffSwitchLabelsStatusDidChange:(__unused NSNotification *)notification
{
BOOL newValue = UIAccessibilityIsOnOffSwitchLabelsEnabled();
if (_isOnOffSwitchLabelsEnabled != newValue) {
_isOnOffSwitchLabelsEnabled = newValue;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"onOffSwitchLabelsChanged"
body:@(_isOnOffSwitchLabelsEnabled)];
#pragma clang diagnostic pop
}
}

- (void)voiceVoiceOverStatusDidChange:(__unused NSNotification *)notification
{
BOOL isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
Expand Down Expand Up @@ -411,6 +430,12 @@ - (void)getCurrentReduceTransparencyState:(RCTResponseSenderBlock)onSuccess
onSuccess(@[ @(_isReduceTransparencyEnabled) ]);
}

- (void)getCurrentOnOffSwitchLabelsState:(RCTResponseSenderBlock)onSuccess
onError:(__unused RCTResponseSenderBlock)onError
{
onSuccess(@[ @(_isOnOffSwitchLabelsEnabled) ]);
}

- (void)getCurrentVoiceOverState:(RCTResponseSenderBlock)onSuccess onError:(__unused RCTResponseSenderBlock)onError
{
onSuccess(@[ @(_isVoiceOverEnabled) ]);
Expand Down
6 changes: 4 additions & 2 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<<b0c8c41e65758fde472ddf1c5b106a27>>
* @generated SignedSource<<6ba8b0e6124573d6ae3d3ee69f9cd742>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -98,6 +98,7 @@ declare const AccessibilityInfo_default: {
isGrayscaleEnabled(): Promise<boolean>
isHighTextContrastEnabled(): Promise<boolean>
isInvertColorsEnabled(): Promise<boolean>
isOnOffSwitchLabelsEnabled(): Promise<boolean>
isReduceMotionEnabled(): Promise<boolean>
isReduceTransparencyEnabled(): Promise<boolean>
isScreenReaderEnabled(): Promise<boolean>
Expand Down Expand Up @@ -973,6 +974,7 @@ declare type AccessibilityEventDefinitionsIOS = {
]
boldTextChanged: [boolean]
darkerSystemColorsChanged: [boolean]
onOffSwitchLabelsChanged: [boolean]
reduceTransparencyChanged: [boolean]
}
declare type AccessibilityEventTypes =
Expand Down Expand Up @@ -5770,7 +5772,7 @@ declare type WrapperComponentProvider = (
export {
AccessibilityActionEvent, // a276ec6e
AccessibilityActionInfo, // c94545a7
AccessibilityInfo, // d92c5eae
AccessibilityInfo, // fe2c84fb
AccessibilityProps, // 95dbb1d8
AccessibilityRole, // f2f2e066
AccessibilityState, // b0c2b3f7
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/__typetests__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,12 @@ class AccessibilityTest extends React.Component {
}
}

AccessibilityInfo.isOnOffSwitchLabelsEnabled().then(isEnabled =>
console.log(`AccessibilityInfo.isOnOffSwitchLabelsEnabled => ${isEnabled}`),
);
AccessibilityInfo.addEventListener('onOffSwitchLabelsChanged', isEnabled =>
console.log(`AccessibilityInfo.onOffSwitchLabelsChanged => ${isEnabled}`),
);
AccessibilityInfo.isBoldTextEnabled().then(isEnabled =>
console.log(`AccessibilityInfo.isBoldTextEnabled => ${isEnabled}`),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export interface Spec extends TurboModule {
onSuccess: (prefersCrossFadeTransitions: boolean) => void,
onError: (error: UnsafeObject) => void,
) => void;
readonly getCurrentOnOffSwitchLabelsState?: (
onSuccess: (isOnOffSwitchLabelsEnabled: boolean) => void,
onError: (error: UnsafeObject) => void,
) => void;
readonly getCurrentReduceTransparencyState: (
onSuccess: (isReduceTransparencyEnabled: boolean) => void,
onError: (error: UnsafeObject) => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ type AccessibilityChangeEventName =
| 'reduceMotionChanged'
/** iOS only */
| 'reduceTransparencyChanged'
| 'screenReaderChanged';
| 'screenReaderChanged'
/** iOS only */
| 'onOffSwitchLabelsChanged';

type AccessibilityChangeEvent = boolean;

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

/**
* Query whether on/off switch labels are currently enabled.
*
* @platform ios
*/
isOnOffSwitchLabelsEnabled: () => Promise<boolean>;

/**
* Query whether darker system colors is currently enabled.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,11 @@ class DisplayOptionsStatusExample extends React.Component<{}> {
optionChecker={AccessibilityInfo.isInvertColorsEnabled}
notification={'invertColorsChanged'}
/>
<DisplayOptionStatusExample
optionName={'On/Off Labels'}
optionChecker={AccessibilityInfo.isOnOffSwitchLabelsEnabled}
notification={'onOffSwitchLabelsChanged'}
/>
<DisplayOptionStatusExample
optionName={'Reduce Transparency'}
optionChecker={AccessibilityInfo.isReduceTransparencyEnabled}
Expand Down
Loading