🌐 AI搜索 & 代理 主页
Skip to content

Commit 144d45f

Browse files
authored
feat(b-navbar-toggle): make default slot scoped (#4995)
Co-authored-by: Jacob Müller
1 parent e8104b9 commit 144d45f

File tree

6 files changed

+125
-43
lines changed

6 files changed

+125
-43
lines changed

src/components/collapse/collapse.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import Vue from '../../utils/vue'
2-
import { isBrowser } from '../../utils/env'
2+
import { BVCollapse } from '../../utils/bv-collapse'
33
import { addClass, hasClass, removeClass, closest, matches, getCS } from '../../utils/dom'
4+
import { isBrowser } from '../../utils/env'
45
import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events'
5-
import { BVCollapse } from '../../utils/bv-collapse'
66
import idMixin from '../../mixins/id'
77
import listenOnRootMixin from '../../mixins/listen-on-root'
88
import normalizeSlotMixin from '../../mixins/normalize-slot'
9+
import {
10+
EVENT_TOGGLE,
11+
EVENT_STATE,
12+
EVENT_STATE_REQUEST,
13+
EVENT_STATE_SYNC
14+
} from '../../directives/toggle/toggle'
15+
16+
// --- Constants ---
917

10-
// Events we emit on $root
11-
const EVENT_STATE = 'bv::collapse::state'
18+
// Accordion event name we emit on `$root`
1219
const EVENT_ACCORDION = 'bv::collapse::accordion'
13-
// Private event we emit on `$root` to ensure the toggle state is
14-
// always synced. It gets emitted even if the state has not changed!
15-
// This event is NOT to be documented as people should not be using it
16-
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
17-
// Events we listen to on `$root`
18-
const EVENT_TOGGLE = 'bv::toggle::collapse'
19-
const EVENT_STATE_REQUEST = 'bv::request::collapse::state'
2020

21+
// --- Main component ---
2122
// @vue/component
2223
export const BCollapse = /*#__PURE__*/ Vue.extend({
2324
name: 'BCollapse',

src/components/navbar/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,39 @@ will reverse the placement of the toggler.
281281
See the first example on this page for reference, and also refer to
282282
[`<b-collapse>`](/docs/components/collapse) for details on the collapse component.
283283

284+
#### Custom navbar toggle
285+
286+
`<b-navbar-toggle>` renders the default Bootstrap v4 _hamburger_ (which is a background SVG image).
287+
You can supply your own content (such as an icon) via the optionally scoped `default` slot. The
288+
default slot scope contains the property `expanded`, which will be `true` when the collapse is
289+
expanded, or `false` when the collapse is collapsed. You can use this to swap the toggle content
290+
based on the collapse state:
291+
292+
```html
293+
<template>
294+
<b-navbar toggleable type="dark" variant="dark">
295+
<b-navbar-brand href="#">NavBar</b-navbar-brand>
296+
297+
<b-navbar-toggle target="navbar-toggle-collapse">
298+
<template v-slot:default="{ expanded }">
299+
<b-icon v-if="expanded" icon="chevron-bar-up"></b-icon>
300+
<b-icon v-else icon="chevron-bar-down"></b-icon>
301+
</template>
302+
</b-navbar-toggle>
303+
304+
<b-collapse id="navbar-toggle-collapse" is-nav>
305+
<b-navbar-nav class="ml-auto">
306+
<b-nav-item href="#">Link 1</b-nav-item>
307+
<b-nav-item href="#">Link 2</b-nav-item>
308+
<b-nav-item href="#" disabled>Disabled</b-nav-item>
309+
</b-navbar-nav>
310+
</b-collapse>
311+
</b-navbar>
312+
</template>
313+
314+
<!-- b-navbar-toggle-slot.vue -->
315+
```
316+
284317
## Printing
285318

286319
Navbars are hidden by default when printing. Force them to be printed by setting the `print` prop.

src/components/navbar/navbar-toggle.js

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import Vue from '../../utils/vue'
2+
import { getComponentConfig } from '../../utils/config'
3+
import { toString } from '../../utils/string'
24
import listenOnRootMixin from '../../mixins/listen-on-root'
35
import normalizeSlotMixin from '../../mixins/normalize-slot'
4-
import { getComponentConfig } from '../../utils/config'
6+
import { EVENT_TOGGLE, EVENT_STATE, EVENT_STATE_SYNC } from '../../directives/toggle/toggle'
57

6-
const NAME = 'BNavbarToggle'
8+
// TODO:
9+
// Switch to using `VBToggle` directive, will reduce code footprint
10+
// Although the `click` event will no longer be cancellable
11+
// Instead add `disabled` prop, and have `VBToggle` check element
12+
// disabled state
713

8-
// TODO: Switch to using VBToggle directive, will reduce code footprint
14+
// --- Constants ---
915

10-
// Events we emit on $root
11-
const EVENT_TOGGLE = 'bv::toggle::collapse'
12-
13-
// Events we listen to on $root
14-
const EVENT_STATE = 'bv::collapse::state'
15-
// This private event is NOT to be documented as people should not be using it.
16-
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
16+
const NAME = 'BNavbarToggle'
17+
const CLASS_NAME = 'navbar-toggler'
1718

19+
// --- Main component ---
1820
// @vue/component
1921
export const BNavbarToggle = /*#__PURE__*/ Vue.extend({
2022
name: NAME,
@@ -52,19 +54,23 @@ export const BNavbarToggle = /*#__PURE__*/ Vue.extend({
5254
}
5355
},
5456
render(h) {
57+
const expanded = this.toggleState
5558
return h(
5659
'button',
5760
{
58-
class: ['navbar-toggler'],
61+
staticClass: CLASS_NAME,
5962
attrs: {
6063
type: 'button',
6164
'aria-label': this.label,
6265
'aria-controls': this.target,
63-
'aria-expanded': this.toggleState ? 'true' : 'false'
66+
'aria-expanded': toString(expanded)
6467
},
6568
on: { click: this.onClick }
6669
},
67-
[this.normalizeSlot('default') || h('span', { class: ['navbar-toggler-icon'] })]
70+
[
71+
this.normalizeSlot('default', { expanded }) ||
72+
h('span', { staticClass: `${CLASS_NAME}-icon` })
73+
]
6874
)
6975
}
7076
})

src/components/navbar/navbar-toggle.spec.js

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('navbar-toggle', () => {
55
it('default has tag "button"', async () => {
66
const wrapper = mount(BNavbarToggle, {
77
propsData: {
8-
target: 'target'
8+
target: 'target-1'
99
}
1010
})
1111
expect(wrapper.is('button')).toBe(true)
@@ -14,7 +14,7 @@ describe('navbar-toggle', () => {
1414
it('default has class "navbar-toggler"', async () => {
1515
const wrapper = mount(BNavbarToggle, {
1616
propsData: {
17-
target: 'target'
17+
target: 'target-2'
1818
}
1919
})
2020
expect(wrapper.classes()).toContain('navbar-toggler')
@@ -24,19 +24,19 @@ describe('navbar-toggle', () => {
2424
it('default has default attributes', async () => {
2525
const wrapper = mount(BNavbarToggle, {
2626
propsData: {
27-
target: 'target'
27+
target: 'target-3'
2828
}
2929
})
3030
expect(wrapper.attributes('type')).toBe('button')
31-
expect(wrapper.attributes('aria-controls')).toBe('target')
31+
expect(wrapper.attributes('aria-controls')).toBe('target-3')
3232
expect(wrapper.attributes('aria-expanded')).toBe('false')
3333
expect(wrapper.attributes('aria-label')).toBe('Toggle navigation')
3434
})
3535

3636
it('default has inner button-close', async () => {
3737
const wrapper = mount(BNavbarToggle, {
3838
propsData: {
39-
target: 'target'
39+
target: 'target-4'
4040
}
4141
})
4242
expect(wrapper.find('span.navbar-toggler-icon')).toBeDefined()
@@ -45,17 +45,45 @@ describe('navbar-toggle', () => {
4545
it('accepts custom label when label prop is set', async () => {
4646
const wrapper = mount(BNavbarToggle, {
4747
propsData: {
48-
target: 'target',
48+
target: 'target-5',
4949
label: 'foobar'
5050
}
5151
})
5252
expect(wrapper.attributes('aria-label')).toBe('foobar')
5353
})
5454

55+
it('default slot scope works', async () => {
56+
let scope = null
57+
const wrapper = mount(BNavbarToggle, {
58+
propsData: {
59+
target: 'target-6'
60+
},
61+
scopedSlots: {
62+
default(ctx) {
63+
scope = ctx
64+
return this.$createElement('div', 'foobar')
65+
}
66+
}
67+
})
68+
69+
expect(scope).not.toBe(null)
70+
expect(scope.expanded).toBe(false)
71+
72+
wrapper.vm.$root.$emit('bv::collapse::state', 'target-6', true)
73+
74+
expect(scope).not.toBe(null)
75+
expect(scope.expanded).toBe(true)
76+
77+
wrapper.vm.$root.$emit('bv::collapse::state', 'target-6', false)
78+
79+
expect(scope).not.toBe(null)
80+
expect(scope.expanded).toBe(false)
81+
})
82+
5583
it('emits click event', async () => {
5684
const wrapper = mount(BNavbarToggle, {
5785
propsData: {
58-
target: 'target'
86+
target: 'target-7'
5987
}
6088
})
6189
let rootClicked = false
@@ -77,22 +105,22 @@ describe('navbar-toggle', () => {
77105
it('sets aria-expanded when receives root emit for target', async () => {
78106
const wrapper = mount(BNavbarToggle, {
79107
propsData: {
80-
target: 'target'
108+
target: 'target-8'
81109
}
82110
})
83111

84112
// Private state event
85-
wrapper.vm.$root.$emit('bv::collapse::state', 'target', true)
113+
wrapper.vm.$root.$emit('bv::collapse::state', 'target-8', true)
86114
expect(wrapper.attributes('aria-expanded')).toBe('true')
87-
wrapper.vm.$root.$emit('bv::collapse::state', 'target', false)
115+
wrapper.vm.$root.$emit('bv::collapse::state', 'target-8', false)
88116
expect(wrapper.attributes('aria-expanded')).toBe('false')
89117
wrapper.vm.$root.$emit('bv::collapse::state', 'foo', true)
90118
expect(wrapper.attributes('aria-expanded')).toBe('false')
91119

92120
// Private sync event
93-
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target', true)
121+
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target-8', true)
94122
expect(wrapper.attributes('aria-expanded')).toBe('true')
95-
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target', false)
123+
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target-8', false)
96124
expect(wrapper.attributes('aria-expanded')).toBe('false')
97125
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'foo', true)
98126
expect(wrapper.attributes('aria-expanded')).toBe('false')

src/components/navbar/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@
8686
}
8787
]
8888
}
89+
],
90+
"slots": [
91+
{
92+
"name": "default",
93+
"description": "Alternate content to replace the default Bootstrap hamburger",
94+
"scope": [
95+
{
96+
"prop": "expanded",
97+
"version": "2.9.0",
98+
"type": "Boolean",
99+
"description": "`true` if the collapse is expanded, `false` otherwise."
100+
}
101+
]
102+
}
89103
]
90104
}
91105
]

src/directives/toggle/toggle.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ const BV_TOGGLE_CONTROLS = '__BV_toggle_CONTROLS__'
1313
const BV_TOGGLE_TARGETS = '__BV_toggle_TARGETS__'
1414

1515
// Emitted control event for collapse (emitted to collapse)
16-
const EVENT_TOGGLE = 'bv::toggle::collapse'
16+
export const EVENT_TOGGLE = 'bv::toggle::collapse'
1717

1818
// Listen to event for toggle state update (emitted by collapse)
19-
const EVENT_STATE = 'bv::collapse::state'
19+
export const EVENT_STATE = 'bv::collapse::state'
2020

21-
// Private event emitted on $root to ensure the toggle state is always synced.
22-
// Gets emitted even if the state of b-collapse has not changed.
23-
// This event is NOT to be documented as people should not be using it.
24-
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
21+
// Private event emitted on `$root` to ensure the toggle state is always synced
22+
// Gets emitted even if the state of b-collapse has not changed
23+
// This event is NOT to be documented as people should not be using it
24+
export const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
2525
// Private event we send to collapse to request state update sync event
26-
const EVENT_STATE_REQUEST = 'bv::request::collapse::state'
26+
export const EVENT_STATE_REQUEST = 'bv::request::collapse::state'
2727

2828
// Reset and remove a property from the provided element
2929
const resetProp = (el, prop) => {

0 commit comments

Comments
 (0)