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

Commit d1a811c

Browse files
committed
feat(BOffcanvas): adapt close button for better customization.
BREAKING-CHANGE: rename prop `dismissLabel` to `headerCloseLabel` like BModal uses the prop (unify prop names).
1 parent cb76f59 commit d1a811c

File tree

2 files changed

+120
-18
lines changed

2 files changed

+120
-18
lines changed

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

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@
3434
{{ title }}
3535
</slot>
3636
</h5>
37-
<BCloseButton
38-
v-if="!noHeaderCloseBoolean"
39-
class="text-reset"
40-
:aria-label="dismissLabel"
41-
@click="hide('close')"
42-
/>
37+
<template v-if="!noHeaderCloseBoolean">
38+
<BButton v-if="hasHeaderCloseSlot" v-bind="headerCloseAttrs" @click="hide('close')">
39+
<slot name="header-close" />
40+
</BButton>
41+
<BCloseButton
42+
v-else
43+
ref="closeButton"
44+
:white="headerCloseWhite"
45+
v-bind="headerCloseAttrs"
46+
@click="hide('close')"
47+
/>
48+
</template>
4349
</slot>
4450
</div>
4551
<div class="offcanvas-body" :class="bodyClass">
@@ -66,7 +72,7 @@
6672
import {computed, nextTick, ref, type RendererElement, useSlots} from 'vue'
6773
import {useEventListener, useFocus, useVModel} from '@vueuse/core'
6874
import {useBooleanish, useId, useSafeScrollLock} from '../../composables'
69-
import type {Booleanish, ColorVariant} from '../../types'
75+
import type {Booleanish, ButtonVariant, ClassValue, ColorVariant} from '../../types'
7076
import {BvTriggerableEvent, isEmptySlot} from '../../utils'
7177
import BOverlay from '../BOverlay/BOverlay.vue'
7278
import BCloseButton from '../BButton/BCloseButton.vue'
@@ -86,7 +92,6 @@ defineOptions({
8692
8793
const props = withDefaults(
8894
defineProps<{
89-
dismissLabel?: string
9095
modelValue?: Booleanish
9196
bodyScrolling?: Booleanish
9297
backdrop?: Booleanish
@@ -104,6 +109,10 @@ const props = withDefaults(
104109
noFocus?: Booleanish
105110
backdropVariant?: ColorVariant | null
106111
headerClass?: string
112+
headerCloseClass?: ClassValue
113+
headerCloseLabel?: string
114+
headerCloseWhite?: Booleanish
115+
headerCloseVariant?: ButtonVariant | null
107116
bodyClass?: string
108117
footerClass?: string
109118
teleportDisabled?: Booleanish
@@ -112,7 +121,6 @@ const props = withDefaults(
112121
// responsive?: Breakpoint
113122
}>(),
114123
{
115-
dismissLabel: 'Close',
116124
id: undefined,
117125
title: undefined,
118126
modelValue: false,
@@ -127,6 +135,10 @@ const props = withDefaults(
127135
noHeaderClose: false,
128136
noHeader: false,
129137
headerClass: undefined,
138+
headerCloseClass: undefined,
139+
headerCloseLabel: 'Close',
140+
headerCloseWhite: false,
141+
headerCloseVariant: 'secondary',
130142
bodyClass: undefined,
131143
footerClass: undefined,
132144
teleportDisabled: false,
@@ -148,16 +160,18 @@ const emit = defineEmits<{
148160
149161
defineSlots<{
150162
// eslint-disable-next-line @typescript-eslint/no-explicit-any
151-
default?: (props: Record<string, never>) => any
163+
'default'?: (props: Record<string, never>) => any
152164
// eslint-disable-next-line @typescript-eslint/no-explicit-any
153-
title?: (props: Record<string, never>) => any
154-
header?: (props: {
165+
'title'?: (props: Record<string, never>) => any
166+
'header'?: (props: {
155167
visible: boolean
156168
placement: 'top' | 'bottom' | 'start' | 'end'
157169
hide: (trigger?: string) => void
158170
// eslint-disable-next-line @typescript-eslint/no-explicit-any
159171
}) => any
160-
footer?: (props: {
172+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
173+
'header-close'?: (props: Record<string, never>) => any
174+
'footer'?: (props: {
161175
visible: boolean
162176
placement: 'top' | 'bottom' | 'start' | 'end'
163177
hide: (trigger?: string) => void
@@ -203,6 +217,17 @@ const lazyShowing = computed(
203217
(lazyBoolean.value === true && modelValueBoolean.value === true)
204218
)
205219
220+
const hasHeaderCloseSlot = computed(() => !isEmptySlot(slots['header-close']))
221+
const headerCloseClasses = computed(() => [
222+
{'text-reset': !hasHeaderCloseSlot.value},
223+
props.headerCloseClass,
224+
])
225+
const headerCloseAttrs = computed(() => ({
226+
'variant': hasHeaderCloseSlot.value ? props.headerCloseVariant : undefined,
227+
'class': headerCloseClasses.value,
228+
'aria-label': props.headerCloseLabel,
229+
}))
230+
206231
const hasFooterSlot = computed(() => !isEmptySlot(slots.footer))
207232
const computedClasses = computed(() => [
208233
// props.responsive === undefined ? 'offcanvas' : `offcanvas-${props.responsive}`,

packages/bootstrap-vue-next/src/components/BOffcanvas/offcanvas.spec.ts

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {enableAutoUnmount, mount} from '@vue/test-utils'
22
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
33
import BOffcanvas from './BOffcanvas.vue'
44
import BCloseButton from '../BButton/BCloseButton.vue'
5+
import BButton from '../BButton/BButton.vue'
56
import BOverlay from '../BOverlay/BOverlay.vue'
67
describe.skip('offcanvas', () => {
78
enableAutoUnmount(afterEach)
@@ -155,27 +156,103 @@ describe.skip('offcanvas', () => {
155156
expect($closebutton.props('type')).toBe('button')
156157
})
157158

159+
it('first child div child BCloseButton has static class text-reset', () => {
160+
const wrapper = mount(BOffcanvas)
161+
const [, $div] = wrapper.findAll('div')
162+
const $closebutton = $div.getComponent(BCloseButton)
163+
expect($closebutton.classes()).toContain('text-reset')
164+
})
165+
158166
it('first child div child BCloseButton has prop ariaLabel to be default close', () => {
159167
const wrapper = mount(BOffcanvas)
160168
const [, $div] = wrapper.findAll('div')
161169
const $closebutton = $div.getComponent(BCloseButton)
162170
expect($closebutton.props('ariaLabel')).toBe('Close')
163171
})
164172

165-
it('first child div child BCloseButton has prop ariaLabel to be prop dismissLabel', () => {
173+
it('first child div child BCloseButton has prop ariaLabel to be prop headerCloseLabel', () => {
166174
const wrapper = mount(BOffcanvas, {
167-
props: {dismissLabel: 'foobar'},
175+
props: {headerCloseLabel: 'foobar'},
168176
})
169177
const [, $div] = wrapper.findAll('div')
170178
const $closebutton = $div.getComponent(BCloseButton)
171179
expect($closebutton.props('ariaLabel')).toBe('foobar')
172180
})
173181

174-
it('first child div child BCloseButton has static class text-reset', () => {
175-
const wrapper = mount(BOffcanvas)
182+
it('first child div child BCloseButton has class when prop headerCloseClass', () => {
183+
const wrapper = mount(BOffcanvas, {
184+
props: {headerCloseClass: 'foobar'},
185+
})
176186
const [, $div] = wrapper.findAll('div')
177187
const $closebutton = $div.getComponent(BCloseButton)
178-
expect($closebutton.classes()).toContain('text-reset')
188+
expect($closebutton.classes()).toContain('foobar')
189+
})
190+
191+
it('first child div child BCloseButton has class when prop headerCloseWhite', () => {
192+
const wrapper = mount(BOffcanvas, {
193+
props: {headerCloseWhite: true},
194+
})
195+
const [, $div] = wrapper.findAll('div')
196+
const $closebutton = $div.getComponent(BCloseButton)
197+
expect($closebutton.classes()).toContain('btn-close-white')
198+
})
199+
200+
it('first child div child BCloseButton has not variant class when headerCloseVariant', () => {
201+
const wrapper = mount(BOffcanvas, {
202+
props: {headerCloseVariant: 'warning'},
203+
})
204+
const [, $div] = wrapper.findAll('div')
205+
const $closebutton = $div.getComponent(BCloseButton)
206+
expect($closebutton.classes()).not.toContain('btn-warning')
207+
})
208+
209+
it('first child div child BButton has prop ariaLabel to be default close', () => {
210+
const wrapper = mount(BOffcanvas, {
211+
slots: {'header-close': 'foobar'},
212+
})
213+
const [, $div] = wrapper.findAll('div')
214+
const $bbutton = $div.getComponent(BButton)
215+
expect($bbutton.props('ariaLabel')).toBe('Close')
216+
})
217+
218+
it('first child div child BButton has prop ariaLabel to be prop headerCloseLabel', () => {
219+
const wrapper = mount(BOffcanvas, {
220+
props: {headerCloseLabel: 'foobar'},
221+
slots: {'header-close': 'foobar'},
222+
})
223+
const [, $div] = wrapper.findAll('div')
224+
const $bbutton = $div.getComponent(BButton)
225+
expect($bbutton.props('ariaLabel')).toBe('foobar')
226+
})
227+
228+
it('first child div child BButton has class when prop headerCloseClass', () => {
229+
const wrapper = mount(BOffcanvas, {
230+
props: {headerCloseClass: 'foobar'},
231+
slots: {'header-close': 'foobar'},
232+
})
233+
const [, $div] = wrapper.findAll('div')
234+
const $bbutton = $div.getComponent(BButton)
235+
expect($bbutton.classes()).toContain('foobar')
236+
})
237+
238+
it('first child div child BButton has class when prop headerCloseWhite', () => {
239+
const wrapper = mount(BOffcanvas, {
240+
props: {headerCloseWhite: true},
241+
slots: {'header-close': 'foobar'},
242+
})
243+
const [, $div] = wrapper.findAll('div')
244+
const $bbutton = $div.getComponent(BButton)
245+
expect($bbutton.classes()).not.toContain('btn-close-white')
246+
})
247+
248+
it('first child div child BButton has variant class when headerCloseVariant', () => {
249+
const wrapper = mount(BOffcanvas, {
250+
props: {headerCloseVariant: 'warning'},
251+
slots: {'header-close': 'foobar'},
252+
})
253+
const [, $div] = wrapper.findAll('div')
254+
const $bbutton = $div.getComponent(BButton)
255+
expect($bbutton.classes()).toContain('btn-warning')
179256
})
180257

181258
it('second child div has static class offcanvas-body', () => {

0 commit comments

Comments
 (0)