From 29955ef66aa89f354b02f7095700bd4bb85a53e8 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:40:16 +0100 Subject: [PATCH 1/8] Support AccessibilityInfo.isBoldTextEnabled and boldTextChanged on Android Android 12 (API 31) exposes the Bold Text setting as Configuration.fontWeightAdjustment. Read it, listen for configuration changes, and emit boldTextChanged. Earlier versions still resolve to false. If the native module predates this change, isBoldTextEnabled still resolves to false. Adds Kotlin unit tests for the adjustment values, Jest tests for the getter and the event wiring, and shows the Bold Text examples on Android in RNTester. Co-Authored-By: Claude Opus 5.5 --- .../AccessibilityInfo/AccessibilityInfo.js | 21 +++-- .../AccessibilityInfo-events-test.js | 78 +++++++++++++++++++ .../__tests__/AccessibilityInfo-test.js | 21 ++++- .../AccessibilityInfoModule.kt | 62 +++++++++++++++ .../AccessibilityInfoModuleTest.kt | 39 ++++++++++ packages/react-native/ReactNativeApi.d.ts | 6 +- .../modules/NativeAccessibilityInfo.js | 3 + .../AccessibilityInfo/AccessibilityInfo.d.ts | 4 +- .../Accessibility/AccessibilityExample.js | 34 ++++---- 9 files changed, 237 insertions(+), 31 deletions(-) create mode 100644 packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-events-test.js create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index 017c1d3c842e..8b82d13eea3d 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -27,7 +27,6 @@ type AccessibilityEventDefinitionsAndroid = { // Events that are only supported on iOS. type AccessibilityEventDefinitionsIOS = { announcementFinished: [{announcement: string, success: boolean}], - boldTextChanged: [boolean], darkerSystemColorsChanged: [boolean], reduceTransparencyChanged: [boolean], showBordersChanged: [boolean], @@ -36,6 +35,7 @@ type AccessibilityEventDefinitionsIOS = { type AccessibilityEventDefinitions = { ...AccessibilityEventDefinitionsAndroid, ...AccessibilityEventDefinitionsIOS, + boldTextChanged: [boolean], change: [boolean], // screenReaderChanged grayscaleChanged: [boolean], invertColorsChanged: [boolean], @@ -53,6 +53,7 @@ const EventNames: Map = ['change', 'touchExplorationDidChange'], ['reduceMotionChanged', 'reduceMotionDidChange'], ['highTextContrastChanged', 'highTextContrastDidChange'], + ['boldTextChanged', 'boldTextDidChange'], ['screenReaderChanged', 'touchExplorationDidChange'], ['accessibilityServiceChanged', 'accessibilityServiceDidChange'], ['invertColorsChanged', 'invertColorDidChange'], @@ -83,11 +84,19 @@ const AccessibilityInfo = { * Returns a promise which resolves to a boolean. * The result is `true` when bold text is enabled and `false` otherwise. * - * @platform ios + * On Android, bold text is supported on Android 12 (API 31) and later; + * earlier versions always resolve to `false`. */ isBoldTextEnabled(): Promise { if (Platform.OS === 'android') { - return Promise.resolve(false); + return new Promise(resolve => { + if (NativeAccessibilityInfoAndroid?.isBoldTextEnabled != null) { + NativeAccessibilityInfoAndroid.isBoldTextEnabled(resolve); + } else { + // Native code that predates Android support keeps the old behavior. + resolve(false); + } + }); } else { return new Promise((resolve, reject) => { if (NativeAccessibilityManagerIOS != null) { @@ -420,12 +429,12 @@ const AccessibilityInfo = { * - `invertColorsChanged`: Fires when the state of the invert colors toggle * changes. The argument to the event handler is a boolean. The boolean is `true` when invert * colors is enabled and `false` otherwise. + * - `boldTextChanged`: Fires when the state of the bold text toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a bold text + * is enabled and `false` otherwise. On Android, requires Android 12 (API 31) or later. * * These events are only supported on iOS: * - * - `boldTextChanged`: iOS-only event. Fires when the state of the bold text toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a bold text - * is enabled and `false` otherwise. * - `reduceTransparencyChanged`: iOS-only event. Fires when the state of the reduce transparency * toggle changes. The argument to the event handler is a boolean. The boolean is `true` * when a reduce transparency is enabled and `false` otherwise. diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-events-test.js b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-events-test.js new file mode 100644 index 000000000000..36687a0ae6ec --- /dev/null +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-events-test.js @@ -0,0 +1,78 @@ +/** + * 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 events', () => { + describe('Android', () => { + it('delivers boldTextDidChange as boldTextChanged', () => { + const {AccessibilityInfo, RCTDeviceEventEmitter} = + loadForPlatform('android'); + const handler = jest.fn(); + + const subscription = AccessibilityInfo.addEventListener( + 'boldTextChanged', + handler, + ); + RCTDeviceEventEmitter.emit('boldTextDidChange', true); + subscription.remove(); + RCTDeviceEventEmitter.emit('boldTextDidChange', false); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(true); + }); + }); + + describe('iOS', () => { + it('delivers boldTextChanged as boldTextChanged', () => { + const {AccessibilityInfo, RCTDeviceEventEmitter} = loadForPlatform('ios'); + const handler = jest.fn(); + + const subscription = AccessibilityInfo.addEventListener( + 'boldTextChanged', + handler, + ); + RCTDeviceEventEmitter.emit('boldTextChanged', true); + subscription.remove(); + RCTDeviceEventEmitter.emit('boldTextChanged', false); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledWith(true); + }); + }); +}); 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 380be9fdd394..773003b1559f 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js @@ -115,6 +115,7 @@ let mockNativeAccessibilityManagerIOS: { }; const ANDROID_RECOMMENDED_TIMEOUT = 6000; +const mockIsBoldTextEnabledAndroid = jest.fn(onSuccess => onSuccess(true)); const mockIsHighTextContrastEnabled = jest.fn(onSuccess => onSuccess(true)); const mockIsGrayscaleEnabled = jest.fn(onSuccess => onSuccess(true)); const mockIsInvertColorsEnabled = jest.fn(onSuccess => onSuccess(true)); @@ -125,6 +126,10 @@ const mockGetRecommendedTimeoutMillis = jest.fn((originalTimeout, onSuccess) => onSuccess(ANDROID_RECOMMENDED_TIMEOUT), ); let mockNativeAccessibilityInfoAndroid: { + isBoldTextEnabled: JestMockFn< + [onSuccess: (isBoldTextEnabled: boolean) => void], + void, + > | null, isHighTextContrastEnabled: JestMockFn< [onSuccess: (isHighTextContrastEnabled: boolean) => void], void, @@ -154,6 +159,7 @@ let mockNativeAccessibilityInfoAndroid: { void, > | null, } = { + isBoldTextEnabled: mockIsBoldTextEnabledAndroid, isHighTextContrastEnabled: mockIsHighTextContrastEnabled, isGrayscaleEnabled: mockIsGrayscaleEnabled, isInvertColorsEnabled: mockIsInvertColorsEnabled, @@ -187,10 +193,22 @@ describe('AccessibilityInfo', () => { describe('isBoldTextEnabled', () => { describe('Android', () => { - it('should return immediately', async () => { + it('should call isBoldTextEnabled if available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'android'; + + const isBoldTextEnabled = await AccessibilityInfo.isBoldTextEnabled(); + + expect(mockIsBoldTextEnabledAndroid).toHaveBeenCalled(); + expect(isBoldTextEnabled).toBe(true); + }); + + it('should return false if isBoldTextEnabled is not available', async () => { /* $FlowFixMe[incompatible-type] */ Platform.OS = 'android'; + mockNativeAccessibilityInfoAndroid.isBoldTextEnabled = null; + const isBoldTextEnabled = await AccessibilityInfo.isBoldTextEnabled(); expect(isBoldTextEnabled).toBe(false); @@ -860,6 +878,7 @@ describe('AccessibilityInfo', () => { mockNativeAccessibilityManagerIOS; mockNativeAccessibilityInfoAndroid = { + isBoldTextEnabled: mockIsBoldTextEnabledAndroid, isHighTextContrastEnabled: mockIsHighTextContrastEnabled, isGrayscaleEnabled: mockIsGrayscaleEnabled, isInvertColorsEnabled: mockIsInvertColorsEnabled, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt index 2bad463beacf..680e1563c8ea 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.kt @@ -7,8 +7,10 @@ package com.facebook.react.modules.accessibilityinfo +import android.content.ComponentCallbacks import android.content.ContentResolver import android.content.Context +import android.content.res.Configuration import android.database.ContentObserver import android.net.Uri import android.os.Build @@ -72,6 +74,22 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : } } } + // Listener that is notified when the bold text setting (font weight adjustment) changes. + private val boldTextCallbacks: ComponentCallbacks = + object : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) { + if (reactApplicationContext.hasActiveReactInstance()) { + updateAndSendBoldTextChangeEvent() + } + } + + @Deprecated( + "onLowMemory is deprecated, use onTrimMemory instead.", + ReplaceWith("onTrimMemory(level)"), + ) + override fun onLowMemory(): Unit = Unit + } + private var boldTextCallbacksRegistered = false private val accessibilityManager: AccessibilityManager? private val touchExplorationStateChangeListener: ReactTouchExplorationStateChangeListener = ReactTouchExplorationStateChangeListener() @@ -80,6 +98,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : private val contentResolver: ContentResolver private var reduceMotionEnabled = false private var highTextContrastEnabled = false + private var boldTextEnabled = false private var touchExplorationEnabled = false private var accessibilityServiceEnabled = false private var recommendedTimeout = 0 @@ -95,6 +114,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : accessibilityServiceEnabled = accessibilityManager?.isEnabled ?: false reduceMotionEnabled = isReduceMotionEnabledValue highTextContrastEnabled = isHighTextContrastEnabledValue + boldTextEnabled = isBoldTextEnabledValue grayscaleModeEnabled = isGrayscaleEnabledValue } @@ -154,6 +174,11 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : ) != 0 } + private val isBoldTextEnabledValue: Boolean + get() = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + isBoldTextEnabled(reactApplicationContext.resources.configuration.fontWeightAdjustment) + override fun isReduceMotionEnabled(successCallback: Callback) { successCallback.invoke(reduceMotionEnabled) } @@ -170,6 +195,10 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : successCallback.invoke(highTextContrastEnabled) } + override fun isBoldTextEnabled(successCallback: Callback) { + successCallback.invoke(boldTextEnabled) + } + override fun isTouchExplorationEnabled(successCallback: Callback) { successCallback.invoke(touchExplorationEnabled) } @@ -208,6 +237,15 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : } } + private fun updateAndSendBoldTextChangeEvent() { + val isBoldTextEnabled = isBoldTextEnabledValue + if (boldTextEnabled != isBoldTextEnabled) { + boldTextEnabled = isBoldTextEnabled + getReactApplicationContextIfActiveOrWarn() + ?.emitDeviceEvent(BOLD_TEXT_EVENT_NAME, boldTextEnabled) + } + } + private fun updateAndSendTouchExplorationChangeEvent(enabled: Boolean) { if (touchExplorationEnabled != enabled) { touchExplorationEnabled = enabled @@ -243,12 +281,17 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : val highTextContrastUri = Settings.Secure.getUriFor(ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_CONSTANT) contentResolver.registerContentObserver(highTextContrastUri, false, highTextContrastObserver) + if (!boldTextCallbacksRegistered) { + reactApplicationContext.applicationContext.registerComponentCallbacks(boldTextCallbacks) + boldTextCallbacksRegistered = true + } updateAndSendTouchExplorationChangeEvent( accessibilityManager?.isTouchExplorationEnabled == true, ) updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true) updateAndSendReduceMotionChangeEvent() updateAndSendHighTextContrastChangeEvent() + updateAndSendBoldTextChangeEvent() updateAndSendInvertColorsChangeEvent() updateAndSendGrayscaleModeChangeEvent() } @@ -260,6 +303,14 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : accessibilityManager?.removeAccessibilityStateChangeListener(accessibilityServiceChangeListener) contentResolver.unregisterContentObserver(animationScaleObserver) contentResolver.unregisterContentObserver(highTextContrastObserver) + unregisterBoldTextCallbacks() + } + + private fun unregisterBoldTextCallbacks() { + if (boldTextCallbacksRegistered) { + reactApplicationContext.applicationContext.unregisterComponentCallbacks(boldTextCallbacks) + boldTextCallbacksRegistered = false + } } override fun initialize() { @@ -270,10 +321,12 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : updateAndSendAccessibilityServiceChangeEvent(accessibilityManager?.isEnabled == true) updateAndSendReduceMotionChangeEvent() updateAndSendHighTextContrastChangeEvent() + updateAndSendBoldTextChangeEvent() } override fun invalidate() { reactApplicationContext.removeLifecycleEventListener(this) + unregisterBoldTextCallbacks() super.invalidate() } @@ -318,5 +371,14 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) : "high_text_contrast_enabled" // constant is marked with @hide private const val INVERT_COLOR_EVENT_NAME = "invertColorDidChange" private const val GRAYSCALE_MODE_EVENT_NAME = "grayscaleModeDidChange" + private const val BOLD_TEXT_EVENT_NAME = "boldTextDidChange" + + /** + * Bold text raises the font weight adjustment. A negative adjustment means lighter text, and + * [Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED] means the value is not known. + */ + internal fun isBoldTextEnabled(fontWeightAdjustment: Int): Boolean = + fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED && + fontWeightAdjustment > 0 } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt new file mode 100644 index 000000000000..f9be0c7f6dac --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package com.facebook.react.modules.accessibilityinfo + +import android.content.res.Configuration +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class AccessibilityInfoModuleTest { + @Test + fun boldTextIsEnabledForAPositiveFontWeightAdjustment() { + assertThat(AccessibilityInfoModule.isBoldTextEnabled(300)).isTrue() + } + + @Test + fun boldTextIsDisabledWithoutAFontWeightAdjustment() { + assertThat(AccessibilityInfoModule.isBoldTextEnabled(0)).isFalse() + } + + @Test + fun boldTextIsDisabledForANegativeFontWeightAdjustment() { + assertThat(AccessibilityInfoModule.isBoldTextEnabled(-100)).isFalse() + } + + @Test + fun boldTextIsDisabledWhenTheFontWeightAdjustmentIsUndefined() { + assertThat( + AccessibilityInfoModule.isBoldTextEnabled( + Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED + ) + ) + .isFalse() + } +} diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c0f8309f0ac5..ee2292fc1a5d 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<<02ba9498145f69e034d52dffd5824a89>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -955,6 +955,7 @@ declare type AccessibilityActionName = declare type AccessibilityEventDefinitions = AccessibilityEventDefinitionsAndroid & AccessibilityEventDefinitionsIOS & { + boldTextChanged: [boolean] change: [boolean] grayscaleChanged: [boolean] invertColorsChanged: [boolean] @@ -972,7 +973,6 @@ declare type AccessibilityEventDefinitionsIOS = { success: boolean }, ] - boldTextChanged: [boolean] darkerSystemColorsChanged: [boolean] reduceTransparencyChanged: [boolean] showBordersChanged: [boolean] @@ -5775,7 +5775,7 @@ declare type WrapperComponentProvider = ( export { AccessibilityActionEvent, // a276ec6e AccessibilityActionInfo, // c94545a7 - AccessibilityInfo, // 7d8718ed + AccessibilityInfo, // e31e919b AccessibilityProps, // 95dbb1d8 AccessibilityRole, // f2f2e066 AccessibilityState, // b0c2b3f7 diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js index d17c50925e9f..ac3144211969 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAccessibilityInfo.js @@ -22,6 +22,9 @@ export interface Spec extends TurboModule { readonly isHighTextContrastEnabled?: ( onSuccess: (isHighTextContrastEnabled: boolean) => void, ) => void; + readonly isBoldTextEnabled?: ( + onSuccess: (isBoldTextEnabled: boolean) => void, + ) => void; readonly isTouchExplorationEnabled: ( onSuccess: (isScreenReaderEnabled: boolean) => void, ) => 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 6f2ccb478328..410c5e85c6cc 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 @@ -13,7 +13,7 @@ import {EmitterSubscription} from '../../vendor/emitter/EventEmitter'; type AccessibilityChangeEventName = /** Android only */ | 'accessibilityServiceChanged' - /** iOS only */ + /** On Android, requires Android 12 (API 31) or later */ | 'boldTextChanged' /** @deprecated Maps to 'screenReaderChanged' */ | 'change' @@ -57,7 +57,7 @@ export interface AccessibilityInfoStatic { /** * Query whether bold text is currently enabled. * - * @platform ios + * On Android, requires Android 12 (API 31) or later. */ isBoldTextEnabled: () => Promise; diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index ff2891104139..620dbbf1db26 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -1392,21 +1392,17 @@ class EnabledExamples extends React.Component<{}> { render(): React.Node { return ( + + + + {Platform.OS === 'ios' ? ( - <> - - - - - - - + + + ) : null} {Platform.OS === 'android' ? ( @@ -1577,13 +1573,13 @@ class DisplayOptionsStatusExample extends React.Component<{}> { optionChecker={AccessibilityInfo.isScreenReaderEnabled} notification={'screenReaderChanged'} /> + {isAndroid ? null : ( <> - Date: Sat, 24 Oct 2026 00:51:13 +0100 Subject: [PATCH 2/8] Update C++ API snapshots for isBoldTextEnabled The Android NativeAccessibilityInfo spec's isBoldTextEnabled now appears in the generated spec, so it shows up in the Apple snapshots. Generated with Doxygen 1.16.1 and Python 3.12, as in validate-cxx-api-snapshots.yml. Co-Authored-By: Claude Opus 5.5 --- scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 1 + scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 1 + 4 files changed, 4 insertions(+) diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 8de2c49759a6..41a89073f386 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2132,6 +2132,7 @@ protocol NativeAccessibilityInfoSpec : public RCTBridgeModule, public RCTTurboMo public virtual void announceForAccessibility:(NSString* announcement); public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void isAccessibilityServiceEnabled:(RCTResponseSenderBlock onSuccess); + public virtual void isBoldTextEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isGrayscaleEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isHighTextContrastEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isInvertColorsEnabled:(RCTResponseSenderBlock onSuccess); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index 6b5e45141f07..f87835e71482 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -2190,6 +2190,7 @@ protocol NativeAccessibilityInfoSpec : public RCTBridgeModule, public RCTTurboMo public virtual void announceForAccessibility:(NSString* announcement); public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void isAccessibilityServiceEnabled:(RCTResponseSenderBlock onSuccess); + public virtual void isBoldTextEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isGrayscaleEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isHighTextContrastEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isInvertColorsEnabled:(RCTResponseSenderBlock onSuccess); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index e8201541b2e1..c1b2a77a7159 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -2125,6 +2125,7 @@ protocol NativeAccessibilityInfoSpec : public RCTBridgeModule, public RCTTurboMo public virtual void announceForAccessibility:(NSString* announcement); public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void isAccessibilityServiceEnabled:(RCTResponseSenderBlock onSuccess); + public virtual void isBoldTextEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isGrayscaleEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isHighTextContrastEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isInvertColorsEnabled:(RCTResponseSenderBlock onSuccess); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index d3ff97cc59c6..e06a5afee89b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2132,6 +2132,7 @@ protocol NativeAccessibilityInfoSpec : public RCTBridgeModule, public RCTTurboMo public virtual void announceForAccessibility:(NSString* announcement); public virtual void getRecommendedTimeoutMillis:onSuccess:(double mSec, RCTResponseSenderBlock onSuccess); public virtual void isAccessibilityServiceEnabled:(RCTResponseSenderBlock onSuccess); + public virtual void isBoldTextEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isGrayscaleEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isHighTextContrastEnabled:(RCTResponseSenderBlock onSuccess); public virtual void isInvertColorsEnabled:(RCTResponseSenderBlock onSuccess); From f3927c13234460c848c95b3bbf0dc9c23d884aec Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:03:46 +0100 Subject: [PATCH 3/8] Test the Android bold text listener and events with Robolectric AccessibilityInfoModuleTest now drives the module through real configuration changes: a change sends boldTextDidChange once, turning bold text off sends false, nothing is sent while paused, after resuming twice and pausing once, or after invalidate, a change made while paused is sent on resume, and before Android 12 the value is always false and no event is sent. Removing the callback registration makes the change tests fail. Co-Authored-By: Claude Opus 5.5 --- .../AccessibilityInfoModuleTest.kt | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt index f9be0c7f6dac..dee70dd06d22 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModuleTest.kt @@ -8,10 +8,40 @@ package com.facebook.react.modules.accessibilityinfo import android.content.res.Configuration +import com.facebook.react.bridge.BridgeReactContext +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.ReactTestHelper +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowSoLoader import org.assertj.core.api.Assertions.assertThat +import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.kotlin.never +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowSoLoader::class, ShadowNativeLoader::class]) class AccessibilityInfoModuleTest { + + private lateinit var reactContext: BridgeReactContext + private lateinit var module: AccessibilityInfoModule + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + reactContext = spy(BridgeReactContext(RuntimeEnvironment.getApplication())) + reactContext.initializeWithInstance(ReactTestHelper.createMockCatalystInstance()) + module = AccessibilityInfoModule(reactContext) + } + @Test fun boldTextIsEnabledForAPositiveFontWeightAdjustment() { assertThat(AccessibilityInfoModule.isBoldTextEnabled(300)).isTrue() @@ -36,4 +66,110 @@ class AccessibilityInfoModuleTest { ) .isFalse() } + + @Test + fun aConfigurationChangeSendsBoldTextDidChangeOnce() { + module.onHostResume() + + dispatchConfigurationChange(setFontWeightAdjustment(300)) + dispatchConfigurationChange(setFontWeightAdjustment(300)) + + verify(reactContext, times(1)).emitDeviceEvent(BOLD_TEXT_EVENT, true) + assertThat(currentBoldText()).isTrue() + } + + @Test + fun turningBoldTextOffSendsFalse() { + setFontWeightAdjustment(300) + module = AccessibilityInfoModule(reactContext) + module.onHostResume() + + dispatchConfigurationChange(setFontWeightAdjustment(0)) + + verify(reactContext, times(1)).emitDeviceEvent(BOLD_TEXT_EVENT, false) + assertThat(currentBoldText()).isFalse() + } + + @Test + fun noEventIsSentWhilePaused() { + module.onHostResume() + module.onHostPause() + + dispatchConfigurationChange(setFontWeightAdjustment(300)) + + verifyNoBoldTextEvent() + } + + @Test + fun resumingTwiceAndPausingOnceStopsTheEvents() { + module.onHostResume() + module.onHostResume() + module.onHostPause() + + dispatchConfigurationChange(setFontWeightAdjustment(300)) + + verifyNoBoldTextEvent() + } + + @Test + fun noEventIsSentAfterInvalidate() { + module.onHostResume() + module.invalidate() + + dispatchConfigurationChange(setFontWeightAdjustment(300)) + + verifyNoBoldTextEvent() + } + + @Test + fun aChangeWhilePausedIsSentOnResume() { + module.onHostResume() + module.onHostPause() + + setFontWeightAdjustment(300) + module.onHostResume() + + verify(reactContext, times(1)).emitDeviceEvent(BOLD_TEXT_EVENT, true) + } + + @Test + @Config(sdk = [30]) + fun boldTextIsAlwaysFalseBeforeAndroid12() { + module = AccessibilityInfoModule(reactContext) + module.onHostResume() + module.onHostPause() + module.invalidate() + + assertThat(currentBoldText()).isFalse() + verifyNoBoldTextEvent() + } + + private fun currentBoldText(): Boolean { + var result: Boolean? = null + module.isBoldTextEnabled(Callback { args -> result = args[0] as Boolean }) + return checkNotNull(result) + } + + private fun verifyNoBoldTextEvent() { + verify(reactContext, never()) + .emitDeviceEvent( + ArgumentMatchers.eq(BOLD_TEXT_EVENT), + ArgumentMatchers.any(), + ) + } + + private fun setFontWeightAdjustment(adjustment: Int): Configuration { + val resources = RuntimeEnvironment.getApplication().resources + val config = Configuration(resources.configuration).apply { fontWeightAdjustment = adjustment } + @Suppress("DEPRECATION") resources.updateConfiguration(config, resources.displayMetrics) + return config + } + + private fun dispatchConfigurationChange(config: Configuration) { + RuntimeEnvironment.getApplication().onConfigurationChanged(config) + } + + private companion object { + const val BOLD_TEXT_EVENT = "boldTextDidChange" + } } From 5ba72d0dba8edd628c198f75c00dcab9fa5e25bb Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:37:54 +0100 Subject: [PATCH 4/8] Explain the isBoldTextEnabled fallback and test a missing native module Co-Authored-By: Claude Opus 5.5 --- .../AccessibilityInfo/AccessibilityInfo.js | 7 +++++-- .../__tests__/AccessibilityInfo-test.js | 15 +++++++++++++++ .../AccessibilityInfo/AccessibilityInfo.d.ts | 5 +++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js index 8b82d13eea3d..0eca6984db62 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js @@ -93,7 +93,10 @@ const AccessibilityInfo = { if (NativeAccessibilityInfoAndroid?.isBoldTextEnabled != null) { NativeAccessibilityInfoAndroid.isBoldTextEnabled(resolve); } else { - // Native code that predates Android support keeps the old behavior. + // `isBoldTextEnabled` is an existing API that resolved `false` on + // Android before native support was added, so it keeps resolving + // `false` when the native module or method is missing. APIs that are + // new reject instead, like `isShowBordersEnabled`. resolve(false); } }); @@ -430,7 +433,7 @@ const AccessibilityInfo = { * changes. The argument to the event handler is a boolean. The boolean is `true` when invert * colors is enabled and `false` otherwise. * - `boldTextChanged`: Fires when the state of the bold text toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a bold text + * The argument to the event handler is a boolean. The boolean is `true` when bold text * is enabled and `false` otherwise. On Android, requires Android 12 (API 31) or later. * * These events are only supported on iOS: 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 773003b1559f..e0aad54026c6 100644 --- a/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js +++ b/packages/react-native/Libraries/Components/AccessibilityInfo/__tests__/AccessibilityInfo-test.js @@ -213,6 +213,21 @@ describe('AccessibilityInfo', () => { expect(isBoldTextEnabled).toBe(false); }); + + it('should return false if NativeAccessibilityInfoAndroid is not available', async () => { + /* $FlowFixMe[incompatible-type] */ + Platform.OS = 'android'; + + const nativeAccessibilityInfoModule = jest.requireMock( + '../NativeAccessibilityInfo', + ); + nativeAccessibilityInfoModule.default = null; + + const isBoldTextEnabled = await AccessibilityInfo.isBoldTextEnabled(); + + expect(mockIsBoldTextEnabledAndroid).not.toHaveBeenCalled(); + expect(isBoldTextEnabled).toBe(false); + }); }); describe('iOS', () => { 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 410c5e85c6cc..4c46aa3a10fe 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 @@ -13,7 +13,7 @@ import {EmitterSubscription} from '../../vendor/emitter/EventEmitter'; type AccessibilityChangeEventName = /** Android only */ | 'accessibilityServiceChanged' - /** On Android, requires Android 12 (API 31) or later */ + /** iOS, and Android 12 (API 31) or later */ | 'boldTextChanged' /** @deprecated Maps to 'screenReaderChanged' */ | 'change' @@ -57,7 +57,8 @@ export interface AccessibilityInfoStatic { /** * Query whether bold text is currently enabled. * - * On Android, requires Android 12 (API 31) or later. + * iOS, and Android 12 (API 31) or later. Earlier Android versions resolve + * `false`. */ isBoldTextEnabled: () => Promise; From 20d916e2d8f2b16268f9d5b93e3fd5e2414b5b52 Mon Sep 17 00:00:00 2001 From: Camilo Pires <45287958+camilopires@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:37:54 +0100 Subject: [PATCH 5/8] Read the starting value and follow events in the RNTester enabled rows Co-Authored-By: Claude Opus 5.5 --- .../Accessibility/AccessibilityExample.js | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index 620dbbf1db26..2d310aa521c3 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -11,7 +11,7 @@ 'use strict'; import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; -import type {EventSubscription, GestureResponderEvent} from 'react-native'; +import type {EventSubscription} from 'react-native'; import RNTesterBlock from '../../components/RNTesterBlock'; import RNTesterText from '../../components/RNTesterText'; @@ -1499,42 +1499,56 @@ class EnabledExample extends React.Component< }, { isEnabled: boolean, + error: ?string, }, > { - state: {isEnabled: boolean} = { + state: {isEnabled: boolean, error: ?string} = { isEnabled: false, + error: null, }; _subscription: EventSubscription; componentDidMount(): null | Promise { this._subscription = AccessibilityInfo.addEventListener( this.props.eventListener, - this._handleToggled, + this._handleChanged, ); + let initialValue: ?Promise = null; switch (this.props.eventListener) { case 'reduceMotionChanged': - return AccessibilityInfo.isReduceMotionEnabled().then(state => { - this.setState({isEnabled: state}); - }); + initialValue = AccessibilityInfo.isReduceMotionEnabled(); + break; case 'accessibilityServiceChanged': - return AccessibilityInfo.isAccessibilityServiceEnabled().then(state => { - this.setState({isEnabled: state}); - }); + initialValue = AccessibilityInfo.isAccessibilityServiceEnabled(); + break; + case 'boldTextChanged': + initialValue = AccessibilityInfo.isBoldTextEnabled(); + break; default: return null; } + return initialValue.then( + state => { + this.setState({isEnabled: state, error: null}); + }, + (error: unknown) => { + this.setState({ + error: error instanceof Error ? error.message : String(error), + }); + }, + ); } componentWillUnmount() { this._subscription?.remove(); } - _handleToggled = (isEnabled: void | GestureResponderEvent | boolean) => { - if (!this.state.isEnabled) { - this.setState({isEnabled: true}); - } else { - this.setState({isEnabled: false}); - } + _handleChanged = (isEnabled: boolean) => { + this.setState({isEnabled, error: null}); + }; + + _handleToggled = () => { + this.setState(state => ({isEnabled: !state.isEnabled})); }; render(): React.Node { @@ -1542,7 +1556,11 @@ class EnabledExample extends React.Component< The {this.props.test} is{' '} - {this.state.isEnabled ? 'enabled' : 'disabled'} + {this.state.error != null + ? `Unavailable: ${this.state.error}` + : this.state.isEnabled + ? 'enabled' + : 'disabled'}