Skip to content

Commit

Permalink
Add skipBubbling property to dispatch config (#23366)
Browse files Browse the repository at this point in the history
  • Loading branch information
lunaleaps authored Mar 14, 2022
1 parent 2bf7c02 commit 43eb283
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 5 deletions.
44 changes: 39 additions & 5 deletions packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,12 @@ function getParent(inst) {
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
export function traverseTwoPhase(
inst: Object,
fn: Function,
arg: Function,
skipBubbling: boolean,
) {
const path = [];
while (inst) {
path.push(inst);
Expand All @@ -99,21 +104,42 @@ export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) {
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
if (skipBubbling) {
// Dispatch on target only
fn(path[0], 'bubbled', arg);
} else {
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
}

function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
traverseTwoPhase(
event._targetInst,
accumulateDirectionalDispatches,
event,
false,
);
}
}

function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

function accumulateCapturePhaseDispatches(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(
event._targetInst,
accumulateDirectionalDispatches,
event,
true,
);
}
}

/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
Expand Down Expand Up @@ -178,7 +204,15 @@ const ReactNativeBridgeEventPlugin = {
nativeEventTarget,
);
if (bubbleDispatchConfig) {
accumulateTwoPhaseDispatches(event);
const skipBubbling =
event != null &&
event.dispatchConfig.phasedRegistrationNames != null &&
event.dispatchConfig.phasedRegistrationNames.skipBubbling;
if (skipBubbling) {
accumulateCapturePhaseDispatches(event);
} else {
accumulateTwoPhaseDispatches(event);
}
} else if (directDispatchConfig) {
accumulateDirectDispatches(event);
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-renderer/src/ReactNativeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type ViewConfig = $ReadOnly<{
phasedRegistrationNames: $ReadOnly<{
captured: string,
bubbled: string,
skipBubble?: ?boolean,
}>,
}>,
...,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,106 @@ describe('ReactFabric', () => {
expect(touchStart2).toBeCalled();
});

describe('skipBubbling', () => {
it('should skip bubbling to ancestor if specified', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {},
uiViewClassName: 'RCTView',
bubblingEventTypes: {
topDefaultBubblingEvent: {
phasedRegistrationNames: {
captured: 'onDefaultBubblingEventCapture',
bubbled: 'onDefaultBubblingEvent',
},
},
topBubblingEvent: {
phasedRegistrationNames: {
captured: 'onBubblingEventCapture',
bubbled: 'onBubblingEvent',
skipBubbling: false,
},
},
topSkipBubblingEvent: {
phasedRegistrationNames: {
captured: 'onSkippedBubblingEventCapture',
bubbled: 'onSkippedBubblingEvent',
skipBubbling: true,
},
},
},
}));
const ancestorBubble = jest.fn();
const ancestorCapture = jest.fn();
const targetBubble = jest.fn();
const targetCapture = jest.fn();

const event = {};

act(() => {
ReactFabric.render(
<View
onSkippedBubblingEventCapture={ancestorCapture}
onDefaultBubblingEventCapture={ancestorCapture}
onBubblingEventCapture={ancestorCapture}
onSkippedBubblingEvent={ancestorBubble}
onDefaultBubblingEvent={ancestorBubble}
onBubblingEvent={ancestorBubble}>
<View
onSkippedBubblingEventCapture={targetCapture}
onDefaultBubblingEventCapture={targetCapture}
onBubblingEventCapture={targetCapture}
onSkippedBubblingEvent={targetBubble}
onDefaultBubblingEvent={targetBubble}
onBubblingEvent={targetBubble}
/>
</View>,
11,
);
});

expect(nativeFabricUIManager.createNode.mock.calls.length).toBe(2);
expect(nativeFabricUIManager.registerEventHandler.mock.calls.length).toBe(
1,
);
const [
,
,
,
,
childInstance,
] = nativeFabricUIManager.createNode.mock.calls[0];
const [
dispatchEvent,
] = nativeFabricUIManager.registerEventHandler.mock.calls[0];

dispatchEvent(childInstance, 'topDefaultBubblingEvent', event);
expect(targetBubble).toHaveBeenCalledTimes(1);
expect(targetCapture).toHaveBeenCalledTimes(1);
expect(ancestorCapture).toHaveBeenCalledTimes(1);
expect(ancestorBubble).toHaveBeenCalledTimes(1);
ancestorBubble.mockReset();
ancestorCapture.mockReset();
targetBubble.mockReset();
targetCapture.mockReset();

dispatchEvent(childInstance, 'topBubblingEvent', event);
expect(targetBubble).toHaveBeenCalledTimes(1);
expect(targetCapture).toHaveBeenCalledTimes(1);
expect(ancestorCapture).toHaveBeenCalledTimes(1);
expect(ancestorBubble).toHaveBeenCalledTimes(1);
ancestorBubble.mockReset();
ancestorCapture.mockReset();
targetBubble.mockReset();
targetCapture.mockReset();

dispatchEvent(childInstance, 'topSkipBubblingEvent', event);
expect(targetBubble).toHaveBeenCalledTimes(1);
expect(targetCapture).toHaveBeenCalledTimes(1);
expect(ancestorCapture).toHaveBeenCalledTimes(1);
expect(ancestorBubble).not.toBeCalled();
});
});

it('dispatches event with target as instance', () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type DispatchConfig = {|
phasedRegistrationNames: {|
bubbled: null | string,
captured: null | string,
skipBubbling?: ?boolean,
|},
registrationName?: string,
|};
Expand All @@ -24,6 +25,7 @@ export type CustomDispatchConfig = {|
phasedRegistrationNames: {|
bubbled: null,
captured: null,
skipBubbling?: ?boolean,
|},
registrationName?: string,
customEvent: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const customBubblingEventTypes: {
phasedRegistrationNames: $ReadOnly<{|
captured: string,
bubbled: string,
skipBubbling?: ?boolean,
|}>,
|}>,
...,
Expand Down

0 comments on commit 43eb283

Please sign in to comment.