Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/twbs/bootstrap.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeoSot <geo.sotis@gmail.com>2021-07-28 17:39:32 +0300
committerGitHub <noreply@github.com>2021-07-28 17:39:32 +0300
commit4bfd8a2cbcb10610b4078cefa45756b4a96301a0 (patch)
tree2358a92299ff8fa105623932caacddbf8cd83a41
parent047145e8086793e7c39747e70f5d74a8860c2e50 (diff)
Use a streamlined way to trigger component dismiss (#34170)
* use a streamlined way to trigger component dismiss * add documentation Co-authored-by: XhmikosR <xhmikosr@gmail.com>
-rw-r--r--js/src/alert.js28
-rw-r--r--js/src/modal.js16
-rw-r--r--js/src/offcanvas.js6
-rw-r--r--js/src/toast.js7
-rw-r--r--js/src/util/component-functions.js34
-rw-r--r--js/tests/unit/toast.spec.js4
-rw-r--r--js/tests/unit/util/component-functions.spec.js108
-rw-r--r--site/content/docs/5.0/components/alerts.md12
-rw-r--r--site/content/docs/5.0/components/modal.md11
-rw-r--r--site/content/docs/5.0/components/offcanvas.md10
-rw-r--r--site/content/docs/5.0/components/toasts.md4
-rw-r--r--site/layouts/shortcodes/js-dismiss.html15
12 files changed, 184 insertions, 71 deletions
diff --git a/js/src/alert.js b/js/src/alert.js
index 0bbe62af59..66c0bee0f7 100644
--- a/js/src/alert.js
+++ b/js/src/alert.js
@@ -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
diff --git a/js/src/modal.js b/js/src/modal.js
index 53a3ccfd1c..bb8d97e481 100644
--- a/js/src/modal.js
+++ b/js/src/modal.js
@@ -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)
/**
* ------------------------------------------------------------------------
diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js
index 6c563cb4ff..7725b0188f 100644
--- a/js/src/offcanvas.js
+++ b/js/src/offcanvas.js
@@ -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
diff --git a/js/src/toast.js b/js/src/toast.js
index 9b3c0f7c8b..bb5f768e6b 100644
--- a/js/src/toast.js
+++ b/js/src/toast.js
@@ -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
index 0000000000..b7d180e0db
--- /dev/null
+++ b/js/src/util/component-functions.js
@@ -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
+}
diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js
index 59d0247b28..c491650b1c 100644
--- a/js/tests/unit/toast.spec.js
+++ b/js/tests/unit/toast.spec.js
@@ -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
index 0000000000..edaedd32ee
--- /dev/null
+++ b/js/tests/unit/util/component-functions.spec.js
@@ -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()
+ })
+ })
+})
diff --git a/site/content/docs/5.0/components/alerts.md b/site/content/docs/5.0/components/alerts.md
index e3862de483..3389763239 100644
--- a/site/content/docs/5.0/components/alerts.md
+++ b/site/content/docs/5.0/components/alerts.md
@@ -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.**
diff --git a/site/content/docs/5.0/components/modal.md b/site/content/docs/5.0/components/modal.md
index 7ba55b3b50..56fad0297d 100644
--- a/site/content/docs/5.0/components/modal.md
+++ b/site/content/docs/5.0/components/modal.md
@@ -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 >}}
diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md
index d0c60db2b2..c28e005e69 100644
--- a/site/content/docs/5.0/components/offcanvas.md
+++ b/site/content/docs/5.0/components/offcanvas.md
@@ -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:
diff --git a/site/content/docs/5.0/components/toasts.md b/site/content/docs/5.0/components/toasts.md
index dc9501b677..d17d8b3df8 100644
--- a/site/content/docs/5.0/components/toasts.md
+++ b/site/content/docs/5.0/components/toasts.md
@@ -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
index 0000000000..45d72d0eb2
--- /dev/null
+++ b/site/layouts/shortcodes/js-dismiss.html
@@ -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>
+```