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
}