diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js new file mode 100644 index 00000000000..75a37587d27 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js @@ -0,0 +1,172 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; +import NativeAccessibilityInfo from './NativeAccessibilityInfo'; +import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import {sendAccessibilityEvent} from '../../Renderer/shims/ReactNative'; +import legacySendAccessibilityEvent from './legacySendAccessibilityEvent'; +import {type ElementRef} from 'react'; + +const REDUCE_MOTION_EVENT = 'reduceMotionDidChange'; +const TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange'; + +type AccessibilityEventDefinitions = { + reduceMotionChanged: [boolean], + screenReaderChanged: [boolean], + // alias for screenReaderChanged + change: [boolean], +}; + +type AccessibilityEventTypes = 'focus' | 'click'; + +const _subscriptions = new Map(); + +/** + * Sometimes it's useful to know whether or not the device has a screen reader + * that is currently active. The `AccessibilityInfo` API is designed for this + * purpose. You can use it to query the current state of the screen reader as + * well as to register to be notified when the state of the screen reader + * changes. + * + * See https://reactnative.dev/docs/accessibilityinfo.html + */ + +const AccessibilityInfo = { + /** + * iOS only + */ + isBoldTextEnabled: function(): Promise { + return Promise.resolve(false); + }, + + /** + * iOS only + */ + isGrayscaleEnabled: function(): Promise { + return Promise.resolve(false); + }, + + /** + * iOS only + */ + isInvertColorsEnabled: function(): Promise { + return Promise.resolve(false); + }, + + isReduceMotionEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfo) { + NativeAccessibilityInfo.isReduceMotionEnabled(resolve); + } else { + reject(false); + } + }); + }, + + /** + * iOS only + */ + isReduceTransparencyEnabled: function(): Promise { + return Promise.resolve(false); + }, + + isScreenReaderEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityInfo) { + NativeAccessibilityInfo.isTouchExplorationEnabled(resolve); + } else { + reject(false); + } + }); + }, + + addEventListener: function>( + eventName: K, + handler: (...$ElementType) => void, + ): EventSubscription { + let listener; + + if (eventName === 'change' || eventName === 'screenReaderChanged') { + listener = RCTDeviceEventEmitter.addListener( + TOUCH_EXPLORATION_EVENT, + handler, + ); + } else if (eventName === 'reduceMotionChanged') { + listener = RCTDeviceEventEmitter.addListener( + REDUCE_MOTION_EVENT, + handler, + ); + } + + // $FlowFixMe[escaped-generic] + _subscriptions.set(handler, listener); + + return { + remove: () => { + // $FlowIssue flow does not recognize handler properly + AccessibilityInfo.removeEventListener(eventName, handler); + }, + }; + }, + + removeEventListener: function>( + eventName: K, + handler: (...$ElementType) => void, + ): void { + // $FlowFixMe[escaped-generic] + const listener = _subscriptions.get(handler); + if (!listener) { + return; + } + listener.remove(); + // $FlowFixMe[escaped-generic] + _subscriptions.delete(handler); + }, + + /** + * Set accessibility focus to a react component. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus + */ + setAccessibilityFocus: function(reactTag: number): void { + legacySendAccessibilityEvent(reactTag, 'focus'); + }, + + /** + * Send a named accessibility event to a HostComponent. + */ + sendAccessibilityEvent_unstable: function( + handle: ElementRef>, + eventType: AccessibilityEventTypes, + ) { + // route through React renderer to distinguish between Fabric and non-Fabric handles + // iOS only supports 'focus' event types + if (eventType === 'focus') { + sendAccessibilityEvent(handle, eventType); + } else if (eventType === 'click') { + // Do nothing! + } + }, + + /** + * Post a string to be announced by the screen reader. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility + */ + announceForAccessibility: function(announcement: string): void { + if (NativeAccessibilityInfo) { + NativeAccessibilityInfo.announceForAccessibility(announcement); + } + }, +}; + +module.exports = AccessibilityInfo; diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js similarity index 52% rename from Libraries/Components/AccessibilityInfo/AccessibilityInfo.js rename to Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index b259eff592a..0730f808425 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -4,57 +4,49 @@ * 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 + * @flow strict-local */ import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; -import {sendAccessibilityEvent} from '../../Renderer/shims/ReactNative'; +import NativeAccessibilityManager from './NativeAccessibilityManager'; +import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -import Platform from '../../Utilities/Platform'; -import type EventEmitter from '../../vendor/emitter/EventEmitter'; -import type {EventSubscription} from '../../vendor/emitter/EventEmitter'; -import NativeAccessibilityInfoAndroid from './NativeAccessibilityInfo'; -import NativeAccessibilityManagerIOS from './NativeAccessibilityManager'; +import {sendAccessibilityEvent} from '../../Renderer/shims/ReactNative'; import legacySendAccessibilityEvent from './legacySendAccessibilityEvent'; -import type {ElementRef} from 'react'; +import {type ElementRef} from 'react'; -// Events that are only supported on iOS. -type AccessibilityEventDefinitionsIOS = { - announcementFinished: [{announcement: string, success: boolean}], - boldTextChanged: [boolean], - grayscaleChanged: [boolean], - invertColorsChanged: [boolean], - reduceTransparencyChanged: [boolean], +const CHANGE_EVENT_NAME = { + announcementFinished: 'announcementFinished', + boldTextChanged: 'boldTextChanged', + grayscaleChanged: 'grayscaleChanged', + invertColorsChanged: 'invertColorsChanged', + reduceMotionChanged: 'reduceMotionChanged', + reduceTransparencyChanged: 'reduceTransparencyChanged', + screenReaderChanged: 'screenReaderChanged', }; type AccessibilityEventDefinitions = { - ...AccessibilityEventDefinitionsIOS, - change: [boolean], // screenReaderChanged + boldTextChanged: [boolean], + grayscaleChanged: [boolean], + invertColorsChanged: [boolean], reduceMotionChanged: [boolean], + reduceTransparencyChanged: [boolean], screenReaderChanged: [boolean], + // alias for screenReaderChanged + change: [boolean], + announcementFinished: [ + { + announcement: string, + success: boolean, + }, + ], }; -type AccessibilityEventTypes = 'click' | 'focus'; +// 'click' event type is not implemented in iOS. It's declared here to avoid flow type errors +type AccessibilityEventTypes = 'focus' | 'click'; -// Mapping of public event names to platform-specific event names. -const EventNames: Map<$Keys, string> = - Platform.OS === 'android' - ? new Map([ - ['change', 'touchExplorationDidChange'], - ['reduceMotionChanged', 'reduceMotionDidChange'], - ['screenReaderChanged', 'touchExplorationDidChange'], - ]) - : new Map([ - ['announcementFinished', 'announcementFinished'], - ['boldTextChanged', 'boldTextChanged'], - ['change', 'screenReaderChanged'], - ['grayscaleChanged', 'grayscaleChanged'], - ['invertColorsChanged', 'invertColorsChanged'], - ['reduceMotionChanged', 'reduceMotionChanged'], - ['reduceTransparencyChanged', 'reduceTransparencyChanged'], - ['screenReaderChanged', 'screenReaderChanged'], - ]); +const _subscriptions = new Map(); /** * Sometimes it's useful to know whether or not the device has a screen reader @@ -74,21 +66,14 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isBoldTextEnabled */ - isBoldTextEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentBoldTextState( - resolve, - reject, - ); - } else { - reject(null); - } - }); - } + isBoldTextEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentBoldTextState(resolve, reject); + } else { + reject(reject); + } + }); }, /** @@ -99,21 +84,14 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isGrayscaleEnabled */ - isGrayscaleEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentGrayscaleState( - resolve, - reject, - ); - } else { - reject(null); - } - }); - } + isGrayscaleEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentGrayscaleState(resolve, reject); + } else { + reject(reject); + } + }); }, /** @@ -124,21 +102,14 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isInvertColorsEnabled */ - isInvertColorsEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentInvertColorsState( - resolve, - reject, - ); - } else { - reject(null); - } - }); - } + isInvertColorsEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentInvertColorsState(resolve, reject); + } else { + reject(reject); + } + }); }, /** @@ -149,23 +120,12 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceMotionEnabled */ - isReduceMotionEnabled(): Promise { + isReduceMotionEnabled: function(): Promise { return new Promise((resolve, reject) => { - if (Platform.OS === 'android') { - if (NativeAccessibilityInfoAndroid != null) { - NativeAccessibilityInfoAndroid.isReduceMotionEnabled(resolve); - } else { - reject(null); - } + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentReduceMotionState(resolve, reject); } else { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentReduceMotionState( - resolve, - reject, - ); - } else { - reject(null); - } + reject(reject); } }); }, @@ -178,21 +138,17 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isReduceTransparencyEnabled */ - isReduceTransparencyEnabled(): Promise { - if (Platform.OS === 'android') { - return Promise.resolve(false); - } else { - return new Promise((resolve, reject) => { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentReduceTransparencyState( - resolve, - reject, - ); - } else { - reject(null); - } - }); - } + isReduceTransparencyEnabled: function(): Promise { + return new Promise((resolve, reject) => { + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentReduceTransparencyState( + resolve, + reject, + ); + } else { + reject(reject); + } + }); }, /** @@ -203,23 +159,12 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#isScreenReaderEnabled */ - isScreenReaderEnabled(): Promise { + isScreenReaderEnabled: function(): Promise { return new Promise((resolve, reject) => { - if (Platform.OS === 'android') { - if (NativeAccessibilityInfoAndroid != null) { - NativeAccessibilityInfoAndroid.isTouchExplorationEnabled(resolve); - } else { - reject(null); - } + if (NativeAccessibilityManager) { + NativeAccessibilityManager.getCurrentVoiceOverState(resolve, reject); } else { - if (NativeAccessibilityManagerIOS != null) { - NativeAccessibilityManagerIOS.getCurrentVoiceOverState( - resolve, - reject, - ); - } else { - reject(null); - } + reject(reject); } }); }, @@ -227,16 +172,6 @@ const AccessibilityInfo = { /** * Add an event handler. Supported events: * - * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. - * The argument to the event handler is a boolean. The boolean is `true` when a reduce - * motion is enabled (or when "Transition Animation Scale" in "Developer options" is - * "Animation off") and `false` otherwise. - * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument - * to the event handler is a boolean. The boolean is `true` when a screen - * reader is enabled and `false` otherwise. - * - * 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. @@ -246,9 +181,16 @@ const AccessibilityInfo = { * - `invertColorsChanged`: iOS-only event. Fires when the state of the invert colors toggle * changes. The argument to the event handler is a boolean. The boolean is `true` when a invert * colors is enabled and `false` otherwise. + * - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes. + * The argument to the event handler is a boolean. The boolean is `true` when a reduce + * motion is enabled (or when "Transition Animation Scale" in "Developer options" is + * "Animation off") 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. + * - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument + * to the event handler is a boolean. The boolean is `true` when a screen + * reader is enabled and `false` otherwise. * - `announcementFinished`: iOS-only event. Fires when the screen reader has * finished making an announcement. The argument to the event handler is a * dictionary with these keys: @@ -258,36 +200,48 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#addeventlistener */ - addEventListener>( + addEventListener: function>( eventName: K, handler: (...$ElementType) => void, ): EventSubscription { - const deviceEventName = EventNames.get(eventName); - return deviceEventName == null - ? {remove(): void {}} - : RCTDeviceEventEmitter.addListener(deviceEventName, handler); + let subscription: EventSubscription; + + if (eventName === 'change') { + subscription = RCTDeviceEventEmitter.addListener( + CHANGE_EVENT_NAME.screenReaderChanged, + handler, + ); + } else if (CHANGE_EVENT_NAME[eventName]) { + subscription = RCTDeviceEventEmitter.addListener(eventName, handler); + } + + // $FlowFixMe[escaped-generic] + _subscriptions.set(handler, subscription); + + return { + remove: () => { + // $FlowIssue[incompatible-call] flow does not recognize handler properly + AccessibilityInfo.removeEventListener(eventName, handler); + }, + }; }, /** - * Set accessibility focus to a React component. + * Set accessibility focus to a react component. * * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus */ - setAccessibilityFocus(reactTag: number): void { + setAccessibilityFocus: function(reactTag: number): void { legacySendAccessibilityEvent(reactTag, 'focus'); }, /** * Send a named accessibility event to a HostComponent. */ - sendAccessibilityEvent_unstable( + sendAccessibilityEvent_unstable: function( handle: ElementRef>, eventType: AccessibilityEventTypes, ) { - // iOS only supports 'focus' event types - if (Platform.OS === 'ios' && eventType === 'click') { - return; - } // route through React renderer to distinguish between Fabric and non-Fabric handles sendAccessibilityEvent(handle, eventType); }, @@ -297,32 +251,30 @@ const AccessibilityInfo = { * * See https://reactnative.dev/docs/accessibilityinfo.html#announceforaccessibility */ - announceForAccessibility(announcement: string): void { - if (Platform.OS === 'android') { - NativeAccessibilityInfoAndroid?.announceForAccessibility(announcement); - } else { - NativeAccessibilityManagerIOS?.announceForAccessibility(announcement); + announceForAccessibility: function(announcement: string): void { + if (NativeAccessibilityManager) { + NativeAccessibilityManager.announceForAccessibility(announcement); } }, /** - * @deprecated Use `remove` on the EventSubscription from `addEventListener`. + * Remove an event handler. + * + * See https://reactnative.dev/docs/accessibilityinfo.html#removeeventlistener */ - removeEventListener>( + removeEventListener: function>( eventName: K, handler: (...$ElementType) => void, ): void { - // NOTE: This will report a deprecation notice via `console.error`. - const deviceEventName = EventNames.get(eventName); - if (deviceEventName != null) { - // $FlowIgnore[incompatible-cast] - (RCTDeviceEventEmitter: EventEmitter<$FlowFixMe>).removeListener( - 'deviceEventName', - // $FlowFixMe[invalid-tuple-arity] - handler, - ); + // $FlowFixMe[escaped-generic] + const listener = _subscriptions.get(handler); + if (!listener) { + return; } + listener.remove(); + // $FlowFixMe[escaped-generic] + _subscriptions.delete(handler); }, }; -export default AccessibilityInfo; +module.exports = AccessibilityInfo; diff --git a/index.js b/index.js index aa0eb7ccf1d..fbbe0847700 100644 --- a/index.js +++ b/index.js @@ -117,8 +117,7 @@ const warnOnce = require('./Libraries/Utilities/warnOnce'); module.exports = { // Components get AccessibilityInfo(): AccessibilityInfo { - return require('./Libraries/Components/AccessibilityInfo/AccessibilityInfo') - .default; + return require('./Libraries/Components/AccessibilityInfo/AccessibilityInfo'); }, get ActivityIndicator(): ActivityIndicator { return require('./Libraries/Components/ActivityIndicator/ActivityIndicator'); diff --git a/jest/setup.js b/jest/setup.js index 8ecd819d7d2..1bb9af5c51b 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -116,20 +116,18 @@ jest mockComponent('../Libraries/Components/View/View', MockNativeMethods), ) .mock('../Libraries/Components/AccessibilityInfo/AccessibilityInfo', () => ({ - __esModule: true, - default: { - addEventListener: jest.fn(), - announceForAccessibility: jest.fn(), - isBoldTextEnabled: jest.fn(), - isGrayscaleEnabled: jest.fn(), - isInvertColorsEnabled: jest.fn(), - isReduceMotionEnabled: jest.fn(), - isReduceTransparencyEnabled: jest.fn(), - isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)), - removeEventListener: jest.fn(), - setAccessibilityFocus: jest.fn(), - sendAccessibilityEvent_unstable: jest.fn(), - }, + addEventListener: jest.fn(), + announceForAccessibility: jest.fn(), + fetch: jest.fn(), + isBoldTextEnabled: jest.fn(), + isGrayscaleEnabled: jest.fn(), + isInvertColorsEnabled: jest.fn(), + isReduceMotionEnabled: jest.fn(), + isReduceTransparencyEnabled: jest.fn(), + isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)), + removeEventListener: jest.fn(), + setAccessibilityFocus: jest.fn(), + sendAccessibilityEvent_unstable: jest.fn(), })) .mock('../Libraries/Components/RefreshControl/RefreshControl', () => jest.requireActual(