diff options
author | GeoSot <geo.sotis@gmail.com> | 2021-07-28 17:39:32 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-28 17:39:32 +0300 |
commit | 4bfd8a2cbcb10610b4078cefa45756b4a96301a0 (patch) | |
tree | 2358a92299ff8fa105623932caacddbf8cd83a41 /js | |
parent | 047145e8086793e7c39747e70f5d74a8860c2e50 (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>
Diffstat (limited to 'js')
-rw-r--r-- | js/src/alert.js | 28 | ||||
-rw-r--r-- | js/src/modal.js | 16 | ||||
-rw-r--r-- | js/src/offcanvas.js | 6 | ||||
-rw-r--r-- | js/src/toast.js | 7 | ||||
-rw-r--r-- | js/src/util/component-functions.js | 34 | ||||
-rw-r--r-- | js/tests/unit/toast.spec.js | 4 | ||||
-rw-r--r-- | js/tests/unit/util/component-functions.spec.js | 108 |
7 files changed, 153 insertions, 50 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() + }) + }) +}) |