Permalink
Browse files

feat(tooltips+popovers): Automatically hide when trigger element is n…

…o longer visible (#978)

* [popover.vue] use beforeDestroy hook rahter than destroyed

* [tooltip.vue] Use beforeDestroy hook rather than destroyed

* [tooltip class]: Add watcher to see if trigger $element is not visible

* [tooltip class] If trigger element is disabled, don't trigger show

* Update popover.vue

* Update tooltip.vue
  • Loading branch information...
tmorehouse committed Aug 31, 2017
1 parent e32b94b commit 09eaaa2ec9dcf03aba6cba915c4b3bb1353f41f0
Showing with 58 additions and 10 deletions.
  1. +56 −8 lib/classes/tooltip.js
  2. +1 −1 lib/components/popover.vue
  3. +1 −1 lib/components/tooltip.vue
@@ -80,6 +80,16 @@ function generateId(name) {
return `__BV_${name}_${NEXTID++}__`;
}

// Determine if an element is visible. Faster than CSS checks
function elVisible(el) {
return el && (el.offsetParent === null || !(el.offsetWidth > 0 || el.offsetHeight > 0));
}

// Determine if an element is disabled
function elDisabled(el) {
return !el || el.disabled || el.classList.contains('disabled') || Boolean(el.getAttribute('disabled'));
}

/*
* Polyfill for Element.closest() for IE :(
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
@@ -109,6 +119,7 @@ class ToolTip {
// New tooltip object
this.$fadeTimeout = null;
this.$hoverTimeout = null;
this.$visibleInterval = null;
this.$hoverState = '';
this.$activeTrigger = {};
this.$popper = null;
@@ -164,6 +175,8 @@ class ToolTip {

// Destroy this instance
destroy() {
clearInterval(this.$visibleInterval);
this.$visibleInterval = null
clearTimeout(this.$hoverTimeout);
this.$hoverTimeout = null;
clearTimeout(this.$fadeTimeout);
@@ -278,16 +291,40 @@ class ToolTip {
tip.classList.add(ClassName.SHOW);
this.setOnTouchStartListener(true);

// Periodically check to make sure $element is visible
// For handling when tip is in <keepalive>, tabs, carousel, etc
this.$visibleInterval = setInterval(() => {
if (this.$tip && !elVisible(this.$element) && this.$tip.classList.contains(ClassName.SHOW)) {
// Element is no longer visible, so force-hide the tooltip
this.forceHide();
}
}, 100);

// Start the transition/animation
this.transitionOnce(tip, complete);
}

// force hide of tip (internal method)
forceHide() {
const tip = this.getTipElement();
// Remove animation for quicker hide
this.$tip.classList.remove(ClassName.FADE);
// Clear any hover enter/leave event
clearTimeout(this.$hoverTimeout);
this.$hoverTimeout = null;
this.$hoverState = '';
// Hide the tip
this.hide(null, true);
}

// Hide tooltip
hide(callback) {
hide(callback, force) {
const tip = this.getTipElement();

// Create a canelable BvEvent
const hideEvt = new BvEvent('hide', {
cancelable: true,
// We disable cancelling if force is true
cancelable: !Boolean(force),
target: this.$element,
relatedTarget: tip
});
@@ -297,6 +334,10 @@ class ToolTip {
return;
}

// Stop checking for visibility of element.
clearInterval(this.$visibleInterval);
this.$visibleInterval = null;

// Transitionend Callback
const complete = () => {
if (this.$hoverState !== HoverState.SHOW && tip.parentNode) {
@@ -305,7 +346,7 @@ class ToolTip {
}
this.removeAriaDescribedby();
this.removePopper();
// Force a re-compile of tip in case template has changed.
// Force a re-compile (next time shown) of tip in case template has changed.
this.$tip = null;
if (callback) {
callback();
@@ -327,6 +368,7 @@ class ToolTip {
this.$activeTrigger.focus = false;
this.$activeTrigger.hover = false;

// Start the hide transition
this.transitionOnce(tip, complete);

this.$hoverState = '';
@@ -510,7 +552,8 @@ class ToolTip {
listen() {
const triggers = this.$config.trigger.trim().split(/\s+/);

// Using "this" as the handler will get automagically directed to this.handleEvent
// Using 'this' as the handler will get automagically directed to this.handleEvent
// And maintain our binding to 'this'
triggers.forEach(trigger => {
if (trigger === 'click') {
this.$element.addEventListener('click', this);
@@ -548,6 +591,11 @@ class ToolTip {
// If this event isn't for us, then just return
return;
}
if (elDisabled(this.$element)) {
// If disabled, don't do anything. Note: if tip is shown before element gets
// disabled, then tip not close until no longer disabled or forcefully closed.
return;
}
if (e.type === 'click') {
this.toggle(e);
} else if (e.type === 'focusin' || e.type === 'mouseenter') {
@@ -565,8 +613,8 @@ class ToolTip {
if (newVal === oldVal) {
return;
}
// If route has changed, we hide the tooltip/popover
this.hide();
// If route has changed, we force hide the tooltip/popover
this.forceHide();
});
}
} else {
@@ -588,11 +636,11 @@ class ToolTip {
if (this.$root) {
if (on) {
MODAL_CLOSE_EVENT.forEach(evtName => {
this.$root.$on(evtName, this.hide.bind(this));
this.$root.$on(evtName, this.forceHide.bind(this));
});
} else {
MODAL_CLOSE_EVENT.forEach(evtName => {
this.$root.$off(evtName, this.hide.bind(this));
this.$root.$off(evtName, this.forceHide.bind(this));
});
}
}
@@ -84,7 +84,7 @@
this.popOver.updateConfig(this.getConfig());
}
},
destroyed() {
beforeDestroyed() {
if (this.popOver) {
// Destroy the popover
this.popOver.destroy();
@@ -78,7 +78,7 @@
this.toolTip.updateConfig(this.getConfig());
}
},
destroyed() {
beforeDestroy() {
if (this.toolTip) {
this.toolTip.destroy();
this.tooltip = null;

0 comments on commit 09eaaa2

Please sign in to comment.