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
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ type AccessibilityEventDefinitionsAndroid = {
// Events that are only supported on iOS.
type AccessibilityEventDefinitionsIOS = {
announcementFinished: [{announcement: string, success: boolean}],
boldTextChanged: [boolean],
darkerSystemColorsChanged: [boolean],
reduceTransparencyChanged: [boolean],
};

type AccessibilityEventDefinitions = {
...AccessibilityEventDefinitionsAndroid,
...AccessibilityEventDefinitionsIOS,
boldTextChanged: [boolean],
change: [boolean], // screenReaderChanged
grayscaleChanged: [boolean],
invertColorsChanged: [boolean],
Expand All @@ -52,6 +52,7 @@ const EventNames: Map<keyof AccessibilityEventDefinitions, string> =
['change', 'touchExplorationDidChange'],
['reduceMotionChanged', 'reduceMotionDidChange'],
['highTextContrastChanged', 'highTextContrastDidChange'],
['boldTextChanged', 'boldTextDidChange'],
['screenReaderChanged', 'touchExplorationDidChange'],
['accessibilityServiceChanged', 'accessibilityServiceDidChange'],
['invertColorsChanged', 'invertColorDidChange'],
Expand Down Expand Up @@ -81,11 +82,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<boolean> {
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) {
Expand Down Expand Up @@ -388,12 +397,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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,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));
Expand All @@ -114,6 +115,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,
Expand Down Expand Up @@ -143,6 +148,7 @@ let mockNativeAccessibilityInfoAndroid: {
void,
> | null,
} = {
isBoldTextEnabled: mockIsBoldTextEnabledAndroid,
isHighTextContrastEnabled: mockIsHighTextContrastEnabled,
isGrayscaleEnabled: mockIsGrayscaleEnabled,
isInvertColorsEnabled: mockIsInvertColorsEnabled,
Expand Down Expand Up @@ -176,10 +182,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);
Expand Down Expand Up @@ -805,6 +823,7 @@ describe('AccessibilityInfo', () => {
mockNativeAccessibilityManagerIOS;

mockNativeAccessibilityInfoAndroid = {
isBoldTextEnabled: mockIsBoldTextEnabledAndroid,
isHighTextContrastEnabled: mockIsHighTextContrastEnabled,
isGrayscaleEnabled: mockIsGrayscaleEnabled,
isInvertColorsEnabled: mockIsInvertColorsEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -95,6 +114,7 @@ internal class AccessibilityInfoModule(context: ReactApplicationContext) :
accessibilityServiceEnabled = accessibilityManager?.isEnabled ?: false
reduceMotionEnabled = isReduceMotionEnabledValue
highTextContrastEnabled = isHighTextContrastEnabledValue
boldTextEnabled = isBoldTextEnabledValue
grayscaleModeEnabled = isGrayscaleEnabledValue
}

Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand All @@ -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() {
Expand All @@ -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()
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading
Loading