Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit ebbdca3

Browse files
committed
feat(Balert): adapt close button for better customization.
BREAKING-CHANGE: rename prop `dismissLabel` to `closeLabel`. Remove `closeContent`. Emit `close` instead of `closed`. (unify props and namings)
1 parent d1a811c commit ebbdca3

File tree

2 files changed

+144
-70
lines changed

2 files changed

+144
-70
lines changed

packages/bootstrap-vue-next/src/components/BAlert/BAlert.vue

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
>
1313
<slot />
1414
<template v-if="dismissibleBoolean">
15-
<!-- TODO this renders incorrectly -->
16-
<BButton v-if="hasCloseSlot || closeContent" v-bind="closeAttrs" @click="closeClicked">
17-
<slot name="close">
18-
{{ closeContent }}
19-
</slot>
15+
<BButton v-if="hasCloseSlot" v-bind="closeAttrs" @click="hide">
16+
<slot name="close" />
2017
</BButton>
21-
<BCloseButton v-else :aria-label="dismissLabel" v-bind="closeAttrs" @click="closeClicked" />
18+
<BCloseButton
19+
v-else
20+
ref="closeButton"
21+
:white="closeWhite"
22+
v-bind="closeAttrs"
23+
@click="hide"
24+
/>
2225
</template>
2326
</div>
2427
</BTransition>
@@ -28,32 +31,34 @@
2831
import BTransition from '../BTransition/BTransition.vue'
2932
import BCloseButton from '../BButton/BCloseButton.vue'
3033
import BButton from '../BButton/BButton.vue'
31-
import type {Booleanish, ButtonType, ButtonVariant, ColorVariant} from '../../types'
34+
import type {Booleanish, ButtonVariant, ClassValue, ColorVariant} from '../../types'
3235
import {computed, onBeforeUnmount, useSlots, watchEffect} from 'vue'
3336
import {useBooleanish, useCountdown} from '../../composables'
3437
import {isEmptySlot} from '../../utils'
3538
import {useVModel} from '@vueuse/core'
3639
3740
const props = withDefaults(
3841
defineProps<{
42+
closeVariant?: ButtonVariant | null
43+
closeClass?: ClassValue
44+
closeLabel?: string
45+
closeWhite?: Booleanish
3946
noHoverPause?: Booleanish
40-
dismissLabel?: string
4147
dismissible?: Booleanish
4248
fade?: Booleanish
43-
closeVariant?: ButtonVariant | null
4449
modelValue?: boolean | number
4550
variant?: ColorVariant | null
46-
closeContent?: string
4751
immediate?: Booleanish
4852
interval?: number
4953
showOnPause?: Booleanish
5054
}>(),
5155
{
52-
closeContent: undefined,
5356
closeVariant: 'secondary',
57+
closeClass: undefined,
58+
closeLabel: 'Close',
59+
closeWhite: false,
5460
noHoverPause: false,
5561
interval: 1000,
56-
dismissLabel: 'Close',
5762
dismissible: false,
5863
fade: false,
5964
modelValue: false,
@@ -64,7 +69,7 @@ const props = withDefaults(
6469
)
6570
6671
const emit = defineEmits<{
67-
'closed': []
72+
'close': []
6873
'close-countdown': [value: number]
6974
'update:modelValue': [value: boolean | number]
7075
}>()
@@ -97,6 +102,8 @@ const computedClasses = computed(() => ({
97102
'alert-dismissible': dismissibleBoolean.value,
98103
}))
99104
105+
const closeClasses = computed(() => [props.closeClass, {'btn-close-custom': hasCloseSlot.value}])
106+
100107
const {
101108
isActive,
102109
pause,
@@ -116,20 +123,22 @@ const isAlertVisible = computed<boolean>(() =>
116123
)
117124
118125
const closeAttrs = computed(() => ({
119-
variant: props.closeVariant,
120-
type: 'button' as ButtonType,
126+
'variant': hasCloseSlot.value ? props.closeVariant : null,
127+
'class': closeClasses.value,
128+
'aria-label': props.closeLabel,
121129
}))
122130
123131
watchEffect(() => emit('close-countdown', remainingMs.value))
124132
125-
const closeClicked = (): void => {
133+
const hide = (): void => {
134+
emit('close')
135+
126136
if (typeof modelValue.value === 'boolean') {
127137
modelValue.value = false
128138
} else {
129139
modelValue.value = 0
130140
stop()
131141
}
132-
emit('closed')
133142
}
134143
135144
// TODO mouseleave/mouseenter could be replaced with useElementHover with a watcher
@@ -142,3 +151,13 @@ onBeforeUnmount(stop)
142151
143152
defineExpose({pause, resume, restart, stop})
144153
</script>
154+
155+
<style lang="scss" scoped>
156+
.btn-close-custom {
157+
position: absolute;
158+
top: 0;
159+
right: 0;
160+
z-index: 2;
161+
margin: var(--bs-alert-padding-y) var(--bs-alert-padding-x);
162+
}
163+
</style>

packages/bootstrap-vue-next/src/components/BAlert/alert.spec.ts

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -156,24 +156,6 @@ describe('alert', () => {
156156
expect($bclosebutton.exists()).toBe(false)
157157
})
158158

159-
it('nested div does not have BCloseButton when prop dismissible but also prop closeContent', () => {
160-
const wrapper = mount(BAlert, {
161-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
162-
})
163-
const $div = wrapper.get('div')
164-
const $bclosebutton = $div.findComponent(BCloseButton)
165-
expect($bclosebutton.exists()).toBe(false)
166-
})
167-
168-
it('nested div has BButton when prop dismissible but also prop closeContent', () => {
169-
const wrapper = mount(BAlert, {
170-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
171-
})
172-
const $div = wrapper.get('div')
173-
const $bbutton = $div.findComponent(BButton)
174-
expect($bbutton.exists()).toBe(true)
175-
})
176-
177159
it('nested div has BButton when prop dismissible but also slot close', () => {
178160
const wrapper = mount(BAlert, {
179161
props: {modelValue: true, dismissible: true},
@@ -193,60 +175,52 @@ describe('alert', () => {
193175
expect($bbutton.exists()).toBe(false)
194176
})
195177

196-
it('nested div BButton has static attr type to be button', () => {
197-
const wrapper = mount(BAlert, {
198-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
199-
})
200-
const $div = wrapper.get('div')
201-
const $bbutton = $div.getComponent(BButton)
202-
expect($bbutton.attributes('type')).toBe('button')
203-
})
204-
205-
it('nested div BButton renders prop closeContent', () => {
178+
it('nested div BButton renders slot close', () => {
206179
const wrapper = mount(BAlert, {
207-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
180+
props: {modelValue: true, dismissible: true},
181+
slots: {close: 'foobar'},
208182
})
209183
const $div = wrapper.get('div')
210184
const $bbutton = $div.getComponent(BButton)
211185
expect($bbutton.text()).toBe('foobar')
212186
})
213187

214-
it('nested div BButton renders slot close', () => {
188+
it('nested div BCloseButton has aria-label to be Close by default', () => {
215189
const wrapper = mount(BAlert, {
216190
props: {modelValue: true, dismissible: true},
217-
slots: {close: 'foobar'},
218191
})
219192
const $div = wrapper.get('div')
220-
const $bbutton = $div.getComponent(BButton)
221-
expect($bbutton.text()).toBe('foobar')
193+
const $bclosebutton = $div.getComponent(BCloseButton)
194+
expect($bclosebutton.attributes('aria-label')).toBe('Close')
222195
})
223196

224-
it('nested div BButton renders slot over props', () => {
197+
it('nested div BButton has aria-label to be Close by default', () => {
225198
const wrapper = mount(BAlert, {
226-
props: {modelValue: true, dismissible: true, closeContent: 'props'},
227-
slots: {close: 'slots'},
199+
props: {modelValue: true, dismissible: true},
200+
slots: {close: 'foobar'},
228201
})
229202
const $div = wrapper.get('div')
230203
const $bbutton = $div.getComponent(BButton)
231-
expect($bbutton.text()).toBe('slots')
204+
expect($bbutton.attributes('aria-label')).toBe('Close')
232205
})
233206

234-
it('nested div BCloseButton has aria-label to be Close by default', () => {
207+
it('nested div BCloseButton has aria-label to be prop closeLabel', () => {
235208
const wrapper = mount(BAlert, {
236-
props: {modelValue: true, dismissible: true},
209+
props: {modelValue: true, dismissible: true, closeLabel: 'foobar'},
237210
})
238211
const $div = wrapper.get('div')
239212
const $bclosebutton = $div.getComponent(BCloseButton)
240-
expect($bclosebutton.attributes('aria-label')).toBe('Close')
213+
expect($bclosebutton.attributes('aria-label')).toBe('foobar')
241214
})
242215

243-
it('nested div BCloseButton has aria-label to be prop dismissLabel', () => {
216+
it('nested div BButton has aria-label to be prop closeLabel', () => {
244217
const wrapper = mount(BAlert, {
245-
props: {modelValue: true, dismissible: true, dismissLabel: 'foobar'},
218+
props: {modelValue: true, dismissible: true, closeLabel: 'foobar'},
219+
slots: {close: 'foobar'},
246220
})
247221
const $div = wrapper.get('div')
248-
const $bclosebutton = $div.getComponent(BCloseButton)
249-
expect($bclosebutton.attributes('aria-label')).toBe('foobar')
222+
const $bbutton = $div.getComponent(BButton)
223+
expect($bbutton.attributes('aria-label')).toBe('foobar')
250224
})
251225

252226
// BCloseButton variant
@@ -292,19 +266,56 @@ describe('alert', () => {
292266
expect(emitted[0][0]).toBe(0)
293267
})
294268

295-
it('nested div BCloseButton clicked emits closed', async () => {
269+
it('nested div BCloseButton clicked emits close', async () => {
296270
const wrapper = mount(BAlert, {
297271
props: {modelValue: 5, dismissible: true},
298272
})
299273
const $div = wrapper.get('div')
300274
const $bclosebutton = $div.getComponent(BCloseButton)
301275
await $bclosebutton.trigger('click')
302-
expect(wrapper.emitted()).toHaveProperty('closed')
276+
expect(wrapper.emitted()).toHaveProperty('close')
277+
})
278+
279+
it('nested div BCloseButton has class when prop closeClass', () => {
280+
const wrapper = mount(BAlert, {
281+
props: {modelValue: true, dismissible: true, closeClass: 'foobar'},
282+
})
283+
const $div = wrapper.get('div')
284+
const $bclosebutton = $div.getComponent(BCloseButton)
285+
expect($bclosebutton.classes()).toContain('foobar')
286+
})
287+
288+
it('nested div BCloseButton has class when prop closeWhite', () => {
289+
const wrapper = mount(BAlert, {
290+
props: {modelValue: true, dismissible: true, closeWhite: true},
291+
})
292+
const $div = wrapper.get('div')
293+
const $bclosebutton = $div.getComponent(BCloseButton)
294+
expect($bclosebutton.classes()).toContain('btn-close-white')
295+
})
296+
297+
it('nested div BCloseButton has no class when no slot close', () => {
298+
const wrapper = mount(BAlert, {
299+
props: {modelValue: true, dismissible: true},
300+
})
301+
const $div = wrapper.get('div')
302+
const $bclosebutton = $div.getComponent(BCloseButton)
303+
expect($bclosebutton.classes()).not.toContain('btn-close-custom')
304+
})
305+
306+
it('nested div BCloseButton has no variant class when closeVariant', () => {
307+
const wrapper = mount(BAlert, {
308+
props: {modelValue: true, dismissible: true, closeVariant: 'warning'},
309+
})
310+
const $div = wrapper.get('div')
311+
const $bclosebutton = $div.getComponent(BCloseButton)
312+
expect($bclosebutton.classes()).not.toContain('btn-warning')
303313
})
304314
// BButton variant
305315
it('nested div BButton emits update:modelValue when clicked when modelValue boolean', async () => {
306316
const wrapper = mount(BAlert, {
307-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
317+
props: {modelValue: true, dismissible: true},
318+
slots: {close: 'foobar'},
308319
})
309320
const $div = wrapper.get('div')
310321
const $bbutton = $div.getComponent(BButton)
@@ -314,7 +325,8 @@ describe('alert', () => {
314325

315326
it('nested div BButton clicked update:modelValue emits arg false when modelValue boolean', async () => {
316327
const wrapper = mount(BAlert, {
317-
props: {modelValue: true, dismissible: true, closeContent: 'foobar'},
328+
props: {modelValue: true, dismissible: true},
329+
slots: {close: 'foobar'},
318330
})
319331
const $div = wrapper.get('div')
320332
const $bbutton = $div.getComponent(BButton)
@@ -325,7 +337,8 @@ describe('alert', () => {
325337

326338
it('nested div BButton emits update:modelValue when clicked when modelValue number > 0', async () => {
327339
const wrapper = mount(BAlert, {
328-
props: {modelValue: 5, dismissible: true, closeContent: 'foobar'},
340+
props: {modelValue: 5, dismissible: true},
341+
slots: {close: 'foobar'},
329342
})
330343
const $div = wrapper.get('div')
331344
const $bbutton = $div.getComponent(BButton)
@@ -335,7 +348,8 @@ describe('alert', () => {
335348

336349
it('nested div BButton clicked update:modelValue emits arg 0 when modelValue number > 0', async () => {
337350
const wrapper = mount(BAlert, {
338-
props: {modelValue: 5, dismissible: true, closeContent: 'foobar'},
351+
props: {modelValue: 5, dismissible: true},
352+
slots: {close: 'foobar'},
339353
})
340354
const $div = wrapper.get('div')
341355
const $bbutton = $div.getComponent(BButton)
@@ -344,14 +358,55 @@ describe('alert', () => {
344358
expect(emitted[0][0]).toBe(0)
345359
})
346360

347-
it('nested div BButton clicked emits closed', async () => {
361+
it('nested div BButton clicked emits close', async () => {
348362
const wrapper = mount(BAlert, {
349-
props: {modelValue: 5, dismissible: true, closeContent: 'foobar'},
363+
props: {modelValue: 5, dismissible: true},
364+
slots: {close: 'foobar'},
350365
})
351366
const $div = wrapper.get('div')
352367
const $bbutton = $div.getComponent(BButton)
353368
await $bbutton.trigger('click')
354-
expect(wrapper.emitted()).toHaveProperty('closed')
369+
expect(wrapper.emitted()).toHaveProperty('close')
370+
})
371+
372+
it('nested div BButton has class when prop closeClass', () => {
373+
const wrapper = mount(BAlert, {
374+
props: {modelValue: true, dismissible: true, closeClass: 'foobar'},
375+
slots: {close: 'foobar'},
376+
})
377+
const $div = wrapper.get('div')
378+
const $bbutton = $div.getComponent(BButton)
379+
expect($bbutton.classes()).toContain('foobar')
380+
})
381+
382+
it('nested div BButton has no class when prop closeWhite', () => {
383+
const wrapper = mount(BAlert, {
384+
props: {modelValue: true, dismissible: true, closeWhite: true},
385+
slots: {close: 'foobar'},
386+
})
387+
const $div = wrapper.get('div')
388+
const $bbutton = $div.getComponent(BButton)
389+
expect($bbutton.classes()).not.toContain('btn-close-white')
390+
})
391+
392+
it('nested div BButton has class when slot close', () => {
393+
const wrapper = mount(BAlert, {
394+
props: {modelValue: true, dismissible: true},
395+
slots: {close: 'foobar'},
396+
})
397+
const $div = wrapper.get('div')
398+
const $bbutton = $div.getComponent(BButton)
399+
expect($bbutton.classes()).toContain('btn-close-custom')
400+
})
401+
402+
it('nested div BButton has variant class when closeVariant', () => {
403+
const wrapper = mount(BAlert, {
404+
props: {modelValue: true, dismissible: true, closeVariant: 'warning'},
405+
slots: {close: 'foobar'},
406+
})
407+
const $div = wrapper.get('div')
408+
const $bbutton = $div.getComponent(BButton)
409+
expect($bbutton.classes()).toContain('btn-warning')
355410
})
356411

357412
// TODO try to test countdown items

0 commit comments

Comments
 (0)