I looked around for a good popover library for Ember. I couldn't find one I liked (and was compatible with Ember 1.13 and Glimmer), so I whipped up a little ditty:
// app/pop-over/component.js
import $ from "jquery";
import Ember from "ember";
const { Component, run } = Ember;
const ESC = 27;
export default Component.extend({
isVisible: false,
didInitAttrs() {
this._closeOnClickOut = this._closeOnClickOut.bind(this);
this._closeOnEsc = this._closeOnEsc.bind(this);
},
didRender() {
run.next(() => {
const method = this.get('isVisible') && !this.get('isDestroyed') ? 'on' : 'off';
$(window)
[method]('click', this._closeOnClickOut)
[method]('keyup', this._closeOnEsc);
});
},
_close() {
if (this.get('isDestroyed')) { return; }
this.set('isVisible', false);
this.sendAction('close');
}
_closeOnClickOut(e) {
const clickIsInside = this.$().find(e.target).length > 0;
if (!clickIsInside) { this._close(); }
},
_closeOnEsc(e) {
if (e.keyCode === ESC) { this._close(); }
}
});
If you don't like the component reaching out to $(window)
to attach the click handler, you could add an invisible scrim behind the popover contents and attach to that. There's not much you can do about the keyup handler, though, since window (or document) is the only good place to catch it.
The Ember.run.next
made this a little difficult to test. My first attempt was
test('popover works', function(assert) {
this.set('isOpen', false);
render(`
<span class='outside'>Outside</span>
{{#pop-over isVisible=isOpen}}
<span class='inside'>Inside</span>
{{/pop-over}}
`);
assert.equal(this.$('.inside').is(':visible'), false)
Ember.run(this, 'set', 'isOpen', true);
assert.equal(this.$('.inside').is(':visible'), true);
Ember.run(this.$('.outside'), 'click');
assert.equal(this.$('.inside').is(':visible'), false);
});
Unfortunately, that runs the click
before the click handler is added. Luckily, ember-qunit lets you return a Promise from your test
. Thus,
test('popover works', function(assert) {
this.set('isOpen', false);
render(`
<span class='outside'>Outside</span>
{{#pop-over isVisible=isOpen}}
<span class='inside'>Inside</span>
{{/pop-over}}
`);
assert.equal(this.$('.inside').is(':visible'), false)
Ember.run(this, 'set', 'isOpen', true);
assert.equal(this.$('.inside').is(':visible'), true);
return new Ember.RSVP.Promise((resolve) => {
Ember.run.next(() => {
Ember.run(this.$('.outside'), 'click');
assert.equal(this.$('.inside').is(':visible'), false);
resolve();
});
});
});
This code is released under the MIT License, copyright James A Rosen 2015.
I love how simple this is @jamesarosen! Thanks for sharing.
I ran into serious problems testing this in phantom though.
this.$()
didn't seem to exist in that environment.Updating
_closeOnClickOut
to this seemed to work. (I also added a check so that it would close if I clicked a link inside of the popover)