@samitouri / QOS-React-2 / commits / 43eb28339a

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 ...,