]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Use a streamlined way to trigger component dismiss (#34170)
authorGeoSot <geo.sotis@gmail.com>
Wed, 28 Jul 2021 14:39:32 +0000 (17:39 +0300)
committerGitHub <noreply@github.com>
Wed, 28 Jul 2021 14:39:32 +0000 (17:39 +0300)
* use a streamlined way to trigger component dismiss

* add documentation

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
12 files changed:
js/src/alert.js
js/src/modal.js
js/src/offcanvas.js
js/src/toast.js
js/src/util/component-functions.js [new file with mode: 0644]
js/tests/unit/toast.spec.js
js/tests/unit/util/component-functions.spec.js [new file with mode: 0644]
site/content/docs/5.0/components/alerts.md
site/content/docs/5.0/components/modal.md
site/content/docs/5.0/components/offcanvas.md
site/content/docs/5.0/components/toasts.md
site/layouts/shortcodes/js-dismiss.html [new file with mode: 0644]

index 0bbe62af59b1edfc1dfcf8dcef2f860fdc1b596c..66c0bee0f740ad0d455e6607d9035cf3073d33d1 100644 (file)
@@ -5,13 +5,10 @@
  * --------------------------------------------------------------------------
  */
 
-import {
-  defineJQueryPlugin,
-  getElementFromSelector,
-  isDisabled
-} from './util/index'
+import { defineJQueryPlugin } from './util/index'
 import EventHandler from './dom/event-handler'
 import BaseComponent from './base-component'
+import { enableDismissTrigger } from './util/component-functions'
 
 /**
  * ------------------------------------------------------------------------
@@ -22,15 +19,9 @@ import BaseComponent from './base-component'
 const NAME = 'alert'
 const DATA_KEY = 'bs.alert'
 const EVENT_KEY = `.${DATA_KEY}`
-const DATA_API_KEY = '.data-api'
-
-const SELECTOR_DISMISS = '[data-bs-dismiss="alert"]'
 
 const EVENT_CLOSE = `close${EVENT_KEY}`
 const EVENT_CLOSED = `closed${EVENT_KEY}`
-const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-
-const CLASS_NAME_ALERT = 'alert'
 const CLASS_NAME_FADE = 'fade'
 const CLASS_NAME_SHOW = 'show'
 
@@ -94,20 +85,7 @@ class Alert extends BaseComponent {
  * ------------------------------------------------------------------------
  */
 
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DISMISS, function (event) {
-  if (['A', 'AREA'].includes(this.tagName)) {
-    event.preventDefault()
-  }
-
-  if (isDisabled(this)) {
-    return
-  }
-
-  const target = getElementFromSelector(this) || this.closest(`.${CLASS_NAME_ALERT}`)
-  const alert = Alert.getOrCreateInstance(target)
-  alert.close()
-})
-
+enableDismissTrigger(Alert, 'close')
 /**
  * ------------------------------------------------------------------------
  * jQuery
index 53a3ccfd1cdd583f9a089b3b145d157d5f62706c..bb8d97e481369c1497ee1be9a63a4fcefd8eb6af 100644 (file)
@@ -20,6 +20,7 @@ import ScrollBarHelper from './util/scrollbar'
 import BaseComponent from './base-component'
 import Backdrop from './util/backdrop'
 import FocusTrap from './util/focustrap'
+import { enableDismissTrigger } from './util/component-functions'
 
 /**
  * ------------------------------------------------------------------------
@@ -62,11 +63,9 @@ const CLASS_NAME_FADE = 'fade'
 const CLASS_NAME_SHOW = 'show'
 const CLASS_NAME_STATIC = 'modal-static'
 
-const SELECTOR = '.modal'
 const SELECTOR_DIALOG = '.modal-dialog'
 const SELECTOR_MODAL_BODY = '.modal-body'
 const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
-const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="modal"]'
 
 /**
  * ------------------------------------------------------------------------
@@ -143,11 +142,7 @@ class Modal extends BaseComponent {
     this._showBackdrop(() => this._showElement(relatedTarget))
   }
 
-  hide(event) {
-    if (event && ['A', 'AREA'].includes(event.target.tagName)) {
-      event.preventDefault()
-    }
-
+  hide() {
     if (!this._isShown || this._isTransitioning) {
       return
     }
@@ -421,12 +416,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
   data.toggle(this)
 })
 
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_DISMISS, function (event) {
-  const target = getElementFromSelector(this) || this.closest(SELECTOR)
-  const modal = Modal.getOrCreateInstance(target)
-
-  modal.hide(event)
-})
+enableDismissTrigger(Modal)
 
 /**
  * ------------------------------------------------------------------------
index 6c563cb4ff70a231ff237c7199b86667370a217b..7725b0188f3652209c6209dc16f01dbcebdc6101 100644 (file)
@@ -19,6 +19,7 @@ import SelectorEngine from './dom/selector-engine'
 import Manipulator from './dom/manipulator'
 import Backdrop from './util/backdrop'
 import FocusTrap from './util/focustrap'
+import { enableDismissTrigger } from './util/component-functions'
 
 /**
  * ------------------------------------------------------------------------
@@ -54,10 +55,8 @@ const EVENT_SHOWN = `shown${EVENT_KEY}`
 const EVENT_HIDE = `hide${EVENT_KEY}`
 const EVENT_HIDDEN = `hidden${EVENT_KEY}`
 const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
 const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
 
-const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]'
 const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]'
 
 /**
@@ -197,8 +196,6 @@ class Offcanvas extends BaseComponent {
   }
 
   _addEventListeners() {
-    EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
-
     EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
       if (this._config.keyboard && event.key === ESCAPE_KEY) {
         this.hide()
@@ -263,6 +260,7 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () =>
   SelectorEngine.find(OPEN_SELECTOR).forEach(el => Offcanvas.getOrCreateInstance(el).show())
 )
 
+enableDismissTrigger(Offcanvas)
 /**
  * ------------------------------------------------------------------------
  * jQuery
index 9b3c0f7c8bfabe1522af5246fd0eadc28f4b112b..bb5f768e6b8e8bf8497497b8e9fe59bfacb4eacc 100644 (file)
@@ -13,6 +13,7 @@ import {
 import EventHandler from './dom/event-handler'
 import Manipulator from './dom/manipulator'
 import BaseComponent from './base-component'
+import { enableDismissTrigger } from './util/component-functions'
 
 /**
  * ------------------------------------------------------------------------
@@ -24,7 +25,6 @@ const NAME = 'toast'
 const DATA_KEY = 'bs.toast'
 const EVENT_KEY = `.${DATA_KEY}`
 
-const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
 const EVENT_MOUSEOVER = `mouseover${EVENT_KEY}`
 const EVENT_MOUSEOUT = `mouseout${EVENT_KEY}`
 const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
@@ -51,8 +51,6 @@ const Default = {
   delay: 5000
 }
 
-const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="toast"]'
-
 /**
  * ------------------------------------------------------------------------
  * Class Definition
@@ -202,7 +200,6 @@ class Toast extends BaseComponent {
   }
 
   _setListeners() {
-    EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())
     EventHandler.on(this._element, EVENT_MOUSEOVER, event => this._onInteraction(event, true))
     EventHandler.on(this._element, EVENT_MOUSEOUT, event => this._onInteraction(event, false))
     EventHandler.on(this._element, EVENT_FOCUSIN, event => this._onInteraction(event, true))
@@ -231,6 +228,8 @@ class Toast extends BaseComponent {
   }
 }
 
+enableDismissTrigger(Toast)
+
 /**
  * ------------------------------------------------------------------------
  * jQuery
diff --git a/js/src/util/component-functions.js b/js/src/util/component-functions.js
new file mode 100644 (file)
index 0000000..b7d180e
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v5.0.2): util/component-functions.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import EventHandler from '../dom/event-handler'
+import { getElementFromSelector, isDisabled } from './index'
+
+const enableDismissTrigger = (component, method = 'hide') => {
+  const clickEvent = `click.dismiss${component.EVENT_KEY}`
+  const name = component.NAME
+
+  EventHandler.on(document, clickEvent, `[data-bs-dismiss="${name}"]`, function (event) {
+    if (['A', 'AREA'].includes(this.tagName)) {
+      event.preventDefault()
+    }
+
+    if (isDisabled(this)) {
+      return
+    }
+
+    const target = getElementFromSelector(this) || this.closest(`.${name}`)
+    const instance = component.getOrCreateInstance(target)
+
+    // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method
+    instance[method]()
+  })
+}
+
+export {
+  enableDismissTrigger
+}
index 59d0247b28cac002b3e560432161db01bc6efb9b..c491650b1cf463ae8c2c6cc9788933bd5e2a1318 100644 (file)
@@ -467,18 +467,14 @@ describe('Toast', () => {
       fixtureEl.innerHTML = '<div></div>'
 
       const toastEl = fixtureEl.querySelector('div')
-      spyOn(toastEl, 'addEventListener').and.callThrough()
-      spyOn(toastEl, 'removeEventListener').and.callThrough()
 
       const toast = new Toast(toastEl)
 
       expect(Toast.getInstance(toastEl)).not.toBeNull()
-      expect(toastEl.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
 
       toast.dispose()
 
       expect(Toast.getInstance(toastEl)).toBeNull()
-      expect(toastEl.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
     })
 
     it('should allow to destroy toast and hide it before that', done => {
diff --git a/js/tests/unit/util/component-functions.spec.js b/js/tests/unit/util/component-functions.spec.js
new file mode 100644 (file)
index 0000000..edaedd3
--- /dev/null
@@ -0,0 +1,108 @@
+/* Test helpers */
+
+import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
+import { enableDismissTrigger } from '../../../src/util/component-functions'
+import BaseComponent from '../../../src/base-component'
+
+class DummyClass2 extends BaseComponent {
+  static get NAME() {
+    return 'test'
+  }
+
+  hide() {
+    return true
+  }
+
+  testMethod() {
+    return true
+  }
+}
+
+describe('Plugin functions', () => {
+  let fixtureEl
+
+  beforeAll(() => {
+    fixtureEl = getFixture()
+  })
+
+  afterEach(() => {
+    clearFixture()
+  })
+
+  describe('data-bs-dismiss functionality', () => {
+    it('should get Plugin and execute the given method, when a click occurred on data-bs-dismiss="PluginName"', () => {
+      fixtureEl.innerHTML = [
+        '<div id="foo" class="test">',
+        '      <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>',
+        '</div>'
+      ].join('')
+
+      spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+      spyOn(DummyClass2.prototype, 'testMethod')
+      const componentWrapper = fixtureEl.querySelector('#foo')
+      const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+      const event = createEvent('click')
+
+      enableDismissTrigger(DummyClass2, 'testMethod')
+      btnClose.dispatchEvent(event)
+
+      expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper)
+      expect(DummyClass2.prototype.testMethod).toHaveBeenCalled()
+    })
+
+    it('if data-bs-dismiss="PluginName" hasn\'t got "data-bs-target", "getOrCreateInstance" has to be initialized by closest "plugin.Name" class', () => {
+      fixtureEl.innerHTML = [
+        '<div id="foo" class="test">',
+        '   <button type="button" data-bs-dismiss="test"></button>',
+        '</div>'
+      ].join('')
+
+      spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+      spyOn(DummyClass2.prototype, 'hide')
+      const componentWrapper = fixtureEl.querySelector('#foo')
+      const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+      const event = createEvent('click')
+
+      enableDismissTrigger(DummyClass2)
+      btnClose.dispatchEvent(event)
+
+      expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper)
+      expect(DummyClass2.prototype.hide).toHaveBeenCalled()
+    })
+
+    it('if data-bs-dismiss="PluginName" is disabled, must not trigger function', () => {
+      fixtureEl.innerHTML = [
+        '<div id="foo" class="test">',
+        '   <button type="button" disabled data-bs-dismiss="test"></button>',
+        '</div>'
+      ].join('')
+
+      spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+      const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+      const event = createEvent('click')
+
+      enableDismissTrigger(DummyClass2)
+      btnClose.dispatchEvent(event)
+
+      expect(DummyClass2.getOrCreateInstance).not.toHaveBeenCalled()
+    })
+
+    it('should prevent default when the trigger is <a> or <area>', () => {
+      fixtureEl.innerHTML = [
+        '<div id="foo" class="test">',
+        '      <a type="button" data-bs-dismiss="test"></a>',
+        '</div>'
+      ].join('')
+
+      const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+      const event = createEvent('click')
+
+      enableDismissTrigger(DummyClass2)
+      spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+      btnClose.dispatchEvent(event)
+
+      expect(Event.prototype.preventDefault).toHaveBeenCalled()
+    })
+  })
+})
index e3862de483034ff0d496d37ca937bd2c57676e58..3389763239bdac5a26436cb52801d120c2a0a2d2 100644 (file)
@@ -204,17 +204,7 @@ See the [triggers](#triggers) section for more details.
 
 ### Triggers
 
-Dismissal can be achieved with `data` attributes on a button **within the alert** as demonstrated above:
-
-```html
-<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
-```
-
-or on a button **outside the alert** using the `data-bs-target` as demonstrated above:
-
-```html
-<button type="button" class="btn-close" data-bs-dismiss="alert" data-bs-target="#my-alert" aria-label="Close"></button>
-```
+{{% js-dismiss "alert" %}}
 
 **Note that closing an alert will remove it from the DOM.**
 
index 7ba55b3b509ac5790e29ce86ac2ac94e692c91f1..56fad0297d5f46e3ffb18726c83b64b5a8ed87b3 100644 (file)
@@ -840,17 +840,8 @@ Activate a modal without writing JavaScript. Set `data-bs-toggle="modal"` on a c
 ```
 #### Dismiss
 
-Dismissal can be achieved with `data` attributes on a button **within the modal** as demonstrated below:
+{{% js-dismiss "modal" %}}
 
-```html
-<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-```
-
-or on a button **outside the modal** using the `data-bs-target` as demonstrated below:
-
-```html
-<button type="button" class="btn-close" data-bs-dismiss="modal" data-bs-target="#my-modal" aria-label="Close"></button>
-```
 {{< callout warning >}}
 While both ways to dismiss a modal are supported, keep in mind that dismissing from outside a modal does not match [the WAI-ARIA modal dialog design pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal). Do this at your own risk.
 {{< /callout >}}
index d0c60db2b2947c000493cc7eccd4771e5078e46c..c28e005e695fe7934ced680a83b205ca2d185f2f 100644 (file)
@@ -194,8 +194,18 @@ Add a dismiss button with the `data-bs-dismiss="offcanvas"` attribute, which tri
 
 ### Via data attributes
 
+#### Toggle
+
 Add `data-bs-toggle="offcanvas"` and a `data-bs-target` or `href` to the element to automatically assign control of one offcanvas element. The `data-bs-target` attribute accepts a CSS selector to apply the offcanvas to. Be sure to add the class `offcanvas` to the offcanvas element. If you'd like it to default open, add the additional class `show`.
 
+#### Dismiss
+
+{{% js-dismiss "offcanvas" %}}
+
+{{< callout warning >}}
+While both ways to dismiss an offcanvas are supported, keep in mind that dismissing from outside an offcanvas does not match [the WAI-ARIA modal dialog design pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal). Do this at your own risk.
+{{< /callout >}}
+
 ### Via JavaScript
 
 Enable manually with:
index dc9501b67734933443c2ee4708590ed84c8fdef2..d17d8b3df880eb3a4bb3d40a4216497f484f7559 100644 (file)
@@ -341,6 +341,10 @@ var toastList = toastElList.map(function (toastEl) {
 })
 ```
 
+### Triggers
+
+{{% js-dismiss "toast" %}}
+
 ### Options
 
 Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-animation=""`.
diff --git a/site/layouts/shortcodes/js-dismiss.html b/site/layouts/shortcodes/js-dismiss.html
new file mode 100644 (file)
index 0000000..45d72d0
--- /dev/null
@@ -0,0 +1,15 @@
+{{- /* Usage: js-dismiss "ComponentName" */ -}}
+
+{{- $name := .Get 0 -}}
+
+Dismissal can be achieved with the `data` attribute on a button **within the {{ $name }}** as demonstrated below:
+
+```html
+<button type="button" class="btn-close" data-bs-dismiss="{{ $name }}" aria-label="Close"></button>
+```
+
+or on a button **outside the {{ $name }}** using the `data-bs-target` as demonstrated below:
+
+```html
+<button type="button" class="btn-close" data-bs-dismiss="{{ $name }}" data-bs-target="#my-{{ $name }}" aria-label="Close"></button>
+```