Skip to content
Closed
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
4 changes: 4 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,10 @@ const AccessibilityInfo = {
isAccessibilityServiceEnabled: jest.fn(() =>
Promise.resolve(false),
) as JestMockFn<$FlowFixMe, $FlowFixMe>,
isShowBordersEnabled: 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],
showBordersChanged: [boolean],
};

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

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

/**
* Query whether Show Borders (called Button Shapes before iOS 26.1) is
* currently enabled.
*
* Returns a promise which resolves to a boolean.
* The result is `true` when show borders is enabled and `false` otherwise.
*
* @platform ios
*/
isShowBordersEnabled(): Promise<boolean> {
if (Platform.OS === 'android') {
return Promise.resolve(false);
} else {
return new Promise((resolve, reject) => {
if (NativeAccessibilityManagerIOS?.getCurrentShowBordersState != null) {
NativeAccessibilityManagerIOS.getCurrentShowBordersState(
resolve,
reject,
);
} else {
reject(
new Error(
'NativeAccessibilityManagerIOS.getCurrentShowBordersState is not available',
),
);
}
});
}
},

/**
* Query whether reduced transparency is currently enabled.
*
Expand Down Expand Up @@ -407,6 +439,10 @@ 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.
*
* - `showBordersChanged`: iOS-only event. Fires when the state of the show borders toggle changes
* (called Button Shapes before iOS 26.1).
* 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 showBordersChanged', () => {
it('delivers showBordersChanged on iOS', () => {
const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios');
const handler = jest.fn();

const subscription = AccessibilityInfo.addEventListener(
'showBordersChanged',
handler,
);
RCTDeviceEventEmitter.emit('showBordersChanged', true);
subscription.remove();
RCTDeviceEventEmitter.emit('showBordersChanged', 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(
'showBordersChanged',
handler,
);
RCTDeviceEventEmitter.emit('showBordersChanged', 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 mockGetCurrentShowBordersState = 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: {
getCurrentShowBordersState: JestMockFn<
[
onSuccess: (isShowBordersEnabled: 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,
} = {
getCurrentShowBordersState: mockGetCurrentShowBordersState,
getCurrentPrefersCrossFadeTransitionsState:
mockGetCurrentPrefersCrossFadeTransitionsState,
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,
Expand Down Expand Up @@ -660,6 +671,49 @@ describe('AccessibilityInfo', () => {
});
});

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

const result = await AccessibilityInfo.isShowBordersEnabled();

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

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

const result = await AccessibilityInfo.isShowBordersEnabled();

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

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

mockNativeAccessibilityManagerIOS.getCurrentShowBordersState = null;

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

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

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

afterEach(() => {
mockNativeAccessibilityManagerIOS = {
getCurrentShowBordersState: mockGetCurrentShowBordersState,
getCurrentPrefersCrossFadeTransitionsState:
mockGetCurrentPrefersCrossFadeTransitionsState,
getCurrentDarkerSystemColorsState: mockGetCurrentDarkerSystemColorsState,
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ let reactCoreRCTWebsocket = RNTarget(
let reactCoreModules = RNTarget(
name: .reactCoreModules,
path: "React/CoreModules",
linkedFrameworks: ["Accessibility"],
excludedPaths: ["PlatformStubs/RCTStatusBarManager.mm"],
dependencies: [.reactNativeDependencies, .jsi, .yoga, .reactTurboModuleCore, .reactFeatureFlags]
)
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 isShowBordersEnabled;
@property (nonatomic, assign) BOOL isVoiceOverEnabled;

@end
Expand Down
54 changes: 54 additions & 0 deletions packages/react-native/React/CoreModules/RCTAccessibilityManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#import "CoreModulesPlugins.h"

#import <Accessibility/Accessibility.h>

NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification =
@"RCTAccessibilityManagerDidUpdateMultiplierNotification";

Expand All @@ -27,6 +29,34 @@ @interface RCTAccessibilityManager () <NativeAccessibilityManagerSpec>

@end

// Show Borders was called Button Shapes before iOS 26.1, where UIKit deprecated
// its API in favor of the Accessibility framework.
static BOOL RCTIsShowBordersEnabled(void)
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 260100
if (@available(iOS 26.1, *)) {
return AXShowBordersEnabled();
}
#endif
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return UIAccessibilityButtonShapesEnabled();
#pragma clang diagnostic pop
}

static NSNotificationName RCTShowBordersStatusDidChangeNotification(void)
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 260100
if (@available(iOS 26.1, *)) {
return AXShowBordersEnabledStatusDidChangeNotification;
}
#endif
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return UIAccessibilityButtonShapesEnabledStatusDidChangeNotification;
#pragma clang diagnostic pop
}

@implementation RCTAccessibilityManager

@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
Expand Down Expand Up @@ -91,6 +121,11 @@ - (instancetype)init
name:UIAccessibilityReduceTransparencyStatusDidChangeNotification
object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(showBordersStatusDidChange:)
name:RCTShowBordersStatusDidChangeNotification()
object:nil];

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

- (void)showBordersStatusDidChange:(__unused NSNotification *)notification
{
BOOL newShowBordersEnabled = RCTIsShowBordersEnabled();
if (_isShowBordersEnabled != newShowBordersEnabled) {
_isShowBordersEnabled = newShowBordersEnabled;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
[[_moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"showBordersChanged"
body:@(_isShowBordersEnabled)];
#pragma clang diagnostic pop
}
}

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

- (void)getCurrentShowBordersState:(RCTResponseSenderBlock)onSuccess onError:(__unused RCTResponseSenderBlock)onError
{
onSuccess(@[ @(_isShowBordersEnabled) ]);
}

- (void)getCurrentVoiceOverState:(RCTResponseSenderBlock)onSuccess onError:(__unused RCTResponseSenderBlock)onError
{
onSuccess(@[ @(_isVoiceOverEnabled) ]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Pod::Spec.new do |s|
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"HEADER_SEARCH_PATHS" => header_search_paths.join(" ")
}
s.framework = "UIKit"
s.frameworks = ["UIKit", "Accessibility"]
s.dependency "RCTTypeSafety", version
s.dependency "React-Core/CoreModulesHeaders", version
s.dependency "React-RCTImage", version
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<<09cb4ee90e2e29b0924c0359d0ebef39>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -101,6 +101,7 @@ declare const AccessibilityInfo_default: {
isReduceMotionEnabled(): Promise<boolean>
isReduceTransparencyEnabled(): Promise<boolean>
isScreenReaderEnabled(): Promise<boolean>
isShowBordersEnabled(): Promise<boolean>
prefersCrossFadeTransitions(): Promise<boolean>
sendAccessibilityEvent(
handle: HostInstance,
Expand Down Expand Up @@ -974,6 +975,7 @@ declare type AccessibilityEventDefinitionsIOS = {
boldTextChanged: [boolean]
darkerSystemColorsChanged: [boolean]
reduceTransparencyChanged: [boolean]
showBordersChanged: [boolean]
}
declare type AccessibilityEventTypes =
"click" | "focus" | "viewHoverEnter" | "windowStateChange"
Expand Down Expand Up @@ -5770,7 +5772,7 @@ declare type WrapperComponentProvider = (
export {
AccessibilityActionEvent, // a276ec6e
AccessibilityActionInfo, // c94545a7
AccessibilityInfo, // d92c5eae
AccessibilityInfo, // 7d8718ed
AccessibilityProps, // 95dbb1d8
AccessibilityRole, // f2f2e066
AccessibilityState, // b0c2b3f7
Expand Down
Loading
Loading