Skip to content

Commit e316f78

Browse files
authored
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
1 parent 9c32622 commit e316f78

11 files changed

+273
-11
lines changed

Diff for: packages/react-native-renderer/src/ReactFabric.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal
3030
import {setBatchingImplementation} from './legacy-events/ReactGenericBatching';
3131
import ReactVersion from 'shared/ReactVersion';
3232

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';
3538

3639
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
3740
import {
@@ -169,6 +172,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
169172
}
170173
}
171174

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+
172196
function render(
173197
element: React$Element<any>,
174198
containerTag: any,
@@ -224,6 +248,7 @@ export {
224248
findHostInstance_DEPRECATED,
225249
findNodeHandle,
226250
dispatchCommand,
251+
sendAccessibilityEvent,
227252
render,
228253
// Deprecated - this function is being renamed to stopSurface, use that instead.
229254
// TODO (T47576999): Delete this once it's no longer called from native code.

Diff for: packages/react-native-renderer/src/ReactNativeRenderer.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ import {
3232
batchedUpdates,
3333
} from './legacy-events/ReactGenericBatching';
3434
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';
3740

3841
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
3942
import {
@@ -168,6 +171,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
168171
}
169172
}
170173

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+
171195
function render(
172196
element: React$Element<any>,
173197
containerTag: any,
@@ -238,6 +262,7 @@ export {
238262
findHostInstance_DEPRECATED,
239263
findNodeHandle,
240264
dispatchCommand,
265+
sendAccessibilityEvent,
241266
render,
242267
unmountComponentAtNode,
243268
unmountComponentAtNodeAndRemoveContainer,

Diff for: packages/react-native-renderer/src/ReactNativeTypes.js

+23-7
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,15 @@ export type ReactNativeType = {
149149
componentOrHandle: any,
150150
): ?ElementRef<HostComponent<mixed>>,
151151
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,
153161
render(
154162
element: React$Element<any>,
155163
containerTag: any,
@@ -168,7 +176,15 @@ export type ReactFabricType = {
168176
componentOrHandle: any,
169177
): ?ElementRef<HostComponent<mixed>>,
170178
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,
172188
render(
173189
element: React$Element<any>,
174190
containerTag: any,
@@ -190,7 +206,7 @@ export type ReactNativeEventTarget = {
190206
...
191207
};
192208

193-
export type ReactFaricEventTouch = {
209+
export type ReactFabricEventTouch = {
194210
identifier: number,
195211
locationX: number,
196212
locationY: number,
@@ -204,10 +220,10 @@ export type ReactFaricEventTouch = {
204220
...
205221
};
206222

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>,
211227
target: number,
212228
...
213229
};

Diff for: packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ const RCTFabricUIManager = {
122122

123123
dispatchCommand: jest.fn(),
124124

125+
sendAccessibilityEvent: jest.fn(),
126+
125127
registerEventHandler: jest.fn(function registerEventHandler(callback) {}),
126128

127129
measure: jest.fn(function measure(node, callback) {

Diff for: packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js

+3
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ module.exports = {
3838
get flattenStyle() {
3939
return require('./flattenStyle');
4040
},
41+
get legacySendAccessibilityEvent() {
42+
return require('./legacySendAccessibilityEvent');
43+
},
4144
};

Diff for: packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const RCTUIManager = {
8888
});
8989
}),
9090
dispatchViewManagerCommand: jest.fn(),
91+
sendAccessibilityEvent: jest.fn(),
9192
setJSResponder: jest.fn(),
9293
setChildren: jest.fn(function setChildren(parentTag, reactTags) {
9394
autoCreateRoot(parentTag);
Original file line numberDiff line numberDiff line change
@@ -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();

Diff for: packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js

+62
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
2424
"Warning: dispatchCommand was called with a ref that isn't a " +
2525
'native component. Use React.forwardRef to get access to the underlying native component';
2626

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+
2731
jest.mock('shared/ReactFeatureFlags', () =>
2832
require('shared/forks/ReactFeatureFlags.native-oss'),
2933
);
@@ -289,6 +293,64 @@ describe('ReactFabric', () => {
289293
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
290294
});
291295

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+
292354
it('should call FabricUIManager.measure on ref.measure', () => {
293355
const View = createReactNativeComponentClass('RCTView', () => ({
294356
validAttributes: {foo: true},

Diff for: packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js

+48
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ let ReactFabric;
1515
let ReactNative;
1616
let UIManager;
1717
let createReactNativeComponentClass;
18+
let ReactNativePrivateInterface;
1819

1920
describe('created with ReactFabric called with ReactNative', () => {
2021
beforeEach(() => {
2122
jest.resetModules();
2223
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
2324
ReactNative = require('react-native-renderer');
2425
jest.resetModules();
26+
ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
2527
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
2628
.UIManager;
2729
jest.mock('shared/ReactFeatureFlags', () =>
@@ -92,6 +94,28 @@ describe('created with ReactFabric called with ReactNative', () => {
9294
).toHaveBeenCalledWith(expect.any(Object), 'myCommand', [10, 20]);
9395
expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
9496
});
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+
});
95119
});
96120

97121
describe('created with ReactNative called with ReactFabric', () => {
@@ -171,4 +195,28 @@ describe('created with ReactNative called with ReactFabric', () => {
171195

172196
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
173197
});
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+
});
174222
});

0 commit comments

Comments
 (0)