diff --git a/src/components/collapse/collapse.js b/src/components/collapse/collapse.js index 7ad53694992..b71211fb89e 100644 --- a/src/components/collapse/collapse.js +++ b/src/components/collapse/collapse.js @@ -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', diff --git a/src/components/navbar/README.md b/src/components/navbar/README.md index 1693244e73d..ab3a510e663 100644 --- a/src/components/navbar/README.md +++ b/src/components/navbar/README.md @@ -281,6 +281,39 @@ will reverse the placement of the toggler. See the first example on this page for reference, and also refer to [``](/docs/components/collapse) for details on the collapse component. +#### Custom 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 + + + +``` + ## Printing Navbars are hidden by default when printing. Force them to be printed by setting the `print` prop. diff --git a/src/components/navbar/navbar-toggle.js b/src/components/navbar/navbar-toggle.js index 74282149d29..94e5bca8e8c 100644 --- a/src/components/navbar/navbar-toggle.js +++ b/src/components/navbar/navbar-toggle.js @@ -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` }) + ] ) } }) diff --git a/src/components/navbar/navbar-toggle.spec.js b/src/components/navbar/navbar-toggle.spec.js index a052fe57270..5131a19ff19 100644 --- a/src/components/navbar/navbar-toggle.spec.js +++ b/src/components/navbar/navbar-toggle.spec.js @@ -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,11 +24,11 @@ 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') }) @@ -36,7 +36,7 @@ describe('navbar-toggle', () => { 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') diff --git a/src/components/navbar/package.json b/src/components/navbar/package.json index dbb8b881b18..563ba0a9398 100644 --- a/src/components/navbar/package.json +++ b/src/components/navbar/package.json @@ -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." + } + ] + } ] } ] diff --git a/src/directives/toggle/toggle.js b/src/directives/toggle/toggle.js index 32ae2a35713..490371f369c 100644 --- a/src/directives/toggle/toggle.js +++ b/src/directives/toggle/toggle.js @@ -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) => {