0
0
mirror of https://github.com/twbs/bootstrap.git synced 2025-01-18 10:52:19 +01:00

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>
This commit is contained in:
GeoSot 2021-07-28 17:39:32 +03:00 committed by GitHub
parent 047145e808
commit 4bfd8a2cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 184 additions and 71 deletions

View 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

View 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)
/**
* ------------------------------------------------------------------------

View 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

View 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

View File

@ -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
}

View 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 => {

View File

@ -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()
})
})
})

View 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.**

View 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 >}}

View 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:

View 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=""`.

View File

@ -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>
```