From ebac83e2fc67335c7b2a7ac89c1846f35fac503e Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 11 Dec 2020 21:09:01 +0200 Subject: [PATCH 1/5] Create backdrop.js util --- build/build-plugins.js | 6 +- js/src/modal.js | 82 +++-------- js/src/util/backdrop.js | 113 ++++++++++++++++ js/src/util/index.js | 19 ++- js/tests/unit/modal.spec.js | 16 +-- js/tests/unit/util/backdrop.spec.js | 157 ++++++++++++++++++++++ scss/_backdrop.scss | 19 +++ scss/_modal.scss | 14 -- scss/bootstrap.scss | 1 + site/content/docs/5.0/components/modal.md | 4 +- 10 files changed, 342 insertions(+), 89 deletions(-) create mode 100644 js/src/util/backdrop.js create mode 100644 js/tests/unit/util/backdrop.spec.js create mode 100644 scss/_backdrop.scss diff --git a/build/build-plugins.js b/build/build-plugins.js index 7fd58bcb647f..53093dc416f1 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -65,7 +65,8 @@ const getConfigByPluginKey = pluginKey => { pluginKey === 'EventHandler' || pluginKey === 'SelectorEngine' || pluginKey === 'Util' || - pluginKey === 'Sanitizer' + pluginKey === 'Sanitizer' || + pluginKey === 'Backdrop' ) { return { external: [] @@ -133,7 +134,8 @@ const getConfigByPluginKey = pluginKey => { const utilObjects = new Set([ 'Util', - 'Sanitizer' + 'Sanitizer', + 'Backdrop' ]) const domObjects = new Set([ diff --git a/js/src/modal.js b/js/src/modal.js index c6d67ac9513e..69ad46b000fd 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -20,6 +20,7 @@ import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' import { getWidth as getScrollBarWidth, hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar' 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_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` -const CLASS_NAME_BACKDROP = 'modal-backdrop' const CLASS_NAME_OPEN = 'modal-open' const CLASS_NAME_FADE = 'fade' const CLASS_NAME_SHOW = 'show' @@ -81,7 +81,7 @@ class Modal extends BaseComponent { this._config = this._getConfig(config) this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element) - this._backdrop = null + this._backdrop = this._initializeBackDrop() this._isShown = false this._ignoreBackdropClick = false this._isTransitioning = false @@ -211,6 +211,12 @@ class Modal extends BaseComponent { this._adjustDialog() } + _initializeBackDrop() { + const isAnimated = this._isAnimated() + + return new Backdrop((this._config.backdrop), isAnimated) + } + // Private _getConfig(config) { @@ -313,7 +319,7 @@ class Modal extends BaseComponent { this._element.removeAttribute('aria-modal') this._element.removeAttribute('role') this._isTransitioning = false - this._showBackdrop(() => { + this._backdrop.hide(() => { document.body.classList.remove(CLASS_NAME_OPEN) this._resetAdjustments() scrollBarReset() @@ -321,73 +327,25 @@ class Modal extends BaseComponent { }) } - _removeBackdrop() { - this._backdrop.parentNode.removeChild(this._backdrop) - this._backdrop = null - } - _showBackdrop(callback) { - const isAnimated = this._isAnimated() - if (this._isShown && this._config.backdrop) { - this._backdrop = document.createElement('div') - 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() + EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => { + if (this._ignoreBackdropClick) { + this._ignoreBackdropClick = false return } - const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) - - 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 (event.target !== event.currentTarget) { + return } - if (isAnimated) { - const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop) - EventHandler.one(this._backdrop, 'transitionend', callbackRemove) - emulateTransitionEnd(this._backdrop, backdropTransitionDuration) + if (this._config.backdrop === true) { + this.hide() } else { - callbackRemove() + this._triggerBackdropTransition() } - } else { - callback() - } + }) + + this._backdrop.show(callback) } _isAnimated() { diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js new file mode 100644 index 000000000000..dc862852bdf9 --- /dev/null +++ b/js/src/util/backdrop.js @@ -0,0 +1,113 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.0.0-beta2): util/backdrop.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import EventHandler from '../dom/event-handler' +import { execute, getTransitionDurationFromElement, promiseTimeout, reflow } from './index' + +const CLASS_NAME_BACKDROP = 'backdrop' +const CLASS_NAME_FADE = 'fade' +const CLASS_NAME_SHOW = 'show' + +const EVENT_MOUSEDOWN = 'mousedown.bs.backdrop' + +class Backdrop { + constructor(isVisible = true, isAnimated = false) { + this._isVisible = isVisible + this._isAnimated = isAnimated + this._isAppended = false + this._elem = this._createElement() + } + + _get() { + return this._elem + } + + onClick(callback) { + this._clickCallback = callback + } + + show(callback) { + if (!this._isVisible) { + execute(callback) + return + } + + if (this._isAnimated) { + this._get().classList.add(CLASS_NAME_FADE) + } + + this._append() + + if (this._isAnimated) { + reflow(this._get()) + } + + this._get().classList.add(CLASS_NAME_SHOW) + + this._emulateAnimation(() => { + execute(callback) + }) + } + + hide(callback) { + EventHandler.off(this._get(), EVENT_MOUSEDOWN) + + if (!this._isVisible) { + execute(callback) + return + } + + this._get().classList.remove(CLASS_NAME_SHOW) + + this._emulateAnimation(() => { + this._remove() + execute(callback) + }) + } + + _createElement() { + const backdrop = document.createElement('div') + backdrop.className = CLASS_NAME_BACKDROP + + return backdrop + } + + _append() { + if (this._isAppended) { + return + } + + document.body.appendChild(this._get()) + + EventHandler.on(this._get(), EVENT_MOUSEDOWN, () => { + execute(this._clickCallback) + }) + + this._isAppended = true + } + + _remove() { + if (!this._isAppended) { + return + } + + this._get().parentNode.removeChild(this._get()) + this._isAppended = false + } + + _emulateAnimation(callback) { + if (!this._isAnimated) { + execute(callback) + return + } + + const backdropTransitionDuration = getTransitionDurationFromElement(this._get()) + promiseTimeout(backdropTransitionDuration).then(() => execute(callback)) + } +} + +export default Backdrop diff --git a/js/src/util/index.js b/js/src/util/index.js index f19d76e036e9..75eea0ae438a 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -102,6 +102,15 @@ const triggerTransitionEnd = element => { const isElement = obj => (obj[0] || obj).nodeType +const promiseTimeout = duration => { + const durationPadding = 5 + const emulatedDuration = (duration || 0) + durationPadding + + return new Promise(resolve => { + setTimeout(resolve, emulatedDuration) + }) +} + const emulateTransitionEnd = (element, duration) => { let called = false const durationPadding = 5 @@ -230,6 +239,12 @@ const defineJQueryPlugin = (name, plugin) => { }) } +const execute = callback => { + if (typeof callback === 'function') { + callback() + } +} + export { getUID, getSelectorFromElement, @@ -247,5 +262,7 @@ export { getjQuery, onDOMContentLoaded, isRTL, - defineJQueryPlugin + defineJQueryPlugin, + promiseTimeout, + execute } diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 99ebbe4b37b5..4104c8e7b074 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -19,7 +19,7 @@ describe('Modal', () => { document.body.removeAttribute('style') document.body.removeAttribute('data-bs-padding-right') - document.querySelectorAll('.modal-backdrop') + document.querySelectorAll('.backdrop') .forEach(backdrop => { document.body.removeChild(backdrop) }) @@ -251,7 +251,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeDefined() + expect(document.querySelector('.backdrop')).toBeDefined() done() }) @@ -275,7 +275,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeNull() + expect(document.querySelector('.backdrop')).toBeNull() done() }) @@ -771,7 +771,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() + expect(document.querySelector('.backdrop')).toBeNull() done() }) @@ -793,7 +793,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() + expect(document.querySelector('.backdrop')).toBeNull() done() }) @@ -901,7 +901,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeDefined() + expect(document.querySelector('.backdrop')).toBeDefined() setTimeout(() => trigger.click(), 10) }) @@ -910,7 +910,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toEqual(null) + expect(document.querySelector('.backdrop')).toEqual(null) done() }) @@ -953,7 +953,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeDefined() + expect(document.querySelector('.backdrop')).toBeDefined() expect(Event.prototype.preventDefault).toHaveBeenCalled() done() }) diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js new file mode 100644 index 000000000000..d95cb2e81bca --- /dev/null +++ b/js/tests/unit/util/backdrop.spec.js @@ -0,0 +1,157 @@ +import Backdrop from '../../../src/util/backdrop' +import { getTransitionDurationFromElement } from '../../../src/util/index' + +const CLASS_BACKDROP = '.backdrop' +const CLASS_NAME_FADE = 'fade' +const CLASS_NAME_SHOW = 'show' + +describe('Backdrop', () => { + afterEach(() => { + 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(true, false) + const elems = () => document.querySelectorAll(CLASS_BACKDROP) + + expect(elems().length).toEqual(0) + + instance.show() + instance.show(() => { + expect(elems().length).toEqual(1) + elems().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(false, true) + const elems = () => document.querySelectorAll(CLASS_BACKDROP) + + expect(elems().length).toEqual(0) + instance.show(() => { + expect(elems().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(true, true) + const elems = () => document.querySelectorAll(CLASS_BACKDROP) + + expect(elems().length).toEqual(0) + + instance.show(() => { + expect(elems().length).toEqual(1) + elems().forEach(el => { + expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true) + }) + done() + }) + }) + }) + + describe('hide', () => { + it('should remove the backdrop html', done => { + const instance = new Backdrop(true, true) + + const elems = () => document.body.querySelectorAll(CLASS_BACKDROP) + + expect(elems().length).toEqual(0) + instance.show(() => { + expect(elems().length).toEqual(1) + instance.hide(() => { + expect(elems().length).toEqual(0) + done() + }) + }) + }) + + it('should remove "show" class', done => { + const instance = new Backdrop(true, true) + const elem = instance._elem + + instance.show() + instance.hide(() => { + expect(elem.classList.contains(CLASS_NAME_SHOW)).toEqual(false) + done() + }) + }) + }) + + describe('click callback', () => { + it('it should execute callback on click', done => { + const spy = jasmine.createSpy('spy') + + const instance = new Backdrop(true, false) + instance.onClick(() => spy()) + const endTest = () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + done() + }, 10) + } + + instance.show(() => { + const clickEvent = document.createEvent('MouseEvents') + clickEvent.initEvent('mousedown', true, true) + document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) + endTest() + }) + }) + }) + + describe('animation callbacks', () => { + it('if it is animated, should show and hide backdrop after counting transition duration', done => { + const instance = new Backdrop(true, 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(true, 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(false, true) + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + + instance.show() + instance.hide(() => { + expect(spy).not.toHaveBeenCalled() + done() + }) + }) + }) +}) diff --git a/scss/_backdrop.scss b/scss/_backdrop.scss new file mode 100644 index 000000000000..b9eebc4fa1ae --- /dev/null +++ b/scss/_backdrop.scss @@ -0,0 +1,19 @@ +// Modal background +.backdrop { + position: fixed; + top: 0; + left: 0; + z-index: $zindex-backdrop; + width: 100vw; + height: 100vh; + background-color: $backdrop-bg; + + // Fade for backdrop + &.fade { + opacity: 0; + } + + &.show { + opacity: $backdrop-opacity; + } +} diff --git a/scss/_modal.scss b/scss/_modal.scss index 513898644d26..ef817da642cd 100644 --- a/scss/_modal.scss +++ b/scss/_modal.scss @@ -92,20 +92,6 @@ outline: 0; } -// Modal background -.modal-backdrop { - position: fixed; - top: 0; - left: 0; - z-index: $zindex-modal-backdrop; - width: 100vw; - height: 100vh; - background-color: $modal-backdrop-bg; - - // Fade for backdrop - &.fade { opacity: 0; } - &.show { opacity: $modal-backdrop-opacity; } -} // Modal header // Top section of the modal w/ title and dismiss diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index 1a975a3db38f..1f5df105e3af 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -32,6 +32,7 @@ @import "breadcrumb"; @import "pagination"; @import "badge"; +@import "backdrop"; @import "alert"; @import "progress"; @import "list-group"; diff --git a/site/content/docs/5.0/components/modal.md b/site/content/docs/5.0/components/modal.md index 3e8ad80102b7..624b093dd22d 100644 --- a/site/content/docs/5.0/components/modal.md +++ b/site/content/docs/5.0/components/modal.md @@ -876,7 +876,7 @@ Another override is the option to pop up a modal that covers the user viewport, ## Usage -The modal plugin toggles your hidden content on demand, via data attributes or JavaScript. It also adds `.modal-open` to the `` to override default scrolling behavior and generates a `.modal-backdrop` to provide a click area for dismissing shown modals when clicking outside the modal. +The modal plugin toggles your hidden content on demand, via data attributes or JavaScript. It also adds `.modal-open` to the `` to override default scrolling behavior and generates a `.backdrop` to provide a click area for dismissing shown modals when clicking outside the modal. ### Via data attributes @@ -912,7 +912,7 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap backdrop boolean or the string 'static' true - Includes a modal-backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click. + Includes a backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click. keyboard From 875e856469a6e888290d756d1121bb78153b5099 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Thu, 11 Feb 2021 23:04:13 +0200 Subject: [PATCH 2/5] revert breaking changes remove PromiseTimout usage revert class name --- js/src/modal.js | 4 ++-- js/src/util/backdrop.js | 6 +++--- js/src/util/index.js | 10 ---------- js/tests/unit/modal.spec.js | 16 ++++++++-------- js/tests/unit/util/backdrop.spec.js | 2 +- js/tests/unit/util/index.spec.js | 8 ++++++++ scss/_backdrop.scss | 19 ------------------- scss/_modal.scss | 14 ++++++++++++++ scss/bootstrap.scss | 1 - site/content/docs/5.0/components/modal.md | 4 ++-- 10 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 scss/_backdrop.scss diff --git a/js/src/modal.js b/js/src/modal.js index 69ad46b000fd..700b8f2b917b 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -211,14 +211,14 @@ class Modal extends BaseComponent { this._adjustDialog() } + // Private + _initializeBackDrop() { const isAnimated = this._isAnimated() return new Backdrop((this._config.backdrop), isAnimated) } - // Private - _getConfig(config) { config = { ...Default, diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index dc862852bdf9..414b43139c87 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -6,9 +6,9 @@ */ import EventHandler from '../dom/event-handler' -import { execute, getTransitionDurationFromElement, promiseTimeout, reflow } from './index' +import { execute, getTransitionDurationFromElement, reflow } from './index' -const CLASS_NAME_BACKDROP = 'backdrop' +const CLASS_NAME_BACKDROP = 'modal-backdrop' const CLASS_NAME_FADE = 'fade' const CLASS_NAME_SHOW = 'show' @@ -106,7 +106,7 @@ class Backdrop { } const backdropTransitionDuration = getTransitionDurationFromElement(this._get()) - promiseTimeout(backdropTransitionDuration).then(() => execute(callback)) + setTimeout(() => execute(callback), backdropTransitionDuration + 5) } } diff --git a/js/src/util/index.js b/js/src/util/index.js index 75eea0ae438a..c27c470e9580 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -102,15 +102,6 @@ const triggerTransitionEnd = element => { const isElement = obj => (obj[0] || obj).nodeType -const promiseTimeout = duration => { - const durationPadding = 5 - const emulatedDuration = (duration || 0) + durationPadding - - return new Promise(resolve => { - setTimeout(resolve, emulatedDuration) - }) -} - const emulateTransitionEnd = (element, duration) => { let called = false const durationPadding = 5 @@ -263,6 +254,5 @@ export { onDOMContentLoaded, isRTL, defineJQueryPlugin, - promiseTimeout, execute } diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 4104c8e7b074..99ebbe4b37b5 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -19,7 +19,7 @@ describe('Modal', () => { document.body.removeAttribute('style') document.body.removeAttribute('data-bs-padding-right') - document.querySelectorAll('.backdrop') + document.querySelectorAll('.modal-backdrop') .forEach(backdrop => { document.body.removeChild(backdrop) }) @@ -251,7 +251,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.backdrop')).toBeDefined() + expect(document.querySelector('.modal-backdrop')).toBeDefined() done() }) @@ -275,7 +275,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.backdrop')).toBeNull() + expect(document.querySelector('.modal-backdrop')).toBeNull() done() }) @@ -771,7 +771,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.backdrop')).toBeNull() + expect(document.querySelector('.modal-backdrop')).toBeNull() done() }) @@ -793,7 +793,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.backdrop')).toBeNull() + expect(document.querySelector('.modal-backdrop')).toBeNull() done() }) @@ -901,7 +901,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.backdrop')).toBeDefined() + expect(document.querySelector('.modal-backdrop')).toBeDefined() setTimeout(() => trigger.click(), 10) }) @@ -910,7 +910,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual(null) expect(modalEl.getAttribute('aria-hidden')).toEqual('true') expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.backdrop')).toEqual(null) + expect(document.querySelector('.modal-backdrop')).toEqual(null) done() }) @@ -953,7 +953,7 @@ describe('Modal', () => { expect(modalEl.getAttribute('role')).toEqual('dialog') expect(modalEl.getAttribute('aria-hidden')).toEqual(null) expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.backdrop')).toBeDefined() + expect(document.querySelector('.modal-backdrop')).toBeDefined() expect(Event.prototype.preventDefault).toHaveBeenCalled() done() }) diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index d95cb2e81bca..f06ca77b4e25 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -1,7 +1,7 @@ import Backdrop from '../../../src/util/backdrop' import { getTransitionDurationFromElement } from '../../../src/util/index' -const CLASS_BACKDROP = '.backdrop' +const CLASS_BACKDROP = '.modal-backdrop' const CLASS_NAME_FADE = 'fade' const CLASS_NAME_SHOW = 'show' diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index 5d144348e458..11b6f7fa49fb 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -568,4 +568,12 @@ describe('Util', () => { 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() + }) + }) }) diff --git a/scss/_backdrop.scss b/scss/_backdrop.scss deleted file mode 100644 index b9eebc4fa1ae..000000000000 --- a/scss/_backdrop.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Modal background -.backdrop { - position: fixed; - top: 0; - left: 0; - z-index: $zindex-backdrop; - width: 100vw; - height: 100vh; - background-color: $backdrop-bg; - - // Fade for backdrop - &.fade { - opacity: 0; - } - - &.show { - opacity: $backdrop-opacity; - } -} diff --git a/scss/_modal.scss b/scss/_modal.scss index ef817da642cd..513898644d26 100644 --- a/scss/_modal.scss +++ b/scss/_modal.scss @@ -92,6 +92,20 @@ outline: 0; } +// Modal background +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: $zindex-modal-backdrop; + width: 100vw; + height: 100vh; + background-color: $modal-backdrop-bg; + + // Fade for backdrop + &.fade { opacity: 0; } + &.show { opacity: $modal-backdrop-opacity; } +} // Modal header // Top section of the modal w/ title and dismiss diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index 1f5df105e3af..1a975a3db38f 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -32,7 +32,6 @@ @import "breadcrumb"; @import "pagination"; @import "badge"; -@import "backdrop"; @import "alert"; @import "progress"; @import "list-group"; diff --git a/site/content/docs/5.0/components/modal.md b/site/content/docs/5.0/components/modal.md index 624b093dd22d..3e8ad80102b7 100644 --- a/site/content/docs/5.0/components/modal.md +++ b/site/content/docs/5.0/components/modal.md @@ -876,7 +876,7 @@ Another override is the option to pop up a modal that covers the user viewport, ## Usage -The modal plugin toggles your hidden content on demand, via data attributes or JavaScript. It also adds `.modal-open` to the `` to override default scrolling behavior and generates a `.backdrop` to provide a click area for dismissing shown modals when clicking outside the modal. +The modal plugin toggles your hidden content on demand, via data attributes or JavaScript. It also adds `.modal-open` to the `` to override default scrolling behavior and generates a `.modal-backdrop` to provide a click area for dismissing shown modals when clicking outside the modal. ### Via data attributes @@ -912,7 +912,7 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap backdrop boolean or the string 'static' true - Includes a backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click. + Includes a modal-backdrop element. Alternatively, specify static for a backdrop which doesn't close the modal on click. keyboard From 7d8dbcae4e4e9cb3677c1bae37ede2bb4832a06c Mon Sep 17 00:00:00 2001 From: GeoSot Date: Thu, 11 Feb 2021 23:37:04 +0200 Subject: [PATCH 3/5] one more test | change bundlewatch.config --- .bundlewatch.config.json | 8 ++++---- js/tests/unit/util/backdrop.spec.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 32826198c3bb..81badf254cb1 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -34,11 +34,11 @@ }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "41 kB" + "maxSize": "41.25 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", - "maxSize": "22 kB" + "maxSize": "22.25 kB" }, { "path": "./dist/js/bootstrap.esm.js", @@ -46,11 +46,11 @@ }, { "path": "./dist/js/bootstrap.esm.min.js", - "maxSize": "18 kB" + "maxSize": "18.25 kB" }, { "path": "./dist/js/bootstrap.js", - "maxSize": "27 kB" + "maxSize": "27.25 kB" }, { "path": "./dist/js/bootstrap.min.js", diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index f06ca77b4e25..e0923ea8132e 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -86,6 +86,23 @@ describe('Backdrop', () => { }) }) + it('if it is not "shown", should not try to remove Node on remove method', done => { + const instance = new Backdrop(false, true) + const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const spy = spyOn(instance, '_remove').and.callThrough() + + expect(elems().length).toEqual(0) + expect(instance._isAppended).toEqual(false) + instance.show(() => { + instance.hide(() => { + expect(elems().length).toEqual(0) + expect(spy).not.toHaveBeenCalled() + expect(instance._isAppended).toEqual(false) + done() + }) + }) + }) + describe('click callback', () => { it('it should execute callback on click', done => { const spy = jasmine.createSpy('spy') From f68d69b1965e0ce03d86c7a4eddbeb896f8c91dd Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sun, 4 Apr 2021 02:06:22 +0300 Subject: [PATCH 4/5] add config obj to backdrop helper | tests for rootElement | use transitionend helper --- js/src/modal.js | 10 ++- js/src/util/backdrop.js | 96 +++++++++++---------- js/tests/unit/util/backdrop.spec.js | 126 ++++++++++++++++++---------- 3 files changed, 143 insertions(+), 89 deletions(-) diff --git a/js/src/modal.js b/js/src/modal.js index 700b8f2b917b..1fb85d86a037 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -201,6 +201,7 @@ class Modal extends BaseComponent { this._config = null this._dialog = null + this._backdrop.dispose() this._backdrop = null this._isShown = null this._ignoreBackdropClick = null @@ -214,9 +215,10 @@ class Modal extends BaseComponent { // Private _initializeBackDrop() { - const isAnimated = this._isAnimated() - - return new Backdrop((this._config.backdrop), isAnimated) + return new Backdrop({ + isVisible: Boolean(this._config.backdrop), // 'static' option want to translated as 'true', and booleans will keep their value + isAnimated: this._isAnimated() + }) } _getConfig(config) { @@ -340,7 +342,7 @@ class Modal extends BaseComponent { if (this._config.backdrop === true) { this.hide() - } else { + } else if (this._config.backdrop === 'static') { this._triggerBackdropTransition() } }) diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 414b43139c87..ab14c23fe466 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -1,52 +1,49 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.0-beta2): util/backdrop.js + * 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 { execute, getTransitionDurationFromElement, reflow } from './index' +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' -const EVENT_MOUSEDOWN = 'mousedown.bs.backdrop' - class Backdrop { - constructor(isVisible = true, isAnimated = false) { - this._isVisible = isVisible - this._isAnimated = isAnimated + constructor(config) { + this._config = this._getConfig(config) this._isAppended = false - this._elem = this._createElement() - } - - _get() { - return this._elem - } - - onClick(callback) { - this._clickCallback = callback + this._element = null } show(callback) { - if (!this._isVisible) { + if (!this._config.isVisible) { execute(callback) return } - if (this._isAnimated) { - this._get().classList.add(CLASS_NAME_FADE) - } - this._append() - if (this._isAnimated) { - reflow(this._get()) + if (this._config.isAnimated) { + reflow(this._getElement()) } - this._get().classList.add(CLASS_NAME_SHOW) + this._getElement().classList.add(CLASS_NAME_SHOW) this._emulateAnimation(() => { execute(callback) @@ -54,26 +51,42 @@ class Backdrop { } hide(callback) { - EventHandler.off(this._get(), EVENT_MOUSEDOWN) - - if (!this._isVisible) { + if (!this._config.isVisible) { execute(callback) return } - this._get().classList.remove(CLASS_NAME_SHOW) + this._getElement().classList.remove(CLASS_NAME_SHOW) this._emulateAnimation(() => { - this._remove() + this.dispose() execute(callback) }) } - _createElement() { - const backdrop = document.createElement('div') - backdrop.className = CLASS_NAME_BACKDROP + // 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) + } - return backdrop + this._element = backdrop + } + + return this._element + } + + _getConfig(config) { + config = { + ...Default, + ...(typeof config === 'object' ? config : {}) + } + typeCheckConfig(NAME, config, DefaultType) + return config } _append() { @@ -81,32 +94,29 @@ class Backdrop { return } - document.body.appendChild(this._get()) - - EventHandler.on(this._get(), EVENT_MOUSEDOWN, () => { - execute(this._clickCallback) - }) + this._config.rootElement.appendChild(this._getElement()) this._isAppended = true } - _remove() { + dispose() { if (!this._isAppended) { return } - this._get().parentNode.removeChild(this._get()) + this._getElement().parentNode.removeChild(this._element) this._isAppended = false } _emulateAnimation(callback) { - if (!this._isAnimated) { + if (!this._config.isAnimated) { execute(callback) return } - const backdropTransitionDuration = getTransitionDurationFromElement(this._get()) - setTimeout(() => execute(callback), backdropTransitionDuration + 5) + const backdropTransitionDuration = getTransitionDurationFromElement(this._getElement()) + EventHandler.one(this._getElement(), 'transitionend', () => execute(callback)) + emulateTransitionEnd(this._getElement(), backdropTransitionDuration) } } diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index e0923ea8132e..ab205139dcbc 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -1,12 +1,20 @@ 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 => { @@ -16,7 +24,10 @@ describe('Backdrop', () => { describe('show', () => { it('if it is "shown", should append the backdrop html once, on show, and contain "show" class', done => { - const instance = new Backdrop(true, false) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) const elems = () => document.querySelectorAll(CLASS_BACKDROP) expect(elems().length).toEqual(0) @@ -32,7 +43,10 @@ describe('Backdrop', () => { }) it('if it is not "shown", should not append the backdrop html', done => { - const instance = new Backdrop(false, true) + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) const elems = () => document.querySelectorAll(CLASS_BACKDROP) expect(elems().length).toEqual(0) @@ -43,7 +57,10 @@ describe('Backdrop', () => { }) it('if it is "shown" and "animated", should append the backdrop html once, and contain "fade" class', done => { - const instance = new Backdrop(true, true) + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) const elems = () => document.querySelectorAll(CLASS_BACKDROP) expect(elems().length).toEqual(0) @@ -56,11 +73,43 @@ describe('Backdrop', () => { done() }) }) + + it('Should be appended on "document.body" by default', done => { + const instance = new Backdrop({ + isVisible: true + }) + const elem = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(elem().parentElement).toEqual(document.body) + done() + }) + }) + + it('Should appended on any element given by the proper config', done => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + rootElement: wrapper + }) + const elem = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(elem().parentElement).toEqual(wrapper) + done() + }) + }) }) describe('hide', () => { it('should remove the backdrop html', done => { - const instance = new Backdrop(true, true) + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) const elems = () => document.body.querySelectorAll(CLASS_BACKDROP) @@ -75,8 +124,11 @@ describe('Backdrop', () => { }) it('should remove "show" class', done => { - const instance = new Backdrop(true, true) - const elem = instance._elem + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const elem = instance._getElement() instance.show() instance.hide(() => { @@ -84,50 +136,34 @@ describe('Backdrop', () => { done() }) }) - }) - it('if it is not "shown", should not try to remove Node on remove method', done => { - const instance = new Backdrop(false, true) - const elems = () => document.querySelectorAll(CLASS_BACKDROP) - const spy = spyOn(instance, '_remove').and.callThrough() - - expect(elems().length).toEqual(0) - expect(instance._isAppended).toEqual(false) - instance.show(() => { - instance.hide(() => { - expect(elems().length).toEqual(0) - expect(spy).not.toHaveBeenCalled() - expect(instance._isAppended).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 }) - }) - }) - - describe('click callback', () => { - it('it should execute callback on click', done => { - const spy = jasmine.createSpy('spy') - - const instance = new Backdrop(true, false) - instance.onClick(() => spy()) - const endTest = () => { - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }, 10) - } + const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const spy = spyOn(instance, 'dispose').and.callThrough() + expect(elems().length).toEqual(0) + expect(instance._isAppended).toEqual(false) instance.show(() => { - const clickEvent = document.createEvent('MouseEvents') - clickEvent.initEvent('mousedown', true, true) - document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) - endTest() + instance.hide(() => { + expect(elems().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(true, true) + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) const spy2 = jasmine.createSpy('spy2') const execDone = () => { @@ -147,7 +183,10 @@ describe('Backdrop', () => { it('if it is not animated, should show and hide backdrop without delay', done => { const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - const instance = new Backdrop(true, false) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) const spy2 = jasmine.createSpy('spy2') instance.show(spy2) @@ -161,7 +200,10 @@ describe('Backdrop', () => { }) it('if it is not "shown", should not call delay callbacks', done => { - const instance = new Backdrop(false, true) + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) instance.show() From 2e0b0a7bc3f5eb1a6ba0f58b6a0d7be273932475 Mon Sep 17 00:00:00 2001 From: Rohit Sharma Date: Wed, 7 Apr 2021 11:02:40 +0530 Subject: [PATCH 5/5] =?UTF-8?q?Minor=20tweaks=20=E2=80=94=20Renaming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/src/modal.js | 2 +- js/tests/unit/util/backdrop.spec.js | 44 ++++++++++++++--------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/js/src/modal.js b/js/src/modal.js index 1fb85d86a037..fabb151cb42a 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -216,7 +216,7 @@ class Modal extends BaseComponent { _initializeBackDrop() { return new Backdrop({ - isVisible: Boolean(this._config.backdrop), // 'static' option want to translated as 'true', and booleans will keep their value + isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value isAnimated: this._isAnimated() }) } diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index ab205139dcbc..c8570f1861e2 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -28,14 +28,14 @@ describe('Backdrop', () => { isVisible: true, isAnimated: false }) - const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) instance.show() instance.show(() => { - expect(elems().length).toEqual(1) - elems().forEach(el => { + expect(getElements().length).toEqual(1) + getElements().forEach(el => { expect(el.classList.contains(CLASS_NAME_SHOW)).toEqual(true) }) done() @@ -47,11 +47,11 @@ describe('Backdrop', () => { isVisible: false, isAnimated: true }) - const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) instance.show(() => { - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) done() }) }) @@ -61,13 +61,13 @@ describe('Backdrop', () => { isVisible: true, isAnimated: true }) - const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) instance.show(() => { - expect(elems().length).toEqual(1) - elems().forEach(el => { + expect(getElements().length).toEqual(1) + getElements().forEach(el => { expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true) }) done() @@ -78,9 +78,9 @@ describe('Backdrop', () => { const instance = new Backdrop({ isVisible: true }) - const elem = () => document.querySelector(CLASS_BACKDROP) + const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { - expect(elem().parentElement).toEqual(document.body) + expect(getElement().parentElement).toEqual(document.body) done() }) }) @@ -96,9 +96,9 @@ describe('Backdrop', () => { isVisible: true, rootElement: wrapper }) - const elem = () => document.querySelector(CLASS_BACKDROP) + const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { - expect(elem().parentElement).toEqual(wrapper) + expect(getElement().parentElement).toEqual(wrapper) done() }) }) @@ -111,13 +111,13 @@ describe('Backdrop', () => { isAnimated: true }) - const elems = () => document.body.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) instance.show(() => { - expect(elems().length).toEqual(1) + expect(getElements().length).toEqual(1) instance.hide(() => { - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) done() }) }) @@ -142,14 +142,14 @@ describe('Backdrop', () => { isVisible: false, isAnimated: true }) - const elems = () => document.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) const spy = spyOn(instance, 'dispose').and.callThrough() - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) expect(instance._isAppended).toEqual(false) instance.show(() => { instance.hide(() => { - expect(elems().length).toEqual(0) + expect(getElements().length).toEqual(0) expect(spy).not.toHaveBeenCalled() expect(instance._isAppended).toEqual(false) done()