Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

29 changes: 27 additions & 2 deletions packages/react-native-renderer/src/ReactFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal
import {setBatchingImplementation} from './legacy-events/ReactGenericBatching';
import ReactVersion from 'shared/ReactVersion';

// Module provided by RN:
import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
// Modules provided by RN:
import {
UIManager,
legacySendAccessibilityEvent,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
import {
Expand Down Expand Up @@ -169,6 +172,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
}
}

function sendAccessibilityEvent(handle: any, eventType: string) {
if (handle._nativeTag == null) {
if (__DEV__) {
console.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 {
legacySendAccessibilityEvent(handle._nativeTag, eventType);
}
}

function render(
element: React$Element<any>,
containerTag: any,
Expand Down Expand Up @@ -224,6 +248,7 @@ export {
findHostInstance_DEPRECATED,
findNodeHandle,
dispatchCommand,
sendAccessibilityEvent,
render,
// Deprecated - this function is being renamed to stopSurface, use that instead.
// TODO (T47576999): Delete this once it's no longer called from native code.
Expand Down
29 changes: 27 additions & 2 deletions packages/react-native-renderer/src/ReactNativeRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ import {
batchedUpdates,
} from './legacy-events/ReactGenericBatching';
import ReactVersion from 'shared/ReactVersion';
// Module provided by RN:
import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
// Modules provided by RN:
import {
UIManager,
legacySendAccessibilityEvent,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
import {
Expand Down Expand Up @@ -168,6 +171,27 @@ function dispatchCommand(handle: any, command: string, args: Array<any>) {
}
}

function sendAccessibilityEvent(handle: any, eventType: string) {
if (handle._nativeTag == null) {
if (__DEV__) {
console.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 {
legacySendAccessibilityEvent(handle._nativeTag, eventType);
}
}

function render(
element: React$Element<any>,
containerTag: any,
Expand Down Expand Up @@ -238,6 +262,7 @@ export {
findHostInstance_DEPRECATED,
findNodeHandle,
dispatchCommand,
sendAccessibilityEvent,
render,
unmountComponentAtNode,
unmountComponentAtNodeAndRemoveContainer,
Expand Down
30 changes: 23 additions & 7 deletions packages/react-native-renderer/src/ReactNativeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,15 @@ export type ReactNativeType = {
componentOrHandle: any,
): ?ElementRef<HostComponent<mixed>>,
findNodeHandle(componentOrHandle: any): ?number,
dispatchCommand(handle: any, command: string, args: Array<any>): void,
dispatchCommand(
handle: ElementRef<HostComponent<mixed>>,
command: string,
args: Array<any>,
): void,
sendAccessibilityEvent(
handle: ElementRef<HostComponent<mixed>>,
eventType: string,
): void,
render(
element: React$Element<any>,
containerTag: any,
Expand All @@ -168,7 +176,15 @@ export type ReactFabricType = {
componentOrHandle: any,
): ?ElementRef<HostComponent<mixed>>,
findNodeHandle(componentOrHandle: any): ?number,
dispatchCommand(handle: any, command: string, args: Array<any>): void,
dispatchCommand(
handle: ElementRef<HostComponent<mixed>>,
command: string,
args: Array<any>,
): void,
sendAccessibilityEvent(
handle: ElementRef<HostComponent<mixed>>,
eventType: string,
): void,
render(
element: React$Element<any>,
containerTag: any,
Expand All @@ -190,7 +206,7 @@ export type ReactNativeEventTarget = {
...
};

export type ReactFaricEventTouch = {
export type ReactFabricEventTouch = {
identifier: number,
locationX: number,
locationY: number,
Expand All @@ -204,10 +220,10 @@ export type ReactFaricEventTouch = {
...
};

export type ReactFaricEvent = {
touches: Array<ReactFaricEventTouch>,
changedTouches: Array<ReactFaricEventTouch>,
targetTouches: Array<ReactFaricEventTouch>,
export type ReactFabricEvent = {
touches: Array<ReactFabricEventTouch>,
changedTouches: Array<ReactFabricEventTouch>,
targetTouches: Array<ReactFabricEventTouch>,
target: number,
...
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ const RCTFabricUIManager = {

dispatchCommand: jest.fn(),

sendAccessibilityEvent: jest.fn(),

registerEventHandler: jest.fn(function registerEventHandler(callback) {}),

measure: jest.fn(function measure(node, callback) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ module.exports = {
get flattenStyle() {
return require('./flattenStyle');
},
get legacySendAccessibilityEvent() {
return require('./legacySendAccessibilityEvent');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const RCTUIManager = {
});
}),
dispatchViewManagerCommand: jest.fn(),
sendAccessibilityEvent: jest.fn(),
setJSResponder: jest.fn(),
setChildren: jest.fn(function setChildren(parentTag, reactTags) {
autoCreateRoot(parentTag);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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.
*/

'use strict';

module.exports = jest.fn();
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
"Warning: dispatchCommand was called with a ref that isn't a " +
'native component. Use React.forwardRef to get access to the underlying native component';

const SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT =
"sendAccessibilityEvent was called with a ref that isn't a " +
'native component. Use React.forwardRef to get access to the underlying native component';

jest.mock('shared/ReactFeatureFlags', () =>
require('shared/forks/ReactFeatureFlags.native-oss'),
);
Expand Down Expand Up @@ -289,6 +293,64 @@ describe('ReactFabric', () => {
expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
});

it('should call sendAccessibilityEvent for native refs', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {foo: true},
uiViewClassName: 'RCTView',
}));

nativeFabricUIManager.sendAccessibilityEvent.mockClear();

let viewRef;
ReactFabric.render(
<View
ref={ref => {
viewRef = ref;
}}
/>,
11,
);

expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
ReactFabric.sendAccessibilityEvent(viewRef, 'focus');
expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledTimes(
1,
);
expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledWith(
expect.any(Object),
'focus',
);
});

it('should warn and no-op if calling sendAccessibilityEvent on non native refs', () => {
class BasicClass extends React.Component {
render() {
return <React.Fragment />;
}
}

nativeFabricUIManager.sendAccessibilityEvent.mockReset();

let viewRef;
ReactFabric.render(
<BasicClass
ref={ref => {
viewRef = ref;
}}
/>,
11,
);

expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
expect(() => {
ReactFabric.sendAccessibilityEvent(viewRef, 'eventTypeName');
}).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], {
withoutStack: true,
});

expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
});

it('should call FabricUIManager.measure on ref.measure', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {foo: true},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ let ReactFabric;
let ReactNative;
let UIManager;
let createReactNativeComponentClass;
let ReactNativePrivateInterface;

describe('created with ReactFabric called with ReactNative', () => {
beforeEach(() => {
jest.resetModules();
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
ReactNative = require('react-native-renderer');
jest.resetModules();
ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.UIManager;
jest.mock('shared/ReactFeatureFlags', () =>
Expand Down Expand Up @@ -92,6 +94,28 @@ describe('created with ReactFabric called with ReactNative', () => {
).toHaveBeenCalledWith(expect.any(Object), 'myCommand', [10, 20]);
expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
});

it('dispatches sendAccessibilityEvent on Fabric nodes with the RN renderer', () => {
nativeFabricUIManager.sendAccessibilityEvent.mockClear();
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
uiViewClassName: 'RCTView',
}));

const ref = React.createRef();

ReactFabric.render(<View title="bar" ref={ref} />, 11);
expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
ReactNative.sendAccessibilityEvent(ref.current, 'focus');
expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledTimes(
1,
);
expect(nativeFabricUIManager.sendAccessibilityEvent).toHaveBeenCalledWith(
expect.any(Object),
'focus',
);
expect(UIManager.sendAccessibilityEvent).not.toBeCalled();
});
});

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

expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
});

it('dispatches sendAccessibilityEvent on Paper nodes with the Fabric renderer', () => {
ReactNativePrivateInterface.legacySendAccessibilityEvent.mockReset();
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {title: true},
uiViewClassName: 'RCTView',
}));

const ref = React.createRef();

ReactNative.render(<View title="bar" ref={ref} />, 11);
expect(
ReactNativePrivateInterface.legacySendAccessibilityEvent,
).not.toBeCalled();
ReactFabric.sendAccessibilityEvent(ref.current, 'focus');
expect(
ReactNativePrivateInterface.legacySendAccessibilityEvent,
).toHaveBeenCalledTimes(1);
expect(
ReactNativePrivateInterface.legacySendAccessibilityEvent,
).toHaveBeenCalledWith(expect.any(Number), 'focus');

expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled();
});
});
Loading