Add skipBubbling property to dispatch config (#23366)
Luna committed
Mar 14, 2022 at 10:59 UTC
43eb28339a0dc645c27b36f06200da63d57c292f
5 files changed
+143
-5
packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js
+39
-5
@@ -89,7 +89,12 @@ function getParent(inst) {
89
/**
90
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
91
*/
92
-export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
92
+export function traverseTwoPhase(
93
+ inst: Object,
94
+ fn: Function,
95
+ arg: Function,
96
+ skipBubbling: boolean,
97
+) {
98
const path = [];
99
while (inst) {
100
path.push(inst);
@@ -99,14 +104,24 @@ export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
104
for (i = path.length; i-- > 0; ) {
105
fn(path[i], 'captured', arg);
106
}
102
- for (i = 0; i < path.length; i++) {
103
- fn(path[i], 'bubbled', arg);
107
+ if (skipBubbling) {
108
+ // Dispatch on target only
109
+ fn(path[0], 'bubbled', arg);
110
+ } else {
111
+ for (i = 0; i < path.length; i++) {
112
+ fn(path[i], 'bubbled', arg);
113
+ }
114
}
115
}
116
117
function accumulateTwoPhaseDispatchesSingle(event) {
118
if (event && event.dispatchConfig.phasedRegistrationNames) {
109
- traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
119
+ traverseTwoPhase(
120
+ event._targetInst,
121
+ accumulateDirectionalDispatches,
122
+ event,
123
+ false,
124
+ );
125
}
126
}
127
@@ -114,6 +129,17 @@ function accumulateTwoPhaseDispatches(events) {
129
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
130
}
131
132
+function accumulateCapturePhaseDispatches(event) {
133
+ if (event && event.dispatchConfig.phasedRegistrationNames) {
134
+ traverseTwoPhase(
135
+ event._targetInst,
136
+ accumulateDirectionalDispatches,
137
+ event,
138
+ true,
139
+ );
140
+ }
141
+}
142
+
143
/**
144
* Accumulates without regard to direction, does not look for phased
145
* registration names. Same as `accumulateDirectDispatchesSingle` but without
@@ -178,7 +204,15 @@ const ReactNativeBridgeEventPlugin = {
204
nativeEventTarget,
205
);
206
if (bubbleDispatchConfig) {
181
- accumulateTwoPhaseDispatches(event);
207
+ const skipBubbling =
208
+ event != null &&
209
+ event.dispatchConfig.phasedRegistrationNames != null &&
210
+ event.dispatchConfig.phasedRegistrationNames.skipBubbling;
211
+ if (skipBubbling) {
212
+ accumulateCapturePhaseDispatches(event);
213
+ } else {
214
+ accumulateTwoPhaseDispatches(event);
215
+ }
216
} else if (directDispatchConfig) {
217
accumulateDirectDispatches(event);
218
} else {
packages/react-native-renderer/src/ReactNativeTypes.js
+1
@@ -73,6 +73,7 @@ export type ViewConfig = $ReadOnly<{
73
phasedRegistrationNames: $ReadOnly<{
74
captured: string,
75
bubbled: string,
76
+ skipBubble?: ?boolean,
77
}>,
78
}>,
79
...,
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+100
@@ -633,6 +633,106 @@ describe('ReactFabric', () => {
633
expect(touchStart2).toBeCalled();
634
});
635
636
+ describe('skipBubbling', () => {
637
+ it('should skip bubbling to ancestor if specified', () => {
638
+ const View = createReactNativeComponentClass('RCTView', () => ({
639
+ validAttributes: {},
640
+ uiViewClassName: 'RCTView',
641
+ bubblingEventTypes: {
642
+ topDefaultBubblingEvent: {
643
+ phasedRegistrationNames: {
644
+ captured: 'onDefaultBubblingEventCapture',
645
+ bubbled: 'onDefaultBubblingEvent',
646
+ },
647
+ },
648
+ topBubblingEvent: {
649
+ phasedRegistrationNames: {
650
+ captured: 'onBubblingEventCapture',
651
+ bubbled: 'onBubblingEvent',
652
+ skipBubbling: false,
653
+ },
654
+ },
655
+ topSkipBubblingEvent: {
656
+ phasedRegistrationNames: {
657
+ captured: 'onSkippedBubblingEventCapture',
658
+ bubbled: 'onSkippedBubblingEvent',
659
+ skipBubbling: true,
660
+ },
661
+ },
662
+ },
663
+ }));
664
+ const ancestorBubble = jest.fn();
665
+ const ancestorCapture = jest.fn();
666
+ const targetBubble = jest.fn();
667
+ const targetCapture = jest.fn();
668
+
669
+ const event = {};
670
+
671
+ act(() => {
672
+ ReactFabric.render(
673
+ <View
674
+ onSkippedBubblingEventCapture={ancestorCapture}
675
+ onDefaultBubblingEventCapture={ancestorCapture}
676
+ onBubblingEventCapture={ancestorCapture}
677
+ onSkippedBubblingEvent={ancestorBubble}
678
+ onDefaultBubblingEvent={ancestorBubble}
679
+ onBubblingEvent={ancestorBubble}>
680
+ <View
681
+ onSkippedBubblingEventCapture={targetCapture}
682
+ onDefaultBubblingEventCapture={targetCapture}
683
+ onBubblingEventCapture={targetCapture}
684
+ onSkippedBubblingEvent={targetBubble}
685
+ onDefaultBubblingEvent={targetBubble}
686
+ onBubblingEvent={targetBubble}
687
+ />
688
+ </View>,
689
+ 11,
690
+ );
691
+ });
692
+
693
+ expect(nativeFabricUIManager.createNode.mock.calls.length).toBe(2);
694
+ expect(nativeFabricUIManager.registerEventHandler.mock.calls.length).toBe(
695
+ 1,
696
+ );
697
+ const [
698
+ ,
699
+ ,
700
+ ,
701
+ ,
702
+ childInstance,
703
+ ] = nativeFabricUIManager.createNode.mock.calls[0];
704
+ const [
705
+ dispatchEvent,
706
+ ] = nativeFabricUIManager.registerEventHandler.mock.calls[0];
707
+
708
+ dispatchEvent(childInstance, 'topDefaultBubblingEvent', event);
709
+ expect(targetBubble).toHaveBeenCalledTimes(1);
710
+ expect(targetCapture).toHaveBeenCalledTimes(1);
711
+ expect(ancestorCapture).toHaveBeenCalledTimes(1);
712
+ expect(ancestorBubble).toHaveBeenCalledTimes(1);
713
+ ancestorBubble.mockReset();
714
+ ancestorCapture.mockReset();
715
+ targetBubble.mockReset();
716
+ targetCapture.mockReset();
717
+
718
+ dispatchEvent(childInstance, 'topBubblingEvent', event);
719
+ expect(targetBubble).toHaveBeenCalledTimes(1);
720
+ expect(targetCapture).toHaveBeenCalledTimes(1);
721
+ expect(ancestorCapture).toHaveBeenCalledTimes(1);
722
+ expect(ancestorBubble).toHaveBeenCalledTimes(1);
723
+ ancestorBubble.mockReset();
724
+ ancestorCapture.mockReset();
725
+ targetBubble.mockReset();
726
+ targetCapture.mockReset();
727
+
728
+ dispatchEvent(childInstance, 'topSkipBubblingEvent', event);
729
+ expect(targetBubble).toHaveBeenCalledTimes(1);
730
+ expect(targetCapture).toHaveBeenCalledTimes(1);
731
+ expect(ancestorCapture).toHaveBeenCalledTimes(1);
732
+ expect(ancestorBubble).not.toBeCalled();
733
+ });
734
+ });
735
+
736
it('dispatches event with target as instance', () => {
737
const View = createReactNativeComponentClass('RCTView', () => ({
738
validAttributes: {
packages/react-native-renderer/src/legacy-events/ReactSyntheticEventType.js
+2
@@ -16,6 +16,7 @@ export type DispatchConfig = {|
16
phasedRegistrationNames: {|
17
bubbled: null | string,
18
captured: null | string,
19
+ skipBubbling?: ?boolean,
20
|},
21
registrationName?: string,
22
|};
@@ -24,6 +25,7 @@ export type CustomDispatchConfig = {|
25
phasedRegistrationNames: {|
26
bubbled: null,
27
captured: null,
28
+ skipBubbling?: ?boolean,
29
|},
30
registrationName?: string,
31
customEvent: true,
scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js
+1
@@ -19,6 +19,7 @@ const customBubblingEventTypes: {
19
phasedRegistrationNames: $ReadOnly<{|
20
captured: string,
21
bubbled: string,
22
+ skipBubbling?: ?boolean,
23
|}>,
24
|}>,
25
...,