From 99b7052248202cee172e0b80e7ee3dfb41316746 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 27 Jan 2021 17:34:34 -0800 Subject: [PATCH] Implement sendAccessibilityEvent in the React(Fabric/non-Fabric) renderer Summary: `sendAccessibilityEvent_unstable` is a cross-platform, Fabric/non-Fabric replacement for previous APIs (which went through UIManager directly on Android, and a native module on iOS). Changelog: [Added] sendAccessibilityEvent_unstable API in AccessibilityInfo and sendAccessibilityEvent in React renderer Reviewed By: kacieb Differential Revision: D25821052 fbshipit-source-id: 03f7a9878c95e8395f9102b3e596bfc9f03730e0 --- .../AccessibilityInfo.android.js | 23 +++++++++++--- .../AccessibilityInfo.ios.js | 21 +++++++++++-- .../legacySendAccessibilityEvent.android.js | 31 +++++++++++++++++++ .../legacySendAccessibilityEvent.ios.js | 28 +++++++++++++++++ .../Touchable/TouchableHighlight.js | 5 ++- .../Components/Touchable/TouchableOpacity.js | 8 ++--- Libraries/ReactNative/FabricUIManager.js | 1 + .../ReactNativePrivateInterface.js | 4 +++ .../implementations/ReactFabric-dev.fb.js | 29 ++++++++++++++++- .../implementations/ReactFabric-prod.fb.js | 12 +++++++ .../ReactFabric-profiling.fb.js | 12 +++++++ .../ReactNativeRenderer-dev.fb.js | 26 ++++++++++++++++ .../ReactNativeRenderer-prod.fb.js | 12 +++++++ .../ReactNativeRenderer-profiling.fb.js | 12 +++++++ Libraries/Renderer/shims/ReactNativeTypes.js | 8 +++++ jest/setup.js | 1 + .../Accessibility/AccessibilityExample.js | 17 +++------- 17 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js create mode 100644 Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js index 63614d22dccc18..cfceeaaa7c719f 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js @@ -11,9 +11,12 @@ 'use strict'; import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; -import UIManager from '../../ReactNative/UIManager'; 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'; @@ -25,6 +28,8 @@ type AccessibilityEventDefinitions = { change: [boolean], }; +type AccessibilityEventTypes = 'focus'; + const _subscriptions = new Map(); /** @@ -148,10 +153,18 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus */ setAccessibilityFocus: function(reactTag: number): void { - UIManager.sendAccessibilityEvent( - reactTag, - UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, - ); + 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 + sendAccessibilityEvent(handle, eventType); }, /** diff --git a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js index a0121e5d4f9a6f..cd55ed59c0383d 100644 --- a/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js +++ b/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js @@ -13,6 +13,10 @@ import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter'; import NativeAccessibilityManager from './NativeAccessibilityManager'; 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 CHANGE_EVENT_NAME = { announcementFinished: 'announcementFinished', @@ -41,6 +45,8 @@ type AccessibilityEventDefinitions = { ], }; +type AccessibilityEventTypes = 'focus'; + const _subscriptions = new Map(); /** @@ -241,9 +247,18 @@ const AccessibilityInfo = { * See https://reactnative.dev/docs/accessibilityinfo.html#setaccessibilityfocus */ setAccessibilityFocus: function(reactTag: number): void { - if (NativeAccessibilityManager) { - NativeAccessibilityManager.setAccessibilityFocus(reactTag); - } + 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 + sendAccessibilityEvent(handle, eventType); }, /** diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js new file mode 100644 index 00000000000000..a5c75a92befd7b --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.android.js @@ -0,0 +1,31 @@ +/** + * 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 + */ + +'use strict'; + +import UIManager from '../../ReactNative/UIManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus') { + UIManager.sendAccessibilityEvent( + reactTag, + UIManager.getConstants().AccessibilityEventTypes.typeViewFocused, + ); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js new file mode 100644 index 00000000000000..cbc1785610a2f0 --- /dev/null +++ b/Libraries/Components/AccessibilityInfo/legacySendAccessibilityEvent.ios.js @@ -0,0 +1,28 @@ +/** + * 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 + */ + +'use strict'; + +import NativeAccessibilityManager from './NativeAccessibilityManager'; + +/** + * This is a function exposed to the React Renderer that can be used by the + * pre-Fabric renderer to emit accessibility events to pre-Fabric nodes. + */ +function legacySendAccessibilityEvent( + reactTag: number, + eventType: string, +): void { + if (eventType === 'focus' && NativeAccessibilityManager) { + NativeAccessibilityManager.setAccessibilityFocus(reactTag); + } +} + +module.exports = legacySendAccessibilityEvent; diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 327d455e283b6c..d01fb7c595d463 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -350,4 +350,7 @@ class TouchableHighlight extends React.Component { module.exports = (React.forwardRef((props, hostRef) => ( -)): React.AbstractComponent<$ReadOnly<$Diff>>); +)): React.AbstractComponent< + $ReadOnly<$Diff|}>>, + React.ElementRef, +>); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 8023030a36f962..eb6a3612b284e1 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -38,7 +38,7 @@ type Props = $ReadOnly<{| activeOpacity?: ?number, style?: ?ViewStyleProp, - hostRef: React.Ref, + hostRef?: ?React.Ref, |}>; type State = $ReadOnly<{| @@ -267,6 +267,6 @@ class TouchableOpacity extends React.Component { } } -module.exports = (React.forwardRef((props, hostRef) => ( - -)): React.AbstractComponent<$ReadOnly<$Diff>>); +module.exports = (React.forwardRef((props, ref) => ( + +)): React.AbstractComponent>); diff --git a/Libraries/ReactNative/FabricUIManager.js b/Libraries/ReactNative/FabricUIManager.js index 2b88069742229e..7d133837ab6774 100644 --- a/Libraries/ReactNative/FabricUIManager.js +++ b/Libraries/ReactNative/FabricUIManager.js @@ -57,6 +57,7 @@ export type Spec = {| // $FlowFixMe errorCallback: (error: Object) => void, ) => void, + +sendAccessibilityEvent: (node: Node, eventType: string) => void, |}; const FabricUIManager: ?Spec = global.nativeFabricUIManager; diff --git a/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 603912af5ba2ae..6155b271799e1c 100644 --- a/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -20,6 +20,7 @@ import typeof deepFreezeAndThrowOnMutationInDev from '../Utilities/deepFreezeAnd import typeof flattenStyle from '../StyleSheet/flattenStyle'; import {type DangerouslyImpreciseStyleProp} from '../StyleSheet/StyleSheet'; import typeof ReactFiberErrorDialog from '../Core/ReactFiberErrorDialog'; +import typeof legacySendAccessibilityEvent from '../Components/AccessibilityInfo/legacySendAccessibilityEvent'; // flowlint unsafe-getters-setters:off module.exports = { @@ -59,4 +60,7 @@ module.exports = { get ReactFiberErrorDialog(): ReactFiberErrorDialog { return require('../Core/ReactFiberErrorDialog'); }, + get legacySendAccessibilityEvent(): legacySendAccessibilityEvent { + return require('../Components/AccessibilityInfo/legacySendAccessibilityEvent'); + }, }; diff --git a/Libraries/Renderer/implementations/ReactFabric-dev.fb.js b/Libraries/Renderer/implementations/ReactFabric-dev.fb.js index af66172f68f0b3..4d07117f346fb9 100644 --- a/Libraries/Renderer/implementations/ReactFabric-dev.fb.js +++ b/Libraries/Renderer/implementations/ReactFabric-dev.fb.js @@ -3730,7 +3730,8 @@ var _nativeFabricUIManage = nativeFabricUIManager, registerEventHandler = _nativeFabricUIManage.registerEventHandler, fabricMeasure = _nativeFabricUIManage.measure, fabricMeasureInWindow = _nativeFabricUIManage.measureInWindow, - fabricMeasureLayout = _nativeFabricUIManage.measureLayout; + fabricMeasureLayout = _nativeFabricUIManage.measureLayout, + sendAccessibilityEvent = _nativeFabricUIManage.sendAccessibilityEvent; var getViewConfigForType = ReactNativePrivateInterface.ReactNativeViewConfigRegistry.get; // Counter for uniquely identifying views. // % 10 === 1 means it is a rootTag. @@ -21695,6 +21696,31 @@ function dispatchCommand(handle, command, args) { } } +function sendAccessibilityEvent(handle, eventType) { + if (handle._nativeTag == null) { + { + error( + "sendAccessibilityEvent was called with a ref that isn't a " + + "native component. Use React.forwardRef to get access to the underlying native component" + ); + } + + return; + } + + if (handle._internalInstanceHandle) { + nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ); + } else { + ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + ); + } +} + function render(element, containerTag, callback) { var root = roots.get(containerTag); @@ -21750,6 +21776,7 @@ exports.createPortal = createPortal$1; exports.dispatchCommand = dispatchCommand; exports.findHostInstance_DEPRECATED = findHostInstance_DEPRECATED; exports.findNodeHandle = findNodeHandle; +exports.sendAccessibilityEvent = sendAccessibilityEvent; exports.render = render; exports.stopSurface = stopSurface; exports.unmountComponentAtNode = unmountComponentAtNode; diff --git a/Libraries/Renderer/implementations/ReactFabric-prod.fb.js b/Libraries/Renderer/implementations/ReactFabric-prod.fb.js index 00a3503a192280..035c6c10c7b3e8 100644 --- a/Libraries/Renderer/implementations/ReactFabric-prod.fb.js +++ b/Libraries/Renderer/implementations/ReactFabric-prod.fb.js @@ -7702,6 +7702,18 @@ exports.render = function(element, containerTag, callback) { else element = null; return element; }; +exports.sendAccessibilityEvent = function(handle, eventType) { + null != handle._nativeTag && + (handle._internalInstanceHandle + ? nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ) + : ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + )); +}; exports.stopSurface = function(containerTag) { var root = roots.get(containerTag); root && diff --git a/Libraries/Renderer/implementations/ReactFabric-profiling.fb.js b/Libraries/Renderer/implementations/ReactFabric-profiling.fb.js index f066762ac44df9..51f399468cdf57 100644 --- a/Libraries/Renderer/implementations/ReactFabric-profiling.fb.js +++ b/Libraries/Renderer/implementations/ReactFabric-profiling.fb.js @@ -7997,6 +7997,18 @@ exports.render = function(element, containerTag, callback) { else element = null; return element; }; +exports.sendAccessibilityEvent = function(handle, eventType) { + null != handle._nativeTag && + (handle._internalInstanceHandle + ? nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ) + : ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + )); +}; exports.stopSurface = function(containerTag) { var root = roots.get(containerTag); root && diff --git a/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js b/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js index 1dfd6b73f36c5d..6b9ea503d3866e 100644 --- a/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js +++ b/Libraries/Renderer/implementations/ReactNativeRenderer-dev.fb.js @@ -22323,6 +22323,31 @@ function dispatchCommand(handle, command, args) { } } +function sendAccessibilityEvent(handle, eventType) { + if (handle._nativeTag == null) { + { + error( + "sendAccessibilityEvent was called with a ref that isn't a " + + "native component. Use React.forwardRef to get access to the underlying native component" + ); + } + + return; + } + + if (handle._internalInstanceHandle) { + nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ); + } else { + ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + ); + } +} + function render(element, containerTag, callback) { var root = roots.get(containerTag); @@ -22396,6 +22421,7 @@ exports.dispatchCommand = dispatchCommand; exports.findHostInstance_DEPRECATED = findHostInstance_DEPRECATED; exports.findNodeHandle = findNodeHandle; exports.render = render; +exports.sendAccessibilityEvent = sendAccessibilityEvent; exports.unmountComponentAtNode = unmountComponentAtNode; exports.unmountComponentAtNodeAndRemoveContainer = unmountComponentAtNodeAndRemoveContainer; exports.unstable_batchedUpdates = batchedUpdates; diff --git a/Libraries/Renderer/implementations/ReactNativeRenderer-prod.fb.js b/Libraries/Renderer/implementations/ReactNativeRenderer-prod.fb.js index 18bc9ba73eff45..ccced617800599 100644 --- a/Libraries/Renderer/implementations/ReactNativeRenderer-prod.fb.js +++ b/Libraries/Renderer/implementations/ReactNativeRenderer-prod.fb.js @@ -7930,6 +7930,18 @@ exports.render = function(element, containerTag, callback) { else element = null; return element; }; +exports.sendAccessibilityEvent = function(handle, eventType) { + null != handle._nativeTag && + (handle._internalInstanceHandle + ? nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ) + : ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + )); +}; exports.unmountComponentAtNode = unmountComponentAtNode; exports.unmountComponentAtNodeAndRemoveContainer = function(containerTag) { unmountComponentAtNode(containerTag); diff --git a/Libraries/Renderer/implementations/ReactNativeRenderer-profiling.fb.js b/Libraries/Renderer/implementations/ReactNativeRenderer-profiling.fb.js index cdb80f6b2e1d17..7937b1e922259d 100644 --- a/Libraries/Renderer/implementations/ReactNativeRenderer-profiling.fb.js +++ b/Libraries/Renderer/implementations/ReactNativeRenderer-profiling.fb.js @@ -8222,6 +8222,18 @@ exports.render = function(element, containerTag, callback) { else element = null; return element; }; +exports.sendAccessibilityEvent = function(handle, eventType) { + null != handle._nativeTag && + (handle._internalInstanceHandle + ? nativeFabricUIManager.sendAccessibilityEvent( + handle._internalInstanceHandle.stateNode.node, + eventType + ) + : ReactNativePrivateInterface.legacySendAccessibilityEvent( + handle._nativeTag, + eventType + )); +}; exports.unmountComponentAtNode = unmountComponentAtNode; exports.unmountComponentAtNodeAndRemoveContainer = function(containerTag) { unmountComponentAtNode(containerTag); diff --git a/Libraries/Renderer/shims/ReactNativeTypes.js b/Libraries/Renderer/shims/ReactNativeTypes.js index a38eaaf1141754..552b118556aaf0 100644 --- a/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/Libraries/Renderer/shims/ReactNativeTypes.js @@ -180,6 +180,10 @@ export type ReactNativeType = { command: string, args: Array, ): void, + sendAccessibilityEvent( + handle: ElementRef>, + eventType: string, + ): void, render( element: MixedElement, containerTag: number, @@ -204,6 +208,10 @@ export type ReactFabricType = { command: string, args: Array, ): void, + sendAccessibilityEvent( + handle: ElementRef>, + eventType: string, + ): void, render( element: MixedElement, containerTag: number, diff --git a/jest/setup.js b/jest/setup.js index 504ff379363746..c5b829374cff8f 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -127,6 +127,7 @@ jest isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)), removeEventListener: jest.fn(), setAccessibilityFocus: jest.fn(), + sendAccessibilityEvent_unstable: jest.fn(), })) .mock('../Libraries/Components/RefreshControl/RefreshControl', () => jest.requireActual( diff --git a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js index b5e0d4a1c2eee7..22b2c0710eb594 100644 --- a/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js +++ b/packages/rn-tester/js/examples/Accessibility/AccessibilityExample.js @@ -18,7 +18,6 @@ const { View, TouchableOpacity, TouchableWithoutFeedback, - findNodeHandle, Alert, StyleSheet, Platform, @@ -712,27 +711,21 @@ class AnnounceForAccessibility extends React.Component<{}> { } class SetAccessibilityFocusExample extends React.Component<{}> { - state = { - reactTag: null, - }; - render() { const myRef = createRef(); const onClose = () => { if (myRef && myRef.current) { - const reactTag = findNodeHandle(myRef.current); - this.setState({reactTag}); - AccessibilityInfo.setAccessibilityFocus(reactTag); + AccessibilityInfo.sendAccessiblityEvent_unstable( + myRef.current, + 'focus', + ); } }; return ( - - SetAccessibilityFocus on ReactTag:{' '} - {this.state.reactTag == null ? 'Null' : this.state.reactTag} - + SetAccessibilityFocus on native element