diff --git a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js index 3e069fddd9c..999b4ed5c50 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js @@ -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: `
`, + }); + + 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); + } } ); diff --git a/packages/@ember/-internals/views/lib/system/event_dispatcher.js b/packages/@ember/-internals/views/lib/system/event_dispatcher.js index d2030ec90b3..e2bcac3feef 100644 --- a/packages/@ember/-internals/views/lib/system/event_dispatcher.js +++ b/packages/@ember/-internals/views/lib/system/event_dispatcher.js @@ -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'; @@ -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}`; @@ -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);