Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/components/collapse/collapse.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import Vue from '../../utils/vue'
import { isBrowser } from '../../utils/env'
import { BVCollapse } from '../../utils/bv-collapse'
import { addClass, hasClass, removeClass, closest, matches, getCS } from '../../utils/dom'
import { isBrowser } from '../../utils/env'
import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events'
import { BVCollapse } from '../../utils/bv-collapse'
import idMixin from '../../mixins/id'
import listenOnRootMixin from '../../mixins/listen-on-root'
import normalizeSlotMixin from '../../mixins/normalize-slot'
import {
EVENT_TOGGLE,
EVENT_STATE,
EVENT_STATE_REQUEST,
EVENT_STATE_SYNC
} from '../../directives/toggle/toggle'

// --- Constants ---

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

// --- Main component ---
// @vue/component
export const BCollapse = /*#__PURE__*/ Vue.extend({
name: 'BCollapse',
Expand Down
33 changes: 33 additions & 0 deletions src/components/navbar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,39 @@ will reverse the placement of the toggler.
See the first example on this page for reference, and also refer to
[`<b-collapse>`](/docs/components/collapse) for details on the collapse component.

#### Custom navbar toggle

`<b-navbar-toggle>` renders the default Bootstrap v4 _hamburger_ (which is a background SVG image).
You can supply your own content (such as an icon) via the optionally scoped `default` slot. The
default slot scope contains the property `expanded`, which will be `true` when the collapse is
expanded, or `false` when the collapse is collapsed. You can use this to swap the toggle content
based on the collapse state:

```html
<template>
<b-navbar toggleable type="dark" variant="dark">
<b-navbar-brand href="#">NavBar</b-navbar-brand>

<b-navbar-toggle target="navbar-toggle-collapse">
<template v-slot:default="{ expanded }">
<b-icon v-if="expanded" icon="chevron-bar-up"></b-icon>
<b-icon v-else icon="chevron-bar-down"></b-icon>
</template>
</b-navbar-toggle>

<b-collapse id="navbar-toggle-collapse" is-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item href="#">Link 1</b-nav-item>
<b-nav-item href="#">Link 2</b-nav-item>
<b-nav-item href="#" disabled>Disabled</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>

<!-- b-navbar-toggle-slot.vue -->
```

## Printing

Navbars are hidden by default when printing. Force them to be printed by setting the `print` prop.
Expand Down
32 changes: 19 additions & 13 deletions src/components/navbar/navbar-toggle.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import Vue from '../../utils/vue'
import { getComponentConfig } from '../../utils/config'
import { toString } from '../../utils/string'
import listenOnRootMixin from '../../mixins/listen-on-root'
import normalizeSlotMixin from '../../mixins/normalize-slot'
import { getComponentConfig } from '../../utils/config'
import { EVENT_TOGGLE, EVENT_STATE, EVENT_STATE_SYNC } from '../../directives/toggle/toggle'

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

// TODO: Switch to using VBToggle directive, will reduce code footprint
// --- Constants ---

// Events we emit on $root
const EVENT_TOGGLE = 'bv::toggle::collapse'

// Events we listen to on $root
const EVENT_STATE = 'bv::collapse::state'
// This private event is NOT to be documented as people should not be using it.
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
const NAME = 'BNavbarToggle'
const CLASS_NAME = 'navbar-toggler'

// --- Main component ---
// @vue/component
export const BNavbarToggle = /*#__PURE__*/ Vue.extend({
name: NAME,
Expand Down Expand Up @@ -52,19 +54,23 @@ export const BNavbarToggle = /*#__PURE__*/ Vue.extend({
}
},
render(h) {
const expanded = this.toggleState
return h(
'button',
{
class: ['navbar-toggler'],
staticClass: CLASS_NAME,
attrs: {
type: 'button',
'aria-label': this.label,
'aria-controls': this.target,
'aria-expanded': this.toggleState ? 'true' : 'false'
'aria-expanded': toString(expanded)
},
on: { click: this.onClick }
},
[this.normalizeSlot('default') || h('span', { class: ['navbar-toggler-icon'] })]
[
this.normalizeSlot('default', { expanded }) ||
h('span', { staticClass: `${CLASS_NAME}-icon` })
]
)
}
})
52 changes: 40 additions & 12 deletions src/components/navbar/navbar-toggle.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('navbar-toggle', () => {
it('default has tag "button"', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-1'
}
})
expect(wrapper.is('button')).toBe(true)
Expand All @@ -14,7 +14,7 @@ describe('navbar-toggle', () => {
it('default has class "navbar-toggler"', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-2'
}
})
expect(wrapper.classes()).toContain('navbar-toggler')
Expand All @@ -24,19 +24,19 @@ describe('navbar-toggle', () => {
it('default has default attributes', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-3'
}
})
expect(wrapper.attributes('type')).toBe('button')
expect(wrapper.attributes('aria-controls')).toBe('target')
expect(wrapper.attributes('aria-controls')).toBe('target-3')
expect(wrapper.attributes('aria-expanded')).toBe('false')
expect(wrapper.attributes('aria-label')).toBe('Toggle navigation')
})

it('default has inner button-close', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-4'
}
})
expect(wrapper.find('span.navbar-toggler-icon')).toBeDefined()
Expand All @@ -45,17 +45,45 @@ describe('navbar-toggle', () => {
it('accepts custom label when label prop is set', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target',
target: 'target-5',
label: 'foobar'
}
})
expect(wrapper.attributes('aria-label')).toBe('foobar')
})

it('default slot scope works', async () => {
let scope = null
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target-6'
},
scopedSlots: {
default(ctx) {
scope = ctx
return this.$createElement('div', 'foobar')
}
}
})

expect(scope).not.toBe(null)
expect(scope.expanded).toBe(false)

wrapper.vm.$root.$emit('bv::collapse::state', 'target-6', true)

expect(scope).not.toBe(null)
expect(scope.expanded).toBe(true)

wrapper.vm.$root.$emit('bv::collapse::state', 'target-6', false)

expect(scope).not.toBe(null)
expect(scope.expanded).toBe(false)
})

it('emits click event', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-7'
}
})
let rootClicked = false
Expand All @@ -77,22 +105,22 @@ describe('navbar-toggle', () => {
it('sets aria-expanded when receives root emit for target', async () => {
const wrapper = mount(BNavbarToggle, {
propsData: {
target: 'target'
target: 'target-8'
}
})

// Private state event
wrapper.vm.$root.$emit('bv::collapse::state', 'target', true)
wrapper.vm.$root.$emit('bv::collapse::state', 'target-8', true)
expect(wrapper.attributes('aria-expanded')).toBe('true')
wrapper.vm.$root.$emit('bv::collapse::state', 'target', false)
wrapper.vm.$root.$emit('bv::collapse::state', 'target-8', false)
expect(wrapper.attributes('aria-expanded')).toBe('false')
wrapper.vm.$root.$emit('bv::collapse::state', 'foo', true)
expect(wrapper.attributes('aria-expanded')).toBe('false')

// Private sync event
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target', true)
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target-8', true)
expect(wrapper.attributes('aria-expanded')).toBe('true')
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target', false)
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'target-8', false)
expect(wrapper.attributes('aria-expanded')).toBe('false')
wrapper.vm.$root.$emit('bv::collapse::sync::state', 'foo', true)
expect(wrapper.attributes('aria-expanded')).toBe('false')
Expand Down
14 changes: 14 additions & 0 deletions src/components/navbar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@
}
]
}
],
"slots": [
{
"name": "default",
"description": "Alternate content to replace the default Bootstrap hamburger",
"scope": [
{
"prop": "expanded",
"version": "2.9.0",
"type": "Boolean",
"description": "`true` if the collapse is expanded, `false` otherwise."
}
]
}
]
}
]
Expand Down
14 changes: 7 additions & 7 deletions src/directives/toggle/toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ const BV_TOGGLE_CONTROLS = '__BV_toggle_CONTROLS__'
const BV_TOGGLE_TARGETS = '__BV_toggle_TARGETS__'

// Emitted control event for collapse (emitted to collapse)
const EVENT_TOGGLE = 'bv::toggle::collapse'
export const EVENT_TOGGLE = 'bv::toggle::collapse'

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

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

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