Skip to content

Commit 18b8d98

Browse files
authored
Merge pull request #202 from airbnb/refactor-stopPropagation
Refactor `stopPropagation` to fix #198
2 parents 60a5cd4 + 13a2c02 commit 18b8d98

File tree

3 files changed

+115
-21
lines changed

3 files changed

+115
-21
lines changed

packages/core/src/events.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,30 @@ const processingEventBeforeDispatch = (e: any) => {
1313
export class GojiEvent {
1414
private instanceMap = new Map<number, ElementInstance>();
1515

16-
private stoppedPropagation = new Map<number, Map<string, number>>();
17-
1816
public registerEventHandler(id: number, instance: ElementInstance) {
1917
this.instanceMap.set(id, instance);
2018
}
2119

2220
public unregisterEventHandler(id: number) {
2321
this.instanceMap.delete(id);
24-
this.stoppedPropagation.delete(id);
2522
}
2623

2724
public triggerEvent(e: any) {
2825
processingEventBeforeDispatch(e);
29-
const { target, currentTarget, timeStamp } = e;
30-
26+
const { currentTarget, timeStamp } = e;
3127
const id = currentTarget.dataset.gojiId;
32-
const sourceId = target.dataset.gojiId;
33-
3428
const type = camelCase(`on-${(e.type || '').toLowerCase()}`);
29+
3530
const instance = this.instanceMap.get(id);
3631
if (!instance) {
3732
return;
3833
}
3934

40-
let stoppedPropagation = this.stoppedPropagation.get(sourceId);
41-
if (stoppedPropagation && stoppedPropagation.get(type) === timeStamp) {
42-
return;
43-
}
44-
4535
e.stopPropagation = () => {
46-
if (!stoppedPropagation) {
47-
stoppedPropagation = new Map<string, number>();
48-
this.stoppedPropagation.set(sourceId, stoppedPropagation);
49-
}
50-
stoppedPropagation.set(type, timeStamp);
36+
instance.stopPropagation(type, timeStamp ?? undefined);
5137
};
5238

53-
instance.triggerEvent(type, e);
39+
instance.triggerEvent(type, timeStamp ?? undefined, e);
5440
}
5541
}
5642

packages/core/src/reconciler/__tests__/instance.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,82 @@ describe('ElementInstance', () => {
166166
setItemsCallback([2, 1, 3]);
167167
expect(getTextList()).toEqual(['2', '1', '3']);
168168
});
169+
170+
test.each([[true], [false]])('stopPropagation = %s', shouldStopPropagation => {
171+
const onRootTap = jest.fn();
172+
const onLeafTap = jest.fn();
173+
const rootRef = createRef<PublicInstance>();
174+
const leafRef = createRef<PublicInstance>();
175+
const App = () => (
176+
<View onTap={onRootTap} ref={rootRef}>
177+
<View>
178+
<View>
179+
<View>
180+
<View>
181+
<View
182+
onTap={e => {
183+
if (shouldStopPropagation) {
184+
e.stopPropagation();
185+
}
186+
onLeafTap();
187+
}}
188+
ref={leafRef}
189+
>
190+
Click
191+
</View>
192+
</View>
193+
</View>
194+
</View>
195+
</View>
196+
</View>
197+
);
198+
const { getContainer } = render(<App />);
199+
const rootNode = (getContainer() as { meta: ElementNodeDevelopment }).meta;
200+
let leafNode: ElementNodeDevelopment;
201+
for (
202+
leafNode = rootNode;
203+
leafNode.children?.length;
204+
leafNode = leafNode.children[0] as ElementNodeDevelopment
205+
) {
206+
// do nothing
207+
}
208+
209+
const timeStamp = Date.now();
210+
// trigger event on leaf
211+
gojiEvents.triggerEvent({
212+
type: 'tap',
213+
timeStamp,
214+
currentTarget: {
215+
dataset: {
216+
gojiId: leafRef.current!.unsafe_gojiId,
217+
},
218+
},
219+
target: {
220+
dataset: {
221+
gojiId: leafRef.current!.unsafe_gojiId,
222+
},
223+
},
224+
});
225+
act(() => {});
226+
expect(onLeafTap).toBeCalledTimes(1);
227+
expect(onRootTap).toBeCalledTimes(0);
228+
// trigger event on root
229+
gojiEvents.triggerEvent({
230+
type: 'tap',
231+
timeStamp,
232+
currentTarget: {
233+
dataset: {
234+
gojiId: rootRef.current!.unsafe_gojiId,
235+
},
236+
},
237+
target: {
238+
dataset: {
239+
gojiId: leafRef.current!.unsafe_gojiId,
240+
},
241+
},
242+
});
243+
act(() => {});
244+
expect(onLeafTap).toBeCalledTimes(1);
245+
expect(onRootTap).toBeCalledTimes(shouldStopPropagation ? 0 : 1);
246+
});
169247
});

packages/core/src/reconciler/instance.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export class ElementInstance extends BaseInstance {
102102

103103
public subtreeDepth?: number;
104104

105+
private stoppedPropagation: Map<string, number> = new Map();
106+
105107
public getSubtreeId(): number | undefined {
106108
const subtreeMaxDepth = useSubtree ? subtreeMaxDepthFromConfig : Infinity;
107109
// wrapped component should return its wrapper as subtree id
@@ -224,13 +226,25 @@ export class ElementInstance extends BaseInstance {
224226
}
225227
}
226228

227-
public triggerEvent(propKey: string, data: any) {
228-
const listener = this.props[propKey];
229+
public triggerEvent(type: string, timeStamp: number | undefined, data: any) {
230+
if (timeStamp === undefined) {
231+
if (process.env.NODE_ENV !== 'production') {
232+
console.warn(
233+
'`triggerEvent` should be called with a `timeStamp`, otherwise it will trigger all events. This might be an internal error in GojiJS',
234+
);
235+
}
236+
return;
237+
}
238+
// prevent triggering if the event has been stopped by `stopPropagation`
239+
if (this.stoppedPropagation.get(type) === timeStamp) {
240+
return;
241+
}
242+
const listener = this.props[type];
229243
if (listener) {
230244
if (typeof listener !== 'function') {
231245
if (process.env.NODE_ENV !== 'production') {
232246
console.warn(
233-
`Expected \`${propKey}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
247+
`Expected \`${type}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
234248
);
235249
}
236250
return;
@@ -240,6 +254,22 @@ export class ElementInstance extends BaseInstance {
240254
}
241255
}
242256

257+
public stopPropagation(type: string, timeStamp: number | undefined) {
258+
if (timeStamp === undefined) {
259+
if (process.env.NODE_ENV !== 'production') {
260+
console.warn(
261+
'`stopPropagation` should be called with a `timeStamp`, otherwise it will stop all events. This might be an internal error in GojiJS.',
262+
);
263+
}
264+
return;
265+
}
266+
// traverse all ancestors and mark as stopped manually instead of using `e.target.dataset.gojiId` because of this bug:
267+
// https://github.com/airbnb/goji-js/issues/198
268+
for (let cursor: ElementInstance | undefined = this; cursor; cursor = cursor.parent) {
269+
cursor.stoppedPropagation.set(type, timeStamp);
270+
}
271+
}
272+
243273
public updateProps(newProps: InstanceProps) {
244274
this.previous = this.pureProps();
245275
this.props = removeChildrenFromProps(newProps);

0 commit comments

Comments
 (0)