From 34a2ddf6764c02660d8e5f11f5fefd2a52b1b3ea Mon Sep 17 00:00:00 2001 From: simonihmig Date: Wed, 2 May 2018 22:09:23 +0200 Subject: [PATCH] [BUGFIX beta] support mouseEnter/Leave events w/o jQuery As these events don't bubble, the `EventDispatcher`'s event delegation approach does not work here, when not using jQuery. jQuery has special handling of these events, by listening to `mouseover`/`mouseout` instead and dispatching fake `mouseenter`/`mouseleave` events. This adds similar handling to `EventDispatcher`'s native mode for these events. Fixes #16591 --- .../integration/event-dispatcher-test.js | 51 ++++++++++++ .../lib/system/event_dispatcher.js | 80 +++++++++++++++---- 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/packages/ember-glimmer/tests/integration/event-dispatcher-test.js b/packages/ember-glimmer/tests/integration/event-dispatcher-test.js index 16ac748b3de..ad879f6ead5 100644 --- a/packages/ember-glimmer/tests/integration/event-dispatcher-test.js +++ b/packages/ember-glimmer/tests/integration/event-dispatcher-test.js @@ -143,6 +143,57 @@ moduleFor( this.$('#is-done').trigger('click'); } + + ['@test delegated event listeners work for mouseEnter/Leave'](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]; + + // mouse moves over #outer + this.runTask(() => { + this.$('#outer').trigger('mouseenter', { canBubble: false, relatedTarget: parent }); + this.$('#outer').trigger('mouseover', { relatedTarget: parent }); + }); + 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 }); + }); + assert.equal(receivedEnterEvents.length, 1, 'mouseenter event was not triggered again'); + + // mouse moves out of #inner + this.runTask(() => { + this.$('#inner').trigger('mouseout', { relatedTarget: outer }); + }); + 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 }); + }); + assert.equal(receivedLeaveEvents.length, 1, 'mouseleave event was triggered'); + assert.strictEqual(receivedLeaveEvents[0].target, outer); + } } ); diff --git a/packages/ember-views/lib/system/event_dispatcher.js b/packages/ember-views/lib/system/event_dispatcher.js index 64dcae49a60..949505893c9 100644 --- a/packages/ember-views/lib/system/event_dispatcher.js +++ b/packages/ember-views/lib/system/event_dispatcher.js @@ -15,6 +15,11 @@ const HAS_JQUERY = jQuery !== undefined; const ROOT_ELEMENT_CLASS = 'ember-application'; const ROOT_ELEMENT_SELECTOR = `.${ROOT_ELEMENT_CLASS}`; +const EVENT_MAP = { + mouseenter: 'mouseover', + mouseleave: 'mouseout' +}; + /** `Ember.EventDispatcher` handles delegating browser events to their corresponding `Ember.Views.` For example, when you click on a view, @@ -326,27 +331,72 @@ export default EmberObject.extend({ } }; - let handleEvent = (this._eventHandlers[event] = event => { - let target = event.target; + // Special handling of events that don't bubble, so event delegation does not work. + // Mimics the way this is handled in jQuery, + // see https://github.com/jquery/jquery/blob/899c56f6ada26821e8af12d9f35fa039100e838e/src/event.js#L666-L700 + if (EVENT_MAP[event] !== undefined) { + let mappedEventType = EVENT_MAP[event]; + let origEventType = event; + + let createFakeEvent = (eventType, event) => { + let fakeEvent = document.createEvent('MouseEvent'); + fakeEvent.initMouseEvent(eventType, false, false, event.view, event.detail, event.screenX, event.screenY, + event.clientX, event.clientY, event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, event.button, + event.relatedTarget); + + // fake event.target as we don't dispatch the event + Object.defineProperty(fakeEvent, 'target', { value: event.target, enumerable: true }); + + return fakeEvent; + }; + + let handleMappedEvent = (this._eventHandlers[mappedEventType] = event => { + let target = event.target; + let related = event.relatedTarget; - do { - if (viewRegistry[target.id]) { - if (viewHandler(target, event) === false) { - event.preventDefault(); - event.stopPropagation(); + do { + // For mouseenter/leave call the handler if related is outside the target. + // No relatedTarget if the mouse left/entered the browser window + if (viewRegistry[target.id]) { + if (!related || (related !== target && !target.contains(related))) { + viewHandler(target, createFakeEvent(origEventType, event)); + } break; - } - } else if (target.hasAttribute('data-ember-action')) { - if (actionHandler(target, event) === false) { + } else if (target.hasAttribute('data-ember-action')) { + if (!related || (related !== target && !target.contains(related))) { + actionHandler(target, createFakeEvent(origEventType, event)); + } break; } - } - target = target.parentNode; - } while (target && target.nodeType === 1); - }); + target = target.parentNode; + } while (target && target.nodeType === 1); + }); + + rootElement.addEventListener(mappedEventType, handleMappedEvent); + } else { + let handleEvent = (this._eventHandlers[event] = event => { + let target = event.target; + + do { + if (viewRegistry[target.id]) { + if (viewHandler(target, event) === false) { + event.preventDefault(); + event.stopPropagation(); + break; + } + } else if (target.hasAttribute('data-ember-action')) { + if (actionHandler(target, event) === false) { + break; + } + } - rootElement.addEventListener(event, handleEvent); + target = target.parentNode; + } while (target && target.nodeType === 1); + }); + + rootElement.addEventListener(event, handleEvent); + } } },