From 548be2ed6604ddfc8488cd4a793c6271c2caf485 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 2 Mar 2021 19:10:10 +0200 Subject: Offcanvas as component (#29017) * Add a new offcanvas component * offcanvas.js: switch to string constants and `event.key` * Remove unneeded code * Sass optimizations * Fixes Make sure the element is hidden and not offscreen when inactive fix close icon negative margins Add content in right & bottom examples Re-fix bottom offcanvas height not to cover all viewport * Wording tweaks * update tests and offcanvas class * separate scrollbar functionality and use it in offcanvas * Update .bundlewatch.config.json * fix focus * update btn-close / fix focus on close * add aria-modal and role return focus on trigger when offcanvas is closed change body scrolling timings * move common code to reusable functions * add aria-labelledby * Replace lorem ipsum text * fix focus when offcanvas is closed * updates * revert modal, add tests for scrollbar * show backdrop by default * Update offcanvas.md * Update offcanvas CSS to better match modals - Add background-clip for borders - Move from outline to border (less clever, more consistent) - Add scss-docs in vars * Revamp offcanvas docs - Add static example to show and explain the components - Split live examples and rename them - Simplify example content - Expand docs notes elsewhere - Add sass docs * Add .offcanvas-title instead of .modal-title * Rename offcanvas example to offcanvas-navbar to reflect it's purpose * labelledby references title and not header * Add default shadow to offcanvas * enable offcanvas-body to fill all the remaining wrapper area * Be more descriptive, on Accessibility area * remove redundant classes * ensure in case of an already open offcanvas, not to open another one * bring back backdrop|scroll combinations * bring back toggling class * refactor scrollbar method, plus tests * add check if element is not full-width, according to #30621 * revert all in modal * use documentElement innerWidth * Rename classes to -start and -end Also copyedit some docs wording * omit some things on scrollbar * PASS BrowserStack tests -- IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling. * Rename '_handleClosing' to '_addEventListeners' * change pipe usage to comma * change Data.getData to Data.get Co-authored-by: XhmikosR Co-authored-by: Martijn Cuppens Co-authored-by: Mark Otto --- js/src/offcanvas.js | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 js/src/offcanvas.js (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js new file mode 100644 index 000000000..148f003e9 --- /dev/null +++ b/js/src/offcanvas.js @@ -0,0 +1,239 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.0.0-beta2): offcanvas.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + defineJQueryPlugin, + getElementFromSelector, + getSelectorFromElement, + getTransitionDurationFromElement, + isVisible +} 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' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'offcanvas' +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 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}` + +const EVENT_SHOW = `show${EVENT_KEY}` +const EVENT_SHOWN = `shown${EVENT_KEY}` +const EVENT_HIDE = `hide${EVENT_KEY}` +const EVENT_HIDDEN = `hidden${EVENT_KEY}` +const EVENT_FOCUSIN = `focusin${EVENT_KEY}` +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` +const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}` + +const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]' +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]' + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class OffCanvas extends BaseComponent { + constructor(element) { + super(element) + + this._isShown = element.classList.contains(CLASS_NAME_SHOW) + this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) || '' + this._addEventListeners() + } + + // Public + + toggle(relatedTarget) { + return this._isShown ? this.hide() : this.show(relatedTarget) + } + + show(relatedTarget) { + if (this._isShown) { + return + } + + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, { relatedTarget }) + + if (showEvent.defaultPrevented) { + return + } + + this._isShown = true + this._element.style.visibility = 'visible' + + if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + document.body.classList.add(CLASS_NAME_BACKDROP_BODY) + } + + if (!this._bodyOptionsHas('scroll')) { + scrollBarHide() + } + + this._element.classList.add(CLASS_NAME_TOGGLING) + this._element.removeAttribute('aria-hidden') + this._element.setAttribute('aria-modal', true) + this._element.setAttribute('role', 'dialog') + this._element.classList.add(CLASS_NAME_SHOW) + + const completeCallBack = () => { + this._element.classList.remove(CLASS_NAME_TOGGLING) + EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) + this._enforceFocusOnElement(this._element) + } + + setTimeout(completeCallBack, getTransitionDurationFromElement(this._element)) + } + + hide() { + if (!this._isShown) { + return + } + + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE) + + if (hideEvent.defaultPrevented) { + return + } + + this._element.classList.add(CLASS_NAME_TOGGLING) + EventHandler.off(document, EVENT_FOCUSIN) + this._element.blur() + this._isShown = false + this._element.classList.remove(CLASS_NAME_SHOW) + + const completeCallback = () => { + this._element.setAttribute('aria-hidden', true) + this._element.removeAttribute('aria-modal') + this._element.removeAttribute('role') + this._element.style.visibility = 'hidden' + + if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { + document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) + } + + if (!this._bodyOptionsHas('scroll')) { + scrollBarReset() + } + + EventHandler.trigger(this._element, EVENT_HIDDEN) + this._element.classList.remove(CLASS_NAME_TOGGLING) + } + + setTimeout(completeCallback, getTransitionDurationFromElement(this._element)) + } + + _enforceFocusOnElement(element) { + EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop + EventHandler.on(document, EVENT_FOCUSIN, event => { + if (document !== event.target && + element !== event.target && + !element.contains(event.target)) { + element.focus() + } + }) + 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) { + this.hide() + } + }) + + EventHandler.on(document, EVENT_CLICK_DATA_API, event => { + const target = SelectorEngine.findOne(getSelectorFromElement(event.target)) + if (!this._element.contains(event.target) && target !== this._element) { + this.hide() + } + }) + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + const data = Data.get(this, DATA_KEY) || new OffCanvas(this) + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config](this) + } + }) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { + const target = getElementFromSelector(this) + + if (['A', 'AREA'].includes(this.tagName)) { + event.preventDefault() + } + + if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) { + return + } + + EventHandler.one(target, EVENT_HIDDEN, () => { + // focus on trigger when it is closed + if (isVisible(this)) { + this.focus() + } + }) + + // avoid conflict when clicking a toggler of an offcanvas, while another is open + const allReadyOpen = SelectorEngine.findOne(ACTIVE_SELECTOR) + if (allReadyOpen && allReadyOpen !== target) { + return + } + + const data = Data.get(target, DATA_KEY) || new OffCanvas(target) + data.toggle(this) +}) + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ + +defineJQueryPlugin(NAME, OffCanvas) + +export default OffCanvas -- cgit v1.2.3 From 6ecd1c626e4129daf45a47b44c2e2eae60a09fa3 Mon Sep 17 00:00:00 2001 From: Rohit Sharma Date: Tue, 16 Mar 2021 10:51:04 +0530 Subject: Change the name of the `Offcanvas` constructor (#33261) --- js/src/offcanvas.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 148f003e9..f4927aacd 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -54,7 +54,7 @@ const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]' * ------------------------------------------------------------------------ */ -class OffCanvas extends BaseComponent { +class Offcanvas extends BaseComponent { constructor(element) { super(element) @@ -181,7 +181,7 @@ 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) if (typeof config === 'string') { if (typeof data[config] === 'undefined') { @@ -224,7 +224,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( return } - const data = Data.get(target, DATA_KEY) || new OffCanvas(target) + const data = Data.get(target, DATA_KEY) || new Offcanvas(target) data.toggle(this) }) @@ -234,6 +234,6 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( * ------------------------------------------------------------------------ */ -defineJQueryPlugin(NAME, OffCanvas) +defineJQueryPlugin(NAME, Offcanvas) -export default OffCanvas +export default Offcanvas -- cgit v1.2.3 From ddf72bc6124618e3f4b6a056503d4f51d49c928e Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 16 Mar 2021 18:35:03 +0200 Subject: Accept data-bs-body option in the configuration object as well (#33248) * Accept data-bs-body option in the configuration object as well Tweak jqueryInterface, add some more tests * Fix Markdown table formatting and tweak the wording on backdrop Co-authored-by: Mark Otto Co-authored-by: XhmikosR --- js/src/offcanvas.js | 76 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 21 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f4927aacd..4b98565e2 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) }) -- cgit v1.2.3 From 1c02ef4f971afe5df75d4e1889435f3edd9f2bbd Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 23 Mar 2021 08:22:59 +0200 Subject: Allow offcanvas to be initialized in open state (#33382) * Update docs to use new .show behavior and clarify some copy for first example Co-authored-by: Mark Otto Co-authored-by: XhmikosR --- js/src/offcanvas.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 4b98565e2..1824b3e3b 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -31,6 +31,7 @@ const NAME = 'offcanvas' const DATA_KEY = 'bs.offcanvas' const EVENT_KEY = `.${DATA_KEY}` const DATA_API_KEY = '.data-api' +const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}` const ESCAPE_KEY = 'Escape' const Default = { @@ -48,7 +49,8 @@ const DefaultType = { const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_TOGGLING = 'offcanvas-toggling' -const ACTIVE_SELECTOR = `.offcanvas.show, .${CLASS_NAME_TOGGLING}` +const OPEN_SELECTOR = '.offcanvas.show' +const ACTIVE_SELECTOR = `${OPEN_SELECTOR}, .${CLASS_NAME_TOGGLING}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` @@ -72,7 +74,7 @@ class Offcanvas extends BaseComponent { super(element) this._config = this._getConfig(config) - this._isShown = element.classList.contains(CLASS_NAME_SHOW) + this._isShown = false this._addEventListeners() } @@ -262,6 +264,10 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( data.toggle(this) }) +EventHandler.on(window, EVENT_LOAD_DATA_API, () => { + SelectorEngine.find(OPEN_SELECTOR).forEach(el => (Data.get(el, DATA_KEY) || new Offcanvas(el)).show()) +}) + /** * ------------------------------------------------------------------------ * jQuery -- cgit v1.2.3 From 220139a89ffc3864bbb6e1b35471667318eadc1f Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Tue, 23 Mar 2021 18:26:54 +0200 Subject: Release v5.0.0-beta3 (#33439) --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 1824b3e3b..02b0b58a9 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.0-beta2): offcanvas.js + * Bootstrap (v5.0.0-beta3): offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3 From a9d7a62658c5d93dcba5ed5fc47d84f3ddd3e0a3 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Mon, 19 Apr 2021 08:20:25 +0300 Subject: Use the backdrop util in offcanvas, enforcing consistency (#33545) * respect /share modal's backdrop functionality, keeping consistency * listen click events over backdrop (only) and trigger `hide()` without add/remove event tricks * achieve to hide foreign open offcanvas instances without glitches `if (allReadyOpen && allReadyOpen !== target)`, in case another is going to be open, when user clicks on trigger button --- js/src/offcanvas.js | 60 +++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 27 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 02b0b58a9..2b6335b39 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -7,8 +7,8 @@ import { defineJQueryPlugin, + emulateTransitionEnd, getElementFromSelector, - getSelectorFromElement, getTransitionDurationFromElement, isDisabled, isVisible, @@ -20,6 +20,7 @@ import EventHandler from './dom/event-handler' import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' import Manipulator from './dom/manipulator' +import Backdrop from './util/backdrop' /** * ------------------------------------------------------------------------ @@ -46,11 +47,8 @@ const DefaultType = { scroll: 'boolean' } -const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' const CLASS_NAME_SHOW = 'show' -const CLASS_NAME_TOGGLING = 'offcanvas-toggling' const OPEN_SELECTOR = '.offcanvas.show' -const ACTIVE_SELECTOR = `${OPEN_SELECTOR}, .${CLASS_NAME_TOGGLING}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` @@ -59,6 +57,7 @@ const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_FOCUSIN = `focusin${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"]' @@ -75,6 +74,7 @@ class Offcanvas extends BaseComponent { this._config = this._getConfig(config) this._isShown = false + this._backdrop = this._initializeBackDrop() this._addEventListeners() } @@ -108,27 +108,25 @@ class Offcanvas extends BaseComponent { this._isShown = true this._element.style.visibility = 'visible' - if (this._config.backdrop) { - document.body.classList.add(CLASS_NAME_BACKDROP_BODY) - } + this._backdrop.show() if (!this._config.scroll) { scrollBarHide() } - this._element.classList.add(CLASS_NAME_TOGGLING) this._element.removeAttribute('aria-hidden') this._element.setAttribute('aria-modal', true) this._element.setAttribute('role', 'dialog') this._element.classList.add(CLASS_NAME_SHOW) const completeCallBack = () => { - this._element.classList.remove(CLASS_NAME_TOGGLING) EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) this._enforceFocusOnElement(this._element) } - setTimeout(completeCallBack, getTransitionDurationFromElement(this._element)) + const transitionDuration = getTransitionDurationFromElement(this._element) + EventHandler.one(this._element, 'transitionend', completeCallBack) + emulateTransitionEnd(this._element, transitionDuration) } hide() { @@ -142,11 +140,11 @@ class Offcanvas extends BaseComponent { return } - this._element.classList.add(CLASS_NAME_TOGGLING) EventHandler.off(document, EVENT_FOCUSIN) this._element.blur() this._isShown = false this._element.classList.remove(CLASS_NAME_SHOW) + this._backdrop.hide() const completeCallback = () => { this._element.setAttribute('aria-hidden', true) @@ -154,19 +152,25 @@ class Offcanvas extends BaseComponent { this._element.removeAttribute('role') this._element.style.visibility = 'hidden' - if (this._config.backdrop) { - document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) - } - if (!this._config.scroll) { scrollBarReset() } EventHandler.trigger(this._element, EVENT_HIDDEN) - this._element.classList.remove(CLASS_NAME_TOGGLING) } - setTimeout(completeCallback, getTransitionDurationFromElement(this._element)) + const transitionDuration = getTransitionDurationFromElement(this._element) + EventHandler.one(this._element, 'transitionend', completeCallback) + emulateTransitionEnd(this._element, transitionDuration) + } + + dispose() { + this._backdrop.dispose() + super.dispose() + EventHandler.off(document, EVENT_FOCUSIN) + + this._config = null + this._backdrop = null } // Private @@ -181,6 +185,15 @@ class Offcanvas extends BaseComponent { return config } + _initializeBackDrop() { + return new Backdrop({ + isVisible: this._config.backdrop, + isAnimated: true, + rootElement: this._element.parentNode, + clickCallback: () => this.hide() + }) + } + _enforceFocusOnElement(element) { EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop EventHandler.on(document, EVENT_FOCUSIN, event => { @@ -196,18 +209,11 @@ class Offcanvas extends BaseComponent { _addEventListeners() { EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide()) - EventHandler.on(document, 'keydown', event => { + EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { if (this._config.keyboard && event.key === ESCAPE_KEY) { this.hide() } }) - - EventHandler.on(document, EVENT_CLICK_DATA_API, event => { - const target = SelectorEngine.findOne(getSelectorFromElement(event.target)) - if (!this._element.contains(event.target) && target !== this._element) { - this.hide() - } - }) } // Static @@ -254,9 +260,9 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( }) // avoid conflict when clicking a toggler of an offcanvas, while another is open - const allReadyOpen = SelectorEngine.findOne(ACTIVE_SELECTOR) + const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR) if (allReadyOpen && allReadyOpen !== target) { - return + Offcanvas.getInstance(allReadyOpen).hide() } const data = Data.get(target, DATA_KEY) || new Offcanvas(target) -- cgit v1.2.3 From 079f2cd90c247225eb71dff4d514faf50f653416 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 20 Apr 2021 08:32:52 +0300 Subject: Offcanvas.js: If scroll is allowed, should allow focus on other elements (#33677) --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 2b6335b39..7fcdfb48a 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -112,6 +112,7 @@ class Offcanvas extends BaseComponent { if (!this._config.scroll) { scrollBarHide() + this._enforceFocusOnElement(this._element) } this._element.removeAttribute('aria-hidden') @@ -121,7 +122,6 @@ class Offcanvas extends BaseComponent { const completeCallBack = () => { EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) - this._enforceFocusOnElement(this._element) } const transitionDuration = getTransitionDurationFromElement(this._element) -- cgit v1.2.3 From bf0936748602c8109fd916c64b4560799fa1c3f8 Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Wed, 5 May 2021 22:32:12 +0300 Subject: Release v5.0.0 (#33647) * Bump version to 5.0.0 * Fix npm tag * Dist --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 7fcdfb48a..f3459e667 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.0-beta3): offcanvas.js + * Bootstrap (v5.0.0): offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3 From 90b1a6907ed7bb3397fe6bd223f09eb12122d7a3 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sun, 11 Apr 2021 02:27:18 +0300 Subject: Merge js-components 'transitionend' listener callbacks into one method --- js/src/offcanvas.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f3459e667..68f8e8142 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -7,9 +7,7 @@ import { defineJQueryPlugin, - emulateTransitionEnd, getElementFromSelector, - getTransitionDurationFromElement, isDisabled, isVisible, typeCheckConfig @@ -124,9 +122,7 @@ class Offcanvas extends BaseComponent { EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) } - const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, 'transitionend', completeCallBack) - emulateTransitionEnd(this._element, transitionDuration) + this._queueCallback(completeCallBack, this._element, true) } hide() { @@ -159,9 +155,7 @@ class Offcanvas extends BaseComponent { EventHandler.trigger(this._element, EVENT_HIDDEN) } - const transitionDuration = getTransitionDurationFromElement(this._element) - EventHandler.one(this._element, 'transitionend', completeCallback) - emulateTransitionEnd(this._element, transitionDuration) + this._queueCallback(completeCallback, this._element, true) } dispose() { -- cgit v1.2.3 From 03842b5f259d6007db02c465e6c55929e551e9cd Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 11 May 2021 09:04:42 +0300 Subject: Refactor: move disposing properties into the base class (#33740) Moves more functionality to `base-component`, transferring the responsibility of disposal to parent class. Each component, dusting disposal, sets its protected properties to `null`. So the same can be done in one place for all children components . --- js/src/offcanvas.js | 3 --- 1 file changed, 3 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 68f8e8142..8ddb776b1 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -162,9 +162,6 @@ class Offcanvas extends BaseComponent { this._backdrop.dispose() super.dispose() EventHandler.off(document, EVENT_FOCUSIN) - - this._config = null - this._backdrop = null } // Private -- cgit v1.2.3 From 9fe36edf683af02574bf6bbd6c9b27de93bd31b1 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 11 May 2021 10:49:30 +0300 Subject: Extract static `DATA_KEY` & `EVENT_KEY` to base-component (#33635) * Force each plugin that extends base-components to implement a static method `NAME()` * Remove redundant `NAME` argument from 'Utils.defineJQueryPlugin' & fix test --- js/src/offcanvas.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 8ddb776b1..65d1e6ba7 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -78,12 +78,12 @@ class Offcanvas extends BaseComponent { // Getters - static get Default() { - return Default + static get NAME() { + return NAME } - static get DATA_KEY() { - return DATA_KEY + static get Default() { + return Default } // Public @@ -271,6 +271,6 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => { * ------------------------------------------------------------------------ */ -defineJQueryPlugin(NAME, Offcanvas) +defineJQueryPlugin(Offcanvas) export default Offcanvas -- cgit v1.2.3 From 58b1be927f43c779377e478df2d119f2ddf956ca Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Thu, 13 May 2021 19:22:20 +0300 Subject: Release v5.0.1 (#33972) * Bump version to 5.0.1. * Dist --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 65d1e6ba7..fed892f4c 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.0): offcanvas.js + * Bootstrap (v5.0.1): offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3 From c98657b8303150bfda3bdea750055b83a29b27a3 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Thu, 3 Jun 2021 18:53:27 +0300 Subject: Add `getOrCreateInstance` method in base-component (#33276) Co-authored-by: Rohit Sharma Co-authored-by: XhmikosR --- js/src/offcanvas.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index fed892f4c..f990ff199 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -13,7 +13,6 @@ import { 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' @@ -211,7 +210,7 @@ class Offcanvas extends BaseComponent { static jQueryInterface(config) { return this.each(function () { - const data = Data.get(this, DATA_KEY) || new Offcanvas(this, typeof config === 'object' ? config : {}) + const data = Offcanvas.getOrCreateInstance(this, config) if (typeof config !== 'string') { return @@ -256,14 +255,13 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( Offcanvas.getInstance(allReadyOpen).hide() } - const data = Data.get(target, DATA_KEY) || new Offcanvas(target) - + const data = Offcanvas.getOrCreateInstance(target) data.toggle(this) }) -EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - SelectorEngine.find(OPEN_SELECTOR).forEach(el => (Data.get(el, DATA_KEY) || new Offcanvas(el)).show()) -}) +EventHandler.on(window, EVENT_LOAD_DATA_API, () => + SelectorEngine.find(OPEN_SELECTOR).forEach(el => Offcanvas.getOrCreateInstance(el).show()) +) /** * ------------------------------------------------------------------------ -- cgit v1.2.3 From cb47b8c9640abcc19c17908475153849b9d4ad60 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sun, 6 Jun 2021 09:26:36 +0300 Subject: Refactor scrollbar.js to be used as a Class (#33947) --- js/src/offcanvas.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f990ff199..71e47668f 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -12,7 +12,7 @@ import { isVisible, typeCheckConfig } from './util/index' -import { hide as scrollBarHide, reset as scrollBarReset } from './util/scrollbar' +import ScrollBarHelper from './util/scrollbar' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' @@ -108,7 +108,7 @@ class Offcanvas extends BaseComponent { this._backdrop.show() if (!this._config.scroll) { - scrollBarHide() + new ScrollBarHelper().hide() this._enforceFocusOnElement(this._element) } @@ -148,7 +148,7 @@ class Offcanvas extends BaseComponent { this._element.style.visibility = 'hidden' if (!this._config.scroll) { - scrollBarReset() + new ScrollBarHelper().reset() } EventHandler.trigger(this._element, EVENT_HIDDEN) -- cgit v1.2.3 From 688bce4fa695cc360a0d084e34f029b0c192b223 Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Tue, 22 Jun 2021 21:29:16 +0300 Subject: Release v5.0.2 (#34276) * Bump version to v5.0.2. * Dist --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 71e47668f..88eb8c997 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.1): offcanvas.js + * Bootstrap (v5.0.2): offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3 From 45d26de72817b295c5f94c8426354fd5b7d0a1f9 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Fri, 25 Jun 2021 13:41:15 -0700 Subject: Variablize backdrop for modal and offcanvas --- js/src/offcanvas.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 88eb8c997..016260437 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -45,6 +45,7 @@ const DefaultType = { } const CLASS_NAME_SHOW = 'show' +const CLASS_NAME_BACKDROP = 'offcanvas-backdrop' const OPEN_SELECTOR = '.offcanvas.show' const EVENT_SHOW = `show${EVENT_KEY}` @@ -177,6 +178,7 @@ class Offcanvas extends BaseComponent { _initializeBackDrop() { return new Backdrop({ + className: CLASS_NAME_BACKDROP, isVisible: this._config.backdrop, isAnimated: true, rootElement: this._element.parentNode, -- cgit v1.2.3 From 7646f6bd33a03132e446fb060880bbf051a1639f Mon Sep 17 00:00:00 2001 From: Ryan Berliner <22206986+RyanBerliner@users.noreply.github.com> Date: Tue, 27 Jul 2021 01:01:04 -0400 Subject: Add shift-tab keyboard support for dialogs (modal & Offcanvas components) (#33865) * consolidate dialog focus trap logic * add shift-tab support to focustrap * remove redundant null check of trap element Co-authored-by: GeoSot * remove area support forom focusableChildren * fix no expectations warning in focustrap tests Co-authored-by: GeoSot Co-authored-by: XhmikosR --- js/src/offcanvas.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 016260437..6c563cb4f 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -18,6 +18,7 @@ import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' import Manipulator from './dom/manipulator' import Backdrop from './util/backdrop' +import FocusTrap from './util/focustrap' /** * ------------------------------------------------------------------------ @@ -52,7 +53,6 @@ const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` const EVENT_HIDE = `hide${EVENT_KEY}` const EVENT_HIDDEN = `hidden${EVENT_KEY}` -const EVENT_FOCUSIN = `focusin${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}` @@ -73,6 +73,7 @@ class Offcanvas extends BaseComponent { this._config = this._getConfig(config) this._isShown = false this._backdrop = this._initializeBackDrop() + this._focustrap = this._initializeFocusTrap() this._addEventListeners() } @@ -110,7 +111,6 @@ class Offcanvas extends BaseComponent { if (!this._config.scroll) { new ScrollBarHelper().hide() - this._enforceFocusOnElement(this._element) } this._element.removeAttribute('aria-hidden') @@ -119,6 +119,10 @@ class Offcanvas extends BaseComponent { this._element.classList.add(CLASS_NAME_SHOW) const completeCallBack = () => { + if (!this._config.scroll) { + this._focustrap.activate() + } + EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) } @@ -136,7 +140,7 @@ class Offcanvas extends BaseComponent { return } - EventHandler.off(document, EVENT_FOCUSIN) + this._focustrap.deactivate() this._element.blur() this._isShown = false this._element.classList.remove(CLASS_NAME_SHOW) @@ -160,8 +164,8 @@ class Offcanvas extends BaseComponent { dispose() { this._backdrop.dispose() + this._focustrap.deactivate() super.dispose() - EventHandler.off(document, EVENT_FOCUSIN) } // Private @@ -186,16 +190,10 @@ class Offcanvas extends BaseComponent { }) } - _enforceFocusOnElement(element) { - EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop - EventHandler.on(document, EVENT_FOCUSIN, event => { - if (document !== event.target && - element !== event.target && - !element.contains(event.target)) { - element.focus() - } + _initializeFocusTrap() { + return new FocusTrap({ + trapElement: this._element }) - element.focus() } _addEventListeners() { -- cgit v1.2.3 From 4bfd8a2cbcb10610b4078cefa45756b4a96301a0 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Wed, 28 Jul 2021 17:39:32 +0300 Subject: Use a streamlined way to trigger component dismiss (#34170) * use a streamlined way to trigger component dismiss * add documentation Co-authored-by: XhmikosR --- js/src/offcanvas.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 6c563cb4f..7725b0188 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 -- cgit v1.2.3 From f20fece3a8cdd0e76a42c2737524b7652bf54d26 Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Wed, 4 Aug 2021 18:41:51 +0300 Subject: Prepare v5.1.0. (#34674) --- js/src/offcanvas.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'js/src/offcanvas.js') diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 7725b0188..60ce8a6c9 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -1,6 +1,6 @@ /** * -------------------------------------------------------------------------- - * Bootstrap (v5.0.2): offcanvas.js + * Bootstrap (v5.1.0): offcanvas.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * -------------------------------------------------------------------------- */ -- cgit v1.2.3