Skip to content

Commit 24f0216

Browse files
author
Luna Wei
committed
Add skipBubbling property to dispatch config
1 parent 5d08a24 commit 24f0216

File tree

5 files changed

+143
-5
lines changed

5 files changed

+143
-5
lines changed

packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ function getParent(inst) {
6666
/**
6767
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
6868
*/
69-
export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
69+
export function traverseTwoPhase(
70+
inst: Object,
71+
fn: Function,
72+
arg: Function,
73+
skipBubbling: boolean,
74+
) {
7075
const path = [];
7176
while (inst) {
7277
path.push(inst);
@@ -76,21 +81,42 @@ export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
7681
for (i = path.length; i-- > 0; ) {
7782
fn(path[i], 'captured', arg);
7883
}
79-
for (i = 0; i < path.length; i++) {
80-
fn(path[i], 'bubbled', arg);
84+
if (skipBubbling) {
85+
// Dispatch on target only
86+
fn(path[0], 'bubbled', arg);
87+
} else {
88+
for (i = 0; i < path.length; i++) {
89+
fn(path[i], 'bubbled', arg);
90+
}
8191
}
8292
}
8393

8494
function accumulateTwoPhaseDispatchesSingle(event) {
8595
if (event && event.dispatchConfig.phasedRegistrationNames) {
86-
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
96+
traverseTwoPhase(
97+
event._targetInst,
98+
accumulateDirectionalDispatches,
99+
event,
100+
false,
101+
);
87102
}
88103
}
89104

90105
function accumulateTwoPhaseDispatches(events) {
91106
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
92107
}
93108

109+
function accumulateCapturePhaseDispatches(event) {
110+
if (event && event.dispatchConfig.phasedRegistrationNames) {
111+
traverseTwoPhase(
112+
event._targetInst,
113+
accumulateDirectionalDispatches,
114+
event,
115+
true,
116+
);
117+
}
118+
}
119+
94120
/**
95121
* Accumulates without regard to direction, does not look for phased
96122
* registration names. Same as `accumulateDirectDispatchesSingle` but without
@@ -162,7 +188,15 @@ const ReactNativeBridgeEventPlugin = {
162188
nativeEventTarget,
163189
);
164190
if (bubbleDispatchConfig) {
165-
accumulateTwoPhaseDispatches(event);
191+
const skipBubbling =
192+
event != null &&
193+
event.dispatchConfig.phasedRegistrationNames != null &&
194+
event.dispatchConfig.phasedRegistrationNames.skipBubbling;
195+
if (skipBubbling) {
196+
accumulateCapturePhaseDispatches(event);
197+
} else {
198+
accumulateTwoPhaseDispatches(event);
199+
}
166200
} else if (directDispatchConfig) {
167201
accumulateDirectDispatches(event);
168202
} else {

packages/react-native-renderer/src/ReactNativeTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type ViewConfig = $ReadOnly<{
7373
phasedRegistrationNames: $ReadOnly<{
7474
captured: string,
7575
bubbled: string,
76+
skipBubble?: ?boolean,
7677
}>,
7778
}>,
7879
...,

packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,106 @@ describe('ReactFabric', () => {
633633
expect(touchStart2).toBeCalled();
634634
});
635635

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+
636736
it('dispatches event with target as instance', () => {
637737
const View = createReactNativeComponentClass('RCTView', () => ({
638738
validAttributes: {

packages/react-native-renderer/src/legacy-events/ReactSyntheticEventType.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type DispatchConfig = {|
1616
phasedRegistrationNames: {|
1717
bubbled: null | string,
1818
captured: null | string,
19+
skipBubbling?: ?boolean,
1920
|},
2021
registrationName?: string,
2122
|};
@@ -24,6 +25,7 @@ export type CustomDispatchConfig = {|
2425
phasedRegistrationNames: {|
2526
bubbled: null,
2627
captured: null,
28+
skipBubbling?: ?boolean,
2729
|},
2830
registrationName?: string,
2931
customEvent: true,

scripts/rollup/shims/react-native/ReactNativeViewConfigRegistry.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const customBubblingEventTypes: {
1919
phasedRegistrationNames: $ReadOnly<{|
2020
captured: string,
2121
bubbled: string,
22+
skipBubbling?: ?boolean,
2223
|}>,
2324
|}>,
2425
...,

0 commit comments

Comments
 (0)