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

1,109 lines (1,088 loc) 33.3 kB
import Vue from '../../vue' import { NAME_MODAL } from '../../constants/components' import { EVENT_OPTIONS_NO_CAPTURE } from '../../constants/events' import { CODE_ESC } from '../../constants/key-codes' import { SLOT_NAME_DEFAULT } from '../../constants/slot-names' import BVTransition from '../../utils/bv-transition' import identity from '../../utils/identity' import observeDom from '../../utils/observe-dom' import { arrayIncludes, concat } from '../../utils/array' import { getComponentConfig } from '../../utils/config' import { attemptFocus, closest, contains, getActiveElement, getTabables, requestAF, select } from '../../utils/dom' import { isBrowser } from '../../utils/env' import { eventOn, eventOff } from '../../utils/events' import { htmlOrText } from '../../utils/html' import { isString, isUndefinedOrNull } from '../../utils/inspect' import { HTMLElement } from '../../utils/safe-types' import { BTransporterSingle } from '../../utils/transporter' import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import listenOnDocumentMixin from '../../mixins/listen-on-document' import listenOnRootMixin from '../../mixins/listen-on-root' import listenOnWindowMixin from '../../mixins/listen-on-window' import normalizeSlotMixin from '../../mixins/normalize-slot' import scopedStyleAttrsMixin from '../../mixins/scoped-style-attrs' import { BButton } from '../button/button' import { BButtonClose } from '../button/button-close' import { modalManager } from './helpers/modal-manager' import { BvModalEvent } from './helpers/bv-modal-event.class' // --- Constants --- // ObserveDom config to detect changes in modal content // so that we can adjust the modal padding if needed const OBSERVER_CONFIG = { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['style', 'class'] } // --- Props --- export const props = { size: { type: String, default: () => getComponentConfig(NAME_MODAL, 'size') }, centered: { type: Boolean, default: false }, scrollable: { type: Boolean, default: false }, buttonSize: { type: String // default: '' }, noStacking: { type: Boolean, default: false }, noFade: { type: Boolean, default: false }, noCloseOnBackdrop: { type: Boolean, default: false }, noCloseOnEsc: { type: Boolean, default: false }, noEnforceFocus: { type: Boolean, default: false }, ignoreEnforceFocusSelector: { type: [Array, String], default: '' }, title: { type: String, default: '' }, titleHtml: { type: String }, titleTag: { type: String, default: () => getComponentConfig(NAME_MODAL, 'titleTag') }, titleClass: { type: [String, Array, Object] // default: null }, titleSrOnly: { type: Boolean, default: false }, ariaLabel: { type: String // default: null }, headerBgVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerBgVariant') }, headerBorderVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerBorderVariant') }, headerTextVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerTextVariant') }, headerCloseVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerCloseVariant') }, headerClass: { type: [String, Array, Object] // default: null }, bodyBgVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'bodyBgVariant') }, bodyTextVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'bodyTextVariant') }, modalClass: { type: [String, Array, Object] // default: null }, dialogClass: { type: [String, Array, Object] // default: null }, contentClass: { type: [String, Array, Object] // default: null }, bodyClass: { type: [String, Array, Object] // default: null }, footerBgVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'footerBgVariant') }, footerBorderVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'footerBorderVariant') }, footerTextVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'footerTextVariant') }, footerClass: { type: [String, Array, Object] // default: null }, // TODO: Rename to `noHeader` and deprecate `hideHeader` hideHeader: { type: Boolean, default: false }, // TODO: Rename to `noFooter` and deprecate `hideFooter` hideFooter: { type: Boolean, default: false }, // TODO: Rename to `noHeaderClose` and deprecate `hideHeaderClose` hideHeaderClose: { type: Boolean, default: false }, // TODO: Rename to `noBackdrop` and deprecate `hideBackdrop` hideBackdrop: { type: Boolean, default: false }, okOnly: { type: Boolean, default: false }, okDisabled: { type: Boolean, default: false }, cancelDisabled: { type: Boolean, default: false }, visible: { type: Boolean, default: false }, returnFocus: { // HTML Element, CSS selector string or Vue component instance type: [HTMLElement, String, Object], default: null }, headerCloseContent: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerCloseContent') }, headerCloseLabel: { type: String, default: () => getComponentConfig(NAME_MODAL, 'headerCloseLabel') }, cancelTitle: { type: String, default: () => getComponentConfig(NAME_MODAL, 'cancelTitle') }, cancelTitleHtml: { type: String }, okTitle: { type: String, default: () => getComponentConfig(NAME_MODAL, 'okTitle') }, okTitleHtml: { type: String }, cancelVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'cancelVariant') }, okVariant: { type: String, default: () => getComponentConfig(NAME_MODAL, 'okVariant') }, lazy: { type: Boolean, default: false }, busy: { type: Boolean, default: false }, static: { type: Boolean, default: false }, autoFocusButton: { type: String, default: null, validator /* istanbul ignore next */: val => { /* istanbul ignore next */ return isUndefinedOrNull(val) || arrayIncludes(['ok', 'cancel', 'close'], val) } } } // @vue/component export const BModal = /*#__PURE__*/ Vue.extend({ name: NAME_MODAL, mixins: [ attrsMixin, idMixin, listenOnDocumentMixin, listenOnRootMixin, listenOnWindowMixin, normalizeSlotMixin, scopedStyleAttrsMixin ], inheritAttrs: false, model: { prop: 'visible', event: 'change' }, props, data() { return { isHidden: true, // If modal should not be in document isVisible: false, // Controls modal visible state isTransitioning: false, // Used for style control isShow: false, // Used for style control isBlock: false, // Used for style control isOpening: false, // To signal that the modal is in the process of opening isClosing: false, // To signal that the modal is in the process of closing ignoreBackdropClick: false, // Used to signify if click out listener should ignore the click isModalOverflowing: false, return_focus: this.returnFocus || null, // The following items are controlled by the modalManager instance scrollbarWidth: 0, zIndex: modalManager.getBaseZIndex(), isTop: true, isBodyOverflowing: false } }, computed: { modalId() { return this.safeId() }, modalOuterId() { return this.safeId('__BV_modal_outer_') }, modalHeaderId() { return this.safeId('__BV_modal_header_') }, modalBodyId() { return this.safeId('__BV_modal_body_') }, modalTitleId() { return this.safeId('__BV_modal_title_') }, modalContentId() { return this.safeId('__BV_modal_content_') }, modalFooterId() { return this.safeId('__BV_modal_footer_') }, modalBackdropId() { return this.safeId('__BV_modal_backdrop_') }, modalClasses() { return [ { fade: !this.noFade, show: this.isShow }, this.modalClass ] }, modalStyles() { const sbWidth = `${this.scrollbarWidth}px` return { paddingLeft: !this.isBodyOverflowing && this.isModalOverflowing ? sbWidth : '', paddingRight: this.isBodyOverflowing && !this.isModalOverflowing ? sbWidth : '', // Needed to fix issue https://github.com/bootstrap-vue/bootstrap-vue/issues/3457 // Even though we are using v-show, we must ensure 'none' is restored in the styles display: this.isBlock ? 'block' : 'none' } }, dialogClasses() { return [ { [`modal-${this.size}`]: this.size, 'modal-dialog-centered': this.centered, 'modal-dialog-scrollable': this.scrollable }, this.dialogClass ] }, headerClasses() { return [ { [`bg-${this.headerBgVariant}`]: this.headerBgVariant, [`text-${this.headerTextVariant}`]: this.headerTextVariant, [`border-${this.headerBorderVariant}`]: this.headerBorderVariant }, this.headerClass ] }, titleClasses() { return [{ 'sr-only': this.titleSrOnly }, this.titleClass] }, bodyClasses() { return [ { [`bg-${this.bodyBgVariant}`]: this.bodyBgVariant, [`text-${this.bodyTextVariant}`]: this.bodyTextVariant }, this.bodyClass ] }, footerClasses() { return [ { [`bg-${this.footerBgVariant}`]: this.footerBgVariant, [`text-${this.footerTextVariant}`]: this.footerTextVariant, [`border-${this.footerBorderVariant}`]: this.footerBorderVariant }, this.footerClass ] }, modalOuterStyle() { // Styles needed for proper stacking of modals return { position: 'absolute', zIndex: this.zIndex } }, slotScope() { return { ok: this.onOk, cancel: this.onCancel, close: this.onClose, hide: this.hide, visible: this.isVisible } }, computeIgnoreEnforceFocusSelector() { // Normalize to an single selector with selectors separated by `,` return concat(this.ignoreEnforceFocusSelector) .filter(identity) .join(',') .trim() }, computedAttrs() { // If the parent has a scoped style attribute, and the modal // is portalled, add the scoped attribute to the modal wrapper const scopedStyleAttrs = !this.static ? this.scopedStyleAttrs : {} return { ...scopedStyleAttrs, ...this.bvAttrs, id: this.modalOuterId } }, computedModalAttrs() { const { isVisible, ariaLabel } = this return { id: this.modalId, role: 'dialog', 'aria-hidden': isVisible ? null : 'true', 'aria-modal': isVisible ? 'true' : null, 'aria-label': ariaLabel, 'aria-labelledby': this.hideHeader || ariaLabel || // TODO: Rename slot to `title` and deprecate `modal-title` !(this.hasNormalizedSlot('modal-title') || this.titleHtml || this.title) ? null : this.modalTitleId, 'aria-describedby': this.modalBodyId } } }, watch: { visible(newVal, oldVal) { if (newVal !== oldVal) { this[newVal ? 'show' : 'hide']() } } }, created() { // Define non-reactive properties this.$_observer = null }, mounted() { // Set initial z-index as queried from the DOM this.zIndex = modalManager.getBaseZIndex() // Listen for events from others to either open or close ourselves // and listen to all modals to enable/disable enforce focus this.listenOnRoot('bv::show::modal', this.showHandler) this.listenOnRoot('bv::hide::modal', this.hideHandler) this.listenOnRoot('bv::toggle::modal', this.toggleHandler) // Listen for `bv:modal::show events`, and close ourselves if the // opening modal not us this.listenOnRoot('bv::modal::show', this.modalListener) // Initially show modal? if (this.visible === true) { this.$nextTick(this.show) } }, beforeDestroy() { // Ensure everything is back to normal this.setObserver(false) if (this.isVisible) { this.isVisible = false this.isShow = false this.isTransitioning = false } }, methods: { setObserver(on = false) { this.$_observer && this.$_observer.disconnect() this.$_observer = null if (on) { this.$_observer = observeDom( this.$refs.content, this.checkModalOverflow.bind(this), OBSERVER_CONFIG ) } }, // Private method to update the v-model updateModel(val) { if (val !== this.visible) { this.$emit('change', val) } }, // Private method to create a BvModalEvent object buildEvent(type, options = {}) { return new BvModalEvent(type, { // Default options cancelable: false, target: this.$refs.modal || this.$el || null, relatedTarget: null, trigger: null, // Supplied options ...options, // Options that can't be overridden vueTarget: this, componentId: this.modalId }) }, // Public method to show modal show() { if (this.isVisible || this.isOpening) { // If already open, or in the process of opening, do nothing /* istanbul ignore next */ return } /* istanbul ignore next */ if (this.isClosing) { // If we are in the process of closing, wait until hidden before re-opening /* istanbul ignore next */ this.$once('hidden', this.show) /* istanbul ignore next */ return } this.isOpening = true // Set the element to return focus to when closed this.return_focus = this.return_focus || this.getActiveElement() const showEvt = this.buildEvent('show', { cancelable: true }) this.emitEvent(showEvt) // Don't show if canceled if (showEvt.defaultPrevented || this.isVisible) { this.isOpening = false // Ensure the v-model reflects the current state this.updateModel(false) return } // Show the modal this.doShow() }, // Public method to hide modal hide(trigger = '') { if (!this.isVisible || this.isClosing) { /* istanbul ignore next */ return } this.isClosing = true const hideEvt = this.buildEvent('hide', { cancelable: trigger !== 'FORCE', trigger: trigger || null }) // We emit specific event for one of the three built-in buttons if (trigger === 'ok') { this.$emit('ok', hideEvt) } else if (trigger === 'cancel') { this.$emit('cancel', hideEvt) } else if (trigger === 'headerclose') { this.$emit('close', hideEvt) } this.emitEvent(hideEvt) // Hide if not canceled if (hideEvt.defaultPrevented || !this.isVisible) { this.isClosing = false // Ensure v-model reflects current state this.updateModel(true) return } // Stop observing for content changes this.setObserver(false) // Trigger the hide transition this.isVisible = false // Update the v-model this.updateModel(false) }, // Public method to toggle modal visibility toggle(triggerEl) { if (triggerEl) { this.return_focus = triggerEl } if (this.isVisible) { this.hide('toggle') } else { this.show() } }, // Private method to get the current document active element getActiveElement() { // Returning focus to `document.body` may cause unwanted scrolls, // so we exclude setting focus on body const activeElement = getActiveElement(isBrowser ? [document.body] : []) // Preset the fallback return focus value if it is not set // `document.activeElement` should be the trigger element that was clicked or // in the case of using the v-model, which ever element has current focus // Will be overridden by some commands such as toggle, etc. // Note: On IE 11, `document.activeElement` may be `null` // So we test it for truthiness first // https://github.com/bootstrap-vue/bootstrap-vue/issues/3206 return activeElement && activeElement.focus ? activeElement : null }, // Private method to finish showing modal doShow() { /* istanbul ignore next: commenting out for now until we can test stacking */ if (modalManager.modalsAreOpen && this.noStacking) { // If another modal(s) is already open, wait for it(them) to close this.listenOnRootOnce('bv::modal::hidden', this.doShow) return } modalManager.registerModal(this) // Place modal in DOM this.isHidden = false this.$nextTick(() => { // We do this in `$nextTick()` to ensure the modal is in DOM first // before we show it this.isVisible = true this.isOpening = false // Update the v-model this.updateModel(true) this.$nextTick(() => { // Observe changes in modal content and adjust if necessary // In a `$nextTick()` in case modal content is lazy this.setObserver(true) }) }) }, // Transition handlers onBeforeEnter() { this.isTransitioning = true this.setResizeEvent(true) }, onEnter() { this.isBlock = true // We add the `show` class 1 frame later // `requestAF()` runs the callback before the next repaint, so we need // two calls to guarantee the next frame has been rendered requestAF(() => { requestAF(() => { this.isShow = true }) }) }, onAfterEnter() { this.checkModalOverflow() this.isTransitioning = false // We use `requestAF()` to allow transition hooks to complete // before passing control over to the other handlers // This will allow users to not have to use `$nextTick()` or `requestAF()` // when trying to pre-focus an element requestAF(() => { this.emitEvent(this.buildEvent('shown')) this.setEnforceFocus(true) this.$nextTick(() => { // Delayed in a `$nextTick()` to allow users time to pre-focus // an element if the wish this.focusFirst() }) }) }, onBeforeLeave() { this.isTransitioning = true this.setResizeEvent(false) this.setEnforceFocus(false) }, onLeave() { // Remove the 'show' class this.isShow = false }, onAfterLeave() { this.isBlock = false this.isTransitioning = false this.isModalOverflowing = false this.isHidden = true this.$nextTick(() => { this.isClosing = false modalManager.unregisterModal(this) this.returnFocusTo() // TODO: Need to find a way to pass the `trigger` property // to the `hidden` event, not just only the `hide` event this.emitEvent(this.buildEvent('hidden')) }) }, // Event emitter emitEvent(bvModalEvt) { const type = bvModalEvt.type // We emit on root first incase a global listener wants to cancel // the event first before the instance emits its event this.emitOnRoot(`bv::modal::${type}`, bvModalEvt, bvModalEvt.componentId) this.$emit(type, bvModalEvt) }, // UI event handlers onDialogMousedown() { // Watch to see if the matching mouseup event occurs outside the dialog // And if it does, cancel the clickOut handler const modal = this.$refs.modal const onceModalMouseup = evt => { eventOff(modal, 'mouseup', onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE) if (evt.target === modal) { this.ignoreBackdropClick = true } } eventOn(modal, 'mouseup', onceModalMouseup, EVENT_OPTIONS_NO_CAPTURE) }, onClickOut(evt) { if (this.ignoreBackdropClick) { // Click was initiated inside the modal content, but finished outside. // Set by the above onDialogMousedown handler this.ignoreBackdropClick = false return } // Do nothing if not visible, backdrop click disabled, or element // that generated click event is no longer in document body if (!this.isVisible || this.noCloseOnBackdrop || !contains(document.body, evt.target)) { return } // If backdrop clicked, hide modal if (!contains(this.$refs.content, evt.target)) { this.hide('backdrop') } }, onOk() { this.hide('ok') }, onCancel() { this.hide('cancel') }, onClose() { this.hide('headerclose') }, onEsc(evt) { // If ESC pressed, hide modal if (evt.keyCode === CODE_ESC && this.isVisible && !this.noCloseOnEsc) { this.hide('esc') } }, // Document focusin listener focusHandler(evt) { // If focus leaves modal content, bring it back const content = this.$refs.content const { target } = evt if ( this.noEnforceFocus || !this.isTop || !this.isVisible || !content || document === target || contains(content, target) || (this.computeIgnoreEnforceFocusSelector && closest(this.computeIgnoreEnforceFocusSelector, target, true)) ) { return } const tabables = getTabables(this.$refs.content) const { bottomTrap, topTrap } = this.$refs if (bottomTrap && target === bottomTrap) { // If user pressed TAB out of modal into our bottom trab trap element // Find the first tabable element in the modal content and focus it if (attemptFocus(tabables[0])) { // Focus was successful return } } else if (topTrap && target === topTrap) { // If user pressed CTRL-TAB out of modal and into our top tab trap element // Find the last tabable element in the modal content and focus it if (attemptFocus(tabables[tabables.length - 1])) { // Focus was successful return } } // Otherwise focus the modal content container attemptFocus(content, { preventScroll: true }) }, // Turn on/off focusin listener setEnforceFocus(on) { this.listenDocument(on, 'focusin', this.focusHandler) }, // Resize listener setResizeEvent(on) { this.listenWindow(on, 'resize', this.checkModalOverflow) this.listenWindow(on, 'orientationchange', this.checkModalOverflow) }, // Root listener handlers showHandler(id, triggerEl) { if (id === this.modalId) { this.return_focus = triggerEl || this.getActiveElement() this.show() } }, hideHandler(id) { if (id === this.modalId) { this.hide('event') } }, toggleHandler(id, triggerEl) { if (id === this.modalId) { this.toggle(triggerEl) } }, modalListener(bvEvt) { // If another modal opens, close this one if stacking not permitted if (this.noStacking && bvEvt.vueTarget !== this) { this.hide() } }, // Focus control handlers focusFirst() { // Don't try and focus if we are SSR if (isBrowser) { requestAF(() => { const modal = this.$refs.modal const content = this.$refs.content const activeElement = this.getActiveElement() // If the modal contains the activeElement, we don't do anything if (modal && content && !(activeElement && contains(content, activeElement))) { const ok = this.$refs['ok-button'] const cancel = this.$refs['cancel-button'] const close = this.$refs['close-button'] // Focus the appropriate button or modal content wrapper const autoFocus = this.autoFocusButton /* istanbul ignore next */ const el = autoFocus === 'ok' && ok ? ok.$el || ok : autoFocus === 'cancel' && cancel ? cancel.$el || cancel : autoFocus === 'close' && close ? close.$el || close : content // Focus the element attemptFocus(el) if (el === content) { // Make sure top of modal is showing (if longer than the viewport) this.$nextTick(() => { modal.scrollTop = 0 }) } } }) } }, returnFocusTo() { // Prefer `returnFocus` prop over event specified // `return_focus` value let el = this.returnFocus || this.return_focus || null this.return_focus = null this.$nextTick(() => { // Is el a string CSS selector? el = isString(el) ? select(el) : el if (el) { // Possibly could be a component reference el = el.$el || el attemptFocus(el) } }) }, checkModalOverflow() { if (this.isVisible) { const modal = this.$refs.modal this.isModalOverflowing = modal.scrollHeight > document.documentElement.clientHeight } }, makeModal(h) { // Modal header let $header = h() if (!this.hideHeader) { // TODO: Rename slot to `header` and deprecate `modal-header` let $modalHeader = this.normalizeSlot('modal-header', this.slotScope) if (!$modalHeader) { let $closeButton = h() if (!this.hideHeaderClose) { $closeButton = h( BButtonClose, { props: { content: this.headerCloseContent, disabled: this.isTransitioning, ariaLabel: this.headerCloseLabel, textVariant: this.headerCloseVariant || this.headerTextVariant }, on: { click: this.onClose }, ref: 'close-button' }, // TODO: Rename slot to `header-close` and deprecate `modal-header-close` [this.normalizeSlot('modal-header-close')] ) } $modalHeader = [ h( this.titleTag, { staticClass: 'modal-title', class: this.titleClasses, attrs: { id: this.modalTitleId }, // TODO: Rename slot to `title` and deprecate `modal-title` domProps: this.hasNormalizedSlot('modal-title') ? {} : htmlOrText(this.titleHtml, this.title) }, // TODO: Rename slot to `title` and deprecate `modal-title` this.normalizeSlot('modal-title', this.slotScope) ), $closeButton ] } $header = h( 'header', { staticClass: 'modal-header', class: this.headerClasses, attrs: { id: this.modalHeaderId }, ref: 'header' }, [$modalHeader] ) } // Modal body const $body = h( 'div', { staticClass: 'modal-body', class: this.bodyClasses, attrs: { id: this.modalBodyId }, ref: 'body' }, this.normalizeSlot(SLOT_NAME_DEFAULT, this.slotScope) ) // Modal footer let $footer = h() if (!this.hideFooter) { // TODO: Rename slot to `footer` and deprecate `modal-footer` let $modalFooter = this.normalizeSlot('modal-footer', this.slotScope) if (!$modalFooter) { let $cancelButton = h() if (!this.okOnly) { $cancelButton = h( BButton, { props: { variant: this.cancelVariant, size: this.buttonSize, disabled: this.cancelDisabled || this.busy || this.isTransitioning }, // TODO: Rename slot to `cancel-button` and deprecate `modal-cancel` domProps: this.hasNormalizedSlot('modal-cancel') ? {} : htmlOrText(this.cancelTitleHtml, this.cancelTitle), on: { click: this.onCancel }, ref: 'cancel-button' }, // TODO: Rename slot to `cancel-button` and deprecate `modal-cancel` this.normalizeSlot('modal-cancel') ) } const $okButton = h( BButton, { props: { variant: this.okVariant, size: this.buttonSize, disabled: this.okDisabled || this.busy || this.isTransitioning }, // TODO: Rename slot to `ok-button` and deprecate `modal-ok` domProps: this.hasNormalizedSlot('modal-ok') ? {} : htmlOrText(this.okTitleHtml, this.okTitle), on: { click: this.onOk }, ref: 'ok-button' }, // TODO: Rename slot to `ok-button` and deprecate `modal-ok` this.normalizeSlot('modal-ok') ) $modalFooter = [$cancelButton, $okButton] } $footer = h( 'footer', { staticClass: 'modal-footer', class: this.footerClasses, attrs: { id: this.modalFooterId }, ref: 'footer' }, [$modalFooter] ) } // Assemble modal content const $modalContent = h( 'div', { staticClass: 'modal-content', class: this.contentClass, attrs: { id: this.modalContentId, tabindex: '-1' }, ref: 'content' }, [$header, $body, $footer] ) // Tab traps to prevent page from scrolling to next element in // tab index during enforce-focus tab cycle let $tabTrapTop = h() let $tabTrapBottom = h() if (this.isVisible && !this.noEnforceFocus) { $tabTrapTop = h('span', { ref: 'topTrap', attrs: { tabindex: '0' } }) $tabTrapBottom = h('span', { ref: 'bottomTrap', attrs: { tabindex: '0' } }) } // Modal dialog wrapper const $modalDialog = h( 'div', { staticClass: 'modal-dialog', class: this.dialogClasses, on: { mousedown: this.onDialogMousedown }, ref: 'dialog' }, [$tabTrapTop, $modalContent, $tabTrapBottom] ) // Modal let $modal = h( 'div', { staticClass: 'modal', class: this.modalClasses, style: this.modalStyles, attrs: this.computedModalAttrs, on: { keydown: this.onEsc, click: this.onClickOut }, directives: [{ name: 'show', value: this.isVisible }], ref: 'modal' }, [$modalDialog] ) // Wrap modal in transition // Sadly, we can't use `BVTransition` here due to the differences in // transition durations for `.modal` and `.modal-dialog` // At least until https://github.com/vuejs/vue/issues/9986 is resolved $modal = h( 'transition', { props: { enterClass: '', enterToClass: '', enterActiveClass: '', leaveClass: '', leaveActiveClass: '', leaveToClass: '' }, on: { beforeEnter: this.onBeforeEnter, enter: this.onEnter, afterEnter: this.onAfterEnter, beforeLeave: this.onBeforeLeave, leave: this.onLeave, afterLeave: this.onAfterLeave } }, [$modal] ) // Modal backdrop let $backdrop = h() if (!this.hideBackdrop && this.isVisible) { $backdrop = h( 'div', { staticClass: 'modal-backdrop', attrs: { id: this.modalBackdropId } }, // TODO: Rename slot to `backdrop` and deprecate `modal-backdrop` this.normalizeSlot('modal-backdrop') ) } $backdrop = h(BVTransition, { props: { noFade: this.noFade } }, [$backdrop]) // Assemble modal and backdrop in an outer <div> return h( 'div', { style: this.modalOuterStyle, attrs: this.computedAttrs, key: `modal-outer-${this._uid}` }, [$modal, $backdrop] ) } }, render(h) { if (this.static) { return this.lazy && this.isHidden ? h() : this.makeModal(h) } else { return this.isHidden ? h() : h(BTransporterSingle, [this.makeModal(h)]) } } })