diff --git a/src/renderers/dom/client/ReactBrowserEventEmitter.js b/src/renderers/dom/client/ReactBrowserEventEmitter.js index 7f203aace9c..ecaabb5e328 100644 --- a/src/renderers/dom/client/ReactBrowserEventEmitter.js +++ b/src/renderers/dom/client/ReactBrowserEventEmitter.js @@ -117,6 +117,8 @@ var topEventMapping = { topLoadedMetadata: 'loadedmetadata', topLoadStart: 'loadstart', topMouseDown: 'mousedown', + topMouseEnter: 'mousenter', + topMouseLeave: 'mouseleave', topMouseMove: 'mousemove', topMouseOut: 'mouseout', topMouseOver: 'mouseover', @@ -280,6 +282,24 @@ var ReactBrowserEventEmitter = assign({}, ReactEventEmitterMixin, { ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE ); } + } else if (dependency === topLevelTypes.topMouseEnter || + dependency === topLevelTypes.topMouseLeave) { + + if (isEventSupported('mouseenter', true)) { + ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( + topLevelTypes.topMouseEnter, + 'mouseenter', + mountAt + ); + ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent( + topLevelTypes.topMouseLeave, + 'mouseleave', + mountAt + ); + } + + isListening[topLevelTypes.topMouseEnter] = true; + isListening[topLevelTypes.topMouseLeave] = true; } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) { diff --git a/src/renderers/dom/client/ReactDOMTreeTraversal.js b/src/renderers/dom/client/ReactDOMTreeTraversal.js index c4767736bb5..2d119141ba4 100644 --- a/src/renderers/dom/client/ReactDOMTreeTraversal.js +++ b/src/renderers/dom/client/ReactDOMTreeTraversal.js @@ -79,6 +79,17 @@ function getParentInstance(inst) { return inst._nativeParent; } +/** + * + */ +function traverseUntil(inst, fn) { + while (inst && !fn(inst)) { + inst = inst._nativeParent; + } + + return inst; +} + /** * Simulates the traversal of a two-phase, capture/bubble event dispatch. */ @@ -97,38 +108,11 @@ function traverseTwoPhase(inst, fn, arg) { } } -/** - * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that - * should would receive a `mouseEnter` or `mouseLeave` event. - * - * Does not invoke the callback on the nearest common ancestor because nothing - * "entered" or "left" that element. - */ -function traverseEnterLeave(from, to, fn, argFrom, argTo) { - var common = from && to ? getLowestCommonAncestor(from, to) : null; - var pathFrom = []; - while (from && from !== common) { - pathFrom.push(from); - from = from._nativeParent; - } - var pathTo = []; - while (to && to !== common) { - pathTo.push(to); - to = to._nativeParent; - } - var i; - for (i = 0; i < pathFrom.length; i++) { - fn(pathFrom[i], true, argFrom); - } - for (i = pathTo.length; i-- > 0;) { - fn(pathTo[i], false, argTo); - } -} module.exports = { isAncestor: isAncestor, getLowestCommonAncestor: getLowestCommonAncestor, getParentInstance: getParentInstance, + traverseUntil: traverseUntil, traverseTwoPhase: traverseTwoPhase, - traverseEnterLeave: traverseEnterLeave, }; diff --git a/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js b/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js index 888e17d1596..fad34db755f 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMTreeTraversal-test.js @@ -19,7 +19,6 @@ var ReactTestUtils = require('ReactTestUtils'); * Ensure that all callbacks are invoked, passing this unique argument. */ var ARG = {arg: true}; -var ARG2 = {arg2: true}; var ChildComponent = React.createClass({ render: function() { @@ -108,114 +107,6 @@ describe('ReactDOMTreeTraversal', function() { }); }); - describe('traverseEnterLeave', function() { - it('should not traverse when enter/leaving outside DOM', function() { - var target = null; - var expectedAggregation = []; - ReactDOMTreeTraversal.traverseEnterLeave( - target, target, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should not traverse if enter/leave the same node', function() { - var parent = renderParentIntoDocument(); - var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1); - var enter = getInst(parent.refs.P_P1_C1.refs.DIV_1); - var expectedAggregation = []; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should traverse enter/leave to sibling - avoids parent', function() { - var parent = renderParentIntoDocument(); - var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1); - var enter = getInst(parent.refs.P_P1_C1.refs.DIV_2); - var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: true, arg: ARG}, - // enter/leave shouldn't fire anything on the parent - {node: parent.refs.P_P1_C1.refs.DIV_2, isUp: false, arg: ARG2}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should traverse enter/leave to parent - avoids parent', function() { - var parent = renderParentIntoDocument(); - var leave = getInst(parent.refs.P_P1_C1.refs.DIV_1); - var enter = getInst(parent.refs.P_P1_C1.refs.DIV); - var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV_1, isUp: true, arg: ARG}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should enter from the window', function() { - var parent = renderParentIntoDocument(); - var leave = null; // From the window or outside of the React sandbox. - var enter = getInst(parent.refs.P_P1_C1.refs.DIV); - var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG2}, - {node: parent.refs.P_P1, isUp: false, arg: ARG2}, - {node: parent.refs.P_P1_C1.refs.DIV, isUp: false, arg: ARG2}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should enter from the window to the shallowest', function() { - var parent = renderParentIntoDocument(); - var leave = null; // From the window or outside of the React sandbox. - var enter = getInst(parent.refs.P); - var expectedAggregation = [ - {node: parent.refs.P, isUp: false, arg: ARG2}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should leave to the window', function() { - var parent = renderParentIntoDocument(); - var enter = null; // From the window or outside of the React sandbox. - var leave = getInst(parent.refs.P_P1_C1.refs.DIV); - var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV, isUp: true, arg: ARG}, - {node: parent.refs.P_P1, isUp: true, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - - it('should leave to the window from the shallowest', function() { - var parent = renderParentIntoDocument(); - var enter = null; // From the window or outside of the React sandbox. - var leave = getInst(parent.refs.P_P1_C1.refs.DIV); - var expectedAggregation = [ - {node: parent.refs.P_P1_C1.refs.DIV, isUp: true, arg: ARG}, - {node: parent.refs.P_P1, isUp: true, arg: ARG}, - {node: parent.refs.P, isUp: true, arg: ARG}, - ]; - ReactDOMTreeTraversal.traverseEnterLeave( - leave, enter, argAggregator, ARG, ARG2 - ); - expect(aggregatedArgs).toEqual(expectedAggregation); - }); - }); - describe('getFirstCommonAncestor', function() { it('should determine the first common ancestor correctly', function() { var parent = renderParentIntoDocument(); diff --git a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js index d50b0d37002..5f2425980aa 100644 --- a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js @@ -13,117 +13,185 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); +var EventPluginHub = require('EventPluginHub'); +var EventPluginUtils = require('EventPluginUtils'); + +var containsNode = require('containsNode'); + var ReactDOMComponentTree = require('ReactDOMComponentTree'); var SyntheticMouseEvent = require('SyntheticMouseEvent'); +var isEventSupported = require('isEventSupported'); + var keyOf = require('keyOf'); var topLevelTypes = EventConstants.topLevelTypes; +var isEnterLeaveSupported = isEventSupported('mouseenter', true); var eventTypes = { mouseEnter: { + registrationName: keyOf({onMouseEnter: null}), + dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, + topLevelTypes.topMouseEnter, ], }, mouseLeave: { + registrationName: keyOf({onMouseLeave: null}), + dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, + topLevelTypes.topMouseLeave, ], }, }; -var EnterLeaveEventPlugin = { - - eventTypes: eventTypes, - - /** - * For almost every interaction we care about, there will be both a top-level - * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that - * we do not extract duplicate events. However, moving the mouse into the - * browser from outside will not fire a `mouseout` event. In this case, we use - * the `mouseover` top-level event. - */ - extractEvents: function( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget +function getNativeEnterLeave( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget +) { + if ( + topLevelType === topLevelTypes.topMouseEnter || + topLevelType === topLevelTypes.topMouseLeave ) { - if (topLevelType === topLevelTypes.topMouseOver && - (nativeEvent.relatedTarget || nativeEvent.fromElement)) { - return null; - } - if (topLevelType !== topLevelTypes.topMouseOut && - topLevelType !== topLevelTypes.topMouseOver) { - // Must not be a mouse in or mouse out - ignoring. - return null; - } + if (targetInst) { + var eventType; - var win; - if (nativeEventTarget.window === nativeEventTarget) { - // `nativeEventTarget` is probably a window object. - win = nativeEventTarget; - } else { - // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. - var doc = nativeEventTarget.ownerDocument; - if (doc) { - win = doc.defaultView || doc.parentWindow; + if (topLevelType === topLevelTypes.topMouseEnter) { + eventType = 'mouseEnter'; } else { - win = window; + eventType = 'mouseLeave'; } + + var event = SyntheticMouseEvent.getPooled( + eventTypes[eventType], + targetInst, + nativeEvent, + nativeEventTarget + ); + + event.type = eventType.toLowerCase(); + + EventPropagators.accumulateDirectDispatches(event); + return event; } + return null; + } +} - var from; - var to; - if (topLevelType === topLevelTypes.topMouseOut) { - from = targetInst; - var related = nativeEvent.relatedTarget || nativeEvent.toElement; - to = related ? - ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; +/** + * Traverse the current target instance ancestors + * until it reaches an instance with a listener for the + * specified eventType + */ +function getEventDelegateTargetInst(targetInst, eventType) { + var registrationName = eventType.registrationName; + + return EventPluginUtils.traverseUntil(targetInst, function(nextInst) { + return !!EventPluginHub.getListener(nextInst, registrationName); + }); +} + +function getEnterLeavePolyfill( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget +) { + + if ( + topLevelType !== topLevelTypes.topMouseOut && + topLevelType !== topLevelTypes.topMouseOver || + !targetInst + ) { + return null; + } + + var win; + if (nativeEventTarget.window === nativeEventTarget) { + // `nativeEventTarget` is probably a window object. + win = nativeEventTarget; + } else { + // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. + var doc = nativeEventTarget.ownerDocument; + if (doc) { + win = doc.defaultView || doc.parentWindow; } else { - // Moving to a node from outside the window. - from = null; - to = targetInst; + win = window; } + } - if (from === to) { - // Nothing pertains to our managed components. - return null; - } + var eventType; - var fromNode = - from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); - var toNode = - to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); + if (topLevelType === topLevelTypes.topMouseOut) { + eventType = 'mouseLeave'; + } else { + eventType = 'mouseEnter'; + } - var leave = SyntheticMouseEvent.getPooled( - eventTypes.mouseLeave, - from, - nativeEvent, - nativeEventTarget - ); - leave.type = 'mouseleave'; - leave.target = fromNode; - leave.relatedTarget = toNode; + // Get the closest instance listening for this event + var delegateTargetInst = getEventDelegateTargetInst(targetInst, eventTypes[eventType]); + + // if this or a parent isn't listening for enter|leave + // there is nothing else to do. + if (!delegateTargetInst) { + return null; + } + + var target = ReactDOMComponentTree.getNodeFromInstance(delegateTargetInst); + var related = nativeEvent.relatedTarget || nativeEvent.toElement; + + // When the mouse moves from or into a listening node, but not + // movements between elements inside that node. + // no relatedTarget means an enter|leave from the document + if (!related || related !== target && !containsNode(target, related)) { + related = related || win; - var enter = SyntheticMouseEvent.getPooled( - eventTypes.mouseEnter, - to, + + var event = SyntheticMouseEvent.getPooled( + eventTypes[eventType], + delegateTargetInst, nativeEvent, - nativeEventTarget + target ); - enter.type = 'mouseenter'; - enter.target = toNode; - enter.relatedTarget = fromNode; - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); + event.type = eventType.toLowerCase(); + event.relatedTarget = related; + + EventPropagators.accumulateDirectDispatches(event); + + return event; + } +} + +var EnterLeaveEventPlugin = { + + eventTypes: eventTypes, + + isEnterLeaveSupported: isEnterLeaveSupported, + + extractEvents: function( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget + ) { + var event; + + if (EnterLeaveEventPlugin.isEnterLeaveSupported) { + event = getNativeEnterLeave(topLevelType, targetInst, nativeEvent, nativeEventTarget); + } else { + event = getEnterLeavePolyfill(topLevelType, targetInst, nativeEvent, nativeEventTarget); + } - return [leave, enter]; + return event; }, }; diff --git a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js index 68606ead4ac..06c0d1d84b3 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js @@ -10,55 +10,163 @@ */ 'use strict'; +var ReactTestUtils = require('ReactTestUtils'); +var EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); +var EventConstants = require('EventConstants'); +var React = require('React'); +var ReactDOM = require('ReactDOM'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var EnterLeaveEventPlugin; -var EventConstants; -var React; -var ReactDOM; -var ReactDOMComponentTree; +var topLevelTypes = EventConstants.topLevelTypes; -var topLevelTypes; +function createIframe() { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + + EnterLeaveEventPlugin.isEnterLeaveSupported = false; + + var iframeDocument = iframe.contentDocument; + + iframeDocument.write( + '
' + ); + iframeDocument.close(); + + return iframe; +} describe('EnterLeaveEventPlugin', function() { - beforeEach(function() { - jest.resetModuleRegistry(); - EnterLeaveEventPlugin = require('EnterLeaveEventPlugin'); - EventConstants = require('EventConstants'); - React = require('React'); - ReactDOM = require('ReactDOM'); - ReactDOMComponentTree = require('ReactDOMComponentTree'); + it('should use native mouseenter is supported', function() { + if (!EnterLeaveEventPlugin.isEnterLeaveSupported) { + return; + } + + var called = 0; + + function onEnter(e) { + called += 1; + expect(e.type).toBe('mouseenter'); + expect(e.relatedTarget).toBe(root); + } + + var inst = ReactTestUtils.renderIntoDocument( +