Skip to content

fix(modal): stacked modal z-index calculations (closes #3015) #3017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 7, 2019
2 changes: 1 addition & 1 deletion docs/plugins/bootstrap-vue.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Vue from 'vue'
import BootstrapVue from '../../src'

Vue.use(BootstrapVue)
Vue.use(BootstrapVue, {})
221 changes: 221 additions & 0 deletions src/components/modal/helpers/modal-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//
// private modalManager helper
//
// Handles controlling modal stacking zIndexes and body adjustments/classes
//
import Vue from 'vue'
import { inBrowser } from '../../../utils/env'
import {
getAttr,
hasAttr,
removeAttr,
setAttr,
addClass,
removeClass,
getBCR,
getCS,
selectAll,
requestAF
} from '../../../utils/dom'

// Default modal backdrop z-index
const DEFAULT_ZINDEX = 1040

// Selectors for padding/margin adjustments
const Selector = {
FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
STICKY_CONTENT: '.sticky-top',
NAVBAR_TOGGLER: '.navbar-toggler'
}

const ModalManager = Vue.extend({
data() {
return {
modals: [],
baseZIndex: null,
scrollbarWidth: null,
isBodyOverflowing: false
}
},
computed: {
modalCount() {
return this.modals.length
},
modalsAreOpen() {
return this.modalCount > 0
}
},
watch: {
modalCount(newCount, oldCount) {
if (inBrowser) {
this.getScrollbarWidth()
if (newCount > 0 && oldCount === 0) {
// Transitioning to modal(s) open
this.checkScrollbar()
this.setScrollbar()
addClass(document.body, 'modal-open')
} else if (newCount === 0 && oldCount > 0) {
// Transitioning to modal(s) closed
this.resetScrollbar()
removeClass(document.body, 'modal-open')
}
setAttr(document.body, 'data-modal-open-count', String(newCount))
}
},
modals(newVal, oldVal) {
this.checkScrollbar()
requestAF(() => {
this.updateModals(newVal || [])
})
}
},
methods: {
// Public methods
registerModal(modal) {
if (modal && this.modals.indexOf(modal) === -1) {
// Add modal to modals array
this.modals.push(modal)
modal.$once('hook:beforeDestroy', () => {
this.unregisterModal(modal)
})
}
},
unregisterModal(modal) {
const index = this.modals.indexOf(modal)
if (index > -1) {
// Remove modal from modals arary
this.modals.splice(index, 1)
// Reset the modal's data
if (!(modal._isBeingDestroyed || modal._isDestroyed)) {
this.resetModal(modal)
}
}
},
getBaseZIndex() {
if (this.baseZIndex === null && inBrowser) {
// Create a temporary div.modal-backdrop to get computed z-index
const div = document.createElement('div')
div.className = 'modal-backdrop d-none'
div.style.display = 'none'
document.body.appendChild(div)
this.baseZIndex = parseInt(getCS(div).zIndex || DEFAULT_ZINDEX, 10)
document.body.removeChild(div)
}
return this.baseZIndex || DEFAULT_ZINDEX
},
getScrollbarWidth() {
if (this.scrollbarWidth === null && inBrowser) {
// Create a temporary div.measure-scrollbar to get computed z-index
const div = document.createElement('div')
div.className = 'modal-scrollbar-measure'
document.body.appendChild(div)
this.scrollbarWidth = getBCR(div).width - div.clientWidth
document.body.removeChild(div)
}
return this.scrollbarWidth || 0
},
// Private methods
updateModals(modals) {
const baseZIndex = this.getBaseZIndex()
const scrollbarWidth = this.getScrollbarWidth()
modals.forEach((modal, index) => {
// We update data values on each modal
modal.zIndex = baseZIndex + index
modal.scrollbarWidth = scrollbarWidth
modal.isTop = index === this.modals.length - 1
modal.isBodyOverflowing = this.isBodyOverflowing
})
},
resetModal(modal) {
if (modal) {
modal.zIndex = this.getBaseZIndex()
modal.isTop = true
modal.isBodyOverflowing = false
}
},
checkScrollbar() {
// Determine if the body element is overflowing
// const { left, right, height } = getBCR(document.body)
// Extra check for body.height needed for stacked modals
// this.isBodyOverflowing = left + right < window.innerWidth || height > window.innerHeight
const { left, right } = getBCR(document.body)
this.isBodyOverflowing = left + right < window.innerWidth
},
setScrollbar() {
const body = document.body
// Storage place to cache changes to margins and padding
// Note: This assumes the following element types are not added to the
// document after the modal has opened.
body._paddingChangedForModal = body._paddingChangedForModal || []
body._marginChangedForModal = body._marginChangedForModal || []
if (this.isBodyOverflowing) {
const scrollbarWidth = this.scrollbarWidth
// Adjust fixed content padding
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.FIXED_CONTENT).forEach(el => {
const actualPadding = el.style.paddingRight
const calculatedPadding = getCS(el).paddingRight || 0
setAttr(el, 'data-padding-right', actualPadding)
el.style.paddingRight = `${parseFloat(calculatedPadding) + scrollbarWidth}px`
body._paddingChangedForModal.push(el)
})
// Adjust sticky content margin
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.STICKY_CONTENT).forEach(el => {
const actualMargin = el.style.marginRight
const calculatedMargin = getCS(el).marginRight || 0
setAttr(el, 'data-margin-right', actualMargin)
el.style.marginRight = `${parseFloat(calculatedMargin) - scrollbarWidth}px`
body._marginChangedForModal.push(el)
})
// Adjust navbar-toggler margin
/* istanbul ignore next: difficult to test in JSDOM */
selectAll(Selector.NAVBAR_TOGGLER).forEach(el => {
const actualMargin = el.style.marginRight
const calculatedMargin = getCS(el).marginRight || 0
setAttr(el, 'data-margin-right', actualMargin)
el.style.marginRight = `${parseFloat(calculatedMargin) + scrollbarWidth}px`
body._marginChangedForModal.push(el)
})
// Adjust body padding
const actualPadding = body.style.paddingRight
const calculatedPadding = getCS(body).paddingRight
setAttr(body, 'data-padding-right', actualPadding)
body.style.paddingRight = `${parseFloat(calculatedPadding) + scrollbarWidth}px`
}
},
resetScrollbar() {
const body = document.body
if (body._paddingChangedForModal) {
// Restore fixed content padding
body._paddingChangedForModal.forEach(el => {
/* istanbul ignore next: difficult to test in JSDOM */
if (hasAttr(el, 'data-padding-right')) {
el.style.paddingRight = getAttr(el, 'data-padding-right') || ''
removeAttr(el, 'data-padding-right')
}
})
}
if (body._marginChangedForModal) {
// Restore sticky content and navbar-toggler margin
body._marginChangedForModal.forEach(el => {
/* istanbul ignore next: difficult to test in JSDOM */
if (hasAttr(el, 'data-margin-right')) {
el.style.marginRight = getAttr(el, 'data-margin-right') || ''
removeAttr(el, 'data-margin-right')
}
})
}
body._paddingChangedForModal = null
body._marginChangedForModal = null
// Restore body padding
if (hasAttr(body, 'data-padding-right')) {
body.style.paddingRight = getAttr(body, 'data-padding-right') || ''
removeAttr(body, 'data-padding-right')
}
}
}
})

// Export our Modal Manager
export default new ModalManager()
Loading