Fix incorrect calculation of onMouseEnter/Leave component path (#11164)
* Add a regression test for #10906 * Turn while conditions into breaks without changing the logic This will be easier to follow when we add more code there. * var => const/let So that I can add a block scoped variable. * Check alternates when comparing to the common ancestor This is the actual bugfix.
Dan Abramov committed
Oct 9, 2017 at 20:30 UTC
80596c2c92dce264f8572038730ce7aef647c5fa
2 files changed
+62
-6
src/renderers/dom/shared/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js
+36
@@ -13,6 +13,7 @@ var EnterLeaveEventPlugin;
13
var React;
14
var ReactDOM;
15
var ReactDOMComponentTree;
16
+var ReactTestUtils;
17
18
describe('EnterLeaveEventPlugin', () => {
19
beforeEach(() => {
@@ -20,6 +21,7 @@ describe('EnterLeaveEventPlugin', () => {
21
22
React = require('react');
23
ReactDOM = require('react-dom');
24
+ ReactTestUtils = require('react-dom/test-utils');
25
// TODO: can we express this test with only public API?
26
ReactDOMComponentTree = require('ReactDOMComponentTree');
27
EnterLeaveEventPlugin = require('EnterLeaveEventPlugin');
@@ -58,4 +60,38 @@ describe('EnterLeaveEventPlugin', () => {
60
expect(enter.target).toBe(div);
61
expect(enter.relatedTarget).toBe(iframe.contentWindow);
62
});
63
+
64
+ // Regression test for https://github.com/facebook/react/issues/10906.
65
+ it('should find the common parent after updates', () => {
66
+ let parentEnterCalls = 0;
67
+ let childEnterCalls = 0;
68
+ let parent = null;
69
+ class Parent extends React.Component {
70
+ render() {
71
+ return (
72
+ <div
73
+ onMouseEnter={() => parentEnterCalls++}
74
+ ref={node => (parent = node)}>
75
+ {this.props.showChild &&
76
+ <div onMouseEnter={() => childEnterCalls++} />}
77
+ </div>
78
+ );
79
+ }
80
+ }
81
+
82
+ const div = document.createElement('div');
83
+ ReactDOM.render(<Parent />, div);
84
+ // The issue only reproduced on insertion during the first update.
85
+ ReactDOM.render(<Parent showChild={true} />, div);
86
+
87
+ // Enter from parent into the child.
88
+ ReactTestUtils.simulateNativeEventOnNode('topMouseOut', parent, {
89
+ target: parent,
90
+ relatedTarget: parent.firstChild,
91
+ });
92
+
93
+ // Entering a child should fire on the child, not on the parent.
94
+ expect(childEnterCalls).toBe(1);
95
+ expect(parentEnterCalls).toBe(0);
96
+ });
97
});
src/renderers/shared/shared/ReactTreeTraversal.js
+26
-6
@@ -110,18 +110,38 @@ function traverseTwoPhase(inst, fn, arg) {
110
* "entered" or "left" that element.
111
*/
112
function traverseEnterLeave(from, to, fn, argFrom, argTo) {
113
- var common = from && to ? getLowestCommonAncestor(from, to) : null;
114
- var pathFrom = [];
115
- while (from && from !== common) {
113
+ const common = from && to ? getLowestCommonAncestor(from, to) : null;
114
+ const pathFrom = [];
115
+ while (true) {
116
+ if (!from) {
117
+ break;
118
+ }
119
+ if (from === common) {
120
+ break;
121
+ }
122
+ const alternate = from.alternate;
123
+ if (alternate !== null && alternate === common) {
124
+ break;
125
+ }
126
pathFrom.push(from);
127
from = getParent(from);
128
}
119
- var pathTo = [];
120
- while (to && to !== common) {
129
+ const pathTo = [];
130
+ while (true) {
131
+ if (!to) {
132
+ break;
133
+ }
134
+ if (to === common) {
135
+ break;
136
+ }
137
+ const alternate = to.alternate;
138
+ if (alternate !== null && alternate === common) {
139
+ break;
140
+ }
141
pathTo.push(to);
142
to = getParent(to);
143
}
124
- var i;
144
+ let i;
145
for (i = 0; i < pathFrom.length; i++) {
146
fn(pathFrom[i], 'bubbled', argFrom);
147
}