From 9f56bd55088d92ca6a9a2989b80ccc08a3c6b80c Mon Sep 17 00:00:00 2001 From: GeoSot Date: Wed, 3 Mar 2021 01:34:26 +0200 Subject: [PATCH 1/2] Accept data-bs-body option in the configuration object as well Tweak jqueryInterface, add some more tests --- js/src/offcanvas.js | 76 +++++++--- js/src/util/index.js | 17 +++ js/tests/unit/offcanvas.spec.js | 134 +++++++++++++++++- js/tests/unit/util/index.spec.js | 105 ++++++++++++++ site/content/docs/5.0/components/offcanvas.md | 47 +++++- 5 files changed, 350 insertions(+), 29 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f4927aacd68f..4b98565e2ce0 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -10,13 +10,16 @@ import { getElementFromSelector, getSelectorFromElement, getTransitionDurationFromElement, - isVisible + isDisabled, + isVisible, + typeCheckConfig } from './util/index' import { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar' import Data from './dom/data' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' +import Manipulator from './dom/manipulator' /** * ------------------------------------------------------------------------ @@ -29,10 +32,20 @@ const DATA_KEY = 'bs.offcanvas' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' const ESCAPE_KEY = 'Escape' -const DATA_BODY_ACTIONS = 'data-bs-body' + +const Default = { + backdrop: true, + keyboard: true, + scroll: false +} + +const DefaultType = { + backdrop: 'boolean', + keyboard: 'boolean', + scroll: 'boolean' +} const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' -const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_TOGGLING = 'offcanvas-toggling' const ACTIVE_SELECTOR = `.offcanvas.show, .${CLASS_NAME_TOGGLING}` @@ -55,14 +68,24 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]' */ class Offcanvas extends BaseComponent { - constructor(element) { + constructor(element, config) { super(element) + this._config = this._getConfig(config) this._isShown = element.classList.contains(CLASS_NAME_SHOW) - this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) || '' this._addEventListeners() } + // Getters + + static get Default() { + return Default + } + + static get DATA_KEY() { + return DATA_KEY + } + // Public toggle(relatedTarget) { @@ -83,11 +106,11 @@ class Offcanvas extends BaseComponent { this._isShown = true this._element.style.visibility = 'visible' - if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + if (this._config.backdrop) { document.body.classList.add(CLASS_NAME_BACKDROP_BODY) } - if (!this._bodyOptionsHas('scroll')) { + if (!this._config.scroll) { scrollBarHide() } @@ -129,11 +152,11 @@ class Offcanvas extends BaseComponent { this._element.removeAttribute('role') this._element.style.visibility = 'hidden' - if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + if (this._config.backdrop) { document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) } - if (!this._bodyOptionsHas('scroll')) { + if (!this._config.scroll) { scrollBarReset() } @@ -144,6 +167,18 @@ class Offcanvas extends BaseComponent { setTimeout(completeCallback, getTransitionDurationFromElement(this._element)) } + // Private + + _getConfig(config) { + config = { + ...Default, + ...Manipulator.getDataAttributes(this._element), + ...(typeof config === 'object' ? config : {}) + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + _enforceFocusOnElement(element) { EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop EventHandler.on(document, EVENT_FOCUSIN, event => { @@ -156,15 +191,11 @@ class Offcanvas extends BaseComponent { element.focus() } - _bodyOptionsHas(option) { - return this._bodyOptions.split(',').includes(option) - } - _addEventListeners() { EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide()) EventHandler.on(document, 'keydown', event => { - if (event.key === ESCAPE_KEY) { + if (this._config.keyboard && event.key === ESCAPE_KEY) { this.hide() } }) @@ -181,15 +212,17 @@ class Offcanvas extends BaseComponent { static jQueryInterface(config) { return this.each(function () { - const data = Data.get(this, DATA_KEY) || new Offcanvas(this) + const data = Data.get(this, DATA_KEY) || new Offcanvas(this, typeof config === 'object' ? config : {}) - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } + if (typeof config !== 'string') { + return + } - data[config](this) + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { + throw new TypeError(`No method named "${config}"`) } + + data[config](this) }) } } @@ -207,7 +240,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( event.preventDefault() } - if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) { + if (isDisabled(this)) { return } @@ -225,6 +258,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( } const data = Data.get(target, DATA_KEY) || new Offcanvas(target) + data.toggle(this) }) diff --git a/js/src/util/index.js b/js/src/util/index.js index ae3cd2ac00a9..e268b07287a0 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -153,6 +153,22 @@ const isVisible = element => { return false } +const isDisabled = element => { + if (!element || element.nodeType !== Node.ELEMENT_NODE) { + return true + } + + if (element.classList.contains('disabled')) { + return true + } + + if (typeof element.disabled !== 'undefined') { + return element.disabled + } + + return element.getAttribute('disabled') !== 'false' +} + const findShadowRoot = element => { if (!document.documentElement.attachShadow) { return null @@ -226,6 +242,7 @@ export { emulateTransitionEnd, typeCheckConfig, isVisible, + isDisabled, findShadowRoot, noop, reflow, diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 07a7cf682b33..4fb6c17ecb9c 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -2,7 +2,7 @@ import Offcanvas from '../../src/offcanvas' import EventHandler from '../../src/dom/event-handler' /** Test helpers */ -import { clearFixture, getFixture, jQueryMock, createEvent } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('Offcanvas', () => { let fixtureEl @@ -22,6 +22,18 @@ describe('Offcanvas', () => { }) }) + describe('Default', () => { + it('should return plugin default config', () => { + expect(Offcanvas.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(Offcanvas.DATA_KEY).toEqual('bs.offcanvas') + }) + }) + describe('constructor', () => { it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => { fixtureEl.innerHTML = [ @@ -70,6 +82,68 @@ describe('Offcanvas', () => { expect(offCanvas.hide).not.toHaveBeenCalled() }) + + it('should not hide if esc is pressed but with keyboard = false', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + spyOn(offCanvas, 'hide') + + document.dispatchEvent(keyDownEsc) + + expect(offCanvas.hide).not.toHaveBeenCalled() + }) + }) + + describe('config', () => { + it('should have default values', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) + + expect(offCanvas._config.backdrop).toEqual(true) + expect(offCanvas._config.keyboard).toEqual(true) + expect(offCanvas._config.scroll).toEqual(false) + }) + + it('should read data attributes and override default config', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) + + expect(offCanvas._config.backdrop).toEqual(false) + expect(offCanvas._config.keyboard).toEqual(false) + expect(offCanvas._config.scroll).toEqual(true) + }) + + it('given a config object must override data attributes', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + backdrop: true, + keyboard: true, + scroll: false + }) + expect(offCanvas._config.backdrop).toEqual(true) + expect(offCanvas._config.keyboard).toEqual(true) + expect(offCanvas._config.scroll).toEqual(false) + }) }) describe('toggle', () => { @@ -280,6 +354,64 @@ describe('Offcanvas', () => { jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] + expect(() => { + jQueryMock.fn.offcanvas.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw error on protected method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = '_getConfig' + + jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.offcanvas.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw error if method "constructor" is being called', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'constructor' + + jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.offcanvas.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw error on protected method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = '_getConfig' + + jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface + jQueryMock.elements = [div] + + try { + jQueryMock.fn.offcanvas.call(jQueryMock, action) + } catch (error) { + expect(error.message).toEqual(`No method named "${action}"`) + } + }) + + it('should throw error if method "constructor" is being called', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'constructor' + + jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface + jQueryMock.elements = [div] + try { jQueryMock.fn.offcanvas.call(jQueryMock, action) } catch (error) { diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index 935e021dd7db..24921d730e7b 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -317,6 +317,111 @@ describe('Util', () => { }) }) + describe('isDisabled', () => { + it('should return true if the element is not defined', () => { + expect(Util.isDisabled(null)).toEqual(true) + expect(Util.isDisabled(undefined)).toEqual(true) + expect(Util.isDisabled()).toEqual(true) + }) + + it('should return true if the element provided is not a dom element', () => { + expect(Util.isDisabled({})).toEqual(true) + expect(Util.isDisabled('test')).toEqual(true) + }) + + it('should return true if the element has disabled attribute', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + '
' + ].join('') + + const div = fixtureEl.querySelector('#element') + const div1 = fixtureEl.querySelector('#element1') + const div2 = fixtureEl.querySelector('#element2') + + expect(Util.isDisabled(div)).toEqual(true) + expect(Util.isDisabled(div1)).toEqual(true) + expect(Util.isDisabled(div2)).toEqual(true) + }) + + it('should return false if the element has disabled attribute with "false" value', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const div = fixtureEl.querySelector('#element') + + expect(Util.isDisabled(div)).toEqual(false) + }) + + it('should return false if the element is not disabled ', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const el = selector => fixtureEl.querySelector(selector) + + expect(Util.isDisabled(el('#button'))).toEqual(false) + expect(Util.isDisabled(el('#select'))).toEqual(false) + expect(Util.isDisabled(el('#input'))).toEqual(false) + }) + it('should return true if the element has disabled attribute', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '
' + ].join('') + + const el = selector => fixtureEl.querySelector(selector) + + expect(Util.isDisabled(el('#input'))).toEqual(true) + expect(Util.isDisabled(el('#input1'))).toEqual(true) + expect(Util.isDisabled(el('#button'))).toEqual(true) + expect(Util.isDisabled(el('#button1'))).toEqual(true) + expect(Util.isDisabled(el('#button2'))).toEqual(true) + expect(Util.isDisabled(el('#input'))).toEqual(true) + }) + + it('should return true if the element has class "disabled"', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const div = fixtureEl.querySelector('#element') + + expect(Util.isDisabled(div)).toEqual(true) + }) + + it('should return true if the element has class "disabled" but disabled attribute is false', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const div = fixtureEl.querySelector('#input') + + expect(Util.isDisabled(div)).toEqual(true) + }) + }) + describe('findShadowRoot', () => { it('should return null if shadow dom is not available', () => { // Only for newer browsers diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 9eacfd6b40fa..ac94bdd8a95d 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -119,14 +119,14 @@ Try the right and bottom examples out below. ## Options -By default, we disable scrolling on the `` when an offcanvas is visible and use a gray backdrop. Use the `data-bs-body` attribute to enable `` scrolling, or a combination of both options +By default, we disable scrolling on the `` when an offcanvas is visible and use a gray backdrop. Use the `data-bs-scroll` attribute to enable/disable `` scrolling, and `data-bs-backdrop` attribute to enable/disable backdrop usage. {{< example >}} -
+
Colored with scrolling
@@ -135,7 +135,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible an

Try scrolling the rest of the page to see this option in action.

-
+
Offcanvas with backdrop
@@ -144,7 +144,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible an

.....

-
+
Backdroped with scrolling
@@ -174,9 +174,6 @@ The offcanvas plugin utilizes a few classes and attributes to handle the heavy l - `.offcanvas-start` hides the offcanvas on the left - `.offcanvas-end` hides the offcanvas on the right - `.offcanvas-bottom` hides the offcanvas on the bottom -- `data-bs-body="scroll"` enables `` scrolling when offcanvas is open -- `data-bs-body="backdrop"` disables scrolling and creates a backdrop over the `` when offcanvas is open `(default)` -- `data-bs-body="backdrop,scroll"` combines both options to enable `` scrolling and create a backdrop over the `` when offcanvas is open Add a dismiss button with the `data-bs-dismiss="offcanvas"` attribute, which triggers the JavaScript functionality. Be sure to use the `
{{< /example >}} -## Options +## Backdrop -By default, we disable scrolling on the `` when an offcanvas is visible and use a gray backdrop. Use the `data-bs-scroll` attribute to enable/disable `` scrolling, and `data-bs-backdrop` attribute to enable/disable backdrop usage. +Scrolling the `` element is disabled when an offcanvas and its backdrop are visible. Use the `data-bs-scroll` attribute to toggle `` scrolling and `data-bs-backdrop` to toggle the backdrop. {{< example >}} @@ -196,37 +196,13 @@ var offcanvasList = offcanvasElementList.map(function (offcanvasEl) { Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-backdrop=""`. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDefaultDescription
backdropbooleantrueApply a backdrop on body while offcanvas is open
keyboardbooleantrueCloses the offcanvas when escape key is pressed
scrollbooleanfalseAllow body scrolling while offcanvas is open
- +{{< bs-table "table" >}} +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `backdrop` | boolean | `true` | Apply a backdrop on body while offcanvas is open | +| `keyboard` | boolean | `true` | Closes the offcanvas when escape key is pressed | +| `scroll` | boolean | `false` | Allow body scrolling while offcanvas is open | +{{< /bs-table >}} ### Methods @@ -243,43 +219,27 @@ var myOffcanvas = document.getElementById('myOffcanvas') var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas) ``` +{{< bs-table "table" >}} | Method | Description | | --- | --- | | `toggle` | Toggles an offcanvas element to shown or hidden. **Returns to the caller before the offcanvas element has actually been shown or hidden** (i.e. before the `shown.bs.offcanvas` or `hidden.bs.offcanvas` event occurs). | | `show` | Shows an offcanvas element. **Returns to the caller before the offcanvas element has actually been shown** (i.e. before the `shown.bs.offcanvas` event occurs).| | `hide` | Hides an offcanvas element. **Returns to the caller before the offcanvas element has actually been hidden** (i.e. before the `hidden.bs.offcanvas` event occurs).| | `_getInstance` | *Static* method which allows you to get the offcanvas instance associated with a DOM element | +{{< /bs-table >}} ### Events Bootstrap's offcanvas class exposes a few events for hooking into offcanvas functionality. - - - - - - - - - - - - - - - - - - - - - - - - - -
Event TypeDescription
show.bs.offcanvasThis event fires immediately when the show instance method is called.
shown.bs.offcanvasThis event is fired when an offcanvas element has been made visible to the user (will wait for CSS transitions to complete).
hide.bs.offcanvasThis event is fired immediately when the hide method has been called.
hidden.bs.offcanvasThis event is fired when an offcanvas element has been hidden from the user (will wait for CSS transitions to complete).
+{{< bs-table "table" >}} +| Event type | Description | +| --- | --- | +| `show.bs.offcanvas` | This event fires immediately when the `show` instance method is called. | +| `shown.bs.offcanvas` | This event is fired when an offcanvas element has been made visible to the user (will wait for CSS transitions to complete). | +| `hide.bs.offcanvas` | This event is fired immediately when the `hide` method has been called. | +| `hidden.bs.offcanvas` | This event is fired when an offcanvas element has been hidden from the user (will wait for CSS transitions to complete). | +{{< /bs-table >}} ```js var myOffcanvas = document.getElementById('myOffcanvas')