Skip to content

Commit

Permalink
[BUGFIX beta] support mouseEnter/Leave events w/o jQuery
Browse files Browse the repository at this point in the history
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
  • Loading branch information
simonihmig committed May 3, 2018
1 parent 7d494bb commit e730fba
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 15 deletions.
50 changes: 50 additions & 0 deletions packages/ember-glimmer/tests/integration/event-dispatcher-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,56 @@ 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: `<div id="inner"></div>`,
});

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

let parent = this.element;
let outer = this.$('#outer')[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);
}
}
);

Expand Down
94 changes: 79 additions & 15 deletions packages/ember-views/lib/system/event_dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -326,27 +331,86 @@ 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);
}
}
},

Expand Down

0 comments on commit e730fba

Please sign in to comment.