[Native] Add FeatureFlag to dispatch events with instance targets (#17323)

* [Native] Add FeatureFlag to dispatch events with instance targets

* Prettier
This commit is contained in:
Eli White
2019-11-11 11:35:29 -08:00
committed by GitHub
parent 01bce8c248
commit 2c6ea0b3ff
13 changed files with 380 additions and 4 deletions

View File

@@ -18,7 +18,10 @@ import {registrationNameModules} from 'legacy-events/EventPluginRegistry';
import {batchedUpdates} from 'legacy-events/ReactGenericBatching';
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import {
enableFlareAPI,
enableNativeTargetAsInstance,
} from 'shared/ReactFeatureFlags';
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
import {dispatchEventForResponderEventSystem} from './ReactFabricEventResponderSystem';
@@ -30,6 +33,7 @@ export function dispatchEvent(
nativeEvent: AnyNativeEvent,
) {
const targetFiber = (target: null | Fiber);
if (enableFlareAPI) {
// React Flare event system
dispatchEventForResponderEventSystem(
@@ -38,13 +42,25 @@ export function dispatchEvent(
(nativeEvent: any),
);
}
let eventTarget;
if (enableNativeTargetAsInstance) {
if (targetFiber == null) {
eventTarget = null;
} else {
eventTarget = targetFiber.stateNode.canonical;
}
} else {
eventTarget = nativeEvent.target;
}
batchedUpdates(function() {
// Heritage plugin event system
runExtractedPluginEventsInBatch(
topLevelType,
targetFiber,
nativeEvent,
nativeEvent.target,
eventTarget,
PLUGIN_EVENT_SYSTEM,
);
});

View File

@@ -15,6 +15,7 @@ import {
import {registrationNameModules} from 'legacy-events/EventPluginRegistry';
import {batchedUpdates} from 'legacy-events/ReactGenericBatching';
import warningWithoutStack from 'shared/warningWithoutStack';
import {enableNativeTargetAsInstance} from 'shared/ReactFeatureFlags';
import {getInstanceFromNode} from './ReactNativeComponentTree';
@@ -98,12 +99,24 @@ function _receiveRootNodeIDEvent(
) {
const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
const inst = getInstanceFromNode(rootNodeID);
let target;
if (enableNativeTargetAsInstance) {
if (inst == null) {
target = null;
} else {
target = inst.stateNode;
}
} else {
target = nativeEvent.target;
}
batchedUpdates(function() {
runExtractedPluginEventsInBatch(
topLevelType,
inst,
nativeEvent,
nativeEvent.target,
target,
PLUGIN_EVENT_SYSTEM,
);
});

View File

@@ -12,6 +12,7 @@
let React;
let ReactFabric;
let ReactFeatureFlags;
let createReactClass;
let createReactNativeComponentClass;
let UIManager;
@@ -38,6 +39,7 @@ describe('ReactFabric', () => {
React = require('react');
StrictMode = React.StrictMode;
ReactFabric = require('react-native-renderer/fabric');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.UIManager;
createReactClass = require('create-react-class/factory')(
@@ -779,6 +781,198 @@ describe('ReactFabric', () => {
expect(touchStart2).toBeCalled();
});
it('dispatches event with target as reactTag', () => {
ReactFeatureFlags.enableNativeTargetAsInstance = false;
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {
id: true,
},
uiViewClassName: 'RCTView',
directEventTypes: {
topTouchStart: {
registrationName: 'onTouchStart',
},
topTouchEnd: {
registrationName: 'onTouchEnd',
},
},
}));
function getViewById(id) {
const [
reactTag,
,
,
,
instanceHandle,
] = nativeFabricUIManager.createNode.mock.calls.find(
args => args[3] && args[3].id === id,
);
return {reactTag, instanceHandle};
}
const ref1 = React.createRef();
const ref2 = React.createRef();
ReactFabric.render(
<View id="parent">
<View
ref={ref1}
id="one"
onResponderStart={event => {
expect(ref1.current).not.toBeNull();
expect(ReactFabric.findNodeHandle(ref1.current)).toEqual(
event.target,
);
}}
onStartShouldSetResponder={() => true}
/>
<View
ref={ref2}
id="two"
onResponderStart={event => {
expect(ref2.current).not.toBeNull();
expect(ReactFabric.findNodeHandle(ref2.current)).toEqual(
event.target,
);
}}
onStartShouldSetResponder={() => true}
/>
</View>,
1,
);
let [
dispatchEvent,
] = nativeFabricUIManager.registerEventHandler.mock.calls[0];
dispatchEvent(getViewById('one').instanceHandle, 'topTouchStart', {
target: getViewById('one').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('one').instanceHandle, 'topTouchEnd', {
target: getViewById('one').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('two').instanceHandle, 'topTouchStart', {
target: getViewById('two').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('two').instanceHandle, 'topTouchEnd', {
target: getViewById('two').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
expect.assertions(4);
});
it('dispatches event with target as instance', () => {
ReactFeatureFlags.enableNativeTargetAsInstance = true;
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {
id: true,
},
uiViewClassName: 'RCTView',
directEventTypes: {
topTouchStart: {
registrationName: 'onTouchStart',
},
topTouchEnd: {
registrationName: 'onTouchEnd',
},
},
}));
function getViewById(id) {
const [
reactTag,
,
,
,
instanceHandle,
] = nativeFabricUIManager.createNode.mock.calls.find(
args => args[3] && args[3].id === id,
);
return {reactTag, instanceHandle};
}
const ref1 = React.createRef();
const ref2 = React.createRef();
ReactFabric.render(
<View id="parent">
<View
ref={ref1}
id="one"
onResponderStart={event => {
expect(ref1.current).not.toBeNull();
// Check for referential equality
expect(ref1.current).toBe(event.target);
}}
onStartShouldSetResponder={() => true}
/>
<View
ref={ref2}
id="two"
onResponderStart={event => {
expect(ref2.current).not.toBeNull();
// Check for referential equality
expect(ref2.current).toBe(event.target);
}}
onStartShouldSetResponder={() => true}
/>
</View>,
1,
);
let [
dispatchEvent,
] = nativeFabricUIManager.registerEventHandler.mock.calls[0];
dispatchEvent(getViewById('one').instanceHandle, 'topTouchStart', {
target: getViewById('one').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('one').instanceHandle, 'topTouchEnd', {
target: getViewById('one').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('two').instanceHandle, 'topTouchStart', {
target: getViewById('two').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('two').instanceHandle, 'topTouchEnd', {
target: getViewById('two').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
expect.assertions(4);
});
it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {foo: true},

View File

@@ -14,6 +14,7 @@ let PropTypes;
let RCTEventEmitter;
let React;
let ReactNative;
let ReactFeatureFlags;
let ResponderEventPlugin;
let UIManager;
let createReactNativeComponentClass;
@@ -68,6 +69,7 @@ beforeEach(() => {
.RCTEventEmitter;
React = require('react');
ReactNative = require('react-native-renderer');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ResponderEventPlugin = require('legacy-events/ResponderEventPlugin').default;
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.UIManager;
@@ -456,3 +458,141 @@ it('handles events without target', () => {
'two responder end',
]);
});
it('dispatches event with target as reactTag', () => {
ReactFeatureFlags.enableNativeTargetAsInstance = false;
const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
const View = fakeRequireNativeComponent('View', {id: true});
function getViewById(id) {
return UIManager.createView.mock.calls.find(
args => args[3] && args[3].id === id,
)[0];
}
const ref1 = React.createRef();
const ref2 = React.createRef();
ReactNative.render(
<View id="parent">
<View
ref={ref1}
id="one"
onResponderStart={event => {
expect(ref1.current).not.toBeNull();
expect(ReactNative.findNodeHandle(ref1.current)).toEqual(
event.target,
);
}}
onStartShouldSetResponder={() => true}
/>
<View
ref={ref2}
id="two"
onResponderStart={event => {
expect(ref2.current).not.toBeNull();
expect(ReactNative.findNodeHandle(ref2.current)).toEqual(
event.target,
);
}}
onStartShouldSetResponder={() => true}
/>
</View>,
1,
);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('one'), identifier: 17}],
[0],
);
EventEmitter.receiveTouches(
'topTouchEnd',
[{target: getViewById('one'), identifier: 17}],
[0],
);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('two'), identifier: 18}],
[0],
);
EventEmitter.receiveTouches(
'topTouchEnd',
[{target: getViewById('two'), identifier: 18}],
[0],
);
expect.assertions(4);
});
it('dispatches event with target as instance', () => {
ReactFeatureFlags.enableNativeTargetAsInstance = true;
const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
const View = fakeRequireNativeComponent('View', {id: true});
function getViewById(id) {
return UIManager.createView.mock.calls.find(
args => args[3] && args[3].id === id,
)[0];
}
const ref1 = React.createRef();
const ref2 = React.createRef();
ReactNative.render(
<View id="parent">
<View
ref={ref1}
id="one"
onResponderStart={event => {
expect(ref1.current).not.toBeNull();
// Check for referential equality
expect(ref1.current).toBe(event.target);
}}
onStartShouldSetResponder={() => true}
/>
<View
ref={ref2}
id="two"
onResponderStart={event => {
expect(ref2.current).not.toBeNull();
// Check for referential equality
expect(ref2.current).toBe(event.target);
}}
onStartShouldSetResponder={() => true}
/>
</View>,
1,
);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('one'), identifier: 17}],
[0],
);
EventEmitter.receiveTouches(
'topTouchEnd',
[{target: getViewById('one'), identifier: 17}],
[0],
);
EventEmitter.receiveTouches(
'topTouchStart',
[{target: getViewById('two'), identifier: 18}],
[0],
);
EventEmitter.receiveTouches(
'topTouchEnd',
[{target: getViewById('two'), identifier: 18}],
[0],
);
expect.assertions(4);
});

View File

@@ -89,3 +89,6 @@ export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
// Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance
export const enableNativeTargetAsInstance = false;

View File

@@ -13,7 +13,9 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb';
// Uncomment to re-export dynamic flags from the fbsource version.
// export const {} = require('../shims/ReactFeatureFlags');
export const {
enableNativeTargetAsInstance,
} = require('../shims/ReactFeatureFlags');
// The rest of the flags are static for better dead code elimination.
export const enableUserTimingAPI = __DEV__;

View File

@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
export const enableNativeTargetAsInstance = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
export const enableNativeTargetAsInstance = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
export const enableNativeTargetAsInstance = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -35,6 +35,7 @@ export const warnAboutStringRefs = false;
export const disableLegacyContext = false;
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
export const enableTrustedTypesIntegration = false;
export const enableNativeTargetAsInstance = false;
// Only used in www builds.
export function addUserTimingListener() {

View File

@@ -85,6 +85,8 @@ export const enableSuspenseCallback = true;
export const flushSuspenseFallbacksInTests = true;
export const enableNativeTargetAsInstance = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;

View File

@@ -181,4 +181,5 @@ declare module 'RTManager' {
// shims/ReactFeatureFlags is generated by the packaging script
declare module '../shims/ReactFeatureFlags' {
declare export var debugRenderPhaseSideEffects: boolean;
declare export var enableNativeTargetAsInstance: boolean;
}

View File

@@ -12,6 +12,7 @@
const ReactFeatureFlags = {
debugRenderPhaseSideEffects: false,
enableNativeTargetAsInstance: false,
};
module.exports = ReactFeatureFlags;