From 7985dcb0a3ef04736c9064fb94e20efc9538f21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Fri, 11 Dec 2020 18:31:55 +0100 Subject: [PATCH] feat(b-sidebar): add `header` slot --- src/components/sidebar/README.md | 2 ++ src/components/sidebar/package.json | 22 ++++++++++++++++++++ src/components/sidebar/sidebar.js | 25 +++++++++++++++-------- src/components/sidebar/sidebar.spec.js | 28 ++++++++++++++++++++++++++ src/mixins/normalize-slot.js | 26 +++++++++++++++--------- 5 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/components/sidebar/README.md b/src/components/sidebar/README.md index 92916c39b10..8b08d7731a0 100644 --- a/src/components/sidebar/README.md +++ b/src/components/sidebar/README.md @@ -185,6 +185,8 @@ You can apply arbitrary classes to the body section via the `body-class` prop. By default, `` has a header with optional title and a close button. You can supply a title via the `title` prop, or via the optionally scoped slot `title`. +If you want to provide a completely custom header, you can use the optionally scoped `header` slot. + You can apply arbitrary classes to the header section via the `header-class` prop, to override the default padding, etc. diff --git a/src/components/sidebar/package.json b/src/components/sidebar/package.json index 5686981caa6..f82709ab5f9 100644 --- a/src/components/sidebar/package.json +++ b/src/components/sidebar/package.json @@ -175,6 +175,28 @@ } ] }, + { + "name": "header", + "version": "2.21.0", + "description": "Content to place in the header", + "scope": [ + { + "prop": "hide", + "type": "Function", + "description": "When called, will close the sidebar" + }, + { + "prop": "right", + "type": "Boolean", + "description": "`true` if the sidebar is on the right" + }, + { + "prop": "visible", + "type": "Boolean", + "description": "`true` if the sidebar is open" + } + ] + }, { "name": "header-close", "description": "Content of the header close button. Defaults to ``" diff --git a/src/components/sidebar/sidebar.js b/src/components/sidebar/sidebar.js index c89b00a9423..8afb7c0b20a 100644 --- a/src/components/sidebar/sidebar.js +++ b/src/components/sidebar/sidebar.js @@ -13,6 +13,7 @@ import { import { SLOT_NAME_DEFAULT, SLOT_NAME_FOOTER, + SLOT_NAME_HEADER, SLOT_NAME_HEADER_CLOSE, SLOT_NAME_TITLE } from '../../constants/slots' @@ -21,7 +22,6 @@ import { getRootActionEventName, getRootEventName } from '../../utils/events' import { makeModelMixin } from '../../utils/model' import { sortKeys } from '../../utils/object' import { makeProp, makePropsConfigurable } from '../../utils/props' -import { toString } from '../../utils/string' import { attrsMixin } from '../../mixins/attrs' import { idMixin, props as idProps } from '../../mixins/id' import { listenOnRootMixin } from '../../mixins/listen-on-root' @@ -92,7 +92,7 @@ export const props = makePropsConfigurable( const renderHeaderTitle = (h, ctx) => { // Render a empty `` when to title was provided - const title = ctx.computedTile + const title = ctx.normalizeSlot(SLOT_NAME_TITLE, ctx.slotScope) || ctx.title if (!title) { return h('span') } @@ -123,8 +123,12 @@ const renderHeader = (h, ctx) => { return h() } - const $title = renderHeaderTitle(h, ctx) - const $close = renderHeaderClose(h, ctx) + let $content = ctx.normalizeSlot(SLOT_NAME_HEADER, ctx.slotScope) + if (!$content) { + const $title = renderHeaderTitle(h, ctx) + const $close = renderHeaderClose(h, ctx) + $content = ctx.right ? [$close, $title] : [$title, $close] + } return h( 'header', @@ -133,7 +137,7 @@ const renderHeader = (h, ctx) => { class: ctx.headerClass, key: 'header' }, - ctx.right ? [$close, $title] : [$title, $close] + $content ) } @@ -227,11 +231,16 @@ export const BSidebar = /*#__PURE__*/ Vue.extend({ const { hide, right, localShow: visible } = this return { hide, right, visible } }, - computedTile() { - return this.normalizeSlot(SLOT_NAME_TITLE, this.slotScope) || toString(this.title) || null + hasTitle() { + const { $scopedSlots, $slots } = this + return ( + !this.noHeader && + !this.hasNormalizedSlot(SLOT_NAME_HEADER) && + !!(this.normalizeSlot(SLOT_NAME_TITLE, this.slotScope, $scopedSlots, $slots) || this.title) + ) }, titleId() { - return this.computedTile ? this.safeId('__title__') : null + return this.hasTitle ? this.safeId('__title__') : null }, computedAttrs() { return { diff --git a/src/components/sidebar/sidebar.spec.js b/src/components/sidebar/sidebar.spec.js index b7f26117883..015c2eb53ce 100644 --- a/src/components/sidebar/sidebar.spec.js +++ b/src/components/sidebar/sidebar.spec.js @@ -325,6 +325,34 @@ describe('sidebar', () => { wrapper.destroy() }) + it('should have expected structure when `header` slot provided', async () => { + const wrapper = mount(BSidebar, { + attachTo: createContainer(), + propsData: { + id: 'sidebar-header-slot', + visible: true, + title: 'TITLE' + }, + slots: { + header: 'Custom header' + } + }) + + expect(wrapper.vm).toBeDefined() + expect(wrapper.element.tagName).toBe('DIV') + + const $header = wrapper.find('.b-sidebar-header') + expect($header.exists()).toBe(true) + expect($header.find('strong').exists()).toBe(false) + expect($header.find('button').exists()).toBe(false) + expect($header.text()).toContain('Custom header') + expect($header.text()).not.toContain('TITLE') + + expect(wrapper.find('.b-sidebar-footer').exists()).toBe(false) + + wrapper.destroy() + }) + it('should have expected structure when `footer` slot provided', async () => { const wrapper = mount(BSidebar, { attachTo: createContainer(), diff --git a/src/mixins/normalize-slot.js b/src/mixins/normalize-slot.js index 38ec54c8739..91662658098 100644 --- a/src/mixins/normalize-slot.js +++ b/src/mixins/normalize-slot.js @@ -6,16 +6,24 @@ import { concat } from '../utils/array' // @vue/component export const normalizeSlotMixin = Vue.extend({ methods: { - hasNormalizedSlot(name = SLOT_NAME_DEFAULT) { - // Returns true if the either a $scopedSlot or $slot exists with the specified name - // `name` can be a string name or an array of names - return hasNormalizedSlot(name, this.$scopedSlots, this.$slots) + // Returns `true` if the either a `$scopedSlot` or `$slot` exists with the specified name + // `name` can be a string name or an array of names + hasNormalizedSlot( + name = SLOT_NAME_DEFAULT, + scopedSlots = this.$scopedSlots, + slots = this.$slots + ) { + return hasNormalizedSlot(name, scopedSlots, slots) }, - normalizeSlot(name = SLOT_NAME_DEFAULT, scope = {}) { - // Returns an array of rendered VNodes if slot found. - // Returns undefined if not found. - // `name` can be a string name or an array of names - const vNodes = normalizeSlot(name, scope, this.$scopedSlots, this.$slots) + // Returns an array of rendered VNodes if slot found, otherwise `undefined` + // `name` can be a string name or an array of names + normalizeSlot( + name = SLOT_NAME_DEFAULT, + scope = {}, + scopedSlots = this.$scopedSlots, + slots = this.$slots + ) { + const vNodes = normalizeSlot(name, scope, scopedSlots, slots) return vNodes ? concat(vNodes) : vNodes } }