From 7bf66f543b18ad8fa90ca58433cacc44728c3148 Mon Sep 17 00:00:00 2001 From: Ryan Berliner Date: Sat, 6 Mar 2021 03:35:28 +0200 Subject: [PATCH 1/4] prevent tooltip from being deleted on quick re-activations --- js/src/tooltip.js | 4 ++++ js/tests/unit/tooltip.spec.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 6f33245f8ee0..e9f9bfff1b44 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -329,6 +329,10 @@ class Tooltip extends BaseComponent { const tip = this.getTipElement() const complete = () => { + if (this._isWithActiveTrigger()) { + return + } + if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) { tip.parentNode.removeChild(tip) } diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 7bf6aa3ab837..41f73d6d8a7d 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -708,6 +708,35 @@ describe('Tooltip', () => { tooltipEl.dispatchEvent(createEvent('mouseover')) }) + it('should not hide tooltip if leave event occurs and enter event occurs during hide transition', done => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) + + setTimeout(() => { + expect(tooltip._popper).not.toBeNull() + tooltipEl.dispatchEvent(createEvent('mouseout')) + + setTimeout(() => { + expect(tooltip.getTipElement().classList.contains('show')).toEqual(false) + tooltipEl.dispatchEvent(createEvent('mouseover')) + }, 100) + + setTimeout(() => { + expect(tooltip._popper).not.toBeNull() + done() + }, 200) + }, 0) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + it('should show a tooltip with custom class provided in data attributes', done => { fixtureEl.innerHTML = '' From 70a68f077de21e78c152246694df7f16edd4fbef Mon Sep 17 00:00:00 2001 From: Ryan Berliner Date: Sat, 6 Mar 2021 23:57:23 +0200 Subject: [PATCH 2/4] prevent quick interactions from misplacing tooltips --- js/src/tooltip.js | 6 +++++- js/tests/unit/tooltip.spec.js | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/js/src/tooltip.js b/js/src/tooltip.js index e9f9bfff1b44..857f72c8ad2c 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -283,6 +283,10 @@ class Tooltip extends BaseComponent { EventHandler.trigger(this._element, this.constructor.Event.INSERTED) + if (this._popper) { + this._popper.destroy() + } + this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) tip.classList.add(CLASS_NAME_SHOW) @@ -650,7 +654,7 @@ class Tooltip extends BaseComponent { if (event) { context._activeTrigger[ event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER - ] = false + ] = context._element.contains(event.relatedTarget) } if (context._isWithActiveTrigger()) { diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 41f73d6d8a7d..1cb301c151aa 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -708,8 +708,9 @@ describe('Tooltip', () => { tooltipEl.dispatchEvent(createEvent('mouseover')) }) - it('should not hide tooltip if leave event occurs and enter event occurs during hide transition', done => { - fixtureEl.innerHTML = '' + it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => { + // Style this tooltip to give it plenty of room for popper to do what it wants + fixtureEl.innerHTML = 'Trigger' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) @@ -721,6 +722,7 @@ describe('Tooltip', () => { setTimeout(() => { expect(tooltip._popper).not.toBeNull() + expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top') tooltipEl.dispatchEvent(createEvent('mouseout')) setTimeout(() => { @@ -730,6 +732,7 @@ describe('Tooltip', () => { setTimeout(() => { expect(tooltip._popper).not.toBeNull() + expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top') done() }, 200) }, 0) From 34620ce042f4d31bdab9441e44779e326bea7f03 Mon Sep 17 00:00:00 2001 From: Ryan Berliner Date: Sun, 7 Mar 2021 08:28:41 -0500 Subject: [PATCH 3/4] reuse existing popper on show during tooltip fadeout --- js/src/tooltip.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 857f72c8ad2c..de7dcca69391 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -284,11 +284,11 @@ class Tooltip extends BaseComponent { EventHandler.trigger(this._element, this.constructor.Event.INSERTED) if (this._popper) { - this._popper.destroy() + this._popper.update() + } else { + this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) } - this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) - tip.classList.add(CLASS_NAME_SHOW) const customClass = typeof this.config.customClass === 'function' ? this.config.customClass() : this.config.customClass From 0bd32a381bb22382232bf1999c76fd9de3998400 Mon Sep 17 00:00:00 2001 From: Ryan Berliner Date: Sun, 7 Mar 2021 17:09:17 +0200 Subject: [PATCH 4/4] only trigger tooltip inserted event on true dom insert --- js/src/tooltip.js | 3 +- js/tests/unit/tooltip.spec.js | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/js/src/tooltip.js b/js/src/tooltip.js index de7dcca69391..979bd077382c 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -279,10 +279,9 @@ class Tooltip extends BaseComponent { if (!this._element.ownerDocument.documentElement.contains(this.tip)) { container.appendChild(tip) + EventHandler.trigger(this._element, this.constructor.Event.INSERTED) } - EventHandler.trigger(this._element, this.constructor.Event.INSERTED) - if (this._popper) { this._popper.update() } else { diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 1cb301c151aa..f9d97e3f7ed4 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -708,6 +708,37 @@ describe('Tooltip', () => { tooltipEl.dispatchEvent(createEvent('mouseover')) }) + it('should not hide tooltip if leave event occurs and interaction remains inside trigger', done => { + fixtureEl.innerHTML = [ + '', + 'Trigger', + 'the tooltip', + '' + ] + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + const triggerChild = tooltipEl.querySelector('b') + + spyOn(tooltip, 'hide').and.callThrough() + + tooltipEl.addEventListener('mouseover', () => { + const moveMouseToChildEvent = createEvent('mouseout') + Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { + value: triggerChild + }) + + tooltipEl.dispatchEvent(moveMouseToChildEvent) + }) + + tooltipEl.addEventListener('mouseout', () => { + expect(tooltip.hide).not.toHaveBeenCalled() + done() + }) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => { // Style this tooltip to give it plenty of room for popper to do what it wants fixtureEl.innerHTML = 'Trigger' @@ -740,6 +771,37 @@ describe('Tooltip', () => { tooltipEl.dispatchEvent(createEvent('mouseover')) }) + it('should only trigger inserted event if a new tooltip element was created', done => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) + + const insertedFunc = jasmine.createSpy() + tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) + + setTimeout(() => { + expect(insertedFunc).toHaveBeenCalledTimes(1) + tooltip.hide() + + setTimeout(() => { + tooltip.show() + }, 100) + + setTimeout(() => { + expect(insertedFunc).toHaveBeenCalledTimes(1) + done() + }, 200) + }, 0) + + tooltip.show() + }) + it('should show a tooltip with custom class provided in data attributes', done => { fixtureEl.innerHTML = ''