diff --git a/js/src/base-component.js b/js/src/base-component.js
index b28d8be33813..26b3762890c2 100644
--- a/js/src/base-component.js
+++ b/js/src/base-component.js
@@ -47,7 +47,16 @@ class BaseComponent extends Config {
// Private
_queueCallback(callback, element, isAnimated = true) {
- executeAfterTransition(callback, element, isAnimated)
+ executeAfterTransition(() => {
+ // `dispose()` nulls every property of the instance, but it cannot cancel a
+ // callback already queued on a transition. Skip it rather than let it run
+ // against a disposed instance and throw
+ if (!this._element) {
+ return
+ }
+
+ callback()
+ }, element, isAnimated)
}
_getConfig(config) {
diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js
index 37f2c230d037..0d021c2a10a5 100644
--- a/js/tests/unit/tooltip.spec.js
+++ b/js/tests/unit/tooltip.spec.js
@@ -443,6 +443,65 @@ describe('Tooltip', () => {
tooltip2.dispose()
expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull()
})
+
+ it('should not run a callback queued by hide() when disposed mid-transition', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = ''
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ const hiddenSpy = jasmine.createSpy('hidden')
+ const errorSpy = jasmine.createSpy('error')
+
+ tooltipEl.addEventListener('hidden.bs.tooltip', hiddenSpy)
+
+ tooltipEl.addEventListener('shown.bs.tooltip', () => {
+ window.addEventListener('error', errorSpy)
+
+ // `hide()` queues its completion callback on the fade transition,
+ // `dispose()` then nulls every property before that callback runs
+ tooltip.hide()
+ tooltip.dispose()
+
+ setTimeout(() => {
+ window.removeEventListener('error', errorSpy)
+
+ expect(errorSpy).not.toHaveBeenCalled()
+ expect(hiddenSpy).not.toHaveBeenCalled()
+ resolve()
+ }, 50)
+ })
+
+ tooltip.show()
+ })
+ })
+
+ it('should not run a callback queued by show() when disposed mid-transition', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = ''
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ const shownSpy = jasmine.createSpy('shown')
+ const errorSpy = jasmine.createSpy('error')
+
+ tooltipEl.addEventListener('shown.bs.tooltip', shownSpy)
+ window.addEventListener('error', errorSpy)
+
+ tooltip.show()
+ tooltip.dispose()
+
+ setTimeout(() => {
+ window.removeEventListener('error', errorSpy)
+
+ expect(errorSpy).not.toHaveBeenCalled()
+ expect(shownSpy).not.toHaveBeenCalled()
+ // the queued callback must not write back onto the disposed instance
+ expect(tooltip._isHovered).toBeNull()
+ resolve()
+ }, 50)
+ })
+ })
})
describe('show', () => {