Skip to content

Commit

Permalink
RN: Implement sendAccessibilityEvent in RN Renderer that proxies be…
Browse files Browse the repository at this point in the history
…tween 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
  • Loading branch information
JoshuaGross authored Jan 27, 2021
1 parent 9c32622 commit e316f78
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 11 deletions.
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

0 comments on commit e316f78

Please sign in to comment.