Skip to content

Commit

Permalink
[BUGFIX release] fix mouseEnter/Leave event delegation w/o jQuery
Browse files Browse the repository at this point in the history
The previous implementation did not work correctly when the element of
the component listening to mouseEnter/Leave was quickly moved over,
(i.e. no mouseover event triggered on this element) and the child node
is from another component, as demonstrated in the replication in #16922.

Related to 16603
Fixes #16922
  • Loading branch information
simonihmig committed Sep 23, 2018
1 parent 183c853 commit 6264318
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,130 @@ moduleFor(

let parent = this.element;
let outer = this.$('#outer')[0];
let inner = this.$('#inner')[0];

// mouse moves over #outer
this.runTask(() => {
this.$('#outer').trigger('mouseenter', { canBubble: false, relatedTarget: parent });
this.$('#outer').trigger('mouseover', { relatedTarget: parent });
this.$(outer).trigger('mouseenter', { canBubble: false, relatedTarget: parent });
this.$(outer).trigger('mouseover', { relatedTarget: parent });
this.$(parent).trigger('mouseout', { relatedTarget: outer });
});
assert.equal(receivedEnterEvents.length, 1, 'mouseenter event was triggered');
assert.strictEqual(receivedEnterEvents[0].target, outer);

// mouse moves over #inner
this.runTask(() => {
this.$('#inner').trigger('mouseover', { relatedTarget: outer });
this.$(inner).trigger('mouseover', { relatedTarget: outer });
this.$(outer).trigger('mouseout', { relatedTarget: inner });
});
assert.equal(receivedEnterEvents.length, 1, 'mouseenter event was not triggered again');

// mouse moves out of #inner
this.runTask(() => {
this.$('#inner').trigger('mouseout', { relatedTarget: outer });
this.$(inner).trigger('mouseout', { relatedTarget: outer });
this.$(outer).trigger('mouseover', { relatedTarget: inner });
});
assert.equal(receivedLeaveEvents.length, 0, 'mouseleave event was not triggered');

// mouse moves out of #outer
this.runTask(() => {
this.$('#outer').trigger('mouseleave', { canBubble: false, relatedTarget: parent });
this.$('#outer').trigger('mouseout', { relatedTarget: parent });
this.$(outer).trigger('mouseleave', { canBubble: false, relatedTarget: parent });
this.$(outer).trigger('mouseout', { relatedTarget: parent });
this.$(parent).trigger('mouseover', { relatedTarget: outer });
});
assert.equal(receivedLeaveEvents.length, 1, 'mouseleave event was triggered');
assert.strictEqual(receivedLeaveEvents[0].target, outer);
}

['@test delegated event listeners work for mouseEnter/Leave with skipped events'](assert) {
let receivedEnterEvents = [];
let receivedLeaveEvents = [];

this.registerComponent('x-foo', {
ComponentClass: Component.extend({
mouseEnter(event) {
receivedEnterEvents.push(event);
},
mouseLeave(event) {
receivedLeaveEvents.push(event);
},
}),
template: `<div id="inner"></div>`,
});

this.render(`{{x-foo id="outer"}}`);

let parent = this.element;
let outer = this.$('#outer')[0];
let inner = this.$('#inner')[0];

// we replicate fast mouse movement, where mouseover is fired directly in #inner, skipping #outer
this.runTask(() => {
this.$(outer).trigger('mouseenter', { canBubble: false, relatedTarget: parent });
this.$(inner).trigger('mouseover', { relatedTarget: parent });
this.$(parent).trigger('mouseout', { relatedTarget: inner });
});
assert.equal(receivedEnterEvents.length, 1, 'mouseenter event was triggered');
assert.strictEqual(receivedEnterEvents[0].target, inner);

// mouse moves out of #outer
this.runTask(() => {
this.$(outer).trigger('mouseleave', { canBubble: false, relatedTarget: parent });
this.$(inner).trigger('mouseout', { relatedTarget: parent });
this.$(parent).trigger('mouseover', { relatedTarget: inner });
});
assert.equal(receivedLeaveEvents.length, 1, 'mouseleave event was triggered');
assert.strictEqual(receivedLeaveEvents[0].target, inner);
}

['@test delegated event listeners work for mouseEnter/Leave with skipped events and subcomponent'](
assert
) {
let receivedEnterEvents = [];
let receivedLeaveEvents = [];

this.registerComponent('x-outer', {
ComponentClass: Component.extend({
mouseEnter(event) {
receivedEnterEvents.push(event);
},
mouseLeave(event) {
receivedLeaveEvents.push(event);
},
}),
template: `{{yield}}`,
});

this.registerComponent('x-inner', {
ComponentClass: Component.extend(),
template: ``,
});

this.render(`{{#x-outer id="outer"}}{{x-inner id="inner"}}{{/x-outer}}`);

let parent = this.element;
let outer = this.$('#outer')[0];
let inner = this.$('#inner')[0];

// we replicate fast mouse movement, where mouseover is fired directly in #inner, skipping #outer
this.runTask(() => {
this.$(outer).trigger('mouseenter', { canBubble: false, relatedTarget: parent });
this.$(inner).trigger('mouseover', { relatedTarget: parent });
this.$(parent).trigger('mouseout', { relatedTarget: inner });
});
assert.equal(receivedEnterEvents.length, 1, 'mouseenter event was triggered');
assert.strictEqual(receivedEnterEvents[0].target, inner);

// mouse moves out of #inner
this.runTask(() => {
this.$(outer).trigger('mouseleave', { canBubble: false, relatedTarget: parent });
this.$(inner).trigger('mouseout', { relatedTarget: parent });
this.$(parent).trigger('mouseover', { relatedTarget: inner });
});

assert.equal(receivedLeaveEvents.length, 1, 'mouseleave event was triggered');
assert.strictEqual(receivedLeaveEvents[0].target, inner);
}
}
);

Expand Down
31 changes: 15 additions & 16 deletions packages/@ember/-internals/views/lib/system/event_dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { getOwner } from '@ember/-internals/owner';
/**
@module ember
*/

import { assign } from '@ember/polyfills';
import { assert } from '@ember/debug';
import { get, set } from '@ember/-internals/metal';
Expand All @@ -12,6 +8,10 @@ import ActionManager from './action_manager';
import fallbackViewRegistry from '../compat/fallback-view-registry';
import addJQueryEventDeprecation from './jquery_event_deprecation';

/**
@module ember
*/

const ROOT_ELEMENT_CLASS = 'ember-application';
const ROOT_ELEMENT_SELECTOR = `.${ROOT_ELEMENT_CLASS}`;

Expand Down Expand Up @@ -332,23 +332,22 @@ export default EmberObject.extend({
let target = event.target;
let related = event.relatedTarget;

do {
// For mouseenter/leave call the handler if related is outside the target.
// No relatedTarget if the mouse left/entered the browser window
while (
target &&
target.nodeType === 1 &&
(!related || (related !== target && !target.contains(related)))
) {
// mouseEnter/Leave don't bubble, so there is no logic to prevent it as with other events
if (viewRegistry[target.id]) {
if (!related || (related !== target && !target.contains(related))) {
viewHandler(target, createFakeEvent(origEventType, event));
}
break;
viewHandler(target, createFakeEvent(origEventType, event));
} else if (target.hasAttribute('data-ember-action')) {
if (!related || (related !== target && !target.contains(related))) {
actionHandler(target, createFakeEvent(origEventType, event));
}
break;
actionHandler(target, createFakeEvent(origEventType, event));
}

// separate mouseEnter/Leave events are dispatched for each listening element
// until the element (related) has been reached that the pointing device exited from/to
target = target.parentNode;
} while (target && target.nodeType === 1);
}
});

rootElement.addEventListener(mappedEventType, handleMappedEvent);
Expand Down

0 comments on commit 6264318

Please sign in to comment.