Skip to content

Commit

Permalink
Fix double-firing mouseenter (#19571)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
gaearon and eps1lon authored Aug 10, 2020
1 parent aa99b0b commit 94c0244
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 6 deletions.
51 changes: 51 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFiber-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,57 @@ describe('ReactDOMFiber', () => {
}
});

// Regression test for https://github.com/facebook/react/issues/19562
it('does not fire mouseEnter twice when relatedTarget is the root node', () => {
let ops = [];
let target = null;

function simulateMouseMove(from, to) {
if (from) {
from.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: to,
}),
);
}
if (to) {
to.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
relatedTarget: from,
}),
);
}
}

ReactDOM.render(
<div
ref={n => (target = n)}
onMouseEnter={() => ops.push('enter')}
onMouseLeave={() => ops.push('leave')}
/>,
container,
);

simulateMouseMove(null, container);
expect(ops).toEqual([]);

ops = [];
simulateMouseMove(container, target);
expect(ops).toEqual(['enter']);

ops = [];
simulateMouseMove(target, container);
expect(ops).toEqual(['leave']);

ops = [];
simulateMouseMove(container, null);
expect(ops).toEqual([]);
});

it('should throw on bad createPortal argument', () => {
expect(() => {
ReactDOM.createPortal(<div>portal</div>, null);
Expand Down
17 changes: 11 additions & 6 deletions packages/react-dom/src/events/plugins/EnterLeaveEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import {
getClosestInstanceFromNode,
getNodeFromInstance,
isContainerMarkedAsRoot,
} from '../../client/ReactDOMComponentTree';
import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMPluginEventSystem';

Expand Down Expand Up @@ -57,15 +58,19 @@ function extractEvents(
domEventName === 'mouseout' || domEventName === 'pointerout';

if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
// If this is an over event with a target, we might have already dispatched
// the event in the out event of the other target. If this is replayed,
// then it's because we couldn't dispatch against this target previously
// so we have to do it now instead.
const related =
(nativeEvent: any).relatedTarget || (nativeEvent: any).fromElement;
if (related) {
// Due to the fact we don't add listeners to the document with the
// modern event system and instead attach listeners to roots, we
// need to handle the over event case. To ensure this, we just need to
// make sure the node that we're coming from is managed by React.
const inst = getClosestInstanceFromNode(related);
if (inst !== null) {
// If the related node is managed by React, we can assume that we have
// already dispatched the corresponding events during its mouseout.
if (
getClosestInstanceFromNode(related) ||
isContainerMarkedAsRoot(related)
) {
return;
}
}
Expand Down

0 comments on commit 94c0244

Please sign in to comment.