Skip to content

Commit

Permalink
feat(hideOn clickout) (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
kybishop authored Aug 28, 2017
1 parent f861109 commit cd39a81
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 10 deletions.
38 changes: 30 additions & 8 deletions addon/components/ember-attacher-inner.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default Component.extend({
this._hideAfterDelay = this._hideAfterDelay.bind(this);
this._hideIfMouseOutsideTargetOrAttachment
= this._hideIfMouseOutsideTargetOrAttachment.bind(this);
this._hideOnClickOut = this._hideOnClickOut.bind(this);
this._hideOnLostFocus = this._hideOnLostFocus.bind(this);
this._hideOnMouseLeaveTarget = this._hideOnMouseLeaveTarget.bind(this);
this._show = this._show.bind(this);
Expand All @@ -75,9 +76,7 @@ export default Component.extend({
},

_initializeAttacher() {
if (this._currentTarget) {
this._removeEventListeners();
}
this._removeEventListeners();

this._currentTarget = this.get('target');

Expand Down Expand Up @@ -112,15 +111,17 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);

// Check if current target was already destroyed
if (this._currentTarget) {
this._removeEventListeners();
}
this._removeEventListeners();
},

_removeEventListeners() {
document.removeEventListener('click', this._hideOnClickOut);
document.removeEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment);

if (!this._currentTarget) {
return;
}

[this._hideListenersOnTargetByEvent, this._showListenersOnTargetByEvent]
.forEach((eventToListener) => {
Object.keys(eventToListener).forEach((event) => {
Expand All @@ -137,7 +138,9 @@ export default Component.extend({
const isShown = this.get('isShown');

if (isShown === true && this._isHidden) {
this._addListenersForHideEvents();
// Add the hide listeners in the next run loop to avoid conflicts
// where clicking triggers both an isShown toggle and a clickout.
next(this, () => this._addListenersForHideEvents());

this._show();
} else if (isShown === false && !this._isHidden) {
Expand Down Expand Up @@ -277,6 +280,10 @@ export default Component.extend({
target.addEventListener('click', this._hideAfterDelay);
}

if (hideOn.indexOf('clickout') !== -1) {
document.addEventListener('click', this._hideOnClickOut);
}

// Hides the attachment when the mouse leaves the target
// (or leaves both target and attachment for interactive attachments)
if (hideOn.indexOf('mouseleave') !== -1) {
Expand Down Expand Up @@ -372,6 +379,18 @@ export default Component.extend({
return false;
},

_hideOnClickOut(event) {
const targetReceivedClick = this._currentTarget.contains(event.target);

if (this.get('interactive')) {
if (!targetReceivedClick && !this.element.contains(event.target)) {
this._hideAfterDelay();
}
} else if (!targetReceivedClick) {
this._hideAfterDelay();
}
},

_hideOnLostFocus(event) {
if (event.relatedTarget === null) {
this._hideAfterDelay();
Expand Down Expand Up @@ -429,6 +448,9 @@ export default Component.extend({
const showOn = this.get('_showOn');
const target = this._currentTarget;

document.removeEventListener('click', this._hideOnClickOut);
document.removeEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment);

// The target was destroyed, nothing to remove listeners from
if (!target) {
return;
Expand Down
2 changes: 1 addition & 1 deletion tests/dummy/app/components/attachment-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default Component.extend({
'scale',
'shift'
],
hideOnOptions: ['click', 'mouseleave blur'],
hideOnOptions: ['click', 'clickout', 'mouseleave blur'],
isConfiguringTooltip: true,
placementOptions: ['bottom', 'left', 'right', 'top'],
showOnOptions: ['click', 'mouseenter focus'],
Expand Down
5 changes: 4 additions & 1 deletion tests/dummy/app/templates/components/attachment-example.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@
hideOn

{{#ember-attacher class="ember-attacher-popper ember-attacher-tooltip"}}
Any combination of "mouseleave", "blur", and "click".
Any combination of "blur", "click", "clickout", and "mouseleave".
"clickout" is a custom event that will hide the attachment when the user clicks
outside the target. When interactive=true, clicking the attachment will not trigger
a hide event
{{/ember-attacher}}
</span>
="{{#power-select onchange=(action (mut service.hideOn))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import hbs from 'htmlbars-inline-precompile';
import { click, find } from 'ember-native-dom-helpers';
import { moduleForComponent, test } from 'ember-qunit';

moduleForComponent('ember-attacher', 'Integration | Component | hideOn "clickout"', {
integration: true
});

test('hides when an element outside the target is clicked', function(assert) {
assert.expect(3);

this.render(hbs`
<input type="text" id="focus-me"/>
<div id="parent">
{{#ember-attacher class='hello'
hideOn='clickout'
isShown=true}}
hideOn click
{{/ember-attacher}}
</div>
`);

const innerAttacher = find('.hello > .inner');

assert.equal(innerAttacher.style.display, '', 'Initially shown');

// Make sure the attachment is still shown when the target is clicked
return click(find('#parent')).then(() => {
assert.equal(innerAttacher.style.display, '', 'Still shown');

return click(find('#focus-me')).then(() => {
assert.equal(innerAttacher.style.display, 'none', 'Now hidden');
});
});
});

test('with interactive=false: hides when attachment is clicked', function(assert) {
assert.expect(2);

this.render(hbs`
<div id="parent">
{{#ember-attacher class='hello'
hideOn='clickout'
isShown=true}}
hideOn click
{{/ember-attacher}}
</div>
`);

const innerAttacher = find('.hello > .inner');

assert.equal(innerAttacher.style.display, '', 'Initially shown');

return click(innerAttacher).then(() => {
assert.equal(innerAttacher.style.display, 'none', 'Now hidden');
});
});

test("with interactive=true: doesn't hide when attachment is clicked", function(assert) {
assert.expect(4);

this.render(hbs`
<input type="text" id="focus-me"/>
<div id="parent">
{{#ember-attacher class='hello'
hideOn='clickout'
interactive=true
isShown=true}}
hideOn click
{{/ember-attacher}}
</div>
`);

const innerAttacher = find('.hello > .inner');

assert.equal(innerAttacher.style.display, '', 'Initially shown');

// Make sure attachment stays shown when attachment clicked
return click(innerAttacher).then(() => {
assert.equal(innerAttacher.style.display, '', 'Still shown');

// Make sure attachment stays shown when target clicked
return click(find('#parent')).then(() => {
assert.equal(innerAttacher.style.display, '', 'Still shown');

// Make sure attachment is hidden once an element outside target or attachment is clicked
return click(find('#focus-me')).then(() => {
assert.equal(innerAttacher.style.display, 'none', 'Now hidden');
});
});
});
});

0 comments on commit cd39a81

Please sign in to comment.