1
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-02-21 03:41:03 +00:00

Decouple BackDrop from modal (#32439)

* Create backdrop.js util

* revert breaking changes

remove PromiseTimout usage

revert class name

* one more test | change bundlewatch.config

* add config obj to backdrop helper | tests for rootElement | use transitionend helper

* Minor tweaks — Renaming

Co-authored-by: Rohit Sharma <rohit2sharma95@gmail.com>
This commit is contained in:
GeoSot 2021-04-14 23:28:50 +03:00 committed by GitHub
parent 0122e020d6
commit 80085a12f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 386 additions and 70 deletions

View File

@ -34,11 +34,11 @@
}, },
{ {
"path": "./dist/js/bootstrap.bundle.js", "path": "./dist/js/bootstrap.bundle.js",
"maxSize": "41 kB" "maxSize": "41.25 kB"
}, },
{ {
"path": "./dist/js/bootstrap.bundle.min.js", "path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "22 kB" "maxSize": "22.25 kB"
}, },
{ {
"path": "./dist/js/bootstrap.esm.js", "path": "./dist/js/bootstrap.esm.js",
@ -46,11 +46,11 @@
}, },
{ {
"path": "./dist/js/bootstrap.esm.min.js", "path": "./dist/js/bootstrap.esm.min.js",
"maxSize": "18 kB" "maxSize": "18.25 kB"
}, },
{ {
"path": "./dist/js/bootstrap.js", "path": "./dist/js/bootstrap.js",
"maxSize": "27 kB" "maxSize": "27.25 kB"
}, },
{ {
"path": "./dist/js/bootstrap.min.js", "path": "./dist/js/bootstrap.min.js",

View File

@ -65,7 +65,8 @@ const getConfigByPluginKey = pluginKey => {
pluginKey === 'EventHandler' || pluginKey === 'EventHandler' ||
pluginKey === 'SelectorEngine' || pluginKey === 'SelectorEngine' ||
pluginKey === 'Util' || pluginKey === 'Util' ||
pluginKey === 'Sanitizer' pluginKey === 'Sanitizer' ||
pluginKey === 'Backdrop'
) { ) {
return { return {
external: [] external: []
@ -133,7 +134,8 @@ const getConfigByPluginKey = pluginKey => {
const utilObjects = new Set([ const utilObjects = new Set([
'Util', 'Util',
'Sanitizer' 'Sanitizer',
'Backdrop'
]) ])
const domObjects = new Set([ const domObjects = new Set([

View File

@ -20,6 +20,7 @@ import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine' import SelectorEngine from './dom/selector-engine'
import { getWidth as getScrollBarWidth, hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar' import { getWidth as getScrollBarWidth, hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar'
import BaseComponent from './base-component' import BaseComponent from './base-component'
import Backdrop from './util/backdrop'
/** /**
* ------------------------------------------------------------------------ * ------------------------------------------------------------------------
@ -58,7 +59,6 @@ const EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}` const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_BACKDROP = 'modal-backdrop'
const CLASS_NAME_OPEN = 'modal-open' const CLASS_NAME_OPEN = 'modal-open'
const CLASS_NAME_FADE = 'fade' const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show' const CLASS_NAME_SHOW = 'show'
@ -81,7 +81,7 @@ class Modal extends BaseComponent {
this._config = this._getConfig(config) this._config = this._getConfig(config)
this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element) this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
this._backdrop = null this._backdrop = this._initializeBackDrop()
this._isShown = false this._isShown = false
this._ignoreBackdropClick = false this._ignoreBackdropClick = false
this._isTransitioning = false this._isTransitioning = false
@ -201,6 +201,7 @@ class Modal extends BaseComponent {
this._config = null this._config = null
this._dialog = null this._dialog = null
this._backdrop.dispose()
this._backdrop = null this._backdrop = null
this._isShown = null this._isShown = null
this._ignoreBackdropClick = null this._ignoreBackdropClick = null
@ -213,6 +214,13 @@ class Modal extends BaseComponent {
// Private // Private
_initializeBackDrop() {
return new Backdrop({
isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value
isAnimated: this._isAnimated()
})
}
_getConfig(config) { _getConfig(config) {
config = { config = {
...Default, ...Default,
@ -313,7 +321,7 @@ class Modal extends BaseComponent {
this._element.removeAttribute('aria-modal') this._element.removeAttribute('aria-modal')
this._element.removeAttribute('role') this._element.removeAttribute('role')
this._isTransitioning = false this._isTransitioning = false
this._showBackdrop(() => { this._backdrop.hide(() => {
document.body.classList.remove(CLASS_NAME_OPEN) document.body.classList.remove(CLASS_NAME_OPEN)
this._resetAdjustments() this._resetAdjustments()
scrollBarReset() scrollBarReset()
@ -321,73 +329,25 @@ class Modal extends BaseComponent {
}) })
} }
_removeBackdrop() {
this._backdrop.parentNode.removeChild(this._backdrop)
this._backdrop = null
}
_showBackdrop(callback) { _showBackdrop(callback) {
const isAnimated = this._isAnimated() EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
if (this._isShown && this._config.backdrop) { if (this._ignoreBackdropClick) {
this._backdrop = document.createElement('div') this._ignoreBackdropClick = false
this._backdrop.className = CLASS_NAME_BACKDROP
if (isAnimated) {
this._backdrop.classList.add(CLASS_NAME_FADE)
}
document.body.appendChild(this._backdrop)
EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
if (this._ignoreBackdropClick) {
this._ignoreBackdropClick = false
return
}
if (event.target !== event.currentTarget) {
return
}
if (this._config.backdrop === 'static') {
this._triggerBackdropTransition()
} else {
this.hide()
}
})
if (isAnimated) {
reflow(this._backdrop)
}
this._backdrop.classList.add(CLASS_NAME_SHOW)
if (!isAnimated) {
callback()
return return
} }
const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) if (event.target !== event.currentTarget) {
return
EventHandler.one(this._backdrop, 'transitionend', callback)
emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
} else if (!this._isShown && this._backdrop) {
this._backdrop.classList.remove(CLASS_NAME_SHOW)
const callbackRemove = () => {
this._removeBackdrop()
callback()
} }
if (isAnimated) { if (this._config.backdrop === true) {
const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) this.hide()
EventHandler.one(this._backdrop, 'transitionend', callbackRemove) } else if (this._config.backdrop === 'static') {
emulateTransitionEnd(this._backdrop, backdropTransitionDuration) this._triggerBackdropTransition()
} else {
callbackRemove()
} }
} else { })
callback()
} this._backdrop.show(callback)
} }
_isAnimated() { _isAnimated() {

123
js/src/util/backdrop.js Normal file
View File

@ -0,0 +1,123 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v5.0.0-beta3): util/backdrop.js
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler from '../dom/event-handler'
import { emulateTransitionEnd, execute, getTransitionDurationFromElement, reflow, typeCheckConfig } from './index'
const Default = {
isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
isAnimated: false,
rootElement: document.body // give the choice to place backdrop under different elements
}
const DefaultType = {
isVisible: 'boolean',
isAnimated: 'boolean',
rootElement: 'element'
}
const NAME = 'backdrop'
const CLASS_NAME_BACKDROP = 'modal-backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
class Backdrop {
constructor(config) {
this._config = this._getConfig(config)
this._isAppended = false
this._element = null
}
show(callback) {
if (!this._config.isVisible) {
execute(callback)
return
}
this._append()
if (this._config.isAnimated) {
reflow(this._getElement())
}
this._getElement().classList.add(CLASS_NAME_SHOW)
this._emulateAnimation(() => {
execute(callback)
})
}
hide(callback) {
if (!this._config.isVisible) {
execute(callback)
return
}
this._getElement().classList.remove(CLASS_NAME_SHOW)
this._emulateAnimation(() => {
this.dispose()
execute(callback)
})
}
// Private
_getElement() {
if (!this._element) {
const backdrop = document.createElement('div')
backdrop.className = CLASS_NAME_BACKDROP
if (this._config.isAnimated) {
backdrop.classList.add(CLASS_NAME_FADE)
}
this._element = backdrop
}
return this._element
}
_getConfig(config) {
config = {
...Default,
...(typeof config === 'object' ? config : {})
}
typeCheckConfig(NAME, config, DefaultType)
return config
}
_append() {
if (this._isAppended) {
return
}
this._config.rootElement.appendChild(this._getElement())
this._isAppended = true
}
dispose() {
if (!this._isAppended) {
return
}
this._getElement().parentNode.removeChild(this._element)
this._isAppended = false
}
_emulateAnimation(callback) {
if (!this._config.isAnimated) {
execute(callback)
return
}
const backdropTransitionDuration = getTransitionDurationFromElement(this._getElement())
EventHandler.one(this._getElement(), 'transitionend', () => execute(callback))
emulateTransitionEnd(this._getElement(), backdropTransitionDuration)
}
}
export default Backdrop

View File

@ -230,6 +230,12 @@ const defineJQueryPlugin = (name, plugin) => {
}) })
} }
const execute = callback => {
if (typeof callback === 'function') {
callback()
}
}
export { export {
getUID, getUID,
getSelectorFromElement, getSelectorFromElement,
@ -247,5 +253,6 @@ export {
getjQuery, getjQuery,
onDOMContentLoaded, onDOMContentLoaded,
isRTL, isRTL,
defineJQueryPlugin defineJQueryPlugin,
execute
} }

View File

@ -0,0 +1,216 @@
import Backdrop from '../../../src/util/backdrop'
import { getTransitionDurationFromElement } from '../../../src/util/index'
import { clearFixture, getFixture } from '../../helpers/fixture'
const CLASS_BACKDROP = '.modal-backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
describe('Backdrop', () => {
let fixtureEl
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
const list = document.querySelectorAll(CLASS_BACKDROP)
list.forEach(el => {
document.body.removeChild(el)
})
})
describe('show', () => {
it('if it is "shown", should append the backdrop html once, on show, and contain "show" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show()
instance.show(() => {
expect(getElements().length).toEqual(1)
getElements().forEach(el => {
expect(el.classList.contains(CLASS_NAME_SHOW)).toEqual(true)
})
done()
})
})
it('if it is not "shown", should not append the backdrop html', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(0)
done()
})
})
it('if it is "shown" and "animated", should append the backdrop html once, and contain "fade" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(1)
getElements().forEach(el => {
expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true)
})
done()
})
})
it('Should be appended on "document.body" by default', done => {
const instance = new Backdrop({
isVisible: true
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(document.body)
done()
})
})
it('Should appended on any element given by the proper config', done => {
fixtureEl.innerHTML = [
'<div id="wrapper">',
'</div>'
].join('')
const wrapper = fixtureEl.querySelector('#wrapper')
const instance = new Backdrop({
isVisible: true,
rootElement: wrapper
})
const getElement = () => document.querySelector(CLASS_BACKDROP)
instance.show(() => {
expect(getElement().parentElement).toEqual(wrapper)
done()
})
})
})
describe('hide', () => {
it('should remove the backdrop html', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP)
expect(getElements().length).toEqual(0)
instance.show(() => {
expect(getElements().length).toEqual(1)
instance.hide(() => {
expect(getElements().length).toEqual(0)
done()
})
})
})
it('should remove "show" class', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const elem = instance._getElement()
instance.show()
instance.hide(() => {
expect(elem.classList.contains(CLASS_NAME_SHOW)).toEqual(false)
done()
})
})
it('if it is not "shown", should not try to remove Node on remove method', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
const spy = spyOn(instance, 'dispose').and.callThrough()
expect(getElements().length).toEqual(0)
expect(instance._isAppended).toEqual(false)
instance.show(() => {
instance.hide(() => {
expect(getElements().length).toEqual(0)
expect(spy).not.toHaveBeenCalled()
expect(instance._isAppended).toEqual(false)
done()
})
})
})
})
describe('animation callbacks', () => {
it('if it is animated, should show and hide backdrop after counting transition duration', done => {
const instance = new Backdrop({
isVisible: true,
isAnimated: true
})
const spy2 = jasmine.createSpy('spy2')
const execDone = () => {
setTimeout(() => {
expect(spy2).toHaveBeenCalledTimes(2)
done()
}, 10)
}
instance.show(spy2)
instance.hide(() => {
spy2()
execDone()
})
expect(spy2).not.toHaveBeenCalled()
})
it('if it is not animated, should show and hide backdrop without delay', done => {
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
const instance = new Backdrop({
isVisible: true,
isAnimated: false
})
const spy2 = jasmine.createSpy('spy2')
instance.show(spy2)
instance.hide(spy2)
setTimeout(() => {
expect(spy2).toHaveBeenCalled()
expect(spy).not.toHaveBeenCalled()
done()
}, 10)
})
it('if it is not "shown", should not call delay callbacks', done => {
const instance = new Backdrop({
isVisible: false,
isAnimated: true
})
const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
instance.show()
instance.hide(() => {
expect(spy).not.toHaveBeenCalled()
done()
})
})
})
})

View File

@ -568,4 +568,12 @@ describe('Util', () => {
expect(typeof fakejQuery.fn.test.noConflict).toEqual('function') expect(typeof fakejQuery.fn.test.noConflict).toEqual('function')
}) })
}) })
describe('execute', () => {
it('should execute if arg is function', () => {
const spy = jasmine.createSpy('spy')
Util.execute(spy)
expect(spy).toHaveBeenCalled()
})
})
}) })