Skip to content

Commit d853c85

Browse files
Daniel15zpao
authored andcommitted
Propagate events to parent components when nested
When events are captured, the nearest React-rendered ancestor is found and events are propagated to its tree. This causes issues when components are nested as only the innermost component receives the events. This change modifies this behaviour so events propagate all the way up the DOM hierarchy. To reduce the performance cost, this DOM traversal is only done if the component is actually nested (determined by the `containerIsNested` map which is lazily initialised).
1 parent b91396b commit d853c85

File tree

3 files changed

+142
-8
lines changed

3 files changed

+142
-8
lines changed

src/core/ReactEventTopLevelCallback.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"use strict";
2121

2222
var ReactEventEmitter = require('ReactEventEmitter');
23+
var ReactInstanceHandles = require('ReactInstanceHandles');
2324
var ReactMount = require('ReactMount');
2425

2526
var getEventTarget = require('getEventTarget');
@@ -30,6 +31,24 @@ var getEventTarget = require('getEventTarget');
3031
*/
3132
var _topLevelListenersEnabled = true;
3233

34+
/**
35+
* Finds the parent React component of `node`.
36+
*
37+
* @param {*} node
38+
* @return {?DOMEventTarget} Parent container, or `null` if the specified node
39+
* is not nested.
40+
*/
41+
function findParent(node) {
42+
// TODO: It may be a good idea to cache this to prevent unnecessary DOM
43+
// traversal, but caching is difficult to do correctly without using a
44+
// mutation observer to listen for all DOM changes.
45+
var nodeID = ReactMount.getID(node);
46+
var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
47+
var container = ReactMount.findReactContainerForID(rootID);
48+
var parent = ReactMount.getFirstReactDOM(container);
49+
return parent;
50+
}
51+
3352
/**
3453
* Top-level callback creator used to implement event handling using delegation.
3554
* This is used via dependency injection.
@@ -69,13 +88,19 @@ var ReactEventTopLevelCallback = {
6988
var topLevelTarget = ReactMount.getFirstReactDOM(
7089
getEventTarget(nativeEvent)
7190
) || window;
72-
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
73-
ReactEventEmitter.handleTopLevel(
74-
topLevelType,
75-
topLevelTarget,
76-
topLevelTargetID,
77-
nativeEvent
78-
);
91+
92+
// Loop through the hierarchy, in case there's any nested components.
93+
while (topLevelTarget) {
94+
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
95+
ReactEventEmitter.handleTopLevel(
96+
topLevelType,
97+
topLevelTarget,
98+
topLevelTargetID,
99+
nativeEvent
100+
);
101+
102+
topLevelTarget = findParent(topLevelTarget);
103+
}
79104
};
80105
}
81106

src/core/ReactMount.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ var ReactMount = {
363363

364364
/**
365365
* Registers a container node into which React components will be rendered.
366-
* This also creates the "reatRoot" ID that will be assigned to the element
366+
* This also creates the "reactRoot" ID that will be assigned to the element
367367
* rendered within.
368368
*
369369
* @param {DOMElement} container DOM element to register as a container.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Copyright 2013 Facebook, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* @emails react-core
17+
*/
18+
19+
'use strict';
20+
21+
require('mock-modules')
22+
.dontMock('ReactEventTopLevelCallback')
23+
.dontMock('ReactMount')
24+
.dontMock('ReactInstanceHandles')
25+
.dontMock('ReactDOM')
26+
.mock('ReactEventEmitter');
27+
28+
var EVENT_TARGET_PARAM = 1;
29+
30+
describe('ReactEventTopLevelCallback', function() {
31+
var ReactEventTopLevelCallback;
32+
var ReactMount;
33+
var ReactDOM;
34+
var ReactEventEmitter; // mocked
35+
36+
beforeEach(function() {
37+
require('mock-modules').dumpCache();
38+
ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
39+
ReactMount = require('ReactMount');
40+
ReactDOM = require('ReactDOM');
41+
ReactEventEmitter = require('ReactEventEmitter'); // mocked
42+
});
43+
44+
describe('Propagation', function() {
45+
it('should propagate events one level down', function() {
46+
var childContainer = document.createElement('div');
47+
var childControl = ReactDOM.div({}, 'Child');
48+
var parentContainer = document.createElement('div');
49+
var parentControl = ReactDOM.div({}, 'Parent');
50+
ReactMount.renderComponent(childControl, childContainer);
51+
ReactMount.renderComponent(parentControl, parentContainer);
52+
parentControl.getDOMNode().appendChild(childContainer);
53+
54+
var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
55+
callback({
56+
target: childControl.getDOMNode()
57+
});
58+
59+
var calls = ReactEventEmitter.handleTopLevel.mock.calls;
60+
expect(calls.length).toBe(2);
61+
expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode());
62+
expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
63+
});
64+
65+
it('should propagate events two levels down', function() {
66+
var childContainer = document.createElement('div');
67+
var childControl = ReactDOM.div({}, 'Child');
68+
var parentContainer = document.createElement('div');
69+
var parentControl = ReactDOM.div({}, 'Parent');
70+
var grandParentContainer = document.createElement('div');
71+
var grandParentControl = ReactDOM.div({}, 'Parent');
72+
ReactMount.renderComponent(childControl, childContainer);
73+
ReactMount.renderComponent(parentControl, parentContainer);
74+
ReactMount.renderComponent(grandParentControl, grandParentContainer);
75+
parentControl.getDOMNode().appendChild(childContainer);
76+
grandParentControl.getDOMNode().appendChild(parentContainer);
77+
78+
var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
79+
callback({
80+
target: childControl.getDOMNode()
81+
});
82+
83+
var calls = ReactEventEmitter.handleTopLevel.mock.calls;
84+
expect(calls.length).toBe(3);
85+
expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode());
86+
expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
87+
expect(calls[2][EVENT_TARGET_PARAM])
88+
.toBe(grandParentControl.getDOMNode());
89+
});
90+
});
91+
92+
it('should not fire duplicate events for a React DOM tree', function() {
93+
var container = document.createElement('div');
94+
var inner = ReactDOM.div({}, 'Inner');
95+
var control = ReactDOM.div({}, [
96+
ReactDOM.div({id: 'outer'}, inner)
97+
]);
98+
ReactMount.renderComponent(control, container);
99+
100+
var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
101+
callback({
102+
target: inner.getDOMNode()
103+
});
104+
105+
var calls = ReactEventEmitter.handleTopLevel.mock.calls;
106+
expect(calls.length).toBe(1);
107+
expect(calls[0][EVENT_TARGET_PARAM]).toBe(inner.getDOMNode());
108+
});
109+
});

0 commit comments

Comments
 (0)