Skip to content

Commit 94c0244

Browse files
gaearoneps1lon
andauthored
Fix double-firing mouseenter (facebook#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>
1 parent aa99b0b commit 94c0244

File tree

2 files changed

+62
-6
lines changed

2 files changed

+62
-6
lines changed

packages/react-dom/src/__tests__/ReactDOMFiber-test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,57 @@ describe('ReactDOMFiber', () => {
989989
}
990990
});
991991

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+
9921043
it('should throw on bad createPortal argument', () => {
9931044
expect(() => {
9941045
ReactDOM.createPortal(<div>portal</div>, null);

packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {
2323
getClosestInstanceFromNode,
2424
getNodeFromInstance,
25+
isContainerMarkedAsRoot,
2526
} from '../../client/ReactDOMComponentTree';
2627
import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMPluginEventSystem';
2728

@@ -57,15 +58,19 @@ function extractEvents(
5758
domEventName === 'mouseout' || domEventName === 'pointerout';
5859

5960
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.
6065
const related =
6166
(nativeEvent: any).relatedTarget || (nativeEvent: any).fromElement;
6267
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+
) {
6974
return;
7075
}
7176
}

0 commit comments

Comments
 (0)