Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 55 additions & 15 deletions src/browser/ReactEventTopLevelCallback.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

"use strict";

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

var getEventTarget = require('getEventTarget');
var mixInto = require('mixInto');

/**
* @type {boolean}
Expand All @@ -49,6 +51,53 @@ function findParent(node) {
return parent;
}

/**
* Calls ReactEventEmitter.handleTopLevel for each node stored in bookKeeping's
* ancestor list. Separated from createTopLevelCallback to avoid try/finally
* deoptimization.
*
* @param {string} topLevelType
* @param {DOMEvent} nativeEvent
* @param {TopLevelCallbackBookKeeping} bookKeeping
*/
function handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping) {
var topLevelTarget = ReactMount.getFirstReactDOM(
getEventTarget(nativeEvent)
) || window;

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
var ancestor = topLevelTarget;
while (ancestor) {
bookKeeping.ancestors.push(ancestor);
ancestor = findParent(ancestor);
}

for (var i = 0, l = bookKeeping.ancestors.length; i < l; i++) {
topLevelTarget = bookKeeping.ancestors[i];
var topLevelTargetID = ReactMount.getID(topLevelTarget) || '';
ReactEventEmitter.handleTopLevel(
topLevelType,
topLevelTarget,
topLevelTargetID,
nativeEvent
);
}
}

// Used to store ancestor hierarchy in top level callback
function TopLevelCallbackBookKeeping() {
this.ancestors = [];
}
mixInto(TopLevelCallbackBookKeeping, {
destructor: function() {
this.ancestors.length = 0;
}
});
PooledClass.addPoolingTo(TopLevelCallbackBookKeeping);

/**
* Top-level callback creator used to implement event handling using delegation.
* This is used via dependency injection.
Expand Down Expand Up @@ -85,21 +134,12 @@ var ReactEventTopLevelCallback = {
if (!_topLevelListenersEnabled) {
return;
}
var topLevelTarget = ReactMount.getFirstReactDOM(
getEventTarget(nativeEvent)
) || window;

// 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);

var bookKeeping = TopLevelCallbackBookKeeping.getPooled();
try {
handleTopLevelImpl(topLevelType, nativeEvent, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
};
}
Expand Down
33 changes: 33 additions & 0 deletions src/browser/__tests__/ReactEventTopLevelCallback-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ describe('ReactEventTopLevelCallback', function() {
expect(calls[2][EVENT_TARGET_PARAM])
.toBe(grandParentControl.getDOMNode());
});

it('should not get confused by disappearing elements', function() {
var childContainer = document.createElement('div');
var childControl = <div>Child</div>;
var parentContainer = document.createElement('div');
var parentControl = <div>Parent</div>;
ReactMount.renderComponent(childControl, childContainer);
ReactMount.renderComponent(parentControl, parentContainer);
parentControl.getDOMNode().appendChild(childContainer);

// ReactEventEmitter.handleTopLevel might remove the target from the DOM.
// Here, we have handleTopLevel remove the node when the first event
// handlers are called; we'll still expect to receive a second call for
// the parent control.
var childNode = childControl.getDOMNode();
ReactEventEmitter.handleTopLevel.mockImplementation(
function(topLevelType, topLevelTarget, topLevelTargetID, nativeEvent) {
if (topLevelTarget === childNode) {
ReactMount.unmountComponentAtNode(childContainer);
}
}
);

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

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

it('should not fire duplicate events for a React DOM tree', function() {
Expand Down