Skip to content

Commit 0b44e4d

Browse files
authored
fix(toaster): let on-demand toasts know the toaster has been destroyed (#3130)
1 parent be53376 commit 0b44e4d

File tree

7 files changed

+104
-26
lines changed

7 files changed

+104
-26
lines changed

src/_variables.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ $b-toaster-offset-top: 0.5rem !default;
1010
$b-toaster-offset-bottom: $b-toaster-offset-top !default;
1111
$b-toaster-offset-left: $b-toaster-offset-top !default;
1212
$b-toaster-offset-right: $b-toaster-offset-top !default;
13+
$b-toaster-max-width: $toast-max-width !default;
14+
// Make toasts fit within small screens
15+
$b-toaster-min-width: calc(#{min(320px, $toast-max-width)} - #{$b-toaster-offset-left + $b-toaster-offset-right}) !default;
1316

1417
$b-toast-bg-level: $alert-bg-level !default;
1518
$b-toast-border-level: $alert-border-level !default;

src/components/toast/README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ contain minimal, to-the-point, non-interactive content.
1313
<p class="alert alert-warning mb-0" role="alert">
1414
<strong>BETA warning</strong><br>
1515
Toasts are in their preliminary stages of being developed,
16-
and usage is subject to change in future releases.
16+
and usage and custom CSS is subject to change in future releases.
1717
</p>
1818

1919
## Overview
@@ -185,9 +185,15 @@ SCSS):
185185

186186
<script>
187187
export default {
188+
data() {
189+
return {
190+
counter: 0
191+
}
192+
},
188193
methods: {
189194
toast(toaster) {
190-
this.$bvToast.toast('Toast body content', {
195+
this.counter++
196+
this.$bvToast.toast(`Toast ${this.counter} body content`, {
191197
title: `Toaster ${toaster}`,
192198
toaster: toaster,
193199
solid: true
@@ -205,11 +211,15 @@ document, stacked and not positioned (appended to `<body>` inside a `<b-toaster>
205211
and ID set to the toaster target name). The only default styling the toaster will have is
206212
`position: fixed;`, a `max-width` and a `z-index` of `1100`.
207213

214+
Avoid using both `b-toaster-top-left` and `b-toaster-top-right`, or `b-toaster-bottom-left` and
215+
`b-toaster-bottom-right`, at the same time in your app as notifications could be obscured on small
216+
screens (i.e. `xs`).
217+
208218
### Prepend and append
209219

210220
Toasts default to prepending themselves to the top of the toasts shown in the specified toaster in
211221
the order they were created. To append new toasts to the bottom, set the `append-toast` prop to
212-
`true`
222+
`true`.
213223

214224
### Auto-hide
215225

@@ -220,7 +230,7 @@ to `true`.
220230
### Toast roles
221231

222232
Toasts are rendered with a default `role` attribute of `'alert'` and `aria-live` attribute of
223-
`'assertive'`. for toasts that are meant for a casual notification, set the `is-status` prop to
233+
`'assertive'`. For toasts that are meant for a casual notification, set the `is-status` prop to
224234
`true`, which will change the `role` and `aria-live` attributes to `'status'` and `'polite'`
225235
respectively.
226236

src/components/toast/_toaster.scss

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
// --- <b-toaster> custom SCSS ---
22

3-
// Configured "toasters".
3+
// Configured "toasters":
44
// The following are columnar toasters (vertically stacked)
5+
// and fixed within the viewport
56
// b-toaster-top-right
67
// b-toaster-top-left
78
// b-toaster-bottom-right
89
// b-toaster-bottom-left
910
//
10-
// Note: This may change to using a flex layout
11-
//
11+
// NOTE:
12+
// This SCSS is prelimiary, and may change in the future.
13+
// We may add an inner wrapper inside .b-toast (a .b-toaster-slot)
14+
// To allow for better customization of positional styling.
1215

1316
.b-toaster {
1417
position: fixed;
15-
max-width: $toast-max-width;
18+
min-width: $b-toaster-min-width;
19+
max-width: $b-toaster-max-width;
1620
overflow: visible;
1721
z-index: $b-toaster-zindex;
1822

1923
// Ensure that an empty toaster isn't occupying any space
2024
// and obscuring access to underlying elements
2125
&:empty {
2226
padding: 0 0;
27+
margin: 0 0;
28+
display: none;
2329
}
2430

2531
&.b-toaster-top-right,
@@ -33,14 +39,12 @@
3339
}
3440

3541
&.b-toaster-top-right,
36-
&.b-toaster-bottom-right {
42+
&.b-toaster-bottom-right {
3743
right: $b-toaster-offset-right;
38-
min-width: calc(#{$toast-max-width} - #{$b-toaster-offset-right * 2});
3944
}
4045

4146
&.b-toaster-top-left,
42-
&.b-toaster-bottom-left {
47+
&.b-toaster-bottom-left {
4348
left: $b-toaster-offset-left;
44-
min-width: calc(#{$toast-max-width} - #{$b-toaster-offset-left * 2});
4549
}
4650
}

src/components/toast/helpers/bv-toast.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ const BToastPop = Vue.extend({
8080
this.$parent.$once('hook:destroyed', handleDestroy)
8181
// Self destruct after hidden
8282
this.$once('hidden', handleDestroy)
83+
// Self destruct when toaster is destroyed
84+
this.listenOnRoot('bv::toaster::destroyed', toaster => {
85+
if (toaster === self.toaster) {
86+
handleDestroy()
87+
}
88+
})
8389
}
8490
})
8591

src/components/toast/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"events": [
1414
{
1515
"event": "change",
16-
"description": "Toast visibility state. Updates the v-modal.",
16+
"description": "Toast visibility state. Used to update the v-model.",
1717
"args": [
1818
{
1919
"arg": "visible",

src/components/toast/toast.js

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Vue from '../../utils/vue'
2-
import { Portal } from 'portal-vue'
2+
import { Portal, Wormhole } from 'portal-vue'
33
import BvEvent from '../../utils/bv-event.class'
44
import { getComponentConfig } from '../../utils/config'
55
import { getById, requestAF } from '../../utils/dom'
@@ -171,9 +171,20 @@ export default Vue.extend({
171171
newVal ? this.show() : this.hide()
172172
},
173173
localShow(newVal) {
174-
if (newVal !== this.show) {
174+
if (newVal !== this.visible) {
175175
this.$emit('change', newVal)
176176
}
177+
},
178+
toaster(newVal) {
179+
// If toaster target changed, make sure toaster exists
180+
this.$nextTick(() => this.ensureToaster)
181+
},
182+
static(newVal) {
183+
// If static changes to true, and the toast is showing,
184+
// ensure the toaster target exists
185+
if (newVal && this.localShow) {
186+
this.ensureToaster()
187+
}
177188
}
178189
},
179190
mounted() {
@@ -185,16 +196,24 @@ export default Vue.extend({
185196
})
186197
}
187198
})
188-
this.listenOnRoot('bv::show:toast', id => {
199+
// Listen for global $root show events
200+
this.listenOnRoot('bv::show::toast', id => {
189201
if (id === this.id) {
190202
this.show()
191203
}
192204
})
193-
this.listenOnRoot('bv::hide:toast', id => {
205+
// Listen for global $root hide events
206+
this.listenOnRoot('bv::hide::toast', id => {
194207
if (!id || id === this.id) {
195208
this.hide()
196209
}
197210
})
211+
// Make sure we hide when toaster is destroyed
212+
this.listenOnRoot('bv::toaster::destroyed', toaster => {
213+
if (toaster === this.toaster) {
214+
this.hide()
215+
}
216+
})
198217
},
199218
beforeDestroy() {
200219
this.clearDismissTimer()
@@ -234,7 +253,10 @@ export default Vue.extend({
234253
this.$emit(type, bvEvt)
235254
},
236255
ensureToaster() {
237-
if (!getById(this.toaster)) {
256+
if (this.static) {
257+
return
258+
}
259+
if (!getById(this.toaster) && !Wormhole.hasTarget(this.toaster)) {
238260
const div = document.createElement('div')
239261
document.body.append(div)
240262
const toaster = new BToaster({

src/components/toast/toaster.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Vue from '../../utils/vue'
22
import { PortalTarget, Wormhole } from 'portal-vue'
33
import warn from '../../utils/warn'
4-
import { getById } from '../../utils/dom'
4+
import { getById, removeClass, requestAF } from '../../utils/dom'
55

66
/* istanbul ignore file: for now until ready for testing */
77

@@ -37,9 +37,35 @@ export const props = {
3737

3838
// @vue/component
3939
export const DefaultTransition = Vue.extend({
40-
functional: true,
41-
render(h, { children }) {
42-
return h('transition-group', { props: { tag: 'div', name: 'b-toaster' } }, children)
40+
// functional: true,
41+
// render(h, { children }) {
42+
// return h('transition-group', { props: { tag: 'div', name: 'b-toaster' } }, children)
43+
data() {
44+
return {
45+
// Transition classes base name
46+
name: 'b-toaster'
47+
}
48+
},
49+
methods: {
50+
onAfterEnter(el) {
51+
// Handle bug where enter-to class is not removed.
52+
// Bug is related to portal-vue and transition-groups.
53+
requestAF(() => {
54+
removeClass(el, `${this.name}-enter-to`)
55+
// The *-move class is also stuck on elements that moved,
56+
// but there are no javascript hooks to handle after move.
57+
})
58+
}
59+
},
60+
render(h) {
61+
return h(
62+
'transition-group',
63+
{
64+
props: { tag: 'div', name: this.name },
65+
on: { afterEnter: this.onAfterEnter }
66+
},
67+
this.$slots.default
68+
)
4369
}
4470
})
4571

@@ -50,15 +76,22 @@ export default Vue.extend({
5076
data() {
5177
return {
5278
// We don't render on SSR or if a an existing target found
53-
doRender: false
79+
doRender: false,
80+
dead: false
5481
}
5582
},
5683
beforeMount() {
5784
/* istanbul ignore if */
58-
if (getById(this.name) || Wormhole.targets[this.name]) {
59-
warn(`b-toaster: A <portal-target> name '${this.name}' already exists in the document.`)
85+
if (getById(this.name) || Wormhole.hasTarget(this.name)) {
86+
warn(`b-toaster: A <portal-target> with name '${this.name}' already exists in the document.`)
87+
this.dead = true
6088
} else {
6189
this.doRender = true
90+
this.$once('hook:beforeDestroy', () => {
91+
// Let toasts made with `this.$bvToast.toast()` know that this toaster
92+
// is being destroyed and should should also destroy/hide themselves
93+
this.$root.$emit('bv::toaster::destroyed', this.name)
94+
})
6295
}
6396
},
6497
destroyed() {
@@ -68,7 +101,7 @@ export default Vue.extend({
68101
}
69102
},
70103
render(h) {
71-
let $target = h('div', { class: 'd-none' })
104+
let $target = h('div', { class: ['d-none', { 'b-dead-toaster': this.dead }] })
72105
if (this.doRender) {
73106
$target = h(PortalTarget, {
74107
staticClass: 'b-toaster',

0 commit comments

Comments
 (0)