From 04961a64fc2eddbdb1183413c7224bb66ea3de70 Mon Sep 17 00:00:00 2001 From: jquense Date: Tue, 29 Dec 2015 14:10:20 -0500 Subject: [PATCH 1/2] Use native mouse{enter|leave} if supported --- .../dom/client/ReactBrowserEventEmitter.js | 20 ++ .../eventPlugins/EnterLeaveEventPlugin.js | 181 +++++++++++------- src/renderers/shared/event/EventConstants.js | 2 + 3 files changed, 137 insertions(+), 66 deletions(-) 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/eventPlugins/EnterLeaveEventPlugin.js b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js index d50b0d37002..dbf91fd2073 100644 --- a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js @@ -15,10 +15,12 @@ var EventConstants = require('EventConstants'); var EventPropagators = require('EventPropagators'); 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: { @@ -26,6 +28,7 @@ var eventTypes = { dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, + topLevelTypes.topMouseEnter, ], }, mouseLeave: { @@ -33,14 +36,121 @@ var eventTypes = { dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, + topLevelTypes.topMouseLeave, ], }, }; +function getNativeEnterLeave( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget +) { + if ( + topLevelType === topLevelTypes.topMouseEnter || + topLevelType === topLevelTypes.topMouseLeave + ) { + if (targetInst) { + var event = SyntheticMouseEvent.getPooled( + topLevelType === topLevelTypes.topMouseEnter + ? eventTypes.mouseEnter + : eventTypes.mouseLeave, + targetInst, + nativeEvent, + nativeEventTarget + ); + EventPropagators.accumulateDirectDispatches(event); + return event; + } + return null; + } +} + +function getEnterLeavePolyfill( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget +) { + 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; + } + + 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 { + win = window; + } + } + + var from; + var to; + if (topLevelType === topLevelTypes.topMouseOut) { + from = targetInst; + var related = nativeEvent.relatedTarget || nativeEvent.toElement; + to = related ? + ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; + } else { + // Moving to a node from outside the window. + from = null; + to = targetInst; + } + + if (from === to) { + // Nothing pertains to our managed components. + return null; + } + + var fromNode = + from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); + var toNode = + to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); + + var leave = SyntheticMouseEvent.getPooled( + eventTypes.mouseLeave, + from, + nativeEvent, + nativeEventTarget + ); + leave.type = 'mouseleave'; + leave.target = fromNode; + leave.relatedTarget = toNode; + + var enter = SyntheticMouseEvent.getPooled( + eventTypes.mouseEnter, + to, + nativeEvent, + nativeEventTarget + ); + enter.type = 'mouseenter'; + enter.target = toNode; + enter.relatedTarget = fromNode; + + EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); + + return [leave, enter]; +} + var EnterLeaveEventPlugin = { eventTypes: eventTypes, + isEnterLeaveSupported: isEnterLeaveSupported, + /** * 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 @@ -54,76 +164,15 @@ var EnterLeaveEventPlugin = { nativeEvent, nativeEventTarget ) { - 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; - } + var event; - var win; - if (nativeEventTarget.window === nativeEventTarget) { - // `nativeEventTarget` is probably a window object. - win = nativeEventTarget; + if (EnterLeaveEventPlugin.isEnterLeaveSupported) { + event = getNativeEnterLeave(topLevelType, targetInst, nativeEvent, nativeEventTarget); } else { - // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. - var doc = nativeEventTarget.ownerDocument; - if (doc) { - win = doc.defaultView || doc.parentWindow; - } else { - win = window; - } - } - - var from; - var to; - if (topLevelType === topLevelTypes.topMouseOut) { - from = targetInst; - var related = nativeEvent.relatedTarget || nativeEvent.toElement; - to = related ? - ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; - } else { - // Moving to a node from outside the window. - from = null; - to = targetInst; - } - - if (from === to) { - // Nothing pertains to our managed components. - return null; + event = getEnterLeavePolyfill(topLevelType, targetInst, nativeEvent, nativeEventTarget); } - var fromNode = - from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); - var toNode = - to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); - - var leave = SyntheticMouseEvent.getPooled( - eventTypes.mouseLeave, - from, - nativeEvent, - nativeEventTarget - ); - leave.type = 'mouseleave'; - leave.target = fromNode; - leave.relatedTarget = toNode; - - var enter = SyntheticMouseEvent.getPooled( - eventTypes.mouseEnter, - to, - nativeEvent, - nativeEventTarget - ); - enter.type = 'mouseenter'; - enter.target = toNode; - enter.relatedTarget = fromNode; - - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); - - return [leave, enter]; + return event; }, }; diff --git a/src/renderers/shared/event/EventConstants.js b/src/renderers/shared/event/EventConstants.js index 5b1f9ca63af..9cb5485f40c 100644 --- a/src/renderers/shared/event/EventConstants.js +++ b/src/renderers/shared/event/EventConstants.js @@ -56,6 +56,8 @@ var topLevelTypes = keyMirror({ topLoadedMetadata: null, topLoadStart: null, topMouseDown: null, + topMouseEnter: null, + topMouseLeave: null, topMouseMove: null, topMouseOut: null, topMouseOver: null, From 9286bbe0e716e991c94e1a7c7d67b1da3af7ed53 Mon Sep 17 00:00:00 2001 From: jquense Date: Wed, 30 Dec 2015 09:46:55 -0500 Subject: [PATCH 2/2] Use single node for enter|leave polyfill logic fixes #4251 --- .../dom/client/ReactDOMTreeTraversal.js | 40 ++--- .../__tests__/ReactDOMTreeTraversal-test.js | 109 ----------- .../eventPlugins/EnterLeaveEventPlugin.js | 127 +++++++------ .../__tests__/EnterLeaveEventPlugin-test.js | 170 ++++++++++++++---- .../shared/event/EventPluginUtils.js | 3 + 5 files changed, 227 insertions(+), 222 deletions(-) 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 dbf91fd2073..5f2425980aa 100644 --- a/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js +++ b/src/renderers/dom/client/eventPlugins/EnterLeaveEventPlugin.js @@ -13,10 +13,16 @@ 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; @@ -24,7 +30,9 @@ var isEnterLeaveSupported = isEventSupported('mouseenter', true); var eventTypes = { mouseEnter: { + registrationName: keyOf({onMouseEnter: null}), + dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, @@ -32,7 +40,9 @@ var eventTypes = { ], }, mouseLeave: { + registrationName: keyOf({onMouseLeave: null}), + dependencies: [ topLevelTypes.topMouseOut, topLevelTypes.topMouseOver, @@ -52,14 +62,23 @@ function getNativeEnterLeave( topLevelType === topLevelTypes.topMouseLeave ) { if (targetInst) { + var eventType; + + if (topLevelType === topLevelTypes.topMouseEnter) { + eventType = 'mouseEnter'; + } else { + eventType = 'mouseLeave'; + } + var event = SyntheticMouseEvent.getPooled( - topLevelType === topLevelTypes.topMouseEnter - ? eventTypes.mouseEnter - : eventTypes.mouseLeave, + eventTypes[eventType], targetInst, nativeEvent, nativeEventTarget ); + + event.type = eventType.toLowerCase(); + EventPropagators.accumulateDirectDispatches(event); return event; } @@ -67,19 +86,31 @@ function getNativeEnterLeave( } } +/** + * 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.topMouseOver && - (nativeEvent.relatedTarget || nativeEvent.fromElement)) { - return null; - } - if (topLevelType !== topLevelTypes.topMouseOut && - topLevelType !== topLevelTypes.topMouseOver) { - // Must not be a mouse in or mouse out - ignoring. + + if ( + topLevelType !== topLevelTypes.topMouseOut && + topLevelType !== topLevelTypes.topMouseOver || + !targetInst + ) { return null; } @@ -97,52 +128,47 @@ function getEnterLeavePolyfill( } } - var from; - var to; + var eventType; + if (topLevelType === topLevelTypes.topMouseOut) { - from = targetInst; - var related = nativeEvent.relatedTarget || nativeEvent.toElement; - to = related ? - ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; + eventType = 'mouseLeave'; } else { - // Moving to a node from outside the window. - from = null; - to = targetInst; + eventType = 'mouseEnter'; } - if (from === to) { - // Nothing pertains to our managed components. + // 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 fromNode = - from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); - var toNode = - to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); + var target = ReactDOMComponentTree.getNodeFromInstance(delegateTargetInst); + var related = nativeEvent.relatedTarget || nativeEvent.toElement; - var leave = SyntheticMouseEvent.getPooled( - eventTypes.mouseLeave, - from, - nativeEvent, - nativeEventTarget - ); - leave.type = 'mouseleave'; - leave.target = fromNode; - leave.relatedTarget = toNode; - - var enter = SyntheticMouseEvent.getPooled( - eventTypes.mouseEnter, - to, - nativeEvent, - nativeEventTarget - ); - enter.type = 'mouseenter'; - enter.target = toNode; - enter.relatedTarget = fromNode; + // 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 event = SyntheticMouseEvent.getPooled( + eventTypes[eventType], + delegateTargetInst, + nativeEvent, + target + ); - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); + event.type = eventType.toLowerCase(); + event.relatedTarget = related; - return [leave, enter]; + EventPropagators.accumulateDirectDispatches(event); + + return event; + } } var EnterLeaveEventPlugin = { @@ -150,14 +176,7 @@ var EnterLeaveEventPlugin = { eventTypes: eventTypes, isEnterLeaveSupported: isEnterLeaveSupported, - - /** - * 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, 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( +
+
+ foo +
+
+ ); + + var root = ReactDOM.findDOMNode(inst); + var inner = root.firstChild; + + ReactTestUtils.SimulateNative.mouseEnter(inner, { relatedTarget: root }); + expect(called).toBe(1); + }); + + it('should use native mouseleave is supported', function() { + if (!EnterLeaveEventPlugin.isEnterLeaveSupported) { + return; + } + + var called = 0; + + function onLeave(e) { + called += 1; + expect(e.type).toBe('mouseleave'); + expect(e.relatedTarget).toBe(root); + } + + var inst = ReactTestUtils.renderIntoDocument( +
+
+ foo +
+
+ ); + + var root = ReactDOM.findDOMNode(inst); + var inner = root.firstChild; - topLevelTypes = EventConstants.topLevelTypes; + ReactTestUtils.SimulateNative.mouseLeave(inner, { relatedTarget: root }); + expect(called).toBe(1); }); - it('should set relatedTarget properly in iframe', function() { - var iframe = document.createElement('iframe'); - document.body.appendChild(iframe); + describe('EnterLeave Polyfill', function() { + + beforeEach(function() { + EnterLeaveEventPlugin.isEnterLeaveSupported = false; + }); + + it('should use the relatedTarget from mouseover', function() { + var called = 0; + + function onEnter(e) { + called += 1; + expect(e.type).toBe('mouseenter'); + expect(e.relatedTarget).toBe(root); + } + + var inst = ReactTestUtils.renderIntoDocument( +
+
+ foo +
+
+ ); + + var root = ReactDOM.findDOMNode(inst); + var inner = root.firstChild; + + ReactTestUtils.SimulateNative.mouseOver(inner, { relatedTarget: root }); + expect(called).toBe(1); + }); + it('should use the relatedTarget from mouseout', function() { + var called = 0; + + function onLeave(e) { + called += 1; + expect(e.type).toBe('mouseleave'); + expect(e.relatedTarget).toBe(root); + } + + var inst = ReactTestUtils.renderIntoDocument( +
+
+ foo +
+
+ ); + + var root = ReactDOM.findDOMNode(inst); + var inner = root.firstChild; + + ReactTestUtils.SimulateNative.mouseOut(inner, { relatedTarget: root }); + expect(called).toBe(1); + }); + }); + + it('should set relatedTarget to the iframe window', function() { + var noop = function() {}; + var iframe = createIframe(); var iframeDocument = iframe.contentDocument; - iframeDocument.write( - '
' + var div = ReactDOM.render( +
, + iframeDocument.body.getElementsByTagName('div')[0] ); - iframeDocument.close(); - var component = ReactDOM.render(
, iframeDocument.body.getElementsByTagName('div')[0]); - var div = ReactDOM.findDOMNode(component); + var inst = ReactDOMComponentTree.getInstanceFromNode(div); - var extracted = EnterLeaveEventPlugin.extractEvents( + var enter = EnterLeaveEventPlugin.extractEvents( topLevelTypes.topMouseOver, - ReactDOMComponentTree.getInstanceFromNode(div), - {target: div}, + inst, + { target: div }, div ); - expect(extracted.length).toBe(2); - - var leave = extracted[0]; - var enter = extracted[1]; - expect(leave.target).toBe(iframe.contentWindow); - expect(leave.relatedTarget).toBe(div); expect(enter.target).toBe(div); expect(enter.relatedTarget).toBe(iframe.contentWindow); }); diff --git a/src/renderers/shared/event/EventPluginUtils.js b/src/renderers/shared/event/EventPluginUtils.js index 5ca1e13d4f7..d5158765970 100644 --- a/src/renderers/shared/event/EventPluginUtils.js +++ b/src/renderers/shared/event/EventPluginUtils.js @@ -248,6 +248,9 @@ var EventPluginUtils = { getParentInstance: function(inst) { return TreeTraversal.getParentInstance(inst); }, + traverseUntil: function(inst, fn) { + return TreeTraversal.traverseUntil(inst, fn); + }, traverseTwoPhase: function(target, fn, arg) { return TreeTraversal.traverseTwoPhase(target, fn, arg); },