Modern Event System: revise ancestor logic (#18886)
Dominic Gannaway committed
May 11, 2020 at 22:09 UTC
e16703e6c7776ff39e30c448df075f48bdbe08ea
2 files changed
+107
-13
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+18
-13
@@ -417,13 +417,13 @@ export function dispatchEventForPluginEventSystem(
417
// sub-tree for that root and make that our ancestor instance.
418
let node = targetInst;
419
420
- while (true) {
420
+ mainLoop: while (true) {
421
if (node === null) {
422
return;
423
}
424
const nodeTag = node.tag;
425
if (nodeTag === HostRoot || nodeTag === HostPortal) {
426
- const container = node.stateNode.containerInfo;
426
+ let container = node.stateNode.containerInfo;
427
if (isMatchingRootContainer(container, targetContainerNode)) {
428
break;
429
}
@@ -449,18 +449,23 @@ export function dispatchEventForPluginEventSystem(
449
grandNode = grandNode.return;
450
}
451
}
452
- const parentSubtreeInst = getClosestInstanceFromNode(container);
453
- if (parentSubtreeInst === null) {
454
- return;
455
- }
456
- const parentTag = parentSubtreeInst.tag;
457
- // getClosestInstanceFromNode can return a HostRoot or SuspenseComponent.
458
- // So we need to ensure we only set the ancestor to a HostComponent or HostText.
459
- if (parentTag === HostComponent || parentTag === HostText) {
460
- ancestorInst = parentSubtreeInst;
452
+ // Now we need to find it's corresponding host fiber in the other
453
+ // tree. To do this we can use getClosestInstanceFromNode, but we
454
+ // need to validate that the fiber is a host instance, otherwise
455
+ // we need to traverse up through the DOM till we find the correct
456
+ // node that is from the other tree.
457
+ while (container !== null) {
458
+ const parentNode = getClosestInstanceFromNode(container);
459
+ if (parentNode === null) {
460
+ return;
461
+ }
462
+ const parentTag = parentNode.tag;
463
+ if (parentTag === HostComponent || parentTag === HostText) {
464
+ node = ancestorInst = parentNode;
465
+ continue mainLoop;
466
+ }
467
+ container = container.parentNode;
468
}
462
- node = parentSubtreeInst;
463
- continue;
469
}
470
node = node.return;
471
}
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+89
@@ -224,6 +224,95 @@ describe('DOMModernPluginEventSystem', () => {
224
expect(log[5]).toEqual(['bubble', buttonElement]);
225
});
226
227
+ it('handle propagation of click events between disjointed roots #2', () => {
228
+ const buttonRef = React.createRef();
229
+ const button2Ref = React.createRef();
230
+ const divRef = React.createRef();
231
+ const spanRef = React.createRef();
232
+ const log = [];
233
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
234
+ const onClickCapture = jest.fn(e =>
235
+ log.push(['capture', e.currentTarget]),
236
+ );
237
+
238
+ function Child() {
239
+ return (
240
+ <div
241
+ ref={divRef}
242
+ onClick={onClick}
243
+ onClickCapture={onClickCapture}>
244
+ Click me!
245
+ </div>
246
+ );
247
+ }
248
+
249
+ function Parent() {
250
+ return (
251
+ <button
252
+ ref={button2Ref}
253
+ onClick={onClick}
254
+ onClickCapture={onClickCapture}
255
+ />
256
+ );
257
+ }
258
+
259
+ function GrandParent() {
260
+ return (
261
+ <button
262
+ ref={buttonRef}
263
+ onClick={onClick}
264
+ onClickCapture={onClickCapture}>
265
+ <span ref={spanRef} />
266
+ </button>
267
+ );
268
+ }
269
+
270
+ // We make a wrapper with an inner container that we
271
+ // render to. So it looks like <div><span></span></div>
272
+ // We then render to all three:
273
+ // - container
274
+ // - parentContainer
275
+ // - childContainer
276
+
277
+ const parentContainer = document.createElement('div');
278
+ const childContainer = document.createElement('div');
279
+
280
+ ReactDOM.render(<GrandParent />, container);
281
+ ReactDOM.render(<Parent />, parentContainer);
282
+ ReactDOM.render(<Child />, childContainer);
283
+
284
+ parentContainer.appendChild(childContainer);
285
+ spanRef.current.appendChild(parentContainer);
286
+
287
+ // Inside <GrandParent />
288
+ const buttonElement = buttonRef.current;
289
+ dispatchClickEvent(buttonElement);
290
+ expect(onClick).toHaveBeenCalledTimes(1);
291
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
292
+ expect(log[0]).toEqual(['capture', buttonElement]);
293
+ expect(log[1]).toEqual(['bubble', buttonElement]);
294
+
295
+ // Inside <Child />
296
+ const divElement = divRef.current;
297
+ dispatchClickEvent(divElement);
298
+ expect(onClick).toHaveBeenCalledTimes(3);
299
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
300
+ expect(log[2]).toEqual(['capture', divElement]);
301
+ expect(log[3]).toEqual(['bubble', divElement]);
302
+ expect(log[4]).toEqual(['capture', buttonElement]);
303
+ expect(log[5]).toEqual(['bubble', buttonElement]);
304
+
305
+ // Inside <Parent />
306
+ const buttonElement2 = button2Ref.current;
307
+ dispatchClickEvent(buttonElement2);
308
+ expect(onClick).toHaveBeenCalledTimes(5);
309
+ expect(onClickCapture).toHaveBeenCalledTimes(5);
310
+ expect(log[6]).toEqual(['capture', buttonElement2]);
311
+ expect(log[7]).toEqual(['bubble', buttonElement2]);
312
+ expect(log[8]).toEqual(['capture', buttonElement]);
313
+ expect(log[9]).toEqual(['bubble', buttonElement]);
314
+ });
315
+
316
it('handle propagation of click events between disjointed comment roots', () => {
317
const buttonRef = React.createRef();
318
const divRef = React.createRef();