diff --git a/src/components/alert/_alert.scss b/src/components/alert/_alert.scss
deleted file mode 100644
index 868649a57c0..00000000000
--- a/src/components/alert/_alert.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-.alert {
- &.fade-enter-active,
- &.alert.fade-leave-active {
- transition: $bv-alert-transition;
- }
-
- &.fade-enter,
- &.fade-leave-to {
- opacity: 0;
- }
-}
diff --git a/src/components/alert/alert.js b/src/components/alert/alert.js
index 8415bef1a11..0d8c72f30ed 100644
--- a/src/components/alert/alert.js
+++ b/src/components/alert/alert.js
@@ -1,5 +1,6 @@
import BButtonClose from '../button/button-close'
import { getComponentConfig } from '../../utils/config'
+import { requestAF } from '../../utils/dom'
const NAME = 'BAlert'
@@ -22,7 +23,7 @@ export default {
},
dismissLabel: {
type: String,
- default: 'Close'
+ default: () => getComponentConfig(NAME, 'dismissLabel')
},
show: {
type: [Boolean, Number],
@@ -36,28 +37,24 @@ export default {
data() {
return {
countDownTimerId: null,
- dismissed: false
- }
- },
- computed: {
- classObject() {
- return ['alert', this.alertVariant, this.dismissible ? 'alert-dismissible' : '']
- },
- alertVariant() {
- const variant = this.variant
- return `alert-${variant}`
- },
- localShow() {
- return !this.dismissed && (this.countDownTimerId || this.show)
+ dismissed: false,
+ localShow: this.show,
+ showClass: this.fade && this.show
}
},
watch: {
- show() {
- this.showChanged()
+ show(newVal) {
+ this.showChanged(newVal)
+ },
+ dismissed(newVal) {
+ if (newVal) {
+ this.localShow = false
+ this.$emit('dismissed')
+ }
}
},
mounted() {
- this.showChanged()
+ this.showChanged(this.show)
},
destroyed /* istanbul ignore next */() {
this.clearCounter()
@@ -65,15 +62,13 @@ export default {
methods: {
dismiss() {
this.clearCounter()
- this.dismissed = true
- this.$emit('dismissed')
- this.$emit('input', false)
if (typeof this.show === 'number') {
this.$emit('dismiss-count-down', 0)
this.$emit('input', 0)
} else {
this.$emit('input', false)
}
+ this.dismissed = true
},
clearCounter() {
if (this.countDownTimerId) {
@@ -81,17 +76,19 @@ export default {
this.countDownTimerId = null
}
},
- showChanged() {
+ showChanged(show) {
// Reset counter status
this.clearCounter()
// Reset dismiss status
this.dismissed = false
+ // Set localShow state
+ this.localShow = Boolean(show)
// No timer for boolean values
- if (this.show === true || this.show === false || this.show === null || this.show === 0) {
+ if (show === true || show === false || show === null || show === 0) {
return
}
// Start counter (ensure we have an integer value)
- let dismissCountDown = parseInt(this.show, 10) || 1
+ let dismissCountDown = parseInt(show, 10) || 1
this.countDownTimerId = setInterval(() => {
if (dismissCountDown < 1) {
this.dismiss()
@@ -101,30 +98,66 @@ export default {
this.$emit('dismiss-count-down', dismissCountDown)
this.$emit('input', dismissCountDown)
}, 1000)
+ },
+ onBeforeEnter() {
+ if (this.fade) {
+ // Add show class one frame after inserted, to make transitions work
+ requestAF(() => {
+ this.showClass = true
+ })
+ }
+ },
+ onBeforeLeave() /* istanbul ignore next: does not appear to be called in vue-test-utils */ {
+ this.showClass = false
}
},
render(h) {
- if (!this.localShow) {
- // If not showing, render placeholder
- return h(false)
- }
- let dismissBtn = h(false)
- if (this.dismissible) {
- // Add dismiss button
- dismissBtn = h(
- 'b-button-close',
- { attrs: { 'aria-label': this.dismissLabel }, on: { click: this.dismiss } },
- [this.$slots.dismiss]
+ const $slots = this.$slots
+ let $alert = h(false)
+ if (this.localShow) {
+ let $dismissBtn = h(false)
+ if (this.dismissible) {
+ $dismissBtn = h(
+ 'b-button-close',
+ { attrs: { 'aria-label': this.dismissLabel }, on: { click: this.dismiss } },
+ [$slots.dismiss]
+ )
+ }
+ $alert = h(
+ 'div',
+ {
+ staticClass: 'alert',
+ class: {
+ fade: this.fade,
+ show: this.showClass,
+ 'alert-dismissible': this.dismissible,
+ [`alert-${this.variant}`]: this.variant
+ },
+ attrs: { role: 'alert', 'aria-live': 'polite', 'aria-atomic': true }
+ },
+ [$dismissBtn, $slots.default]
)
+ $alert = [$alert]
}
- const alert = h(
- 'div',
+ return h(
+ 'transition',
{
- class: this.classObject,
- attrs: { role: 'alert', 'aria-live': 'polite', 'aria-atomic': true }
+ props: {
+ mode: 'out-in',
+ // Disable use of built-in transition classes
+ 'enter-class': '',
+ 'enter-active-class': '',
+ 'enter-to-class': '',
+ 'leave-class': 'show',
+ 'leave-active-class': '',
+ 'leave-to-class': ''
+ },
+ on: {
+ beforeEnter: this.onBeforeEnter,
+ beforeLeave: this.onBeforeLeave
+ }
},
- [dismissBtn, this.$slots.default]
+ $alert
)
- return !this.fade ? alert : h('transition', { props: { name: 'fade', appear: true } }, [alert])
}
}
diff --git a/src/components/alert/alert.spec.js b/src/components/alert/alert.spec.js
index e049e2f8294..997911ee6d8 100644
--- a/src/components/alert/alert.spec.js
+++ b/src/components/alert/alert.spec.js
@@ -1,75 +1,275 @@
-import { loadFixture, testVM, nextTick, setData } from '../../../tests/utils'
+import Alert from './alert'
+import { mount } from '@vue/test-utils'
describe('alert', () => {
- jest.useFakeTimers()
+ it('hidden alert renders comment node', async () => {
+ const wrapper = mount(Alert)
+ expect(wrapper.isVueInstance()).toBe(true)
+ await wrapper.vm.$nextTick()
+ expect(wrapper.isEmpty()).toBe(true)
+ expect(wrapper.html()).not.toBeDefined()
- beforeEach(loadFixture(__dirname, 'alert'))
- testVM()
+ wrapper.destroy()
+ })
+
+ it('visible alert has default class names and attributes', async () => {
+ const wrapper = mount(Alert, {
+ propsData: {
+ show: true
+ }
+ })
+ expect(wrapper.is('div')).toBe(true)
+
+ await wrapper.vm.$nextTick()
+ await new Promise(resolve => requestAnimationFrame(resolve))
- it('visible alerts have class names', async () => {
- const { app } = window
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.classes()).toContain('alert')
+ expect(wrapper.classes()).toContain('alert-info')
+ expect(wrapper.classes()).not.toContain('fade')
+ expect(wrapper.classes()).not.toContain('show')
- expect(app.$refs.default_alert).toHaveClass('alert alert-info')
- expect(app.$refs.success_alert).toHaveClass('alert alert-success')
+ expect(wrapper.attributes('role')).toBe('alert')
+ expect(wrapper.attributes('aria-live')).toBe('polite')
+ expect(wrapper.attributes('aria-atomic')).toBe('true')
+
+ wrapper.destroy()
})
- it('show prop set to true displays hidden alert', async () => {
- const { app } = window
+ it('visible alert has variant when prop variant is set', async () => {
+ const wrapper = mount(Alert, {
+ propsData: {
+ show: true,
+ variant: 'success'
+ }
+ })
+ expect(wrapper.is('div')).toBe(true)
+
+ await wrapper.vm.$nextTick()
- // Default is hidden
- expect(app.$el.textContent).not.toContain('Dismissible Alert!')
+ expect(wrapper.is('div')).toBe(true)
+ expect(wrapper.classes()).toContain('alert')
+ expect(wrapper.classes()).toContain('alert-success')
+ expect(wrapper.attributes('role')).toBe('alert')
+ expect(wrapper.attributes('aria-live')).toBe('polite')
+ expect(wrapper.attributes('aria-atomic')).toBe('true')
- // Make visible by changing visible state
- await setData(app, 'showDismissibleAlert', true)
- expect(app.$el.textContent).toContain('Dismissible Alert!')
+ wrapper.destroy()
})
- it('dismiss should have class alert-dismissible', async () => {
- const { app } = window
- const alert = app.$refs.success_alert
- expect(alert).toHaveClass('alert-dismissible')
+ it('renders content from default slot', async () => {
+ const wrapper = mount(Alert, {
+ propsData: {
+ show: true
+ },
+ slots: {
+ default: '