UNPKG

bootstrap-vue

Version:

With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens

439 lines (407 loc) 12.3 kB
import { extend } from '../../vue' import { NAME_COLLAPSE, NAME_SIDEBAR } from '../../constants/components' import { IS_BROWSER } from '../../constants/env' import { EVENT_NAME_CHANGE, EVENT_NAME_HIDDEN, EVENT_NAME_SHOWN } from '../../constants/events' import { CODE_ESC } from '../../constants/key-codes' import { PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_BOOLEAN_STRING, PROP_TYPE_NUMBER_STRING, PROP_TYPE_STRING } from '../../constants/props' import { SLOT_NAME_DEFAULT, SLOT_NAME_FOOTER, SLOT_NAME_HEADER, SLOT_NAME_HEADER_CLOSE, SLOT_NAME_TITLE } from '../../constants/slots' import { attemptFocus, contains, getActiveElement, getTabables } from '../../utils/dom' import { getRootActionEventName, getRootEventName } from '../../utils/events' import { makeModelMixin } from '../../utils/model' import { sortKeys } from '../../utils/object' import { makeProp, makePropsConfigurable } from '../../utils/props' import { attrsMixin } from '../../mixins/attrs' import { idMixin, props as idProps } from '../../mixins/id' import { listenOnRootMixin } from '../../mixins/listen-on-root' import { normalizeSlotMixin } from '../../mixins/normalize-slot' import { BIconX } from '../../icons/icons' import { BButtonClose } from '../button/button-close' import { BVTransition } from '../transition/bv-transition' // --- Constants --- const CLASS_NAME = 'b-sidebar' const ROOT_ACTION_EVENT_NAME_REQUEST_STATE = getRootActionEventName(NAME_COLLAPSE, 'request-state') const ROOT_ACTION_EVENT_NAME_TOGGLE = getRootActionEventName(NAME_COLLAPSE, 'toggle') const ROOT_EVENT_NAME_STATE = getRootEventName(NAME_COLLAPSE, 'state') const ROOT_EVENT_NAME_SYNC_STATE = getRootEventName(NAME_COLLAPSE, 'sync-state') const { mixin: modelMixin, props: modelProps, prop: MODEL_PROP_NAME, event: MODEL_EVENT_NAME } = makeModelMixin('visible', { type: PROP_TYPE_BOOLEAN, defaultValue: false, event: EVENT_NAME_CHANGE }) // --- Props --- export const props = makePropsConfigurable( sortKeys({ ...idProps, ...modelProps, ariaLabel: makeProp(PROP_TYPE_STRING), ariaLabelledby: makeProp(PROP_TYPE_STRING), // If `true`, shows a basic backdrop backdrop: makeProp(PROP_TYPE_BOOLEAN, false), backdropVariant: makeProp(PROP_TYPE_STRING, 'dark'), bgVariant: makeProp(PROP_TYPE_STRING, 'light'), bodyClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), // `aria-label` for close button closeLabel: makeProp(PROP_TYPE_STRING), footerClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), footerTag: makeProp(PROP_TYPE_STRING, 'footer'), headerClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), headerTag: makeProp(PROP_TYPE_STRING, 'header'), lazy: makeProp(PROP_TYPE_BOOLEAN, false), noCloseOnBackdrop: makeProp(PROP_TYPE_BOOLEAN, false), noCloseOnEsc: makeProp(PROP_TYPE_BOOLEAN, false), noCloseOnRouteChange: makeProp(PROP_TYPE_BOOLEAN, false), noEnforceFocus: makeProp(PROP_TYPE_BOOLEAN, false), noHeader: makeProp(PROP_TYPE_BOOLEAN, false), noHeaderClose: makeProp(PROP_TYPE_BOOLEAN, false), noSlide: makeProp(PROP_TYPE_BOOLEAN, false), right: makeProp(PROP_TYPE_BOOLEAN, false), shadow: makeProp(PROP_TYPE_BOOLEAN_STRING, false), sidebarClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), tag: makeProp(PROP_TYPE_STRING, 'div'), textVariant: makeProp(PROP_TYPE_STRING, 'dark'), title: makeProp(PROP_TYPE_STRING), width: makeProp(PROP_TYPE_STRING), zIndex: makeProp(PROP_TYPE_NUMBER_STRING) }), NAME_SIDEBAR ) // --- Render methods --- const renderHeaderTitle = (h, ctx) => { // Render a empty `<span>` when to title was provided const title = ctx.normalizeSlot(SLOT_NAME_TITLE, ctx.slotScope) || ctx.title if (!title) { return h('span') } return h('strong', { attrs: { id: ctx.safeId('__title__') } }, [title]) } const renderHeaderClose = (h, ctx) => { if (ctx.noHeaderClose) { return h() } const { closeLabel, textVariant, hide } = ctx return h( BButtonClose, { props: { ariaLabel: closeLabel, textVariant }, on: { click: hide }, ref: 'close-button' }, [ctx.normalizeSlot(SLOT_NAME_HEADER_CLOSE) || h(BIconX)] ) } const renderHeader = (h, ctx) => { if (ctx.noHeader) { return h() } 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( ctx.headerTag, { staticClass: `${CLASS_NAME}-header`, class: ctx.headerClass, key: 'header' }, $content ) } const renderBody = (h, ctx) => { return h( 'div', { staticClass: `${CLASS_NAME}-body`, class: ctx.bodyClass, key: 'body' }, [ctx.normalizeSlot(SLOT_NAME_DEFAULT, ctx.slotScope)] ) } const renderFooter = (h, ctx) => { const $footer = ctx.normalizeSlot(SLOT_NAME_FOOTER, ctx.slotScope) if (!$footer) { return h() } return h( ctx.footerTag, { staticClass: `${CLASS_NAME}-footer`, class: ctx.footerClass, key: 'footer' }, [$footer] ) } const renderContent = (h, ctx) => { // We render the header even if `lazy` is enabled as it // acts as the accessible label for the sidebar const $header = renderHeader(h, ctx) if (ctx.lazy && !ctx.isOpen) { return $header } return [$header, renderBody(h, ctx), renderFooter(h, ctx)] } const renderBackdrop = (h, ctx) => { if (!ctx.backdrop) { return h() } const { backdropVariant } = ctx return h('div', { directives: [{ name: 'show', value: ctx.localShow }], staticClass: 'b-sidebar-backdrop', class: { [`bg-${backdropVariant}`]: backdropVariant }, on: { click: ctx.onBackdropClick } }) } // --- Main component --- // @vue/component export const BSidebar = /*#__PURE__*/ extend({ name: NAME_SIDEBAR, mixins: [attrsMixin, idMixin, modelMixin, listenOnRootMixin, normalizeSlotMixin], inheritAttrs: false, props, data() { const visible = !!this[MODEL_PROP_NAME] return { // Internal `v-model` state localShow: visible, // For lazy render triggering isOpen: visible } }, computed: { transitionProps() { return this.noSlide ? /* istanbul ignore next */ { css: true } : { css: true, enterClass: '', enterActiveClass: 'slide', enterToClass: 'show', leaveClass: 'show', leaveActiveClass: 'slide', leaveToClass: '' } }, slotScope() { const { hide, right, localShow: visible } = this return { hide, right, visible } }, 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.hasTitle ? this.safeId('__title__') : null }, computedAttrs() { return { ...this.bvAttrs, id: this.safeId(), tabindex: '-1', role: 'dialog', 'aria-modal': this.backdrop ? 'true' : 'false', 'aria-hidden': this.localShow ? null : 'true', 'aria-label': this.ariaLabel || null, 'aria-labelledby': this.ariaLabelledby || this.titleId || null } } }, watch: { [MODEL_PROP_NAME](newValue, oldValue) { if (newValue !== oldValue) { this.localShow = newValue } }, localShow(newValue, oldValue) { if (newValue !== oldValue) { this.emitState(newValue) this.$emit(MODEL_EVENT_NAME, newValue) } }, /* istanbul ignore next */ $route(newValue = {}, oldValue = {}) { if (!this.noCloseOnRouteChange && newValue.fullPath !== oldValue.fullPath) { this.hide() } } }, created() { // Define non-reactive properties this.$_returnFocusEl = null }, mounted() { // Add `$root` listeners this.listenOnRoot(ROOT_ACTION_EVENT_NAME_TOGGLE, this.handleToggle) this.listenOnRoot(ROOT_ACTION_EVENT_NAME_REQUEST_STATE, this.handleSync) // Send out a gratuitous state event to ensure toggle button is synced this.$nextTick(() => { this.emitState(this.localShow) }) }, /* istanbul ignore next */ activated() { this.emitSync() }, beforeDestroy() { this.localShow = false this.$_returnFocusEl = null }, methods: { hide() { this.localShow = false }, emitState(state = this.localShow) { this.emitOnRoot(ROOT_EVENT_NAME_STATE, this.safeId(), state) }, emitSync(state = this.localShow) { this.emitOnRoot(ROOT_EVENT_NAME_SYNC_STATE, this.safeId(), state) }, handleToggle(id) { // Note `safeId()` can be null until after mount if (id && id === this.safeId()) { this.localShow = !this.localShow } }, handleSync(id) { // Note `safeId()` can be null until after mount if (id && id === this.safeId()) { this.$nextTick(() => { this.emitSync(this.localShow) }) } }, onKeydown(event) { const { keyCode } = event if (!this.noCloseOnEsc && keyCode === CODE_ESC && this.localShow) { this.hide() } }, onBackdropClick() { if (this.localShow && !this.noCloseOnBackdrop) { this.hide() } }, /* istanbul ignore next */ onTopTrapFocus() { const tabables = getTabables(this.$refs.content) this.enforceFocus(tabables.reverse()[0]) }, /* istanbul ignore next */ onBottomTrapFocus() { const tabables = getTabables(this.$refs.content) this.enforceFocus(tabables[0]) }, onBeforeEnter() { // Returning focus to `document.body` may cause unwanted scrolls, // so we exclude setting focus on body this.$_returnFocusEl = getActiveElement(IS_BROWSER ? [document.body] : []) // Trigger lazy render this.isOpen = true }, onAfterEnter(el) { if (!contains(el, getActiveElement())) { this.enforceFocus(el) } this.$emit(EVENT_NAME_SHOWN) }, onAfterLeave() { this.enforceFocus(this.$_returnFocusEl) this.$_returnFocusEl = null // Trigger lazy render this.isOpen = false this.$emit(EVENT_NAME_HIDDEN) }, enforceFocus(el) { if (!this.noEnforceFocus) { attemptFocus(el) } } }, render(h) { const { bgVariant, width, textVariant, localShow } = this const shadow = this.shadow === '' ? true : this.shadow let $sidebar = h( this.tag, { staticClass: CLASS_NAME, class: [ { shadow: shadow === true, [`shadow-${shadow}`]: shadow && shadow !== true, [`${CLASS_NAME}-right`]: this.right, [`bg-${bgVariant}`]: bgVariant, [`text-${textVariant}`]: textVariant }, this.sidebarClass ], style: { width }, attrs: this.computedAttrs, directives: [{ name: 'show', value: localShow }], ref: 'content' }, [renderContent(h, this)] ) $sidebar = h( 'transition', { props: this.transitionProps, on: { beforeEnter: this.onBeforeEnter, afterEnter: this.onAfterEnter, afterLeave: this.onAfterLeave } }, [$sidebar] ) const $backdrop = h(BVTransition, { props: { noFade: this.noSlide } }, [ renderBackdrop(h, this) ]) let $tabTrapTop = h() let $tabTrapBottom = h() if (this.backdrop && localShow) { $tabTrapTop = h('div', { attrs: { tabindex: '0' }, on: { focus: this.onTopTrapFocus } }) $tabTrapBottom = h('div', { attrs: { tabindex: '0' }, on: { focus: this.onBottomTrapFocus } }) } return h( 'div', { staticClass: 'b-sidebar-outer', style: { zIndex: this.zIndex }, attrs: { tabindex: '-1' }, on: { keydown: this.onKeydown } }, [$tabTrapTop, $sidebar, $tabTrapBottom, $backdrop] ) } })