Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 11 additions & 20 deletions js/src/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import BaseComponent from './base-component.js'
import EventHandler, { type BootstrapEvent } from './dom/event-handler.js'
import { enableDismissTrigger } from './util/component-functions.js'
import { reflow } from './util/index.js'

/**
* Constants
Expand All @@ -27,25 +26,20 @@ 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_INSTANT = 'toast-instant'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_SHOWING = 'showing'

type ToastConfig = {
animation: boolean
autohide: boolean
delay: number
}

const DefaultType = {
animation: 'boolean',
autohide: 'boolean',
delay: 'number'
}

const Default: ToastConfig = {
animation: true,
autohide: true,
delay: 5000
}
Expand Down Expand Up @@ -92,22 +86,15 @@ class Toast extends BaseComponent {

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._element.classList.add(CLASS_NAME_SHOW)

await this._queueCallback(complete, this._element, this._config.animation)
await this._queueCallback(complete, this._element, this._isAnimated())
}

async hide(): Promise<void> {
Expand All @@ -122,13 +109,13 @@ class Toast extends BaseComponent {
}

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)
await this._queueCallback(complete, this._element, this._config.animation)
// Removing .show starts the fade-out. The discrete `display` transition
// keeps the toast laid out until the fade finishes.
this._element.classList.remove(CLASS_NAME_SHOW)
await this._queueCallback(complete, this._element, this._isAnimated())
}

override dispose(): void {
Expand All @@ -146,6 +133,10 @@ class Toast extends BaseComponent {
}

// Private
protected _isAnimated(): boolean {
return !this._element.classList.contains(CLASS_NAME_INSTANT)
}

protected _maybeScheduleHide(): void {
if (!this._config.autohide) {
return
Expand Down
62 changes: 41 additions & 21 deletions js/tests/unit/toast.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('Toast', () => {
it('should close toast when close element with data-bs-dismiss attribute is set', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">',
'<div class="toast toast-instant" data-bs-delay="1" data-bs-autohide="false">',
' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
'</div>'
].join('')
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('Toast', () => {
Toast.Default.delay = defaultDelay

fixtureEl.innerHTML = [
'<div class="toast" data-bs-autohide="false" data-bs-animation="false">',
'<div class="toast toast-instant" data-bs-autohide="false">',
' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
'</div>'
].join('')
Expand Down Expand Up @@ -139,32 +139,31 @@ describe('Toast', () => {
})
})

it('should not add fade class', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')
it('should trigger shown synchronously when the toast is instant', () => {
fixtureEl.innerHTML = [
'<div class="toast toast-instant" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')

const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = jasmine.createSpy('shown')

toastEl.addEventListener('shown.bs.toast', () => {
expect(toastEl).not.toHaveClass('fade')
resolve()
})
toastEl.addEventListener('shown.bs.toast', spy)

toast.show()
})
toast.show()

expect(spy).toHaveBeenCalled()
expect(toastEl).toHaveClass('show')
})

it('should not trigger shown if show is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
'<div class="toast toast-instant" data-bs-delay="1">',
' <div class="toast-body">',
' a simple toast',
' </div>',
Expand Down Expand Up @@ -437,6 +436,27 @@ describe('Toast', () => {
})
})

it('should trigger hidden synchronously when the toast is instant', () => {
fixtureEl.innerHTML = [
'<div class="toast toast-instant show" data-bs-autohide="false">',
' <div class="toast-body">',
' a simple toast',
' </div>',
'</div>'
].join('')

const toastEl = fixtureEl.querySelector('.toast')
const toast = new Toast(toastEl)
const spy = jasmine.createSpy('hidden')

toastEl.addEventListener('hidden.bs.toast', spy)

toast.hide()

expect(spy).toHaveBeenCalled()
expect(toastEl).not.toHaveClass('show')
})

it('should do nothing when we call hide on a non shown toast', () => {
fixtureEl.innerHTML = '<div></div>'

Expand All @@ -453,7 +473,7 @@ describe('Toast', () => {
it('should not trigger hidden if hide is prevented', () => {
return new Promise((resolve, reject) => {
fixtureEl.innerHTML = [
'<div class="toast" data-bs-delay="1" data-bs-animation="false">',
'<div class="toast toast-instant" data-bs-delay="1">',
' <div class="toast-body">',
' a simple toast',
' </div>',
Expand Down
26 changes: 22 additions & 4 deletions scss/_toasts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@use "functions" as *;
@use "mixins/border-radius" as *;
@use "mixins/tokens" as *;
@use "mixins/transition" as *;

$toast-tokens: () !default;

Expand All @@ -25,6 +26,8 @@ $toast-tokens: defaults(
--toast-header-color: var(--fg-3),
--toast-header-bg: var(--bg-1),
--toast-header-border-color: var(--border-color-translucent),
--toast-transition-duration: .15s,
--toast-transition-timing: linear,
),
$toast-tokens
);
Expand All @@ -35,7 +38,7 @@ $toast-tokens: defaults(
.toast {
@include tokens($toast-tokens);

display: flex;
display: none;
flex-direction: column;
width: var(--toast-max-width);
max-width: 100%;
Expand All @@ -49,12 +52,27 @@ $toast-tokens: defaults(
box-shadow: var(--toast-box-shadow);
@include border-radius(var(--toast-border-radius, var(--radius-7)));

&.showing {
// Animated variant (default). `display` transitions discretely so the toast
// stays laid out until the fade-out finishes. Add .toast-instant to skip it.
&:not(.toast-instant) {
opacity: 0;
@include transition(
opacity var(--toast-transition-duration) var(--toast-transition-timing),
display var(--toast-transition-duration) allow-discrete
);
}

&:not(.show) {
display: none;
&.show {
display: flex;
opacity: 1;
}
}

// The toast is not rendered before .show lands, so the fade-in needs an
// explicit starting state — the base opacity above cannot serve as one.
@starting-style {
.toast:not(.toast-instant).show {
opacity: 0;
}
}

Expand Down
18 changes: 16 additions & 2 deletions site/src/content/docs/components/toasts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,22 @@ Alternatively, you can also add additional controls and components to toasts.
</div>
</div>`} />

### Instant

By default, toasts fade in and out. To disable the animation, add `.toast-instant` to the toast. The `show` and `hide` methods then finish immediately, so `shown.bs.toast` and `hidden.bs.toast` fire right away.

<Example class="bg-1" code={`<div class="toast toast-instant" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
<strong class="me-auto">Bootstrap</strong>
<small>11 mins ago</small>
<CloseButton dismiss="toast" />
</div>
<div class="toast-body">
This toast appears and disappears instantly.
</div>
</div>`} />

## 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`.
Expand Down Expand Up @@ -375,7 +391,6 @@ const toastList = [...toastElList].map(toastEl => new bootstrap.Toast(toastEl, o
| `data-bs-dismiss="toast"` | On a close control, dismisses the toast when activated. |
| `data-bs-autohide` | Whether the toast hides automatically after the delay. |
| `data-bs-delay` | Time in milliseconds before hiding (when autohide is enabled). |
| `data-bs-animation` | Whether to use the fade transition when showing and hiding. |
</BsTable>

### Triggers
Expand All @@ -393,7 +408,6 @@ const toastList = [...toastElList].map(toastEl => new bootstrap.Toast(toastEl, o
<BsTable>
| 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. |
</BsTable>
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/customize/optimize.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export default {
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: {
// Classes added at runtime by Bootstrap's JS that never appear in your HTML
standard: ['show', 'showing', 'collapsing', 'active', 'fade', 'dialog-open'],
standard: ['show', 'collapsing', 'active', 'fade', 'dialog-open'],
// Keep component families that are generated or toggled dynamically
greedy: [/^carousel/, /^menu/, /^drawer/, /^dialog/, /^tooltip/, /^popover/]
}
Expand Down
5 changes: 5 additions & 0 deletions site/src/content/docs/guides/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ Bootstrap 6 is a major release with many breaking changes to modernize our codeb
- **New `.drawer-sheet` variant** for flush-to-edge panels with no inset, border-radius, or shadow.
- **Swipe-to-dismiss** gesture support on touch devices for Drawer components. Drawers automatically detect their placement and dismiss on the appropriate swipe direction.
- **Dialog and Drawer share `DialogBase`** — the show/hide/toggle lifecycle, keyboard handling, backdrop clicks, and static backdrop bounce are consolidated in a shared base class.
- **Toast transitions moved to CSS.** The fade now uses `@starting-style` with a discrete `display` transition, so the component no longer toggles helper classes to drive the animation:
- Removed the `animation` option and `data-bs-animation`. Add `.toast-instant` to skip the animation, matching `.dialog-instant` and `.drawer-instant`.
- Removed the `.showing` class and the deprecated `.hide` class. Only `.show` is toggled now. Replace any CSS or tests that depend on them.
- Toasts no longer get the generic `.fade` class. The transition lives on `.toast` itself, tuned with `--toast-transition-duration` and `--toast-transition-timing`.
- `isShown()` returns `false` as soon as `hide()` is called, rather than when the fade-out ends.
- **Reworked button variants.** The v5 per-color classes like `.btn-primary`, `.btn-outline-primary`, `.btn-secondary`, etc. are replaced by a composition of variant + theme classes:
- `.btn-primary` &rarr; `.btn-solid .theme-primary`
- `.btn-outline-primary` &rarr; `.btn-outline .theme-primary`
Expand Down