From f68d69b1965e0ce03d86c7a4eddbeb896f8c91dd Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sun, 4 Apr 2021 02:06:22 +0300 Subject: [PATCH] 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()