diff --git a/js/src/toast.js b/js/src/toast.js index 4c3ea2f63ef3..05a05090ba70 100644 --- a/js/src/toast.js +++ b/js/src/toast.js @@ -8,7 +8,6 @@ import BaseComponent from './base-component.js' import EventHandler from './dom/event-handler.js' import { enableDismissTrigger } from './util/component-functions.js' -import { reflow } from './util/index.js' /** * Constants @@ -18,57 +17,28 @@ const NAME = 'toast' const DATA_KEY = 'bs.toast' const EVENT_KEY = `.${DATA_KEY}` -const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}` -const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}` -const EVENT_FOCUSIN = `focusin${EVENT_KEY}` -const EVENT_FOCUSOUT = `focusout${EVENT_KEY}` const EVENT_HIDE = `hide${EVENT_KEY}` const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` -const CLASS_NAME_FADE = 'fade' -const CLASS_NAME_HIDE = 'hide' // @deprecated - kept here only for backwards compatibility const CLASS_NAME_SHOW = 'show' -const CLASS_NAME_SHOWING = 'showing' - -const DefaultType = { - animation: 'boolean', - autohide: 'boolean', - delay: 'number' -} - -const Default = { - animation: true, - autohide: true, - delay: 5000 -} +const ANIMATION_NAME_AUTOHIDE = 'toast-autohide' /** * Class definition */ class Toast extends BaseComponent { - constructor(element, config) { - super(element, config) - - this._timeout = null - this._hasMouseInteraction = false - this._hasKeyboardInteraction = false - this._setListeners() - } - // Getters - static get Default() { - return Default + static get NAME() { + return NAME } - static get DefaultType() { - return DefaultType - } + constructor(element, config) { + super(element, config) - static get NAME() { - return NAME + EventHandler.on(this._element, 'animationend', event => this._onAnimationEnd(event)) } // Public @@ -79,118 +49,26 @@ class Toast extends BaseComponent { return } - this._clearTimeout() - - if (this._config.animation) { - this._element.classList.add(CLASS_NAME_FADE) - } - - const complete = () => { - this._element.classList.remove(CLASS_NAME_SHOWING) - EventHandler.trigger(this._element, EVENT_SHOWN) - - this._maybeScheduleHide() - } - - this._element.classList.remove(CLASS_NAME_HIDE) // @deprecated - reflow(this._element) - this._element.classList.add(CLASS_NAME_SHOW, CLASS_NAME_SHOWING) - - this._queueCallback(complete, this._element, this._config.animation) + this._element.classList.add(CLASS_NAME_SHOW) + EventHandler.trigger(this._element, EVENT_SHOWN) } hide() { - if (!this.isShown()) { - return - } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE) if (hideEvent.defaultPrevented) { return } - const complete = () => { - this._element.classList.add(CLASS_NAME_HIDE) // @deprecated - this._element.classList.remove(CLASS_NAME_SHOWING, CLASS_NAME_SHOW) - EventHandler.trigger(this._element, EVENT_HIDDEN) - } - - this._element.classList.add(CLASS_NAME_SHOWING) - this._queueCallback(complete, this._element, this._config.animation) - } - - dispose() { - this._clearTimeout() - - if (this.isShown()) { - this._element.classList.remove(CLASS_NAME_SHOW) - } - - super.dispose() - } - - isShown() { - return this._element.classList.contains(CLASS_NAME_SHOW) + this._element.classList.remove(CLASS_NAME_SHOW) + EventHandler.trigger(this._element, EVENT_HIDDEN) } // Private - _maybeScheduleHide() { - if (!this._config.autohide) { - return - } - - if (this._hasMouseInteraction || this._hasKeyboardInteraction) { - return - } - - this._timeout = setTimeout(() => { + _onAnimationEnd(event) { + if (event.animationName === ANIMATION_NAME_AUTOHIDE) { this.hide() - }, this._config.delay) - } - - _onInteraction(event, isInteracting) { - switch (event.type) { - case 'mouseover': - case 'mouseout': { - this._hasMouseInteraction = isInteracting - break - } - - case 'focusin': - case 'focusout': { - this._hasKeyboardInteraction = isInteracting - break - } - - default: { - break - } } - - if (isInteracting) { - this._clearTimeout() - return - } - - const nextElement = event.relatedTarget - if (this._element === nextElement || this._element.contains(nextElement)) { - return - } - - this._maybeScheduleHide() - } - - _setListeners() { - EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true)) - EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false)) - EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true)) - EventHandler.on(this._element, EVENT_FOCUSOUT, event => this._onInteraction(event, false)) - } - - _clearTimeout() { - clearTimeout(this._timeout) - this._timeout = null } } diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js index 7dcf82de89e6..bd6cfd8c79f4 100644 --- a/js/tests/unit/toast.spec.js +++ b/js/tests/unit/toast.spec.js @@ -1,6 +1,6 @@ import Toast from '../../src/toast.js' import { - clearFixture, createEvent, getFixture + clearFixture, getFixture } from '../helpers/fixture.js' describe('Toast', () => { @@ -14,599 +14,239 @@ describe('Toast', () => { clearFixture() }) - describe('VERSION', () => { - it('should return plugin version', () => { - expect(Toast.VERSION).toEqual(jasmine.any(String)) - }) - }) + describe('constructor', () => { + it('should create a Toast instance', () => { + fixtureEl.innerHTML = '
a toast
' + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - describe('DATA_KEY', () => { - it('should return plugin data key', () => { - expect(Toast.DATA_KEY).toEqual('bs.toast') + expect(toast).toBeInstanceOf(Toast) + expect(Toast.getInstance(toastEl)).toBe(toast) }) }) - describe('constructor', () => { - it('should take care of element either passed as a CSS selector or DOM element', () => { - fixtureEl.innerHTML = '
' + describe('show', () => { + it('should add .show class', () => { + fixtureEl.innerHTML = '
a toast
' const toastEl = fixtureEl.querySelector('.toast') - const toastBySelector = new Toast('.toast') - const toastByElement = new Toast(toastEl) - - expect(toastBySelector._element).toEqual(toastEl) - expect(toastByElement._element).toEqual(toastEl) - }) + const toast = new Toast(toastEl) - it('should allow to config in js', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl, { - delay: 1 - }) - - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') - resolve() - }) - - toast.show() - }) - }) + expect(toastEl).not.toHaveClass('show') - it('should close toast when close element with data-bs-dismiss attribute is set', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + toast.show() - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) + expect(toastEl).toHaveClass('show') + }) - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') + it('should trigger show and shown events', () => { + fixtureEl.innerHTML = '
a toast
' - const button = toastEl.querySelector('.btn-close') + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const showSpy = jasmine.createSpy('show') + const shownSpy = jasmine.createSpy('shown') - button.click() - }) + toastEl.addEventListener('show.bs.toast', showSpy) + toastEl.addEventListener('shown.bs.toast', shownSpy) - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - resolve() - }) + toast.show() - toast.show() - }) + expect(showSpy).toHaveBeenCalled() + expect(shownSpy).toHaveBeenCalled() }) - }) - describe('Default', () => { - it('should expose default setting to allow to override them', () => { - const defaultDelay = 1000 + it('should be preventable via show.bs.toast', () => { + fixtureEl.innerHTML = '
a toast
' - Toast.Default.delay = defaultDelay + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + toastEl.addEventListener('show.bs.toast', event => event.preventDefault()) - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) + toast.show() - expect(toast._config.delay).toEqual(defaultDelay) + expect(toastEl).not.toHaveClass('show') }) }) - describe('DefaultType', () => { - it('should expose default setting types for read', () => { - expect(Toast.DefaultType).toEqual(jasmine.any(Object)) - }) - }) + describe('hide', () => { + it('should remove .show class', () => { + fixtureEl.innerHTML = '
a toast
' - describe('show', () => { - it('should auto hide', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - resolve() - }) - - toast.show() - }) - }) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - it('should not add fade class', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).not.toHaveClass('fade') - resolve() - }) - - toast.show() - }) - }) + expect(toastEl).toHaveClass('show') - it('should not trigger shown if show is prevented', () => { - return new Promise((resolve, reject) => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - const assertDone = () => { - setTimeout(() => { - expect(toastEl).not.toHaveClass('show') - resolve() - }, 20) - } - - toastEl.addEventListener('show.bs.toast', event => { - event.preventDefault() - assertDone() - }) - - toastEl.addEventListener('shown.bs.toast', () => { - reject(new Error('shown event should not be triggered if show is prevented')) - }) - - toast.show() - }) - }) + toast.hide() - it('should clear timeout if toast is shown again before it is hidden', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toast._config.autohide = false - toastEl.addEventListener('shown.bs.toast', () => { - expect(spy).toHaveBeenCalled() - expect(toast._timeout).toBeNull() - resolve() - }) - toast.show() - }, toast._config.delay / 2) - - const spy = spyOn(toast, '_clearTimeout').and.callThrough() - - toast.show() - }) + expect(toastEl).not.toHaveClass('show') }) - it('should clear timeout if toast is interacted with mouse', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() - - setTimeout(() => { - spy.calls.reset() - - toastEl.addEventListener('mouseover', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - resolve() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() - }) - }) + it('should trigger hide and hidden events', () => { + fixtureEl.innerHTML = '
a toast
' - it('should clear timeout if toast is interacted with keyboard', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() - - setTimeout(() => { - spy.calls.reset() - - toastEl.addEventListener('focusin', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - resolve() - }) - - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }, toast._config.delay / 2) - - toast.show() - }) - }) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const hideSpy = jasmine.createSpy('hide') + const hiddenSpy = jasmine.createSpy('hidden') - it('should still auto hide after being interacted with mouse and keyboard', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) - - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) - - toastEl.addEventListener('mouseout', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) - - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).not.toBeNull() - resolve() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() - }) - }) + toastEl.addEventListener('hide.bs.toast', hideSpy) + toastEl.addEventListener('hidden.bs.toast', hiddenSpy) - it('should not auto hide if focus leaves but mouse pointer remains inside', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) - - toastEl.addEventListener('focusin', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) - - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).toBeNull() - resolve() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() - }) - }) + toast.hide() - it('should not auto hide if mouse pointer leaves but focus remains inside', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) - - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) - - toastEl.addEventListener('mouseout', () => { - expect(toast._timeout).toBeNull() - resolve() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() - }) + expect(hideSpy).toHaveBeenCalled() + expect(hiddenSpy).toHaveBeenCalled() }) - }) - describe('hide', () => { - it('should allow to hide toast manually', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() - }) - - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - resolve() - }) - - toast.show() - }) - }) + it('should be preventable via hide.bs.toast', () => { + fixtureEl.innerHTML = '
a toast
' - it('should do nothing when we call hide on a non shown toast', () => { - fixtureEl.innerHTML = '
' - - const toastEl = fixtureEl.querySelector('div') + const toastEl = fixtureEl.querySelector('.toast') const toast = new Toast(toastEl) - const spy = spyOn(toastEl.classList, 'contains') + toastEl.addEventListener('hide.bs.toast', event => event.preventDefault()) toast.hide() - expect(spy).toHaveBeenCalled() - }) - - it('should not trigger hidden if hide is prevented', () => { - return new Promise((resolve, reject) => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - const assertDone = () => { - setTimeout(() => { - expect(toastEl).toHaveClass('show') - resolve() - }, 20) - } - - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() - }) - - toastEl.addEventListener('hide.bs.toast', event => { - event.preventDefault() - assertDone() - }) - - toastEl.addEventListener('hidden.bs.toast', () => { - reject(new Error('hidden event should not be triggered if hide is prevented')) - }) - - toast.show() - }) + expect(toastEl).toHaveClass('show') }) }) - describe('dispose', () => { - it('should allow to destroy toast', () => { - fixtureEl.innerHTML = '
' - - const toastEl = fixtureEl.querySelector('div') + describe('dismiss button', () => { + it('should hide toast when dismiss button is clicked', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
a toast
', + '
' + ].join('') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const dismissBtn = fixtureEl.querySelector('[data-bs-dismiss="toast"]') - expect(Toast.getInstance(toastEl)).not.toBeNull() + expect(toastEl).toHaveClass('show') - toast.dispose() + dismissBtn.click() - expect(Toast.getInstance(toastEl)).toBeNull() + expect(toastEl).not.toHaveClass('show') }) - it('should allow to destroy toast and hide it before that', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should work with nested dismiss button', () => { + fixtureEl.innerHTML = [ + '
', + '
', + ' Title', + ' ', + '
', + '
a toast
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) - const expected = () => { - expect(toastEl).toHaveClass('show') - expect(Toast.getInstance(toastEl)).not.toBeNull() + const toastEl = fixtureEl.querySelector('.toast') + const dismissBtn = fixtureEl.querySelector('[data-bs-dismiss="toast"]') - toast.dispose() + dismissBtn.click() - expect(Toast.getInstance(toastEl)).toBeNull() - expect(toastEl).not.toHaveClass('show') + expect(toastEl).not.toHaveClass('show') + }) + }) - resolve() - } + describe('container with :has()', () => { + it('should have correct structure with toast-container', () => { + fixtureEl.innerHTML = [ + '
', + '
toast 1
', + '
toast 2
', + '
' + ].join('') - toastEl.addEventListener('shown.bs.toast', () => { - setTimeout(expected, 1) - }) + const container = fixtureEl.querySelector('.toast-container') + const toasts = fixtureEl.querySelectorAll('.toast') - toast.show() - }) + expect(container).not.toBeNull() + expect(toasts.length).toBe(2) }) - }) - describe('getInstance', () => { - it('should return a toast instance', () => { - fixtureEl.innerHTML = '
' + it('should support multiple toasts in a container', () => { + fixtureEl.innerHTML = [ + '
', + '
toast 1
', + '
toast 2
', + '
toast 3
', + '
' + ].join('') - const div = fixtureEl.querySelector('div') - const toast = new Toast(div) + const toasts = fixtureEl.querySelectorAll('.toast') - expect(Toast.getInstance(div)).toEqual(toast) - expect(Toast.getInstance(div)).toBeInstanceOf(Toast) - }) + for (const toastEl of toasts) { + new Toast(toastEl).show() + } - it('should return null when there is no toast instance', () => { - fixtureEl.innerHTML = '
' + expect(fixtureEl.querySelectorAll('.toast.show').length).toBe(3) - const div = fixtureEl.querySelector('div') + Toast.getInstance(toasts[0]).hide() - expect(Toast.getInstance(div)).toBeNull() + expect(fixtureEl.querySelectorAll('.toast.show').length).toBe(2) }) }) - describe('getOrCreateInstance', () => { - it('should return toast instance', () => { - fixtureEl.innerHTML = '
' + describe('CSS custom property --bs-toast-delay', () => { + it('should accept custom delay via style attribute', () => { + fixtureEl.innerHTML = '
a toast
' - const div = fixtureEl.querySelector('div') - const toast = new Toast(div) + const toastEl = fixtureEl.querySelector('.toast') + const computedDelay = toastEl.style.getPropertyValue('--bs-toast-delay') - expect(Toast.getOrCreateInstance(div)).toEqual(toast) - expect(Toast.getInstance(div)).toEqual(Toast.getOrCreateInstance(div, {})) - expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast) + expect(computedDelay).toBe('10s') }) - it('should return new instance when there is no toast instance', () => { - fixtureEl.innerHTML = '
' + it('should accept custom delay via setProperty', () => { + fixtureEl.innerHTML = '
a toast
' - const div = fixtureEl.querySelector('div') + const toastEl = fixtureEl.querySelector('.toast') + toastEl.style.setProperty('--bs-toast-delay', '3s') - expect(Toast.getInstance(div)).toBeNull() - expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast) + expect(toastEl.style.getPropertyValue('--bs-toast-delay')).toBe('3s') }) + }) - it('should return new instance when there is no toast instance with given configuration', () => { - fixtureEl.innerHTML = '
' + describe('accessibility', () => { + it('should have correct ARIA attributes', () => { + fixtureEl.innerHTML = [ + '' + ].join('') - const div = fixtureEl.querySelector('div') + const toastEl = fixtureEl.querySelector('.toast') - expect(Toast.getInstance(div)).toBeNull() - const toast = Toast.getOrCreateInstance(div, { - delay: 1 - }) - expect(toast).toBeInstanceOf(Toast) + expect(toastEl.getAttribute('role')).toBe('alert') + expect(toastEl.getAttribute('aria-live')).toBe('assertive') + expect(toastEl.getAttribute('aria-atomic')).toBe('true') + }) + }) + + describe('getOrCreateInstance', () => { + it('should return existing instance', () => { + fixtureEl.innerHTML = '
a toast
' - expect(toast._config.delay).toEqual(1) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + expect(Toast.getOrCreateInstance(toastEl)).toBe(toast) }) - it('should return the instance when exists without given configuration', () => { - fixtureEl.innerHTML = '
' + it('should create new instance if none exists', () => { + fixtureEl.innerHTML = '
a toast
' - const div = fixtureEl.querySelector('div') - const toast = new Toast(div, { - delay: 1 - }) - expect(Toast.getInstance(div)).toEqual(toast) + const toastEl = fixtureEl.querySelector('.toast') + const toast = Toast.getOrCreateInstance(toastEl) - const toast2 = Toast.getOrCreateInstance(div, { - delay: 2 - }) expect(toast).toBeInstanceOf(Toast) - expect(toast2).toEqual(toast) - - expect(toast2._config.delay).toEqual(1) }) }) }) diff --git a/js/tests/visual/toast.html b/js/tests/visual/toast.html index e64fd1d880fb..8eb6f2532169 100644 --- a/js/tests/visual/toast.html +++ b/js/tests/visual/toast.html @@ -5,13 +5,6 @@ Toast -
@@ -25,8 +18,8 @@

Toast Bootstrap Visual Test

-
- `} /> + +### With icon badges + +For a subtler look, pair a neutral toast background with small colored icon circles. The badge draws the eye to the status without overwhelming the message. + + + + + + + +
`} /> + +### Undo action + +Include an undo link next to the dismiss button so users can reverse recent actions. + + +
+
+ Message moved to trash. +
+ Undo + +
+ `} /> + +### Call to action + +Pair an icon badge with a heading, description, and action buttons for prompts that need user interaction. + + +
+
+
+ + + + +
+ New version available +
+ v6.0.1 includes performance improvements and bug fixes. +
+ + +
+
+ `} /> + +### With illustration + +Use a placeholder image alongside a call-to-action for onboarding or promotional toasts. + + +
+ +
+ Complete your profile + Add a photo and bio so teammates can recognize you. +
+ + +
+
+
+ `} /> + +### With progress bar + +Show task progress inside a toast using Bootstrap's [progress component]([[docsref:/components/progress]]). + + +
+ Importing data... + Step 3 of 4 + +
+
+
+ customers-2024.csv + 75% +
+
+
+
+
+ `} /> + ## Placement Place toasts with custom CSS as you need them. The top right is often used for notifications, as is the top middle. If you’re only ever going to show one toast at a time, put the positioning styles right on the `.toast`. @@ -266,17 +435,17 @@ Note that the live region needs to be present in the markup *before* the toast i You also need to adapt the `role` and `aria-live` level depending on the content. If it’s an important message like an error, use `role="alert" aria-live="assertive"`, otherwise use `role="status" aria-live="polite"` attributes. -As the content you’re displaying changes, be sure to update the [`delay` timeout](#options) so that users have enough time to read the toast. +As the content you're displaying changes, be sure to update the [`--bs-toast-delay`](#variables) value so that users have enough time to read the toast. ```html -`} /> -While technically it’s possible to add focusable/actionable controls (such as additional buttons or links) in your toast, you should avoid doing this for autohiding toasts. Even if you give the toast a long [`delay` timeout](#options), keyboard and assistive technology users may find it difficult to reach the toast in time to take action (since toasts don’t receive focus when they are displayed). If you absolutely must have further controls, we recommend using a toast with `autohide: false`. +While technically it's possible to add focusable/actionable controls (such as additional buttons or links) in your toast, you should avoid doing this for autohiding toasts. Even if you give the toast a long `--bs-toast-delay`, keyboard and assistive technology users may find it difficult to reach the toast in time to take action (since toasts don't receive focus when they are displayed). If you absolutely must have further controls, we recommend using a toast with a very long `--bs-toast-delay`. ## CSS @@ -304,58 +473,66 @@ While technically it’s possible to add focusable/actionable controls (such as ## Usage -Initialize toasts via JavaScript: +Show and hide toasts via the `Toast` class, which provides instance methods and custom events. + +### Show and hide ```js -const toastElList = document.querySelectorAll('.toast') -const toastList = [...toastElList].map(toastEl => new bootstrap.Toast(toastEl, option)) +const myToastEl = document.getElementById('myToast') +const toast = bootstrap.Toast.getOrCreateInstance(myToastEl) + +// Show a toast +toast.show() + +// Hide a toast +toast.hide() ``` -### Triggers +### Autohide delay - +Toasts automatically hide after `5s` by default. Customize the delay with the `--bs-toast-delay` CSS custom property: -### Options +```html + +
...
- + +
...
+``` - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `animation` | boolean | `true` | Apply a CSS fade transition to the toast. | -| `autohide` | boolean | `true` | Automatically hide the toast after the delay. | -| `delay` | number | `5000` | Delay in milliseconds before hiding the toast. | - +You can also set the delay programmatically: -### Methods +```js +document.getElementById('myToast').style.setProperty('--bs-toast-delay', '10s') +``` - +### Dismiss - -| Method | Description | -| --- | --- | -| `dispose` | Hides an element’s toast. Your toast will remain on the DOM but won’t show anymore. | -| `getInstance` | *Static* method which allows you to get the toast instance associated with a DOM element.
For example: `const myToastEl = document.getElementById('myToastEl')` `const myToast = bootstrap.Toast.getInstance(myToastEl)` Returns a Bootstrap toast instance. | -| `getOrCreateInstance` | *Static* method which allows you to get the toast instance associated with a DOM element, or create a new one, in case it wasn’t initialized.
`const myToastEl = document.getElementById('myToastEl')` `const myToast = bootstrap.Toast.getOrCreateInstance(myToastEl)` Returns a Bootstrap toast instance. | -| `hide` | Hides an element’s toast. **Returns to the caller before the toast has actually been hidden** (i.e. before the `hidden.bs.toast` event occurs). You have to manually call this method if you made `autohide` to `false`. | -| `isShown` | Returns a boolean according to toast’s visibility state. | -| `show` | Reveals an element’s toast. **Returns to the caller before the toast has actually been shown** (i.e. before the `shown.bs.toast` event occurs). You have to manually call this method, instead your toast won’t show. | -
+Dismissal is handled automatically via a delegated click handler. Add `data-bs-dismiss="toast"` to a button **within the toast**: + +```html + +``` + +### Pause on interaction + +Toasts pause their autohide countdown when hovered or when they contain a focused element (`:focus-within`). This is handled entirely via CSS `animation-play-state`. ### Events | Event | Description | | --- | --- | -| `hide.bs.toast` | This event is fired immediately when the `hide` instance method has been called. | -| `hidden.bs.toast` | This event is fired when the toast has finished being hidden from the user. | -| `show.bs.toast` | This event fires immediately when the `show` instance method is called. | -| `shown.bs.toast` | This event is fired when the toast has been made visible to the user. | +| `show.bs.toast` | Fires immediately when the `show` instance method is called. | +| `shown.bs.toast` | Fired when the toast has been made visible. | +| `hide.bs.toast` | Fires immediately when the `hide` instance method is called. | +| `hidden.bs.toast` | Fired when the toast has been hidden. | ```js const myToastEl = document.getElementById('myToast') + myToastEl.addEventListener('hidden.bs.toast', () => { - // do something... + // do something when the toast is hidden }) ```