@samitouri / QOS-React / commits / 94c0244bab

Fix double-firing mouseenter (#19571)

* test: Simulate mouseover in browser * Fix duplicate onMouseEnter event when relatedTarget is a root * Test leave as well Co-authored-by: Sebastian Silbermann <silbermann.sebastian@gmail.com>

Dan Abramov committed Aug 10, 2020 at 15:08 UTC 94c0244bab7515c9b2c00b8e5312a9b6f31ef13a
2 files changed +62 -6
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+51
@@ -989,6 +989,57 @@ describe('ReactDOMFiber', () => {
989 }
990 });
991
992 + // Regression test for https://github.com/facebook/react/issues/19562
993 + it('does not fire mouseEnter twice when relatedTarget is the root node', () => {
994 + let ops = [];
995 + let target = null;
996 +
997 + function simulateMouseMove(from, to) {
998 + if (from) {
999 + from.dispatchEvent(
1000 + new MouseEvent('mouseout', {
1001 + bubbles: true,
1002 + cancelable: true,
1003 + relatedTarget: to,
1004 + }),
1005 + );
1006 + }
1007 + if (to) {
1008 + to.dispatchEvent(
1009 + new MouseEvent('mouseover', {
1010 + bubbles: true,
1011 + cancelable: true,
1012 + relatedTarget: from,
1013 + }),
1014 + );
1015 + }
1016 + }
1017 +
1018 + ReactDOM.render(
1019 + <div
1020 + ref={n => (target = n)}
1021 + onMouseEnter={() => ops.push('enter')}
1022 + onMouseLeave={() => ops.push('leave')}
1023 + />,
1024 + container,
1025 + );
1026 +
1027 + simulateMouseMove(null, container);
1028 + expect(ops).toEqual([]);
1029 +
1030 + ops = [];
1031 + simulateMouseMove(container, target);
1032 + expect(ops).toEqual(['enter']);
1033 +
1034 + ops = [];
1035 + simulateMouseMove(target, container);
1036 + expect(ops).toEqual(['leave']);
1037 +
1038 + ops = [];
1039 + simulateMouseMove(container, null);
1040 + expect(ops).toEqual([]);
1041 + });
1042 +
1043 it('should throw on bad createPortal argument', () => {
1044 expect(() => {
1045 ReactDOM.createPortal(<div>portal</div>, null);
packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js
+11 -6
@@ -22,6 +22,7 @@ import {
22 import {
23 getClosestInstanceFromNode,
24 getNodeFromInstance,
25 + isContainerMarkedAsRoot,
26 } from '../../client/ReactDOMComponentTree';
27 import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMPluginEventSystem';
28
@@ -57,15 +58,19 @@ function extractEvents(
58 domEventName === 'mouseout' || domEventName === 'pointerout';
59
60 if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
61 + // If this is an over event with a target, we might have already dispatched
62 + // the event in the out event of the other target. If this is replayed,
63 + // then it's because we couldn't dispatch against this target previously
64 + // so we have to do it now instead.
65 const related =
66 (nativeEvent: any).relatedTarget || (nativeEvent: any).fromElement;
67 if (related) {
63 - // Due to the fact we don't add listeners to the document with the
64 - // modern event system and instead attach listeners to roots, we
65 - // need to handle the over event case. To ensure this, we just need to
66 - // make sure the node that we're coming from is managed by React.
67 - const inst = getClosestInstanceFromNode(related);
68 - if (inst !== null) {
68 + // If the related node is managed by React, we can assume that we have
69 + // already dispatched the corresponding events during its mouseout.
70 + if (
71 + getClosestInstanceFromNode(related) ||
72 + isContainerMarkedAsRoot(related)
73 + ) {
74 return;
75 }
76 }