]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Decouple BackDrop from modal (#32439)
authorGeoSot <geo.sotis@gmail.com>
Wed, 14 Apr 2021 20:28:50 +0000 (23:28 +0300)
committerGitHub <noreply@github.com>
Wed, 14 Apr 2021 20:28:50 +0000 (13:28 -0700)
* 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>
.bundlewatch.config.json
build/build-plugins.js
js/src/modal.js
js/src/util/backdrop.js [new file with mode: 0644]
js/src/util/index.js
js/tests/unit/util/backdrop.spec.js [new file with mode: 0644]
js/tests/unit/util/index.spec.js

index 32826198c3bb70f63d3f07021ad00db70b4d9fee..81badf254cb11eaccfd148a4cfc1e4d07a71e23e 100644 (file)
     },
     {
       "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",
     },
     {
       "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",
index 7fd58bcb647f61d9739ba4415289cb314edd681d..53093dc416f19a8c68a19e95fc2b6e48bc63fc37 100644 (file)
@@ -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([
index c6d67ac9513e22769d3f1fd9c22cb3a589c22105..fabb151cb42aca1c259828805472c6095a1adca7 100644 (file)
@@ -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
@@ -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
@@ -213,6 +214,13 @@ class Modal extends BaseComponent {
 
   // 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) {
     config = {
       ...Default,
@@ -313,7 +321,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 +329,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)
+    EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => {
+      if (this._ignoreBackdropClick) {
+        this._ignoreBackdropClick = false
+        return
       }
 
-      this._backdrop.classList.add(CLASS_NAME_SHOW)
-
-      if (!isAnimated) {
-        callback()
+      if (event.target !== event.currentTarget) {
         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 (this._config.backdrop === true) {
+        this.hide()
+      } else if (this._config.backdrop === 'static') {
+        this._triggerBackdropTransition()
       }
+    })
 
-      if (isAnimated) {
-        const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)
-        EventHandler.one(this._backdrop, 'transitionend', callbackRemove)
-        emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
-      } else {
-        callbackRemove()
-      }
-    } 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 (file)
index 0000000..ab14c23
--- /dev/null
@@ -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
index f19d76e036e917afa788d3cb184b6f9f6d04f14b..c27c470e9580920f94878624fb54cc59730d2cf4 100644 (file)
@@ -230,6 +230,12 @@ const defineJQueryPlugin = (name, plugin) => {
   })
 }
 
+const execute = callback => {
+  if (typeof callback === 'function') {
+    callback()
+  }
+}
+
 export {
   getUID,
   getSelectorFromElement,
@@ -247,5 +253,6 @@ export {
   getjQuery,
   onDOMContentLoaded,
   isRTL,
-  defineJQueryPlugin
+  defineJQueryPlugin,
+  execute
 }
diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js
new file mode 100644 (file)
index 0000000..c8570f1
--- /dev/null
@@ -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()
+      })
+    })
+  })
+})
index 5d144348e458ca9b49cbd473daebbd77b9e5de79..11b6f7fa49fbd8e0453b33d115d7108c2f8b0692 100644 (file)
@@ -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()
+    })
+  })
 })