Skip to content
Permalink
Browse files
feat(b-navbar-toggle): make default slot scoped (#4995)
Co-authored-by: Jacob Müller
  • Loading branch information
tmorehouse committed Mar 24, 2020
1 parent e8104b9 commit 144d45fb0e4d66bbf243b4a4df39d7f3b9b5c7cc
Showing 6 changed files with 125 additions and 43 deletions.
@@ -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',
@@ -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.
@@ -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,
@@ -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` })
]
)
}
})
@@ -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)
@@ -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')
@@ -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()
@@ -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
@@ -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')
@@ -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."
}
]
}
]
}
]
@@ -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) => {

0 comments on commit 144d45f

Please sign in to comment.