Skip to content

Commit

Permalink
Propagate events to parent components when nested
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
Daniel15 authored and zpao committed Nov 18, 2013
1 parent b91396b commit d853c85
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 8 deletions.
39 changes: 32 additions & 7 deletions src/core/ReactEventTopLevelCallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"use strict";

var ReactEventEmitter = require('ReactEventEmitter');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactMount = require('ReactMount');

var getEventTarget = require('getEventTarget');
Expand All @@ -30,6 +31,24 @@ var getEventTarget = require('getEventTarget');
*/
var _topLevelListenersEnabled = true;

/**
* Finds the parent React component of `node`.
*
* @param {*} node
* @return {?DOMEventTarget} Parent container, or `null` if the specified node
* is not nested.
*/
function findParent(node) {
// TODO: It may be a good idea to cache this to prevent unnecessary DOM
// traversal, but caching is difficult to do correctly without using a
// mutation observer to listen for all DOM changes.
var nodeID = ReactMount.getID(node);
var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID);
var container = ReactMount.findReactContainerForID(rootID);
var parent = ReactMount.getFirstReactDOM(container);
return parent;
}

/**
* Top-level callback creator used to implement event handling using delegation.
* This is used via dependency injection.
Expand Down Expand Up @@ -69,13 +88,19 @@ var ReactEventTopLevelCallback = {
var topLevelTarget = ReactMount.getFirstReactDOM(
getEventTarget(nativeEvent)
) || window;
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
ReactEventEmitter.handleTopLevel(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent
);

// Loop through the hierarchy, in case there's any nested components.
while (topLevelTarget) {
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
ReactEventEmitter.handleTopLevel(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent
);

topLevelTarget = findParent(topLevelTarget);
}
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/ReactMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ var ReactMount = {

/**
* Registers a container node into which React components will be rendered.
* This also creates the "reatRoot" ID that will be assigned to the element
* This also creates the "reactRoot" ID that will be assigned to the element
* rendered within.
*
* @param {DOMElement} container DOM element to register as a container.
Expand Down
109 changes: 109 additions & 0 deletions src/core/__tests__/ReactEventTopLevelCallback-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright 2013 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @emails react-core
*/

'use strict';

require('mock-modules')
.dontMock('ReactEventTopLevelCallback')
.dontMock('ReactMount')
.dontMock('ReactInstanceHandles')
.dontMock('ReactDOM')
.mock('ReactEventEmitter');

var EVENT_TARGET_PARAM = 1;

describe('ReactEventTopLevelCallback', function() {
var ReactEventTopLevelCallback;
var ReactMount;
var ReactDOM;
var ReactEventEmitter; // mocked

beforeEach(function() {
require('mock-modules').dumpCache();
ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
ReactMount = require('ReactMount');
ReactDOM = require('ReactDOM');
ReactEventEmitter = require('ReactEventEmitter'); // mocked
});

describe('Propagation', function() {
it('should propagate events one level down', function() {
var childContainer = document.createElement('div');
var childControl = ReactDOM.div({}, 'Child');
var parentContainer = document.createElement('div');
var parentControl = ReactDOM.div({}, 'Parent');
ReactMount.renderComponent(childControl, childContainer);
ReactMount.renderComponent(parentControl, parentContainer);
parentControl.getDOMNode().appendChild(childContainer);

var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
callback({
target: childControl.getDOMNode()
});

var calls = ReactEventEmitter.handleTopLevel.mock.calls;
expect(calls.length).toBe(2);
expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode());
expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
});

it('should propagate events two levels down', function() {
var childContainer = document.createElement('div');
var childControl = ReactDOM.div({}, 'Child');
var parentContainer = document.createElement('div');
var parentControl = ReactDOM.div({}, 'Parent');
var grandParentContainer = document.createElement('div');
var grandParentControl = ReactDOM.div({}, 'Parent');
ReactMount.renderComponent(childControl, childContainer);
ReactMount.renderComponent(parentControl, parentContainer);
ReactMount.renderComponent(grandParentControl, grandParentContainer);
parentControl.getDOMNode().appendChild(childContainer);
grandParentControl.getDOMNode().appendChild(parentContainer);

var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
callback({
target: childControl.getDOMNode()
});

var calls = ReactEventEmitter.handleTopLevel.mock.calls;
expect(calls.length).toBe(3);
expect(calls[0][EVENT_TARGET_PARAM]).toBe(childControl.getDOMNode());
expect(calls[1][EVENT_TARGET_PARAM]).toBe(parentControl.getDOMNode());
expect(calls[2][EVENT_TARGET_PARAM])
.toBe(grandParentControl.getDOMNode());
});
});

it('should not fire duplicate events for a React DOM tree', function() {
var container = document.createElement('div');
var inner = ReactDOM.div({}, 'Inner');
var control = ReactDOM.div({}, [
ReactDOM.div({id: 'outer'}, inner)
]);
ReactMount.renderComponent(control, container);

var callback = ReactEventTopLevelCallback.createTopLevelCallback('test');
callback({
target: inner.getDOMNode()
});

var calls = ReactEventEmitter.handleTopLevel.mock.calls;
expect(calls.length).toBe(1);
expect(calls[0][EVENT_TARGET_PARAM]).toBe(inner.getDOMNode());
});
});

0 comments on commit d853c85

Please sign in to comment.