@samitouri / QOS-React-2 / commits / e316f78552

RN: Implement `sendAccessibilityEvent` in RN Renderer that proxies between Fabric/non-Fabric (#20554)

* RN: Implement `sendAccessibilityEvent` on HostComponent Implement `sendAccessibilityEvent` on HostComponent for Fabric and non-Fabric RN. Currently the Fabric version is a noop and non-Fabric uses AccessibilityInfo directly. The Fabric version will be updated once native Fabric Android/iOS support this method in the native UIManager. * Move methods out of HostComponent * Properly type dispatchCommand and sendAccessibilityEvent handle arg * Implement Fabric side of sendAccessibilityEvent * Add tests: 1. Fabric->Fabric, 2. Paper->Fabric, 3. Fabric->Paper, 4. Paper->Paper * Fix typo: ReactFaricEventTouch -> ReactFabricEventTouch * fix flow types * prettier

Joshua Gross committed Jan 26, 2021 at 20:02 UTC e316f785526e503eceed1ae33b7a06440084b9c9
11 files changed +273 -11
packages/react-native-renderer/src/ReactFabric.js
+27 -2
@@ -30,8 +30,11 @@ import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal
30 import {setBatchingImplementation} from './legacy-events/ReactGenericBatching';
31 import ReactVersion from 'shared/ReactVersion';
32
33 -// Module provided by RN:
34 -import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
33 +// Modules provided by RN:
34 +import {
35 + UIManager,
36 + legacySendAccessibilityEvent,
37 +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
38
39 import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
40 import {
@@ -169,6 +172,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
172 }
173 }
174
175 +function sendAccessibilityEvent(handle: any, eventType: string) {
176 + if (handle._nativeTag == null) {
177 + if (__DEV__) {
178 + console.error(
179 + "sendAccessibilityEvent was called with a ref that isn't a " +
180 + 'native component. Use React.forwardRef to get access to the underlying native component',
181 + );
182 + }
183 + return;
184 + }
185 +
186 + if (handle._internalInstanceHandle) {
187 + nativeFabricUIManager.sendAccessibilityEvent(
188 + handle._internalInstanceHandle.stateNode.node,
189 + eventType,
190 + );
191 + } else {
192 + legacySendAccessibilityEvent(handle._nativeTag, eventType);
193 + }
194 +}
195 +
196 function render(
197 element: React$Element<any>,
198 containerTag: any,
@@ -224,6 +248,7 @@ export {
248 findHostInstance_DEPRECATED,
249 findNodeHandle,
250 dispatchCommand,
251 + sendAccessibilityEvent,
252 render,
253 // Deprecated - this function is being renamed to stopSurface, use that instead.
254 // TODO (T47576999): Delete this once it's no longer called from native code.
packages/react-native-renderer/src/ReactNativeRenderer.js
+27 -2
@@ -32,8 +32,11 @@ import {
32 batchedUpdates,
33 } from './legacy-events/ReactGenericBatching';
34 import ReactVersion from 'shared/ReactVersion';
35 -// Module provided by RN:
36 -import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
35 +// Modules provided by RN:
36 +import {
37 + UIManager,
38 + legacySendAccessibilityEvent,
39 +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
40
41 import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
42 import {
@@ -168,6 +171,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
171 }
172 }
173
174 +function sendAccessibilityEvent(handle: any, eventType: string) {
175 + if (handle._nativeTag == null) {
176 + if (__DEV__) {
177 + console.error(
178 + "sendAccessibilityEvent was called with a ref that isn't a " +
179 + 'native component. Use React.forwardRef to get access to the underlying native component',
180 + );
181 + }
182 + return;
183 + }
184 +
185 + if (handle._internalInstanceHandle) {
186 + nativeFabricUIManager.sendAccessibilityEvent(
187 + handle._internalInstanceHandle.stateNode.node,
188 + eventType,
189 + );
190 + } else {
191 + legacySendAccessibilityEvent(handle._nativeTag, eventType);
192 + }
193 +}
194 +
195 function render(
196 element: React$Element<any>,
197 containerTag: any,
@@ -238,6 +262,7 @@ export {
262 findHostInstance_DEPRECATED,
263 findNodeHandle,
264 dispatchCommand,
265 + sendAccessibilityEvent,
266 render,
267 unmountComponentAtNode,
268 unmountComponentAtNodeAndRemoveContainer,
packages/react-native-renderer/src/ReactNativeTypes.js
+23 -7
@@ -149,7 +149,15 @@ export type ReactNativeType = {
149 componentOrHandle: any,
150 ): ?ElementRef<HostComponent<mixed>>,
151 findNodeHandle(componentOrHandle: any): ?number,
152 - dispatchCommand(handle: any, command: string, args: Array<any>): void,
152 + dispatchCommand(
153 + handle: ElementRef<HostComponent<mixed>>,
154 + command: string,
155 + args: Array<any>,
156 + ): void,
157 + sendAccessibilityEvent(
158 + handle: ElementRef<HostComponent<mixed>>,
159 + eventType: string,
160 + ): void,
161 render(
162 element: React$Element<any>,
163 containerTag: any,
@@ -168,7 +176,15 @@ export type ReactFabricType = {
176 componentOrHandle: any,
177 ): ?ElementRef<HostComponent<mixed>>,
178 findNodeHandle(componentOrHandle: any): ?number,
171 - dispatchCommand(handle: any, command: string, args: Array<any>): void,
179 + dispatchCommand(
180 + handle: ElementRef<HostComponent<mixed>>,
181 + command: string,
182 + args: Array<any>,
183 + ): void,
184 + sendAccessibilityEvent(
185 + handle: ElementRef<HostComponent<mixed>>,
186 + eventType: string,
187 + ): void,
188 render(
189 element: React$Element<any>,
190 containerTag: any,
@@ -190,7 +206,7 @@ export type ReactNativeEventTarget = {
206 ...
207 };
208
193 -export type ReactFaricEventTouch = {
209 +export type ReactFabricEventTouch = {
210 identifier: number,
211 locationX: number,
212 locationY: number,
@@ -204,10 +220,10 @@ export type ReactFaricEventTouch = {
220 ...
221 };
222
207 -export type ReactFaricEvent = {
208 - touches: Array<ReactFaricEventTouch>,
209 - changedTouches: Array<ReactFaricEventTouch>,
210 - targetTouches: Array<ReactFaricEventTouch>,
223 +export type ReactFabricEvent = {
224 + touches: Array<ReactFabricEventTouch>,
225 + changedTouches: Array<ReactFabricEventTouch>,
226 + targetTouches: Array<ReactFabricEventTouch>,
227 target: number,
228 ...
229 };
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js
+2
@@ -122,6 +122,8 @@ const RCTFabricUIManager = {
122
123 dispatchCommand: jest.fn(),
124
125 + sendAccessibilityEvent: jest.fn(),
126 +
127 registerEventHandler: jest.fn(function registerEventHandler(callback) {}),
128
129 measure: jest.fn(function measure(node, callback) {
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js
+3
@@ -38,4 +38,7 @@ module.exports = {
38 get flattenStyle() {
39 return require('./flattenStyle');
40 },
41 + get legacySendAccessibilityEvent() {
42 + return require('./legacySendAccessibilityEvent');
43 + },
44 };
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js
+1
@@ -88,6 +88,7 @@ const RCTUIManager = {
88 });
89 }),
90 dispatchViewManagerCommand: jest.fn(),
91 + sendAccessibilityEvent: jest.fn(),
92 setJSResponder: jest.fn(),
93 setChildren: jest.fn(function setChildren(parentTag, reactTags) {
94 autoCreateRoot(parentTag);
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/legacySendAccessibilityEvent.js new
+10
@@ -0,0 +1,10 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +'use strict';
9 +
10 +module.exports = jest.fn();
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+62
@@ -24,6 +24,10 @@ const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
24 "Warning: dispatchCommand was called with a ref that isn't a " +
25 'native component. Use React.forwardRef to get access to the underlying native component';
26
27 +const SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT =
28 + "sendAccessibilityEvent was called with a ref that isn't a " +
29 + 'native component. Use React.forwardRef to get access to the underlying native component';
30 +
31 jest.mock('shared/ReactFeatureFlags', () =>
32 require('shared/forks/ReactFeatureFlags.native-oss'),
33 );
@@ -289,6 +293,64 @@ describe('ReactFabric', () => {
293 expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
294 });
295
296 + it('should call sendAccessibilityEvent for native refs', () => {
297 + const View = createReactNativeComponentClass('RCTView', () => ({
298 + validAttributes: {foo: true},
299 + uiViewClassName: 'RCTView',
300 + }));
301 +
302 + nativeFabricUIManager.sendAccessibilityEvent.mockClear();
303 +
304 + let viewRef;
305 + ReactFabric.render(
306 + <View
307 + ref={ref => {
308 + viewRef = ref;
309 + }}
310 + />,
311 + 11,
312 + );
313 +
314 + expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
315 + ReactFabric.sendAccessibilityEvent(viewRef, 'focus');
316 + expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledTimes(
317 + 1,
318 + );
319 + expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledWith(
320 + expect.any(Object),
321 + 'focus',
322 + );
323 + });
324 +
325 + it('should warn and no-op if calling sendAccessibilityEvent on non native refs', () => {
326 + class BasicClass extends React.Component {
327 + render() {
328 + return <React.Fragment />;
329 + }
330 + }
331 +
332 + nativeFabricUIManager.sendAccessibilityEvent.mockReset();
333 +
334 + let viewRef;
335 + ReactFabric.render(
336 + <BasicClass
337 + ref={ref => {
338 + viewRef = ref;
339 + }}
340 + />,
341 + 11,
342 + );
343 +
344 + expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
345 + expect(() => {
346 + ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName');
347 + }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], {
348 + withoutStack: true,
349 + });
350 +
351 + expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
352 + });
353 +
354 it('should call FabricUIManager.measure on ref.measure', () => {
355 const View = createReactNativeComponentClass('RCTView', () => ({
356 validAttributes: {foo: true},
packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js
+48
@@ -15,6 +15,7 @@ let ReactFabric;
15 let ReactNative;
16 let UIManager;
17 let createReactNativeComponentClass;
18 +let ReactNativePrivateInterface;
19
20 describe('created with ReactFabric called with ReactNative', () => {
21 beforeEach(() => {
@@ -22,6 +23,7 @@ describe('created with ReactFabric called with ReactNative', () => {
23 require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
24 ReactNative = require('react-native-renderer');
25 jest.resetModules();
26 + ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
27 UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
28 .UIManager;
29 jest.mock('shared/ReactFeatureFlags', () =>
@@ -92,6 +94,28 @@ describe('created with ReactFabric called with ReactNative', () => {
94 ).toHaveBeenCalledWith(expect.any(Object), 'myCommand', [10, 20]);
95 expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
96 });
97 +
98 + it('dispatches sendAccessibilityEvent on Fabric nodes with the RN renderer', () => {
99 + nativeFabricUIManager.sendAccessibilityEvent.mockClear();
100 + const View = createReactNativeComponentClass('RCTView', () => ({
101 + validAttributes: {title: true},
102 + uiViewClassName: 'RCTView',
103 + }));
104 +
105 + const ref = React.createRef();
106 +
107 + ReactFabric.render(<View title="bar" ref={ref} />, 11);
108 + expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
109 + ReactNative.sendAccessibilityEvent(ref.current, 'focus');
110 + expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledTimes(
111 + 1,
112 + );
113 + expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledWith(
114 + expect.any(Object),
115 + 'focus',
116 + );
117 + expect(UIManager.sendAccessibilityEvent).not.toBeCalled();
118 + });
119 });
120
121 describe('created with ReactNative called with ReactFabric', () => {
@@ -171,4 +195,28 @@ describe('created with ReactNative called with ReactFabric', () => {
195
196 expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
197 });
198 +
199 + it('dispatches sendAccessibilityEvent on Paper nodes with the Fabric renderer', () => {
200 + ReactNativePrivateInterface.legacySendAccessibilityEvent.mockReset();
201 + const View = createReactNativeComponentClass('RCTView', () => ({
202 + validAttributes: {title: true},
203 + uiViewClassName: 'RCTView',
204 + }));
205 +
206 + const ref = React.createRef();
207 +
208 + ReactNative.render(<View title="bar" ref={ref} />, 11);
209 + expect(
210 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
211 + ).not.toBeCalled();
212 + ReactFabric.sendAccessibilityEvent(ref.current, 'focus');
213 + expect(
214 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
215 + ).toHaveBeenCalledTimes(1);
216 + expect(
217 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
218 + ).toHaveBeenCalledWith(expect.any(Number), 'focus');
219 +
220 + expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
221 + });
222 });
packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js
+65
@@ -16,11 +16,16 @@ let ReactNative;
16 let createReactNativeComponentClass;
17 let UIManager;
18 let TextInputState;
19 +let ReactNativePrivateInterface;
20
21 const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
22 "Warning: dispatchCommand was called with a ref that isn't a " +
23 'native component. Use React.forwardRef to get access to the underlying native component';
24
25 +const SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT =
26 + "Warning: sendAccessibilityEvent was called with a ref that isn't a " +
27 + 'native component. Use React.forwardRef to get access to the underlying native component';
28 +
29 describe('ReactNative', () => {
30 beforeEach(() => {
31 jest.resetModules();
@@ -28,6 +33,7 @@ describe('ReactNative', () => {
33 React = require('react');
34 StrictMode = React.StrictMode;
35 ReactNative = require('react-native-renderer');
36 + ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
37 UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
38 .UIManager;
39 createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
@@ -151,6 +157,65 @@ describe('ReactNative', () => {
157 expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
158 });
159
160 + it('should call sendAccessibilityEvent for native refs', () => {
161 + const View = createReactNativeComponentClass('RCTView', () => ({
162 + validAttributes: {foo: true},
163 + uiViewClassName: 'RCTView',
164 + }));
165 +
166 + ReactNativePrivateInterface.legacySendAccessibilityEvent.mockClear();
167 +
168 + let viewRef;
169 + ReactNative.render(
170 + <View
171 + ref={ref => {
172 + viewRef = ref;
173 + }}
174 + />,
175 + 11,
176 + );
177 +
178 + expect(
179 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
180 + ).not.toBeCalled();
181 + ReactNative.sendAccessibilityEvent(viewRef, 'focus');
182 + expect(
183 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
184 + ).toHaveBeenCalledTimes(1);
185 + expect(
186 + ReactNativePrivateInterface.legacySendAccessibilityEvent,
187 + ).toHaveBeenCalledWith(expect.any(Number), 'focus');
188 + });
189 +
190 + it('should warn and no-op if calling sendAccessibilityEvent on non native refs', () => {
191 + class BasicClass extends React.Component {
192 + render() {
193 + return <React.Fragment />;
194 + }
195 + }
196 +
197 + UIManager.sendAccessibilityEvent.mockReset();
198 +
199 + let viewRef;
200 + ReactNative.render(
201 + <BasicClass
202 + ref={ref => {
203 + viewRef = ref;
204 + }}
205 + />,
206 + 11,
207 + );
208 +
209 + expect(UIManager.sendAccessibilityEvent).not.toBeCalled();
210 + expect(() => {
211 + ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]);
212 + }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], {
213 + withoutStack: true,
214 + });
215 +
216 + expect(UIManager.sendAccessibilityEvent).not.toBeCalled();
217 + });
218 +
219 it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => {
220 const View = createReactNativeComponentClass('RCTView', () => ({
221 validAttributes: {foo: true},
scripts/flow/react-native-host-hooks.js
+5
@@ -110,6 +110,10 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
110 ) => void,
111 ...
112 };
113 + declare export var legacySendAccessibilityEvent: (
114 + reactTag: number,
115 + eventTypeName: string,
116 + ) => void;
117 declare export var BatchedBridge: {
118 registerCallableModule: (name: string, module: Object) => void,
119 ...
@@ -156,6 +160,7 @@ declare var nativeFabricUIManager: {
160 ) => void,
161
162 dispatchCommand: (node: Object, command: string, args: Array<any>) => void,
163 + sendAccessibilityEvent: (node: Object, eventTypeName: string) => void,
164
165 measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
166 measureInWindow: (