diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index b5567d0356981..87fe4e8e2bf1f 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -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 { @@ -169,6 +172,27 @@ function dispatchCommand(handle: any, command: string, args: Array) { } } +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, containerTag: any, @@ -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. diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 762fb55e4f86f..fdf9ebf5ac597 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -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 { @@ -168,6 +171,27 @@ function dispatchCommand(handle: any, command: string, args: Array) { } } +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, containerTag: any, @@ -238,6 +262,7 @@ export { findHostInstance_DEPRECATED, findNodeHandle, dispatchCommand, + sendAccessibilityEvent, render, unmountComponentAtNode, unmountComponentAtNodeAndRemoveContainer, diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 2f9876384b835..8b4ab311bff79 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -149,7 +149,15 @@ export type ReactNativeType = { componentOrHandle: any, ): ?ElementRef>, findNodeHandle(componentOrHandle: any): ?number, - dispatchCommand(handle: any, command: string, args: Array): void, + dispatchCommand( + handle: ElementRef>, + command: string, + args: Array, + ): void, + sendAccessibilityEvent( + handle: ElementRef>, + eventType: string, + ): void, render( element: React$Element, containerTag: any, @@ -168,7 +176,15 @@ export type ReactFabricType = { componentOrHandle: any, ): ?ElementRef>, findNodeHandle(componentOrHandle: any): ?number, - dispatchCommand(handle: any, command: string, args: Array): void, + dispatchCommand( + handle: ElementRef>, + command: string, + args: Array, + ): void, + sendAccessibilityEvent( + handle: ElementRef>, + eventType: string, + ): void, render( element: React$Element, containerTag: any, @@ -190,7 +206,7 @@ export type ReactNativeEventTarget = { ... }; -export type ReactFaricEventTouch = { +export type ReactFabricEventTouch = { identifier: number, locationX: number, locationY: number, @@ -204,10 +220,10 @@ export type ReactFaricEventTouch = { ... }; -export type ReactFaricEvent = { - touches: Array, - changedTouches: Array, - targetTouches: Array, +export type ReactFabricEvent = { + touches: Array, + changedTouches: Array, + targetTouches: Array, target: number, ... }; diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js index 98b7e66ac661e..686dc72702de9 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js @@ -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) { diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 9d49212e0044e..3def12a904f63 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -38,4 +38,7 @@ module.exports = { get flattenStyle() { return require('./flattenStyle'); }, + get legacySendAccessibilityEvent() { + return require('./legacySendAccessibilityEvent'); + }, }; diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js index f5245f74fa9c9..245eecc4dc7ea 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js @@ -88,6 +88,7 @@ const RCTUIManager = { }); }), dispatchViewManagerCommand: jest.fn(), + sendAccessibilityEvent: jest.fn(), setJSResponder: jest.fn(), setChildren: jest.fn(function setChildren(parentTag, reactTags) { autoCreateRoot(parentTag); diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/legacySendAccessibilityEvent.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/legacySendAccessibilityEvent.js new file mode 100644 index 0000000000000..326128fd00573 --- /dev/null +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/legacySendAccessibilityEvent.js @@ -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(); diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 026c3b4d38d89..a935da5e4df63 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -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'), ); @@ -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( + { + 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 ; + } + } + + nativeFabricUIManager.sendAccessibilityEvent.mockReset(); + + let viewRef; + ReactFabric.render( + { + 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}, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index 50939f7170d31..c266dab6020c7 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -15,6 +15,7 @@ let ReactFabric; let ReactNative; let UIManager; let createReactNativeComponentClass; +let ReactNativePrivateInterface; describe('created with ReactFabric called with ReactNative', () => { beforeEach(() => { @@ -22,6 +23,7 @@ describe('created with ReactFabric called with ReactNative', () => { 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', () => @@ -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(, 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', () => { @@ -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(, 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(); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 894d4c8aa134f..9026bef793767 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -16,11 +16,16 @@ let ReactNative; let createReactNativeComponentClass; let UIManager; let TextInputState; +let ReactNativePrivateInterface; 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 = + "Warning: sendAccessibilityEvent was called with a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component'; + describe('ReactNative', () => { beforeEach(() => { jest.resetModules(); @@ -28,6 +33,7 @@ describe('ReactNative', () => { React = require('react'); StrictMode = React.StrictMode; ReactNative = require('react-native-renderer'); + ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'); UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .UIManager; createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') @@ -151,6 +157,65 @@ describe('ReactNative', () => { expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); }); + it('should call sendAccessibilityEvent for native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + ReactNativePrivateInterface.legacySendAccessibilityEvent.mockClear(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect( + ReactNativePrivateInterface.legacySendAccessibilityEvent, + ).not.toBeCalled(); + ReactNative.sendAccessibilityEvent(viewRef, 'focus'); + expect( + ReactNativePrivateInterface.legacySendAccessibilityEvent, + ).toHaveBeenCalledTimes(1); + expect( + ReactNativePrivateInterface.legacySendAccessibilityEvent, + ).toHaveBeenCalledWith(expect.any(Number), 'focus'); + }); + + it('should warn and no-op if calling sendAccessibilityEvent on non native refs', () => { + class BasicClass extends React.Component { + render() { + return ; + } + } + + UIManager.sendAccessibilityEvent.mockReset(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); + expect(() => { + ReactNative.sendAccessibilityEvent(viewRef, 'updateCommand', [10, 20]); + }).toErrorDev([SEND_ACCESSIBILITY_EVENT_REQUIRES_HOST_COMPONENT], { + withoutStack: true, + }); + + expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); + }); + it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index c42159e0fc22e..17cc6fe9c952e 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -110,6 +110,10 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' ) => void, ... }; + declare export var legacySendAccessibilityEvent: ( + reactTag: number, + eventTypeName: string, + ) => void; declare export var BatchedBridge: { registerCallableModule: (name: string, module: Object) => void, ... @@ -156,6 +160,7 @@ declare var nativeFabricUIManager: { ) => void, dispatchCommand: (node: Object, command: string, args: Array) => void, + sendAccessibilityEvent: (node: Object, eventTypeName: string) => void, measure: (node: Node, callback: MeasureOnSuccessCallback) => void, measureInWindow: (