[Native] Add FeatureFlag to dispatch events with instance targets (#17323)
* [Native] Add FeatureFlag to dispatch events with instance targets * Prettier
Eli White committed
Nov 11, 2019 at 11:35 UTC
2c6ea0b3ffffd1a110845327262ecea59ee48dab
13 files changed
+380
-4
packages/react-native-renderer/src/ReactFabricEventEmitter.js
+18
-2
@@ -18,7 +18,10 @@ import {registrationNameModules} from 'legacy-events/EventPluginRegistry';
18
import {batchedUpdates} from 'legacy-events/ReactGenericBatching';
19
20
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
21
-import {enableFlareAPI} from 'shared/ReactFeatureFlags';
21
+import {
22
+ enableFlareAPI,
23
+ enableNativeTargetAsInstance,
24
+} from 'shared/ReactFeatureFlags';
25
import type {TopLevelType} from 'legacy-events/TopLevelEventTypes';
26
import {dispatchEventForResponderEventSystem} from './ReactFabricEventResponderSystem';
27
@@ -30,6 +33,7 @@ export function dispatchEvent(
33
nativeEvent: AnyNativeEvent,
34
) {
35
const targetFiber = (target: null | Fiber);
36
+
37
if (enableFlareAPI) {
38
// React Flare event system
39
dispatchEventForResponderEventSystem(
@@ -38,13 +42,25 @@ export function dispatchEvent(
42
(nativeEvent: any),
43
);
44
}
45
+
46
+ let eventTarget;
47
+ if (enableNativeTargetAsInstance) {
48
+ if (targetFiber == null) {
49
+ eventTarget = null;
50
+ } else {
51
+ eventTarget = targetFiber.stateNode.canonical;
52
+ }
53
+ } else {
54
+ eventTarget = nativeEvent.target;
55
+ }
56
+
57
batchedUpdates(function() {
58
// Heritage plugin event system
59
runExtractedPluginEventsInBatch(
60
topLevelType,
61
targetFiber,
62
nativeEvent,
47
- nativeEvent.target,
63
+ eventTarget,
64
PLUGIN_EVENT_SYSTEM,
65
);
66
});
packages/react-native-renderer/src/ReactNativeEventEmitter.js
+14
-1
@@ -15,6 +15,7 @@ import {
15
import {registrationNameModules} from 'legacy-events/EventPluginRegistry';
16
import {batchedUpdates} from 'legacy-events/ReactGenericBatching';
17
import warningWithoutStack from 'shared/warningWithoutStack';
18
+import {enableNativeTargetAsInstance} from 'shared/ReactFeatureFlags';
19
20
import {getInstanceFromNode} from './ReactNativeComponentTree';
21
@@ -98,12 +99,24 @@ function _receiveRootNodeIDEvent(
99
) {
100
const nativeEvent = nativeEventParam || EMPTY_NATIVE_EVENT;
101
const inst = getInstanceFromNode(rootNodeID);
102
+
103
+ let target;
104
+ if (enableNativeTargetAsInstance) {
105
+ if (inst == null) {
106
+ target = null;
107
+ } else {
108
+ target = inst.stateNode;
109
+ }
110
+ } else {
111
+ target = nativeEvent.target;
112
+ }
113
+
114
batchedUpdates(function() {
115
runExtractedPluginEventsInBatch(
116
topLevelType,
117
inst,
118
nativeEvent,
106
- nativeEvent.target,
119
+ target,
120
PLUGIN_EVENT_SYSTEM,
121
);
122
});
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+194
@@ -12,6 +12,7 @@
12
13
let React;
14
let ReactFabric;
15
+let ReactFeatureFlags;
16
let createReactClass;
17
let createReactNativeComponentClass;
18
let UIManager;
@@ -38,6 +39,7 @@ describe('ReactFabric', () => {
39
React = require('react');
40
StrictMode = React.StrictMode;
41
ReactFabric = require('react-native-renderer/fabric');
42
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
43
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
44
.UIManager;
45
createReactClass = require('create-react-class/factory')(
@@ -779,6 +781,198 @@ describe('ReactFabric', () => {
781
expect(touchStart2).toBeCalled();
782
});
783
784
+ it('dispatches event with target as reactTag', () => {
785
+ ReactFeatureFlags.enableNativeTargetAsInstance = false;
786
+
787
+ const View = createReactNativeComponentClass('RCTView', () => ({
788
+ validAttributes: {
789
+ id: true,
790
+ },
791
+ uiViewClassName: 'RCTView',
792
+ directEventTypes: {
793
+ topTouchStart: {
794
+ registrationName: 'onTouchStart',
795
+ },
796
+ topTouchEnd: {
797
+ registrationName: 'onTouchEnd',
798
+ },
799
+ },
800
+ }));
801
+
802
+ function getViewById(id) {
803
+ const [
804
+ reactTag,
805
+ ,
806
+ ,
807
+ ,
808
+ instanceHandle,
809
+ ] = nativeFabricUIManager.createNode.mock.calls.find(
810
+ args => args[3] && args[3].id === id,
811
+ );
812
+
813
+ return {reactTag, instanceHandle};
814
+ }
815
+
816
+ const ref1 = React.createRef();
817
+ const ref2 = React.createRef();
818
+
819
+ ReactFabric.render(
820
+ <View id="parent">
821
+ <View
822
+ ref={ref1}
823
+ id="one"
824
+ onResponderStart={event => {
825
+ expect(ref1.current).not.toBeNull();
826
+ expect(ReactFabric.findNodeHandle(ref1.current)).toEqual(
827
+ event.target,
828
+ );
829
+ }}
830
+ onStartShouldSetResponder={() => true}
831
+ />
832
+ <View
833
+ ref={ref2}
834
+ id="two"
835
+ onResponderStart={event => {
836
+ expect(ref2.current).not.toBeNull();
837
+ expect(ReactFabric.findNodeHandle(ref2.current)).toEqual(
838
+ event.target,
839
+ );
840
+ }}
841
+ onStartShouldSetResponder={() => true}
842
+ />
843
+ </View>,
844
+ 1,
845
+ );
846
+
847
+ let [
848
+ dispatchEvent,
849
+ ] = nativeFabricUIManager.registerEventHandler.mock.calls[0];
850
+
851
+ dispatchEvent(getViewById('one').instanceHandle, 'topTouchStart', {
852
+ target: getViewById('one').reactTag,
853
+ identifier: 17,
854
+ touches: [],
855
+ changedTouches: [],
856
+ });
857
+ dispatchEvent(getViewById('one').instanceHandle, 'topTouchEnd', {
858
+ target: getViewById('one').reactTag,
859
+ identifier: 17,
860
+ touches: [],
861
+ changedTouches: [],
862
+ });
863
+
864
+ dispatchEvent(getViewById('two').instanceHandle, 'topTouchStart', {
865
+ target: getViewById('two').reactTag,
866
+ identifier: 17,
867
+ touches: [],
868
+ changedTouches: [],
869
+ });
870
+
871
+ dispatchEvent(getViewById('two').instanceHandle, 'topTouchEnd', {
872
+ target: getViewById('two').reactTag,
873
+ identifier: 17,
874
+ touches: [],
875
+ changedTouches: [],
876
+ });
877
+
878
+ expect.assertions(4);
879
+ });
880
+
881
+ it('dispatches event with target as instance', () => {
882
+ ReactFeatureFlags.enableNativeTargetAsInstance = true;
883
+
884
+ const View = createReactNativeComponentClass('RCTView', () => ({
885
+ validAttributes: {
886
+ id: true,
887
+ },
888
+ uiViewClassName: 'RCTView',
889
+ directEventTypes: {
890
+ topTouchStart: {
891
+ registrationName: 'onTouchStart',
892
+ },
893
+ topTouchEnd: {
894
+ registrationName: 'onTouchEnd',
895
+ },
896
+ },
897
+ }));
898
+
899
+ function getViewById(id) {
900
+ const [
901
+ reactTag,
902
+ ,
903
+ ,
904
+ ,
905
+ instanceHandle,
906
+ ] = nativeFabricUIManager.createNode.mock.calls.find(
907
+ args => args[3] && args[3].id === id,
908
+ );
909
+
910
+ return {reactTag, instanceHandle};
911
+ }
912
+
913
+ const ref1 = React.createRef();
914
+ const ref2 = React.createRef();
915
+
916
+ ReactFabric.render(
917
+ <View id="parent">
918
+ <View
919
+ ref={ref1}
920
+ id="one"
921
+ onResponderStart={event => {
922
+ expect(ref1.current).not.toBeNull();
923
+ // Check for referential equality
924
+ expect(ref1.current).toBe(event.target);
925
+ }}
926
+ onStartShouldSetResponder={() => true}
927
+ />
928
+ <View
929
+ ref={ref2}
930
+ id="two"
931
+ onResponderStart={event => {
932
+ expect(ref2.current).not.toBeNull();
933
+ // Check for referential equality
934
+ expect(ref2.current).toBe(event.target);
935
+ }}
936
+ onStartShouldSetResponder={() => true}
937
+ />
938
+ </View>,
939
+ 1,
940
+ );
941
+
942
+ let [
943
+ dispatchEvent,
944
+ ] = nativeFabricUIManager.registerEventHandler.mock.calls[0];
945
+
946
+ dispatchEvent(getViewById('one').instanceHandle, 'topTouchStart', {
947
+ target: getViewById('one').reactTag,
948
+ identifier: 17,
949
+ touches: [],
950
+ changedTouches: [],
951
+ });
952
+ dispatchEvent(getViewById('one').instanceHandle, 'topTouchEnd', {
953
+ target: getViewById('one').reactTag,
954
+ identifier: 17,
955
+ touches: [],
956
+ changedTouches: [],
957
+ });
958
+
959
+ dispatchEvent(getViewById('two').instanceHandle, 'topTouchStart', {
960
+ target: getViewById('two').reactTag,
961
+ identifier: 17,
962
+ touches: [],
963
+ changedTouches: [],
964
+ });
965
+
966
+ dispatchEvent(getViewById('two').instanceHandle, 'topTouchEnd', {
967
+ target: getViewById('two').reactTag,
968
+ identifier: 17,
969
+ touches: [],
970
+ changedTouches: [],
971
+ });
972
+
973
+ expect.assertions(4);
974
+ });
975
+
976
it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', () => {
977
const View = createReactNativeComponentClass('RCTView', () => ({
978
validAttributes: {foo: true},
packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js
+140
@@ -14,6 +14,7 @@ let PropTypes;
14
let RCTEventEmitter;
15
let React;
16
let ReactNative;
17
+let ReactFeatureFlags;
18
let ResponderEventPlugin;
19
let UIManager;
20
let createReactNativeComponentClass;
@@ -68,6 +69,7 @@ beforeEach(() => {
69
.RCTEventEmitter;
70
React = require('react');
71
ReactNative = require('react-native-renderer');
72
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
73
ResponderEventPlugin = require('legacy-events/ResponderEventPlugin').default;
74
UIManager = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
75
.UIManager;
@@ -456,3 +458,141 @@ it('handles events without target', () => {
458
'two responder end',
459
]);
460
});
461
+
462
+it('dispatches event with target as reactTag', () => {
463
+ ReactFeatureFlags.enableNativeTargetAsInstance = false;
464
+ const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
465
+
466
+ const View = fakeRequireNativeComponent('View', {id: true});
467
+
468
+ function getViewById(id) {
469
+ return UIManager.createView.mock.calls.find(
470
+ args => args[3] && args[3].id === id,
471
+ )[0];
472
+ }
473
+
474
+ const ref1 = React.createRef();
475
+ const ref2 = React.createRef();
476
+
477
+ ReactNative.render(
478
+ <View id="parent">
479
+ <View
480
+ ref={ref1}
481
+ id="one"
482
+ onResponderStart={event => {
483
+ expect(ref1.current).not.toBeNull();
484
+ expect(ReactNative.findNodeHandle(ref1.current)).toEqual(
485
+ event.target,
486
+ );
487
+ }}
488
+ onStartShouldSetResponder={() => true}
489
+ />
490
+ <View
491
+ ref={ref2}
492
+ id="two"
493
+ onResponderStart={event => {
494
+ expect(ref2.current).not.toBeNull();
495
+ expect(ReactNative.findNodeHandle(ref2.current)).toEqual(
496
+ event.target,
497
+ );
498
+ }}
499
+ onStartShouldSetResponder={() => true}
500
+ />
501
+ </View>,
502
+ 1,
503
+ );
504
+
505
+ EventEmitter.receiveTouches(
506
+ 'topTouchStart',
507
+ [{target: getViewById('one'), identifier: 17}],
508
+ [0],
509
+ );
510
+
511
+ EventEmitter.receiveTouches(
512
+ 'topTouchEnd',
513
+ [{target: getViewById('one'), identifier: 17}],
514
+ [0],
515
+ );
516
+
517
+ EventEmitter.receiveTouches(
518
+ 'topTouchStart',
519
+ [{target: getViewById('two'), identifier: 18}],
520
+ [0],
521
+ );
522
+
523
+ EventEmitter.receiveTouches(
524
+ 'topTouchEnd',
525
+ [{target: getViewById('two'), identifier: 18}],
526
+ [0],
527
+ );
528
+
529
+ expect.assertions(4);
530
+});
531
+
532
+it('dispatches event with target as instance', () => {
533
+ ReactFeatureFlags.enableNativeTargetAsInstance = true;
534
+ const EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
535
+
536
+ const View = fakeRequireNativeComponent('View', {id: true});
537
+
538
+ function getViewById(id) {
539
+ return UIManager.createView.mock.calls.find(
540
+ args => args[3] && args[3].id === id,
541
+ )[0];
542
+ }
543
+
544
+ const ref1 = React.createRef();
545
+ const ref2 = React.createRef();
546
+
547
+ ReactNative.render(
548
+ <View id="parent">
549
+ <View
550
+ ref={ref1}
551
+ id="one"
552
+ onResponderStart={event => {
553
+ expect(ref1.current).not.toBeNull();
554
+ // Check for referential equality
555
+ expect(ref1.current).toBe(event.target);
556
+ }}
557
+ onStartShouldSetResponder={() => true}
558
+ />
559
+ <View
560
+ ref={ref2}
561
+ id="two"
562
+ onResponderStart={event => {
563
+ expect(ref2.current).not.toBeNull();
564
+ // Check for referential equality
565
+ expect(ref2.current).toBe(event.target);
566
+ }}
567
+ onStartShouldSetResponder={() => true}
568
+ />
569
+ </View>,
570
+ 1,
571
+ );
572
+
573
+ EventEmitter.receiveTouches(
574
+ 'topTouchStart',
575
+ [{target: getViewById('one'), identifier: 17}],
576
+ [0],
577
+ );
578
+
579
+ EventEmitter.receiveTouches(
580
+ 'topTouchEnd',
581
+ [{target: getViewById('one'), identifier: 17}],
582
+ [0],
583
+ );
584
+
585
+ EventEmitter.receiveTouches(
586
+ 'topTouchStart',
587
+ [{target: getViewById('two'), identifier: 18}],
588
+ [0],
589
+ );
590
+
591
+ EventEmitter.receiveTouches(
592
+ 'topTouchEnd',
593
+ [{target: getViewById('two'), identifier: 18}],
594
+ [0],
595
+ );
596
+
597
+ expect.assertions(4);
598
+});
packages/shared/ReactFeatureFlags.js
+3
@@ -89,3 +89,6 @@ export const disableLegacyContext = false;
89
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
90
91
export const enableTrustedTypesIntegration = false;
92
+
93
+// Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance
94
+export const enableNativeTargetAsInstance = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+3
-1
@@ -13,7 +13,9 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
13
import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb';
14
15
// Uncomment to re-export dynamic flags from the fbsource version.
16
-// export const {} = require('../shims/ReactFeatureFlags');
16
+export const {
17
+ enableNativeTargetAsInstance,
18
+} = require('../shims/ReactFeatureFlags');
19
20
// The rest of the flags are static for better dead code elimination.
21
export const enableUserTimingAPI = __DEV__;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
37
export const disableLegacyContext = false;
38
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39
export const enableTrustedTypesIntegration = false;
40
+export const enableNativeTargetAsInstance = false;
41
42
// Only used in www builds.
43
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
37
export const disableLegacyContext = false;
38
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39
export const enableTrustedTypesIntegration = false;
40
+export const enableNativeTargetAsInstance = false;
41
42
// Only used in www builds.
43
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -37,6 +37,7 @@ export const warnAboutStringRefs = false;
37
export const disableLegacyContext = false;
38
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
39
export const enableTrustedTypesIntegration = false;
40
+export const enableNativeTargetAsInstance = false;
41
42
// Only used in www builds.
43
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -35,6 +35,7 @@ export const warnAboutStringRefs = false;
35
export const disableLegacyContext = false;
36
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
37
export const enableTrustedTypesIntegration = false;
38
+export const enableNativeTargetAsInstance = false;
39
40
// Only used in www builds.
41
export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -85,6 +85,8 @@ export const enableSuspenseCallback = true;
85
86
export const flushSuspenseFallbacksInTests = true;
87
88
+export const enableNativeTargetAsInstance = false;
89
+
90
// Flow magic to verify the exports of this file match the original version.
91
// eslint-disable-next-line no-unused-vars
92
type Check<_X, Y: _X, X: Y = _X> = null;
scripts/flow/react-native-host-hooks.js
+1
@@ -181,4 +181,5 @@ declare module 'RTManager' {
181
// shims/ReactFeatureFlags is generated by the packaging script
182
declare module '../shims/ReactFeatureFlags' {
183
declare export var debugRenderPhaseSideEffects: boolean;
184
+ declare export var enableNativeTargetAsInstance: boolean;
185
}
scripts/rollup/shims/react-native/ReactFeatureFlags.js
+1
@@ -12,6 +12,7 @@
12
13
const ReactFeatureFlags = {
14
debugRenderPhaseSideEffects: false,
15
+ enableNativeTargetAsInstance: false,
16
};
17
18
module.exports = ReactFeatureFlags;