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: (