Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mouse enter leave #9824

Closed
wants to merge 3 commits into from
Closed
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
3 changes: 3 additions & 0 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* should not onMouseLeave when staying in the portal

src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* should not blow away user-entered text on successful reconnect to an uncontrolled checkbox
* should not blow away user-entered text on successful reconnect to a controlled checkbox
Expand Down
15 changes: 5 additions & 10 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -555,14 +555,6 @@ src/renderers/__tests__/ReactTreeTraversal-test.js
* should not traverse when traversing outside DOM
* should traverse two phase across component boundary
* should traverse two phase at shallowest node
* should not traverse when enter/leaving outside DOM
* should not traverse if enter/leave the same node
* should traverse enter/leave to sibling - avoids parent
* should traverse enter/leave to parent - avoids parent
* should enter from the window
* should enter from the window to the shallowest
* should leave to the window
* should leave to the window from the shallowest
* should determine the first common ancestor correctly

src/renderers/__tests__/ReactUpdates-test.js
Expand Down Expand Up @@ -657,7 +649,6 @@ src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
* should update portal context if it changes due to re-render
* findDOMNode should find dom element after expanding a fragment
* should bubble events from the portal to the parent
* should not onMouseLeave when staying in the portal
* should not update event handlers until commit
* should not crash encountering low-priority tree
* throws if non-element passed to top-level render
Expand Down Expand Up @@ -1398,7 +1389,11 @@ src/renderers/dom/shared/eventPlugins/__tests__/ChangeEventPlugin-test.js
* should only fire events when the value changes for range inputs

src/renderers/dom/shared/eventPlugins/__tests__/EnterLeaveEventPlugin-test.js
* should set relatedTarget properly in iframe
* should use native mouseenter if supported
* should use native mouseleave is supported
* should use the relatedTarget from mouseover
* should use the relatedTarget from mouseout
* should set relatedTarget to the iframe window

src/renderers/dom/shared/eventPlugins/__tests__/FallbackCompositionState-test.js
* extracts value via `getText()`
Expand Down
85 changes: 0 additions & 85 deletions src/renderers/__tests__/ReactTreeTraversal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,91 +100,6 @@ describe('ReactTreeTraversal', () => {
});
});

describe('traverseEnterLeave', () => {
it('should not traverse when enter/leaving outside DOM', () => {
ReactTreeTraversal.traverseEnterLeave(null, null, callback, ARG, ARG2);
expect(mockFn).not.toHaveBeenCalled();
});

it('should not traverse if enter/leave the same node', () => {
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);
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn).not.toHaveBeenCalled();
});

it('should traverse enter/leave to sibling - avoids parent', () => {
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 expectedCalls = [
['P_P1_C1__DIV_1', 'bubbled', ARG],
// enter/leave shouldn't fire anything on the parent
['P_P1_C1__DIV_2', 'captured', ARG2],
];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});

it('should traverse enter/leave to parent - avoids parent', () => {
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 expectedCalls = [['P_P1_C1__DIV_1', 'bubbled', ARG]];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});

it('should enter from the window', () => {
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 expectedCalls = [
['P', 'captured', ARG2],
['P_P1', 'captured', ARG2],
['P_P1_C1__DIV', 'captured', ARG2],
];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});

it('should enter from the window to the shallowest', () => {
var parent = renderParentIntoDocument();
var leave = null; // From the window or outside of the React sandbox.
var enter = getInst(parent.refs.P);
var expectedCalls = [['P', 'captured', ARG2]];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});

it('should leave to the window', () => {
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 expectedCalls = [
['P_P1_C1__DIV', 'bubbled', ARG],
['P_P1', 'bubbled', ARG],
['P', 'bubbled', ARG],
];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});

it('should leave to the window from the shallowest', () => {
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 expectedCalls = [
['P_P1_C1__DIV', 'bubbled', ARG],
['P_P1', 'bubbled', ARG],
['P', 'bubbled', ARG],
];
ReactTreeTraversal.traverseEnterLeave(leave, enter, callback, ARG, ARG2);
expect(mockFn.mock.calls).toEqual(expectedCalls);
});
});

describe('getFirstCommonAncestor', () => {
it('should determine the first common ancestor correctly', () => {
var parent = renderParentIntoDocument();
Expand Down
19 changes: 19 additions & 0 deletions src/renderers/dom/shared/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ var ReactBrowserEventEmitter = Object.assign({}, ReactEventEmitterMixin, {
'scroll',
mountAt,
);
} else if (
dependency === 'topMouseEnter' ||
dependency === 'topMouseLeave'
) {
if (isEventSupported('mouseenter', true)) {
ReactDOMEventListener.trapCapturedEvent(
'topMouseEnter',
'mouseenter',
mountAt,
);
ReactDOMEventListener.trapCapturedEvent(
'topMouseLeave',
'mouseleave',
mountAt,
);
}

isListening.topMouseEnter = true;
isListening.topMouseLeave = true;
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
ReactDOMEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
ReactDOMEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
Expand Down
203 changes: 128 additions & 75 deletions src/renderers/dom/shared/eventPlugins/EnterLeaveEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,145 @@
'use strict';

var EventPropagators = require('EventPropagators');
var EventPluginHub = require('EventPluginHub');
var ReactTreeTraversal = require('ReactTreeTraversal');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var SyntheticMouseEvent = require('SyntheticMouseEvent');
var isEventSupported = require('isEventSupported');
var containsNode = require('fbjs/lib/containsNode');

var isEnterLeaveSupported = isEventSupported('mouseenter', true);

var eventTypes = {
mouseEnter: {
registrationName: 'onMouseEnter',
dependencies: ['topMouseOut', 'topMouseOver'],
dependencies: ['topMouseOut', 'topMouseOver', 'topMouseEnter'],
},
mouseLeave: {
registrationName: 'onMouseLeave',
dependencies: ['topMouseOut', 'topMouseOver'],
dependencies: ['topMouseOut', 'topMouseOver', 'topMouseLeave'],
},
};

function getNativeEnterLeave(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
if (topLevelType === 'topMouseEnter' || topLevelType === 'topMouseLeave') {
if (targetInst) {
var eventType;

if (topLevelType === 'topMouseEnter') {
eventType = 'mouseEnter';
} else {
eventType = 'mouseLeave';
}

var event = SyntheticMouseEvent.getPooled(
eventTypes[eventType],
targetInst,
nativeEvent,
nativeEventTarget,
);

event.type = eventType.toLowerCase();

EventPropagators.accumulateDirectDispatches(event);
return event;
}
return 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 ReactTreeTraversal.traverseUntil(
targetInst,
nextInst => !!EventPluginHub.getListener(nextInst, registrationName),
);
}

function getEnterLeavePolyfill(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
if (
(topLevelType !== 'topMouseOut' && topLevelType !== '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 {
win = window;
}
}

var eventType = topLevelType === 'topMouseOut' ? 'mouseLeave' : 'mouseEnter';

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

// Trigger _only_ when focus moves into or out of the listening node; not
// when focus shifts between children on the listening node
if (!related || (related !== target && !containsNode(target, related))) {
related = related || win;

var event = SyntheticMouseEvent.getPooled(
eventTypes[eventType],
delegateTargetInst,
nativeEvent,
target,
);

event.type = eventType.toLowerCase();
event.relatedTarget = related;

EventPropagators.accumulateDirectDispatches(event);

return event;
}
}

var EnterLeaveEventPlugin = {
eventTypes: eventTypes,

/**
* Exposed for testing
*/
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
Expand All @@ -42,80 +164,11 @@ var EnterLeaveEventPlugin = {
nativeEvent,
nativeEventTarget,
) {
if (
topLevelType === 'topMouseOver' &&
(nativeEvent.relatedTarget || nativeEvent.fromElement)
) {
return null;
}
if (topLevelType !== 'topMouseOut' && topLevelType !== '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 === '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);
var getEvent = EnterLeaveEventPlugin.isEnterLeaveSupported
? getNativeEnterLeave
: getEnterLeavePolyfill;

return [leave, enter];
return getEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget);
},
};

Expand Down
Loading