From b4b99d7260847da19419ad04589d324e2da6e0fe Mon Sep 17 00:00:00 2001 From: Sotis Date: Sat, 12 Oct 2019 13:30:37 +0300 Subject: [PATCH 01/44] Add a new offcanvas component --- build/build-plugins.js | 3 +- js/index.esm.js | 2 + js/index.umd.js | 2 + js/src/offcanvas.js | 237 +++++++++++++ js/tests/unit/offcanvas.spec.js | 324 ++++++++++++++++++ scss/_offcanvas.scss | 78 +++++ scss/_variables.scss | 28 +- scss/bootstrap.scss | 1 + site/content/docs/5.0/components/offcanvas.md | 242 +++++++++++++ site/data/examples.yml | 8 +- site/data/sidebar.yml | 1 + 11 files changed, 915 insertions(+), 11 deletions(-) create mode 100644 js/src/offcanvas.js create mode 100644 js/tests/unit/offcanvas.spec.js create mode 100644 scss/_offcanvas.scss create mode 100644 site/content/docs/5.0/components/offcanvas.md diff --git a/build/build-plugins.js b/build/build-plugins.js index d1930b8556ed..7175df4dd562 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -35,6 +35,7 @@ const bsPlugins = { Collapse: path.resolve(__dirname, '../js/src/collapse.js'), Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), Modal: path.resolve(__dirname, '../js/src/modal.js'), + OffCanvas: path.resolve(__dirname, '../js/src/offcanvas.js'), Popover: path.resolve(__dirname, '../js/src/popover.js'), ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'), Tab: path.resolve(__dirname, '../js/src/tab.js'), @@ -71,7 +72,7 @@ const getConfigByPluginKey = pluginKey => { } } - if (pluginKey === 'Alert' || pluginKey === 'Tab') { + if (pluginKey === 'Alert' || pluginKey === 'Tab' || pluginKey === 'OffCanvas') { return defaultPluginConfig } diff --git a/js/index.esm.js b/js/index.esm.js index b1bbf53a8f71..c041beffe255 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -11,6 +11,7 @@ import Carousel from './src/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' import Modal from './src/modal' +import OffCanvas from './src/offcanvas' import Popover from './src/popover' import ScrollSpy from './src/scrollspy' import Tab from './src/tab' @@ -24,6 +25,7 @@ export { Collapse, Dropdown, Modal, + OffCanvas, Popover, ScrollSpy, Tab, diff --git a/js/index.umd.js b/js/index.umd.js index 5c44ce8d2def..a99a3be896a3 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -11,6 +11,7 @@ import Carousel from './src/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' import Modal from './src/modal' +import OffCanvas from './src/offcanvas' import Popover from './src/popover' import ScrollSpy from './src/scrollspy' import Tab from './src/tab' @@ -24,6 +25,7 @@ export default { Collapse, Dropdown, Modal, + OffCanvas, Popover, ScrollSpy, Tab, diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js new file mode 100644 index 000000000000..c512ef2ee487 --- /dev/null +++ b/js/src/offcanvas.js @@ -0,0 +1,237 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v5.0.0-beta2): offcanvas.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { getjQuery, getSelectorFromElement, getTransitionDurationFromElement } from './util/index' +import Data from './dom/data' +import EventHandler from './dom/event-handler' +import SelectorEngine from './dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'offcanvas' +const VERSION = '5.0.0-alpha1' +const DATA_KEY = 'bs.offcanvas' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const ESCAPE_KEYCODE = 27 // ESC +const DATA_BODY_ACTIONS = 'data-body' + +const Selector = { + DATA_DISMISS: '[data-dismiss="offcanvas"]', + DATA_TOGGLE: '[data-toggle="offcanvas"]' +} + +const Event = { + SHOW: `show${EVENT_KEY}`, + SHOWN: `shown${EVENT_KEY}`, + HIDE: `hide${EVENT_KEY}`, + HIDDEN: `hidden${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`, + CLICK_DISMISS: `click.dismiss${EVENT_KEY}` +} + +const ClassName = { + BACKDROP_BODY: 'offcanvas-backdrop', + DISABLED: 'disabled', + OPEN: 'offcanvas-open', + TOGGLING: 'offcanvas-toggling', + SHOW: 'show', + STOP_OVERFLOW: 'offcanvas-freeze' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class OffCanvas { + constructor(element) { + this._element = element + this._isShown = element.classList.contains(ClassName.SHOW) + this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) + + this._handleClosing() + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + toggle(relatedTarget) { + return this._isShown ? this.hide(relatedTarget) : 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 + document.body.classList.add(ClassName.TOGGLING) + + if (this._bodyOptions === 'backdrop') { + document.body.classList.add(ClassName.BACKDROP_BODY) + } + + if (this._bodyOptions !== 'scroll') { + document.body.classList.add(ClassName.STOP_OVERFLOW) + } + + this._element.removeAttribute('aria-hidden') + this._element.classList.add(ClassName.SHOW) + + setTimeout(() => { + this._element.setAttribute('aria-expanded', true) + this._element.setAttribute('aria-offcanvas', true) + + document.body.classList.add(ClassName.OPEN) + document.body.classList.remove(ClassName.TOGGLING) + this._enforceFocus() + EventHandler.trigger(this._element, Event.SHOWN, { relatedTarget }) + }, getTransitionDurationFromElement(this._element)) + } + + hide(relatedTarget) { + if (!this._isShown) { + return + } + + const hideEvent = EventHandler.trigger(this._element, Event.HIDE, { relatedTarget }) + + if (hideEvent.defaultPrevented) { + return + } + + this._isShown = false + + if (!document.body.classList.contains(ClassName.TOGGLING)) { + document.body.classList.remove(ClassName.OPEN) + } + + if (this._bodyOptions === 'backdrop') { + document.body.classList.remove(ClassName.BACKDROP_BODY) + } + + if (this._bodyOptions !== 'scroll') { + document.body.classList.remove(ClassName.STOP_OVERFLOW) + } + + document.body.classList.add(ClassName.TOGGLING) + this._element.classList.remove(ClassName.SHOW) + this._element.blur() + + setTimeout(() => { + document.body.classList.remove(ClassName.TOGGLING) + this._element.setAttribute('aria-hidden', true) + this._element.setAttribute('aria-expanded', false) + this._element.removeAttribute('aria-offcanvas') + + EventHandler.trigger(this._element, Event.HIDDEN, { relatedTarget }) + }, getTransitionDurationFromElement(this._element)) + } + + // Private + _enforceFocus() { + this._element.setAttribute('tabindex', '0') + this._element.focus() + this._element.setAttribute('tabindex', 1) + } + + _handleClosing() { + EventHandler.on(this._element, Event.CLICK_DISMISS, Selector.DATA_DISMISS, event => this.hide(event)) + + EventHandler.on(document, 'keydown', event => { + if (event.which === ESCAPE_KEYCODE) { + this.hide(event.target) + } + }) + + 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(event.target) + } + }) + } + + // Static + + static jQueryInterface(config) { + return this.each(function () { + const data = Data.getData(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) + } + }) + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + if (['A', 'AREA'].indexOf(this.tagName) > -1) { + event.preventDefault() + } + + if (this.disabled || this.classList.contains(ClassName.DISABLED)) { + return + } + + const target = SelectorEngine.findOne(getSelectorFromElement(this)) + const data = Data.getData(target, DATA_KEY) || new OffCanvas(target) + + data.toggle(this) +}) + +const $ = getjQuery() + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + */ +/* istanbul ignore if */ +if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = OffCanvas.jQueryInterface + $.fn[NAME].Constructor = OffCanvas + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return OffCanvas.jQueryInterface + } +} + +export default OffCanvas diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js new file mode 100644 index 000000000000..aa8b4b04fa86 --- /dev/null +++ b/js/tests/unit/offcanvas.spec.js @@ -0,0 +1,324 @@ +import OffCanvas from '../../src/offcanvas' +import EventHandler from '../../src/dom/event-handler' + +/** Test helpers */ +import { clearFixture, getFixture, jQueryMock, createEvent } from '../helpers/fixture' + +describe('OffCanvas', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + document.body.classList.remove('offcanvas-open') + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(OffCanvas.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('constructor', () => { + it('should call hide when a element with data-dismiss="offcanvas" is clicked', () => { + fixtureEl.innerHTML = [ + '
', + ' Close', + '
' + ].join('') + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const closeEl = fixtureEl.querySelector('a') + const offCanvas = new OffCanvas(offCanvasEl) + + spyOn(offCanvas, 'hide') + + closeEl.click() + + expect(offCanvas.hide).toHaveBeenCalled() + }) + + it('should hide if esc is pressed', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new OffCanvas(offCanvasEl) + const keyDownEsc = createEvent('keydown') + keyDownEsc.which = 27 + + spyOn(offCanvas, 'hide') + + document.dispatchEvent(keyDownEsc) + + expect(offCanvas.hide).toHaveBeenCalled() + }) + + it('should not hide if esc is not pressed', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new OffCanvas(offCanvasEl) + const keyDownEsc = createEvent('keydown') + keyDownEsc.which = 9 + + spyOn(offCanvas, 'hide') + + document.dispatchEvent(keyDownEsc) + + expect(offCanvas.hide).not.toHaveBeenCalled() + }) + }) + + describe('toggle', () => { + it('should call show method if show class is not present', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new OffCanvas(offCanvasEl) + + spyOn(offCanvas, 'show') + + offCanvas.toggle() + + expect(offCanvas.show).toHaveBeenCalled() + }) + + it('should call hide method if show class is present', () => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('.show') + const offCanvas = new OffCanvas(offCanvasEl) + + spyOn(offCanvas, 'hide') + + offCanvas.toggle() + + expect(offCanvas.hide).toHaveBeenCalled() + }) + }) + + describe('show', () => { + it('should do nothing if already shown', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + offCanvas.show() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should show a hidden element', done => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl.classList.contains('show')).toEqual(true) + done() + }) + + offCanvas.show() + }) + + it('should not fire shown when show is prevented', done => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + done() + }, 10) + } + + offCanvasEl.addEventListener('show.bs.offcanvas', e => { + e.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + throw new Error('should not fire shown event') + }) + + offCanvas.show() + }) + }) + + describe('hide', () => { + it('should do nothing if already shown', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + offCanvas.hide() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should hide a shown element', done => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl.classList.contains('show')).toEqual(false) + done() + }) + + offCanvas.hide() + }) + + it('should not fire hidden when hide is prevented', done => { + fixtureEl.innerHTML = '
' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(offCanvasEl) + + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + done() + }, 10) + } + + offCanvasEl.addEventListener('hide.bs.offcanvas', e => { + e.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + throw new Error('should not fire hidden event') + }) + + offCanvas.hide() + }) + }) + + describe('data-api', () => { + it('should not prevent event for input', done => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const target = fixtureEl.querySelector('input') + const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl.classList.contains('show')).toEqual(true) + expect(target.checked).toEqual(true) + done() + }) + + target.click() + }) + + it('should not call toggle on disabled elements', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const target = fixtureEl.querySelector('a') + + spyOn(OffCanvas.prototype, 'toggle') + + target.click() + + expect(OffCanvas.prototype.toggle).not.toHaveBeenCalled() + }) + }) + + describe('jQueryInterface', () => { + it('should create an offcanvas', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.offcanvas.call(jQueryMock) + + expect(OffCanvas.getInstance(div)).toBeDefined() + }) + + it('should not re create an offcanvas', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(div) + + jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.offcanvas.call(jQueryMock) + + expect(OffCanvas.getInstance(div)).toEqual(offCanvas) + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + 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 call offcanvas method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(div) + + spyOn(offCanvas, 'show') + + jQueryMock.fn.offcanvas = OffCanvas.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.offcanvas.call(jQueryMock, 'show') + expect(offCanvas.show).toHaveBeenCalled() + }) + }) + + describe('getInstance', () => { + it('should return offcanvas instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const offCanvas = new OffCanvas(div) + + expect(OffCanvas.getInstance(div)).toEqual(offCanvas) + }) + + it('should return null when there is no offcanvas instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(OffCanvas.getInstance(div)).toEqual(null) + }) + }) +}) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss new file mode 100644 index 000000000000..545b72cc69f2 --- /dev/null +++ b/scss/_offcanvas.scss @@ -0,0 +1,78 @@ +.offcanvas { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: $zindex-offcanvas; + display: flex; + flex-direction: column; + width: $offcanvas-horizontal-width-mobile; + max-width: 100%; + color: $offcanvas-color; + background-color: $offcanvas-bg-color; + border: $offcanvas-border-width solid $offcanvas-border-color; + outline: 0; + @include transition(transform $offcanvas-transition-duration ease-in-out); + transform: translateX(-100%); +} + +.offcanvas-header { + display: flex; + justify-content: space-between; + padding: $offcanvas-padding-y $offcanvas-padding-x; + + .close { + padding: ($offcanvas-padding-y) ($offcanvas-padding-x / 2); + margin: (-$offcanvas-padding-y * 2) (-$offcanvas-padding-x) (-$offcanvas-padding-y) auto; + } +} +.offcanvas-body { + padding: 0 $offcanvas-padding-x; + margin: $offcanvas-padding-y 0; + overflow-y: auto; +} + +.offcanvas-right { + right: 0; + left: auto; + transform: translateX(100%); +} + +.offcanvas-bottom { + top: auto; + right: 0; + width: 100%; + max-height: $offcanvas-vertical-max-height-mobile; + transform: translateY(100%); +} + +@include media-breakpoint-up($offcanvas-mobile-breakpoint) { + .offcanvas:not(.offcanvas-bottom) { + width: $offcanvas-horizontal-width; + } + + .offcanvas-bottom { + max-height: $offcanvas-vertical-max-height; + } +} + +.offcanvas.show { + transform: none; +} + +.offcanvas-freeze, +.offcanvas-backdrop { + // stylelint-disable-next-line declaration-no-important + overflow: hidden !important; +} + +.offcanvas-backdrop::before { + position: fixed; + top: 0; + left: 0; + z-index: #{$zindex-offcanvas - 1}; + width: 100vw; + height: 100vh; + content: ""; + background-color: $offcanvas-body-backdrop-color; +} diff --git a/scss/_variables.scss b/scss/_variables.scss index 4ffcf18edc45..2ffb6ce3de9d 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -902,10 +902,11 @@ $form-validation-states: ( $zindex-dropdown: 1000 !default; $zindex-sticky: 1020 !default; $zindex-fixed: 1030 !default; -$zindex-modal-backdrop: 1040 !default; -$zindex-modal: 1050 !default; -$zindex-popover: 1060 !default; -$zindex-tooltip: 1070 !default; +$zindex-offcanvas: 1040 !default; +$zindex-modal-backdrop: 1050 !default; +$zindex-modal: 1060 !default; +$zindex-popover: 1070 !default; +$zindex-tooltip: 1080 !default; // scss-docs-end zindex-stack @@ -1431,6 +1432,25 @@ $btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default // scss-docs-end close-variables +// Offcanvas + +$offcanvas-padding-y: $modal-inner-padding !default; +$offcanvas-padding-x: $modal-inner-padding !default; + +$offcanvas-horizontal-width: 390px !default; +$offcanvas-horizontal-width-mobile: 100% !default; +$offcanvas-vertical-max-height: 200px !default; +$offcanvas-vertical-max-height-mobile: 50% !default; +$offcanvas-mobile-breakpoint: md !default; +$offcanvas-transition-duration: .3s !default; + +$offcanvas-border-color: $modal-content-border-color !default; +$offcanvas-border-width: $modal-content-border-width !default; +$offcanvas-bg-color: $modal-content-bg !default; +$offcanvas-color: $modal-content-color !default; +$offcanvas-body-backdrop-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity) !default; + + // Code $code-font-size: $small-font-size !default; diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index f5f411753399..2687d099154a 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -42,6 +42,7 @@ @import "popover"; @import "carousel"; @import "spinners"; +@import "offcanvas"; // Helpers @import "helpers"; diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md new file mode 100644 index 000000000000..b4e99f7c56b1 --- /dev/null +++ b/site/content/docs/5.0/components/offcanvas.md @@ -0,0 +1,242 @@ +--- +layout: docs +title: Offcanvas +description: Build hidden sidebars into your project for navigation, shopping carts, and more with a few classes and our JavaScript plugin. +group: components +toc: true +--- + +## How it works + +The offcanvas JavaScript plugin shows and hides sidebar on the left, right, or bottom of your viewport. Buttons or anchors are used as triggers that are attached to specific elements you toggle. + +Given how CSS handles animations, you cannot use `margin` or `translate` on a `.offcanvas` element. Instead, use the class as an independent wrapping element. + +## Example + +Click the buttons below to show and hide an offcanvas element via class changes: + +- `.offcanvas` hides content +- `.offcanvas.show` shows content + +You can use a link with the `href` attribute, or a button with the `data-target` attribute. In both cases, the `data-toggle="offcanvas"` is required. + +{{< example >}} +

+ + +

+
+
+
Offcanvas
+ +
+
+ +
+ Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. +
+ + +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

+

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

+

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

+

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

+

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+
+
+
+{{< /example >}} + +## Position + +Change the placement of an offcanvas element with modifier classes: + +- `.offcanvas-right` places offcanvas on the right of the viewport +- `.offcanvas-bottom` places offcanvas on the bottom of the viewport + +{{< example >}} +

+ + +

+
+
+
Offcanvas right
+ +
+
+
+
+
Offcanvas bottom
+ +
+
+{{< /example >}} + +## Color schemes + +Easily style an offcanvas element with a different `background-color` or `color` with our [color utilities]({{< docsref "/utilities/colors" >}}). + +{{< example >}} +

+ +

+
+
+ Colored offcanvas +
+
+ +{{< /example >}} + +## Options + +By default, when an offcanvas is visible, the `` of your page cannot be scrolled. You can use the following data-options to change this behavior: + +- `data-body="scroll"` enables scrolling on the `` when offcanvas is open +- `data-body="backdrop"` disables scrolling and creates a backdrop over the `` when offcanvas is open + +{{< example >}} +

+ + +

+
+
+ Offcanvas with scrolling +
+
+

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

+
+ +
+
+
+ Offcanvas with backdrop +
+
+

Try clicking on the page's content to toggle the offcanvas.

+
+
+{{< /example >}} + +## Accessibility + +For more information refer to collapsible [accessibility docs]({{< docsref "/components/collapse#accessibility" >}}). + +## Usage + +The offcanvas plugin utilizes a few classes and attributes to handle the heavy lifting: + +- `.offcanvas` hides the content +- `.offcanvas.show` shows the content +- `.offcanvas-right` hides the offcanvas on the right +- `.offcanvas-bottom` hides the offcanvas on the bottom +- `data-body="scroll"` enables `` scrolling when offcanvas is open +- `data-body="backdrop"` disables `` scrolling and adds backdrop when offcanvas is open + +Add a dismiss button with the `data-dismiss="offcanvas"` attribute, which triggers the JavaScript functionality. Be sure to use the ` -

+ + +
Offcanvas
@@ -80,10 +79,10 @@ Change the placement of an offcanvas element with modifier classes: - `.offcanvas-bottom` places offcanvas on the bottom of the viewport {{< example >}} -

- - -

+ + + +
Offcanvas right
@@ -107,9 +106,8 @@ Change the placement of an offcanvas element with modifier classes: Easily style an offcanvas element with a different `background-color` or `color` with our [color utilities]({{< docsref "/utilities/colors" >}}). {{< example >}} -

- -

+ +
Colored offcanvas @@ -126,10 +124,9 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc - `data-body="backdrop"` disables scrolling and creates a backdrop over the `` when offcanvas is open {{< example >}} -

- - -

+ + +
Offcanvas with scrolling @@ -137,7 +134,6 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc

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

-
From f504db0c32a0bec40c602c932ea3f2dfed456b25 Mon Sep 17 00:00:00 2001 From: Martijn Cuppens Date: Sat, 4 Apr 2020 10:45:56 +0200 Subject: [PATCH 04/44] Sass optimizations --- scss/_offcanvas.scss | 45 ++++++++----------- scss/_variables.scss | 6 +-- site/content/docs/5.0/components/offcanvas.md | 29 +++++++++--- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index 545b72cc69f2..3c4e4a5fe765 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -1,19 +1,15 @@ .offcanvas { position: fixed; - top: 0; bottom: 0; - left: 0; z-index: $zindex-offcanvas; display: flex; flex-direction: column; - width: $offcanvas-horizontal-width-mobile; max-width: 100%; color: $offcanvas-color; background-color: $offcanvas-bg-color; - border: $offcanvas-border-width solid $offcanvas-border-color; - outline: 0; + // By using an outline, the "border" is always on the correct side + outline: $offcanvas-border-width solid $offcanvas-border-color; @include transition(transform $offcanvas-transition-duration ease-in-out); - transform: translateX(-100%); } .offcanvas-header { @@ -26,51 +22,46 @@ margin: (-$offcanvas-padding-y * 2) (-$offcanvas-padding-x) (-$offcanvas-padding-y) auto; } } + .offcanvas-body { - padding: 0 $offcanvas-padding-x; - margin: $offcanvas-padding-y 0; + padding: $offcanvas-padding-y $offcanvas-padding-x; overflow-y: auto; } +.offcanvas-left { + top: 0; + left: 0; + width: $offcanvas-width; + transform: translateX(-100%); +} + .offcanvas-right { + top: 0; right: 0; - left: auto; + width: $offcanvas-width; transform: translateX(100%); } .offcanvas-bottom { - top: auto; right: 0; - width: 100%; - max-height: $offcanvas-vertical-max-height-mobile; + left: 0; + max-height: 100%; transform: translateY(100%); } -@include media-breakpoint-up($offcanvas-mobile-breakpoint) { - .offcanvas:not(.offcanvas-bottom) { - width: $offcanvas-horizontal-width; - } - - .offcanvas-bottom { - max-height: $offcanvas-vertical-max-height; - } -} - .offcanvas.show { transform: none; } -.offcanvas-freeze, -.offcanvas-backdrop { - // stylelint-disable-next-line declaration-no-important - overflow: hidden !important; +.offcanvas-freeze { + overflow: hidden; } .offcanvas-backdrop::before { position: fixed; top: 0; left: 0; - z-index: #{$zindex-offcanvas - 1}; + z-index: $zindex-offcanvas - 1; width: 100vw; height: 100vh; content: ""; diff --git a/scss/_variables.scss b/scss/_variables.scss index 2ffb6ce3de9d..cf7a327c2b5e 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1437,11 +1437,7 @@ $btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default $offcanvas-padding-y: $modal-inner-padding !default; $offcanvas-padding-x: $modal-inner-padding !default; -$offcanvas-horizontal-width: 390px !default; -$offcanvas-horizontal-width-mobile: 100% !default; -$offcanvas-vertical-max-height: 200px !default; -$offcanvas-vertical-max-height-mobile: 50% !default; -$offcanvas-mobile-breakpoint: md !default; +$offcanvas-width: 400px !default; $offcanvas-transition-duration: .3s !default; $offcanvas-border-color: $modal-content-border-color !default; diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 48be1f45feba..6676a64005fc 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -29,7 +29,7 @@ You can use a link with the `href` attribute, or a button with the `data-target` Button with data-target -
+
Offcanvas
-
+
Offcanvas right
-
+
Offcanvas bottom
-
+
Colored offcanvas + +
+
+

+ Maecenas sed diam eget risus varius blandit sit amet non magna. Duis mollis, + est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. +

+

+ Aenean lacinia bibendum nulla sed consectetur. Maecenas faucibus mollis interdum. + Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut +

+

+ fermentum massa justo sit amet risus. Nullam quis risus eget urna mollis ornare + vel eu leo. Curabitur blandit tempus porttitor. +

@@ -127,7 +144,7 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc -
+
Offcanvas with scrolling
@@ -135,7 +152,7 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc

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

-
+
Offcanvas with backdrop
From bcc2b9609ff5c144804fe10f88d2391a7dec3502 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sat, 13 Jun 2020 03:37:23 +0300 Subject: [PATCH 05/44] 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 --- js/src/offcanvas.js | 3 ++- scss/_offcanvas.scss | 9 +++++---- scss/_variables.scss | 3 ++- site/content/docs/5.0/components/offcanvas.md | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 08b031242d58..f11914f2e763 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -93,7 +93,7 @@ class OffCanvas { this._element.removeAttribute('aria-hidden') this._element.classList.add(CLASS_NAME_SHOW) - + this.hidden = false setTimeout(() => { this._element.setAttribute('aria-expanded', true) this._element.setAttribute('aria-offcanvas', true) @@ -139,6 +139,7 @@ class OffCanvas { this._element.setAttribute('aria-hidden', true) this._element.setAttribute('aria-expanded', false) this._element.removeAttribute('aria-offcanvas') + this.hidden = true EventHandler.trigger(this._element, EVENT_HIDDEN, { relatedTarget }) }, getTransitionDurationFromElement(this._element)) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index 3c4e4a5fe765..2f5e707d3191 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -18,8 +18,8 @@ padding: $offcanvas-padding-y $offcanvas-padding-x; .close { - padding: ($offcanvas-padding-y) ($offcanvas-padding-x / 2); - margin: (-$offcanvas-padding-y * 2) (-$offcanvas-padding-x) (-$offcanvas-padding-y) auto; + padding: ($offcanvas-padding-y / 2) ($offcanvas-padding-x / 2); + margin: (-$offcanvas-padding-y) (-$offcanvas-padding-x) (-$offcanvas-padding-y) auto; } } @@ -31,20 +31,21 @@ .offcanvas-left { top: 0; left: 0; - width: $offcanvas-width; + width: $offcanvas-horizontal-width; transform: translateX(-100%); } .offcanvas-right { top: 0; right: 0; - width: $offcanvas-width; + width: $offcanvas-horizontal-width; transform: translateX(100%); } .offcanvas-bottom { right: 0; left: 0; + height: $offcanvas-vertical-height; max-height: 100%; transform: translateY(100%); } diff --git a/scss/_variables.scss b/scss/_variables.scss index cf7a327c2b5e..e60a7a215881 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1437,7 +1437,8 @@ $btn-close-white-filter: invert(1) grayscale(100%) brightness(200%) !default $offcanvas-padding-y: $modal-inner-padding !default; $offcanvas-padding-x: $modal-inner-padding !default; -$offcanvas-width: 400px !default; +$offcanvas-horizontal-width: 400px !default; +$offcanvas-vertical-height: 30vh !default; $offcanvas-transition-duration: .3s !default; $offcanvas-border-color: $modal-content-border-color !default; diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 6676a64005fc..e20406631e94 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -90,6 +90,18 @@ Change the placement of an offcanvas element with modifier classes:
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

+

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

+

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

+

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

+

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

+

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

+

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

+

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

+

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+
@@ -98,6 +110,13 @@ Change the placement of an offcanvas element with modifier classes:
+
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

+

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

+

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

+

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

+

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+
{{< /example >}} From 2b57bbe68eeb27fcc7b635e5d79549d761c09fdb Mon Sep 17 00:00:00 2001 From: XhmikosR Date: Sun, 6 Dec 2020 09:15:06 +0200 Subject: [PATCH 06/44] Wording tweaks --- site/content/docs/5.0/components/offcanvas.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index e20406631e94..6627aa1d62d6 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -219,9 +219,9 @@ var offcanvasList = offcanvasElementList.map(function (offcanvasEl) { {{< partial "callout-danger-async-methods.md" >}} {{< /callout >}} -Activates your content as a offcanvas element. Accepts an optional options `object`. +Activates your content as an offcanvas element. Accepts an optional options `object`. -You can create a offcanvas instance with the constructor, for example: +You can create an offcanvas instance with the constructor, for example: {{< highlight js >}} var myOffcanvas = document.getElementById('myOffcanvas') @@ -230,9 +230,9 @@ var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas) | Method | Description | | --- | --- | -| `toggle` | Toggles a 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 a 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 a offcanvas element. **Returns to the caller before the offcanvas element has actually been hidden** (i.e. before the `hidden.bs.offcanvas` event occurs).| +| `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 | ### Events @@ -253,7 +253,7 @@ Bootstrap's offcanvas class exposes a few events for hooking into offcanvas func shown.bs.offcanvas - This event is fired when a offcanvas element has been made visible to the user (will wait for CSS transitions to complete). + 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 @@ -261,7 +261,7 @@ Bootstrap's offcanvas class exposes a few events for hooking into offcanvas func hidden.bs.offcanvas - This event is fired when a offcanvas element has been hidden from the user (will wait for CSS transitions to complete). + This event is fired when an offcanvas element has been hidden from the user (will wait for CSS transitions to complete). From de607c73a9d9527bd1ce03cda1bb23f3e2d41379 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Tue, 8 Dec 2020 02:20:29 +0200 Subject: [PATCH 07/44] update --- js/src/offcanvas.js | 91 +++++++++---------- site/content/docs/5.0/components/offcanvas.md | 52 +++++------ 2 files changed, 68 insertions(+), 75 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index f11914f2e763..67d6fcaaa759 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -5,11 +5,17 @@ * -------------------------------------------------------------------------- */ -import { getjQuery, getSelectorFromElement, getTransitionDurationFromElement } from './util/index' +import { + getElementFromSelector, + getjQuery, + getSelectorFromElement, + getTransitionDurationFromElement, + onDOMContentLoaded +} from './util/index' import Data from './dom/data' import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' import SelectorEngine from './dom/selector-engine' - /** * ------------------------------------------------------------------------ * Constants @@ -17,15 +23,18 @@ import SelectorEngine from './dom/selector-engine' */ const NAME = 'offcanvas' -const VERSION = '5.0.0-alpha1' 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-body' +const DATA_BODY_ACTIONS = 'data-bs-body' -const SELECTOR_DATA_DISMISS = '[data-dismiss="offcanvas"]' -const SELECTOR_DATA_TOGGLE = '[data-toggle="offcanvas"]' +const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' +const CLASS_NAME_DISABLED = 'disabled' +const CLASS_NAME_OPEN = 'offcanvas-open' +const CLASS_NAME_TOGGLING = 'offcanvas-toggling' +const CLASS_NAME_SHOW = 'show' +const CLASS_NAME_STOP_OVERFLOW = 'offcanvas-freeze' const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` @@ -34,12 +43,8 @@ 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 CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' -const CLASS_NAME_DISABLED = 'disabled' -const CLASS_NAME_OPEN = 'offcanvas-open' -const CLASS_NAME_TOGGLING = 'offcanvas-toggling' -const CLASS_NAME_SHOW = 'show' -const CLASS_NAME_STOP_OVERFLOW = 'offcanvas-freeze' +const SELECTOR_DATA_DISMISS = '[data-bs-dismiss="offcanvas"]' +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="offcanvas"]' /** * ------------------------------------------------------------------------ @@ -47,30 +52,24 @@ const CLASS_NAME_STOP_OVERFLOW = 'offcanvas-freeze' * ------------------------------------------------------------------------ */ -class OffCanvas { +class OffCanvas extends BaseComponent { constructor(element) { - this._element = element + super(element) + this._isShown = element.classList.contains(CLASS_NAME_SHOW) this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) this._handleClosing() - Data.setData(element, DATA_KEY, this) - } - - // Getters - - static get VERSION() { - return VERSION } // Public toggle(relatedTarget) { - return this._isShown ? this.hide(relatedTarget) : this.show(relatedTarget) + return this._isShown ? this.hide() : this.show(relatedTarget) } show(relatedTarget) { - if (this._isShown) { + if (this._isShown || this._isTransitioning) { return } @@ -105,12 +104,12 @@ class OffCanvas { }, getTransitionDurationFromElement(this._element)) } - hide(relatedTarget) { + hide() { if (!this._isShown) { return } - const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, { relatedTarget }) + const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE) if (hideEvent.defaultPrevented) { return @@ -141,7 +140,7 @@ class OffCanvas { this._element.removeAttribute('aria-offcanvas') this.hidden = true - EventHandler.trigger(this._element, EVENT_HIDDEN, { relatedTarget }) + EventHandler.trigger(this._element, EVENT_HIDDEN) }, getTransitionDurationFromElement(this._element)) } @@ -153,18 +152,18 @@ class OffCanvas { } _handleClosing() { - EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, event => this.hide(event)) + EventHandler.on(this._element, EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide()) EventHandler.on(document, 'keydown', event => { if (event.key === ESCAPE_KEY) { - this.hide(event.target) + 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(event.target) + this.hide() } }) } @@ -184,10 +183,6 @@ class OffCanvas { } }) } - - static getInstance(element) { - return Data.getData(element, DATA_KEY) - } } /** @@ -197,7 +192,9 @@ class OffCanvas { */ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { - if (['A', 'AREA'].indexOf(this.tagName) > -1) { + const target = getElementFromSelector(this) + + if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() } @@ -205,28 +202,28 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( return } - const target = SelectorEngine.findOne(getSelectorFromElement(this)) + window.xx = target const data = Data.getData(target, DATA_KEY) || new OffCanvas(target) - data.toggle(this) }) -const $ = getjQuery() - /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ -/* istanbul ignore if */ -if ($) { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = OffCanvas.jQueryInterface - $.fn[NAME].Constructor = OffCanvas - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return OffCanvas.jQueryInterface +onDOMContentLoaded(() => { + const $ = getjQuery() + /* istanbul ignore if */ + if ($) { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = OffCanvas.jQueryInterface + $.fn[NAME].Constructor = OffCanvas + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return OffCanvas.jQueryInterface + } } -} +}) export default OffCanvas diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 6627aa1d62d6..a2a78a789d32 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -19,21 +19,20 @@ Click the buttons below to show and hide an offcanvas element via class changes: - `.offcanvas` hides content - `.offcanvas.show` shows content -You can use a link with the `href` attribute, or a button with the `data-target` attribute. In both cases, the `data-toggle="offcanvas"` is required. +You can use a link with the `href` attribute, or a button with the `data-bs-target` attribute. In both cases, the `data-bs-toggle="offcanvas"` is required. {{< example >}} - -
Offcanvas
-
@@ -41,7 +40,7 @@ You can use a link with the `href` attribute, or a button with the `data-target` Cras justo odio - Dapibus ac facilisis in + Dapibus ac facilisis in Morbi leo risus Porta ac consectetur ac Vestibulum at eros @@ -51,7 +50,7 @@ You can use a link with the `href` attribute, or a button with the `data-target`
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

-

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

-

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

-

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

-

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+

Some random text as placeholder in place of another lorem ipsum. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by.

+

Folly words widow one downs few age every seven. If miss part by fact he park just shew. Discovered had get considered projection who favourable. Necessary up knowledge it tolerably. Unwilling departure education is be dashwoods or an. Use off agreeable law unwilling sir deficient curiosity instantly. Easy mind life fact with see has bore ten.

+

Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education.

+

Village did removed enjoyed explain nor ham saw calling talking. Securing as informed declared or margaret. Joy horrible moreover man feelings own shy. Request norland neither mistake for yet. Between the for morning assured country believe. On even feet time have an no at. Relation so in confined smallest children unpacked delicate. Why sir end believe uncivil respect.

+

In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.

@@ -89,16 +89,14 @@ Change the placement of an offcanvas element with modifier classes:
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

-

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

-

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

-

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

-

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

-

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

-

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

-

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

-

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+

Powering more than 18 million websites, Bootstrap is the go-to toolkit for many front-end developers. There are a few factors driving Bootstrap’s popularity. First and foremost, it’s open-source and therefore free to download and use. It’s also fully customizable, and compatible with all modern browsers. This is true of many CSS frameworks, however.

+

What sets Bootstrap apart from other development toolkits is that it was developed mobile-first. Meaning, the code was optimized for mobile devices (i.e. the smallest screen size) first and then scaled up to display on larger screens. As a result, building with Bootstrap CSS ensures that your site supports proper rendering and touch zooming for all devices.

+

Another reason Bootstrap is so popular is that it’s easy to use. It comes bundled with templates for typography, forms, buttons, drop-down menus, navigation, and other interface components. Using these pre-styled templates, you can add features that enrich the user experience on your site without having to code them from scratch.

+

Building a responsive site is much easier using Bootstrap than doing so from scratch. Bootstrap comes with responsive styles, like containers and media queries, to ensure your site adjusts to the viewport. That means you don’t have to worry about whether your visitors are using desktops, tablets, or mobile devices.

+

You can build your site quickly with Bootstrap. Once you download the framework, you can get started with a basic template and then add the components you need. These components are fundamental HTML elements, like tables, forms, buttons, images, and icons, that are styled with a base class and extended with modifier classes. Using these pre-designed components significantly limits the amount of custom CSS you have to write.

+

If you have multiple collaborators working on a site, then consistency is important. You don’t want buttons on your homepage to look different from buttons on your landing page, or to use a different website typography on your blog than anywhere else on your site — and so on. Using Bootstrap and its default settings, utility classes, and component elements can help ensure the front end of your site looks consistent.

+

Since Bootstrap comes with pre-styled content, components, and templates, Bootstrap sites tend to look the same out-of-the-box. In fact, Bootstrap has been blamed for why websites today all look the same. You can customize a Bootstrap site, but it’ll take time. Plus, if you have to override too much of the default styling, then it might make more sense to create your own stylesheet in the first place.

+

Bootstrap is considered an easy to use platform. It offers extensive documentation for every part of its framework, from its layout to content to components and more. That means virtually anyone can learn Bootstrap, but it also means it will take time to read through the documentation and learn the framework. If you are looking to build a website as quickly as possible, then Bootstrap might not be as ideal as other solutions, like website builders.

@@ -108,11 +106,11 @@ Change the placement of an offcanvas element with modifier classes:
-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam bibendum ante ac est viverra tincidunt. Suspendisse lobortis lorem nec est congue, sed scelerisque mauris iaculis. Phasellus enim lorem, sagittis a eros at, feugiat tempor leo. Nunc in massa nibh. Vivamus vestibulum orci sem, et consectetur velit laoreet eget.

-

Curabitur et suscipit justo, sed pretium enim. Vivamus lobortis dapibus purus, eget pharetra nibh tincidunt in. Integer accumsan elit nisi, eget commodo magna vehicula ut. Cras nec pellentesque diam. Donec porta posuere urna in pellentesque. Nulla facilisi. Aliquam erat volutpat.

-

Donec lobortis eget tortor ac mattis. Maecenas sit amet sollicitudin libero. Quisque sodales pulvinar ipsum, vel fermentum enim convallis a. Proin rutrum orci scelerisque, accumsan quam vel, hendrerit erat. Cras vitae mauris vel massa sollicitudin scelerisque posuere sed lorem. Aenean volutpat libero non urna elementum aliquet.

-

Phasellus eros ex, efficitur in ex et, maximus lobortis magna. Suspendisse convallis, neque a varius tempor, nisi tellus blandit velit, at dignissim nibh ligula et leo. Mauris vel elit sit amet sem blandit congue. Pellentesque vulputate, nibh quis semper pharetra, tellus nunc pretium mi, et volutpat purus turpis porttitor lectus.

-

Proin rhoncus tortor ac dictum varius. Praesent congue condimentum tempus. Ut nec purus ut orci feugiat tristique sit amet sit amet mi. Etiam malesuada nibh sed porta faucibus. Aliquam dolor orci, gravida vel augue in, auctor eleifend lectus. Fusce viverra varius elementum. Nam quis ipsum eu ante venenatis vehicula. Duis eu nibh eget nulla interdum condimentum.

+

Some random text as placeholder in place of another lorem ipsum. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by.

+

Folly words widow one downs few age every seven. If miss part by fact he park just shew. Discovered had get considered projection who favourable. Necessary up knowledge it tolerably. Unwilling departure education is be dashwoods or an. Use off agreeable law unwilling sir deficient curiosity instantly. Easy mind life fact with see has bore ten.

+

Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education.

+

Village did removed enjoyed explain nor ham saw calling talking. Securing as informed declared or margaret. Joy horrible moreover man feelings own shy. Request norland neither mistake for yet. Between the for morning assured country believe. On even feet time have an no at. Relation so in confined smallest children unpacked delicate. Why sir end believe uncivil respect.

+

In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.

{{< /example >}} @@ -132,16 +130,16 @@ Easily style an offcanvas element with a different `background-color` or `color`

- Maecenas sed diam eget risus varius blandit sit amet non magna. Duis mollis, - est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. + Bootstrap is an open source and intuitive CSS framework, which is used primarily for mobile first front-end website development. + It was discovered by Mark Otto and Jacob Thornton at Twitter, and it was formerly named as ‘Twitter Blueprint’.

- Aenean lacinia bibendum nulla sed consectetur. Maecenas faucibus mollis interdum. - Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut + CSS is principally used, but HTML templates are also a part of it. Besides HTML and CSS templates, + it also depends closely on JavaScript components, mostly in the type of jQuery plugins.

- fermentum massa justo sit amet risus. Nullam quis risus eget urna mollis ornare - vel eu leo. Curabitur blandit tempus porttitor. + Bootstrap also has widespread support and vast coding documentation with extensive online resources and assistance from the community of developers. + This, in turn, makes it easier to understand this framework better and how to employ it.

From cbba2ddfa27ebb3ae1be2881f7c464f1baa744af Mon Sep 17 00:00:00 2001 From: GeoSot Date: Mon, 8 Feb 2021 02:15:02 +0200 Subject: [PATCH 17/44] fix focus when offcanvas is closed --- js/src/offcanvas.js | 4 ++-- scss/_offcanvas.scss | 1 + site/content/docs/5.0/components/offcanvas.md | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 2eacc218a760..ee91adcfe6b4 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -85,6 +85,7 @@ class OffCanvas extends BaseComponent { } this._isShown = true + this._element.style.visibility = 'visible' document.body.classList.add(CLASS_NAME_TOGGLING) if (this._bodyOptions === 'backdrop') { @@ -101,7 +102,6 @@ class OffCanvas extends BaseComponent { this._element.setAttribute('aria-modal', true) this._element.setAttribute('role', 'dialog') this._element.classList.add(CLASS_NAME_SHOW) - this.hidden = false const completeCallBack = () => { document.body.classList.add(CLASS_NAME_OPEN) @@ -140,7 +140,7 @@ class OffCanvas extends BaseComponent { this._element.setAttribute('aria-hidden', true) this._element.removeAttribute('aria-modal') this._element.removeAttribute('role') - this.hidden = true + this._element.style.visibility = 'hidden' if (this._bodyOptions !== 'scroll') { document.body.classList.remove(CLASS_NAME_STOP_OVERFLOW) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index b173fdae9245..09fc5fbb4810 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -6,6 +6,7 @@ flex-direction: column; max-width: 100%; color: $offcanvas-color; + visibility: hidden; background-color: $offcanvas-bg-color; // By using an outline, the "border" is always on the correct side outline: $offcanvas-border-width solid $offcanvas-border-color; diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 39629178e72b..9c6b323ed9af 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -160,6 +160,8 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc
+

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

@@ -168,6 +170,8 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc
+

Try clicking on the page's content to toggle the offcanvas.

@@ -177,7 +181,7 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc ## Accessibility -For more information refer to collapsible [accessibility docs]({{< docsref "/components/collapse#accessibility" >}}). +Be sure to add aria-labelledby="...", referencing the modal title, to .offcanvas. Note that you don’t need to add role="dialog" since we already add it via JavaScript. ## Usage From f0d9ae93476649a9bb441d8c93832364f29fde1a Mon Sep 17 00:00:00 2001 From: GeoSot Date: Thu, 11 Feb 2021 01:15:14 +0200 Subject: [PATCH 18/44] updates --- js/src/offcanvas.js | 32 +++++++++++++++++--------------- js/src/util/scrollbar.js | 12 ++++++------ 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index ee91adcfe6b4..910bd6ebeb64 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -12,7 +12,7 @@ import { getTransitionDurationFromElement, isVisible } from './util/index' -import { getScrollBarWidth, resetScrollbar, setScrollbar } from './util/scrollbar' +import * as Scrollbar from './util/scrollbar' import Data from './dom/data' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' @@ -62,7 +62,6 @@ class OffCanvas extends BaseComponent { this._isShown = element.classList.contains(CLASS_NAME_SHOW) this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) this._isBodyOverflowing = null - this._scrollbarWidth = null this._handleClosing() } @@ -88,13 +87,12 @@ class OffCanvas extends BaseComponent { this._element.style.visibility = 'visible' document.body.classList.add(CLASS_NAME_TOGGLING) - if (this._bodyOptions === 'backdrop') { + if (this._bodyOptionsHas('backdrop')) { document.body.classList.add(CLASS_NAME_BACKDROP_BODY) } - if (this._bodyOptions !== 'scroll') { - this._scrollbarWidth = getScrollBarWidth() - setScrollbar(this._scrollbarWidth) + if (!this._bodyOptionsHas('scroll')) { + Scrollbar.setCustom(Scrollbar.getWidth()) document.body.classList.add(CLASS_NAME_STOP_OVERFLOW) } @@ -106,7 +104,7 @@ class OffCanvas extends BaseComponent { const completeCallBack = () => { document.body.classList.add(CLASS_NAME_OPEN) document.body.classList.remove(CLASS_NAME_TOGGLING) - this._enforceFocus() + this._enforceFocusOnElement(this._element) EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) } @@ -128,7 +126,7 @@ class OffCanvas extends BaseComponent { this._element.blur() this._isShown = false - if (this._bodyOptions === 'backdrop') { + if (this._bodyOptionsHas('backdrop')) { document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) } @@ -142,9 +140,9 @@ class OffCanvas extends BaseComponent { this._element.removeAttribute('role') this._element.style.visibility = 'hidden' - if (this._bodyOptions !== 'scroll') { + if (!this._bodyOptionsHas('scroll')) { document.body.classList.remove(CLASS_NAME_STOP_OVERFLOW) - resetScrollbar() + Scrollbar.reset() } EventHandler.trigger(this._element, EVENT_HIDDEN) @@ -153,16 +151,20 @@ class OffCanvas extends BaseComponent { setTimeout(completeCallback, getTransitionDurationFromElement(this._element)) } - _enforceFocus() { + _enforceFocusOnElement(element) { EventHandler.off(document, EVENT_FOCUSIN) // guard against infinite focus loop EventHandler.on(document, EVENT_FOCUSIN, event => { if (document !== event.target && - this._element !== event.target && - !this._element.contains(event.target)) { - this._element.focus() + element !== event.target && + !element.contains(event.target)) { + element.focus() } }) - this._element.focus() + element.focus() + } + + _bodyOptionsHas(option) { + return this._bodyOptions === option } _handleClosing() { diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 07fd74034204..b8121198a2f3 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -12,7 +12,7 @@ const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure' const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top' const SELECTOR_STICKY_CONTENT = '.sticky-top' -const getScrollBarWidth = () => { // thx d.walsh +const getWidth = () => { // thx d.walsh const scrollDiv = document.createElement('div') scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER document.body.appendChild(scrollDiv) @@ -21,7 +21,7 @@ const getScrollBarWidth = () => { // thx d.walsh return scrollbarWidth } -const setScrollbar = width => { +const setCustom = width => { _setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width) _setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width) _setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + width) @@ -37,7 +37,7 @@ const _setElementAttributes = (selector, styleProp, callback) => { }) } -const resetScrollbar = () => { +const reset = () => { _resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight') _resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight') _resetElementAttributes('body', 'paddingRight') @@ -56,7 +56,7 @@ const _resetElementAttributes = (selector, styleProp) => { } export { - getScrollBarWidth, - setScrollbar, - resetScrollbar + getWidth, + setCustom, + reset } From cafb8e76466aa5454c961d579369b4b34cc09b3c Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sat, 13 Feb 2021 01:12:31 +0200 Subject: [PATCH 19/44] revert modal, add tests for scrollbar --- js/src/offcanvas.js | 11 +-- js/src/util/index.js | 8 +- js/src/util/scrollbar.js | 24 +++--- js/tests/unit/util/scrollbar.spec.js | 122 +++++++++++++++++++++++++++ scss/_offcanvas.scss | 4 - 5 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 js/tests/unit/util/scrollbar.spec.js diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 910bd6ebeb64..8a2b58c98d36 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -12,7 +12,7 @@ import { getTransitionDurationFromElement, isVisible } from './util/index' -import * as Scrollbar from './util/scrollbar' +import { reset as scrollBarReset, hide as scrollBarHide } from './util/scrollbar' import Data from './dom/data' import EventHandler from './dom/event-handler' import BaseComponent from './base-component' @@ -36,7 +36,6 @@ const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_OPEN = 'offcanvas-open' const CLASS_NAME_TOGGLING = 'offcanvas-toggling' const CLASS_NAME_SHOW = 'show' -const CLASS_NAME_STOP_OVERFLOW = 'offcanvas-freeze' const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` @@ -61,8 +60,6 @@ class OffCanvas extends BaseComponent { this._isShown = element.classList.contains(CLASS_NAME_SHOW) this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) - this._isBodyOverflowing = null - this._handleClosing() } @@ -92,8 +89,7 @@ class OffCanvas extends BaseComponent { } if (!this._bodyOptionsHas('scroll')) { - Scrollbar.setCustom(Scrollbar.getWidth()) - document.body.classList.add(CLASS_NAME_STOP_OVERFLOW) + scrollBarHide() } this._element.removeAttribute('aria-hidden') @@ -141,8 +137,7 @@ class OffCanvas extends BaseComponent { this._element.style.visibility = 'hidden' if (!this._bodyOptionsHas('scroll')) { - document.body.classList.remove(CLASS_NAME_STOP_OVERFLOW) - Scrollbar.reset() + scrollBarReset() } EventHandler.trigger(this._element, EVENT_HIDDEN) diff --git a/js/src/util/index.js b/js/src/util/index.js index 99c7da1fc38d..ae3cd2ac00a9 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -216,11 +216,6 @@ const defineJQueryPlugin = (name, plugin) => { }) } -const isBodyOverflowing = () => { - const rect = document.body.getBoundingClientRect() - return Math.round(rect.left + rect.right) < window.innerWidth -} - export { getUID, getSelectorFromElement, @@ -237,6 +232,5 @@ export { getjQuery, onDOMContentLoaded, isRTL, - defineJQueryPlugin, - isBodyOverflowing + defineJQueryPlugin } diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index b8121198a2f3..06a865ab50c0 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -8,20 +8,16 @@ import SelectorEngine from '../dom/selector-engine' import Manipulator from '../dom/manipulator' -const CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure' const SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top' const SELECTOR_STICKY_CONTENT = '.sticky-top' -const getWidth = () => { // thx d.walsh - const scrollDiv = document.createElement('div') - scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER - document.body.appendChild(scrollDiv) - const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth - document.body.removeChild(scrollDiv) - return scrollbarWidth +const getWidth = () => { + // https://muffinman.io/blog/get-scrollbar-width-in-javascript/ + return Math.abs(window.innerWidth - document.documentElement.clientWidth) } -const setCustom = width => { +const hide = (width = getWidth()) => { + document.body.style.overflow = 'hidden' _setElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight', calculatedValue => calculatedValue + width) _setElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight', calculatedValue => calculatedValue - width) _setElementAttributes('body', 'paddingRight', calculatedValue => calculatedValue + width) @@ -38,6 +34,7 @@ const _setElementAttributes = (selector, styleProp, callback) => { } const reset = () => { + document.body.style.overflow = 'auto' _resetElementAttributes(SELECTOR_FIXED_CONTENT, 'paddingRight') _resetElementAttributes(SELECTOR_STICKY_CONTENT, 'marginRight') _resetElementAttributes('body', 'paddingRight') @@ -55,8 +52,15 @@ const _resetElementAttributes = (selector, styleProp) => { }) } +const isBodyOverflowing = () => { + // maybe getWidth > 0 + const rect = document.body.getBoundingClientRect() + return Math.round(rect.left + rect.right) < window.innerWidth +} + export { getWidth, - setCustom, + hide, + isBodyOverflowing, reset } diff --git a/js/tests/unit/util/scrollbar.spec.js b/js/tests/unit/util/scrollbar.spec.js new file mode 100644 index 000000000000..6ac4be0c9026 --- /dev/null +++ b/js/tests/unit/util/scrollbar.spec.js @@ -0,0 +1,122 @@ +import * as Scrollbar from '../../../src/util/scrollbar' +import { clearFixture, getFixture } from '../../helpers/fixture' + +describe('ScrollBar', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + document.body.style.overflowY = 'auto' + }) + + describe('isBodyOverflowing', () => { + it('should return true if body is overflowing', () => { + fixtureEl.innerHTML = [ + '
' + + '
' + + '
' + ].join('') + document.body.style.overflowY = 'scroll' + const result = Scrollbar.isBodyOverflowing() + + expect(result).toEqual(true) + }) + + it('should return false if body is overflowing', () => { + fixtureEl.innerHTML = [ + '
' + + '
' + + '
' + ].join('') + document.body.style.overflowY = 'hidden' + + const result = Scrollbar.isBodyOverflowing() + + expect(result).toEqual(false) + }) + }) + + describe('getWidth', () => { + it('should return an integer if body is overflowing', () => { + fixtureEl.innerHTML = [ + '
' + + '
' + + '
' + ].join('') + document.body.style.overflowY = 'scroll' + const result = Scrollbar.getWidth() + + expect(result).toBeGreaterThan(1) + }) + + it('should return 0 if body is overflowing', () => { + fixtureEl.innerHTML = [ + '
' + + '
' + + '
' + ].join('') + document.body.style.overflowY = 'hidden' + + const result = Scrollbar.getWidth() + + expect(result).toEqual(0) + }) + }) + + describe('hide - reset', () => { + it('should adjust the inline padding of fixed elements', done => { + fixtureEl.innerHTML = [ + '
' + + '
', + '
' + ].join('') + document.body.style.overflowY = 'scroll' + + const fixedEl = fixtureEl.querySelector('.fixed-top') + const originalPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10) + const expectedPadding = originalPadding + Scrollbar.getWidth() + + Scrollbar.hide() + + let currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10) + expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual('0px', 'original fixed element padding should be stored in data-bs-padding-right') + expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening') + + Scrollbar.reset() + currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10) + expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing') + expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing') + done() + }) + + it('should adjust the inline padding of sticky elements', done => { + fixtureEl.innerHTML = [ + '
' + + '
', + '
' + ].join('') + document.body.style.overflowY = 'scroll' + + const stickyTopEl = fixtureEl.querySelector('.sticky-top') + const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10) + const expectedMargin = originalMargin - Scrollbar.getWidth() + + Scrollbar.hide() + + let currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10) + expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual('0px', 'original sticky element margin should be stored in data-bs-margin-right') + expect(currentMargin).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening') + + Scrollbar.reset() + currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10) + + expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual(null, 'data-bs-margin-right should be cleared after closing') + expect(currentMargin).toEqual(originalMargin, 'sticky element margin should be reset after closing') + done() + }) + }) +}) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index 09fc5fbb4810..bbfe27dd5921 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -55,10 +55,6 @@ transform: none; } -.offcanvas-freeze { - overflow: hidden; -} - .offcanvas-backdrop::before { position: fixed; top: 0; From 334cf937614089c7c67f3390e9335fd1d1838e51 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Sat, 13 Feb 2021 18:40:51 +0200 Subject: [PATCH 20/44] show backdrop by default --- js/src/offcanvas.js | 12 +++--------- site/content/docs/5.0/components/offcanvas.md | 15 +-------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 8a2b58c98d36..2964c53c30b7 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -12,7 +12,7 @@ import { getTransitionDurationFromElement, isVisible } from './util/index' -import { reset as scrollBarReset, hide as scrollBarHide } from './util/scrollbar' +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' @@ -84,11 +84,8 @@ class OffCanvas extends BaseComponent { this._element.style.visibility = 'visible' document.body.classList.add(CLASS_NAME_TOGGLING) - if (this._bodyOptionsHas('backdrop')) { - document.body.classList.add(CLASS_NAME_BACKDROP_BODY) - } - if (!this._bodyOptionsHas('scroll')) { + document.body.classList.add(CLASS_NAME_BACKDROP_BODY) scrollBarHide() } @@ -122,10 +119,6 @@ class OffCanvas extends BaseComponent { this._element.blur() this._isShown = false - if (this._bodyOptionsHas('backdrop')) { - document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) - } - document.body.classList.add(CLASS_NAME_TOGGLING) this._element.classList.remove(CLASS_NAME_SHOW) @@ -138,6 +131,7 @@ class OffCanvas extends BaseComponent { if (!this._bodyOptionsHas('scroll')) { scrollBarReset() + document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) } EventHandler.trigger(this._element, EVENT_HIDDEN) diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 9c6b323ed9af..d7ac9c9f9386 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -122,7 +122,7 @@ Easily style an offcanvas element with a different `background-color` or `color` {{< example >}} -
+
-
@@ -167,16 +165,6 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc

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

-
-
- - -
-
-

Try clicking on the page's content to toggle the offcanvas.

-
-
{{< /example >}} ## Accessibility @@ -192,7 +180,6 @@ The offcanvas plugin utilizes a few classes and attributes to handle the heavy l - `.offcanvas-right` 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 adds backdrop 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 ` @@ -50,7 +50,7 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ
-
-

Some random text as placeholder in place of another lorem ipsum. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by.

-

Folly words widow one downs few age every seven. If miss part by fact he park just shew. Discovered had get considered projection who favourable. Necessary up knowledge it tolerably. Unwilling departure education is be dashwoods or an. Use off agreeable law unwilling sir deficient curiosity instantly. Easy mind life fact with see has bore ten.

-

Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education.

-

Village did removed enjoyed explain nor ham saw calling talking. Securing as informed declared or margaret. Joy horrible moreover man feelings own shy. Request norland neither mistake for yet. Between the for morning assured country believe. On even feet time have an no at. Relation so in confined smallest children unpacked delicate. Why sir end believe uncivil respect.

-

In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.

-
{{< /example >}} -## Position +## Placement -Change the placement of an offcanvas element with modifier classes: +There's no default placement for offcanvas components, so you'll always have to declare one by adding one of the modifier classes below. +- `.offcanvas-left` places offcanvas on the left of the viewport (shown above) - `.offcanvas-right` places offcanvas on the right of the viewport - `.offcanvas-bottom` places offcanvas on the bottom of the viewport +Try the right and bottom options out below. + {{< example >}} - - + -
+
-
Offcanvas right
- +
Offcanvas right
+
-
-

Powering more than 18 million websites, Bootstrap is the go-to toolkit for many front-end developers. There are a few factors driving Bootstrap’s popularity. First and foremost, it’s open-source and therefore free to download and use. It’s also fully customizable, and compatible with all modern browsers. This is true of many CSS frameworks, however.

-

What sets Bootstrap apart from other development toolkits is that it was developed mobile-first. Meaning, the code was optimized for mobile devices (i.e. the smallest screen size) first and then scaled up to display on larger screens. As a result, building with Bootstrap CSS ensures that your site supports proper rendering and touch zooming for all devices.

-

Another reason Bootstrap is so popular is that it’s easy to use. It comes bundled with templates for typography, forms, buttons, drop-down menus, navigation, and other interface components. Using these pre-styled templates, you can add features that enrich the user experience on your site without having to code them from scratch.

-

Building a responsive site is much easier using Bootstrap than doing so from scratch. Bootstrap comes with responsive styles, like containers and media queries, to ensure your site adjusts to the viewport. That means you don’t have to worry about whether your visitors are using desktops, tablets, or mobile devices.

-

You can build your site quickly with Bootstrap. Once you download the framework, you can get started with a basic template and then add the components you need. These components are fundamental HTML elements, like tables, forms, buttons, images, and icons, that are styled with a base class and extended with modifier classes. Using these pre-designed components significantly limits the amount of custom CSS you have to write.

-

If you have multiple collaborators working on a site, then consistency is important. You don’t want buttons on your homepage to look different from buttons on your landing page, or to use a different website typography on your blog than anywhere else on your site — and so on. Using Bootstrap and its default settings, utility classes, and component elements can help ensure the front end of your site looks consistent.

-

Since Bootstrap comes with pre-styled content, components, and templates, Bootstrap sites tend to look the same out-of-the-box. In fact, Bootstrap has been blamed for why websites today all look the same. You can customize a Bootstrap site, but it’ll take time. Plus, if you have to override too much of the default styling, then it might make more sense to create your own stylesheet in the first place.

-

Bootstrap is considered an easy to use platform. It offers extensive documentation for every part of its framework, from its layout to content to components and more. That means virtually anyone can learn Bootstrap, but it also means it will take time to read through the documentation and learn the framework. If you are looking to build a website as quickly as possible, then Bootstrap might not be as ideal as other solutions, like website builders.

-
-
-
-
- - -
-
-

Some random text as placeholder in place of another lorem ipsum. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by.

-

Folly words widow one downs few age every seven. If miss part by fact he park just shew. Discovered had get considered projection who favourable. Necessary up knowledge it tolerably. Unwilling departure education is be dashwoods or an. Use off agreeable law unwilling sir deficient curiosity instantly. Easy mind life fact with see has bore ten.

-

Another journey chamber way yet females man. Way extensive and dejection get delivered deficient sincerity gentleman age. Too end instrument possession contrasted motionless. Calling offence six joy feeling. Coming merits and was talent enough far. Sir joy northward sportsmen education.

-

Village did removed enjoyed explain nor ham saw calling talking. Securing as informed declared or margaret. Joy horrible moreover man feelings own shy. Request norland neither mistake for yet. Between the for morning assured country believe. On even feet time have an no at. Relation so in confined smallest children unpacked delicate. Why sir end believe uncivil respect.

-

In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.

+
+ ...
{{< /example >}} -## Color schemes - -Easily style an offcanvas element with a different `background-color` or `color` with our [color utilities]({{< docsref "/utilities/colors" >}}). - {{< example >}} - + -
+
- - + +
-
-

- Bootstrap is an open source and intuitive CSS framework, which is used primarily for mobile first front-end website development. - It was discovered by Mark Otto and Jacob Thornton at Twitter, and it was formerly named as ‘Twitter Blueprint’. -

-

- CSS is principally used, but HTML templates are also a part of it. Besides HTML and CSS templates, - it also depends closely on JavaScript components, mostly in the type of jQuery plugins. -

-

- Bootstrap also has widespread support and vast coding documentation with extensive online resources and assistance from the community of developers. - This, in turn, makes it easier to understand this framework better and how to employ it. -

+
+ ...
- {{< /example >}} ## Options -By default, when an offcanvas is visible, the `` of your page cannot be scrolled. You can use the following data-options to change this behavior: - -- `data-bs-body="scroll"` enables scrolling on the `` when offcanvas is open +By default, we disable scrolling on the `` when an offcanvas is visible. Use the `data-bs-body` attribute to enable `` scrolling. {{< example >}} - + -
+
- - + +

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

@@ -170,6 +139,12 @@ By default, when an offcanvas is visible, the `` of your page cannot be sc Be sure to add `aria-labelledby="..."`, referencing the modal title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. +## Sass + +### Variables + +{{< scss-docs name="offcanvas-variables" file="scss/_variables.scss" >}} + ## Usage The offcanvas plugin utilizes a few classes and attributes to handle the heavy lifting: From aad78ec4d79dfaf014b85374f797fc82dda3b1da Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 18 Feb 2021 13:57:03 -0800 Subject: [PATCH 24/44] Add .offcanvas-title instead of .modal-title --- scss/_offcanvas.scss | 5 +++++ scss/_variables.scss | 1 + site/content/docs/5.0/components/offcanvas.md | 10 +++++----- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index b52833cc51a7..f6c574511212 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -24,6 +24,11 @@ } } +.offcanvas-title { + margin-bottom: 0; + line-height: $offcanvas-title-line-height; +} + .offcanvas-body { padding: $offcanvas-padding-y $offcanvas-padding-x; overflow-y: auto; diff --git a/scss/_variables.scss b/scss/_variables.scss index 13e2ac88c964..6d66f5f4944b 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1442,6 +1442,7 @@ $offcanvas-vertical-height: 30vh !default; $offcanvas-transition-duration: .3s !default; $offcanvas-border-color: $modal-content-border-color !default; $offcanvas-border-width: $modal-content-border-width !default; +$offcanvas-title-line-height: $modal-title-line-height !default; $offcanvas-bg-color: $modal-content-bg !default; $offcanvas-color: $modal-content-color !default; $offcanvas-body-backdrop-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity) !default; diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 3264bbcd2635..8bfb4a2ed71b 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -30,7 +30,7 @@ Below is a _static_ offcanvas example (meaning its `position`, `display`, `visib {{< example class="bd-example-offcanvas p-0 bg-light" >}}
- +
Offcanvas
@@ -58,7 +58,7 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ
- +
Offcanvas
@@ -108,7 +108,7 @@ Try the right and bottom options out below.
- +
Offcanvas bottom
@@ -126,7 +126,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible. U
- +
Colored with scrolling
@@ -137,7 +137,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible. U ## Accessibility -Be sure to add `aria-labelledby="..."`, referencing the modal title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. +Be sure to add `aria-labelledby="..."`, referencing the offcanvas header, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. ## Sass From 95cd2d1a00589ad04ae28b755d4d8394a26a9765 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 18 Feb 2021 14:44:06 -0800 Subject: [PATCH 25/44] Rename offcanvas example to offcanvas-navbar to reflect it's purpose --- .../{offcanvas => offcanvas-navbar}/index.html | 2 +- .../{offcanvas => offcanvas-navbar}/offcanvas.css | 0 .../{offcanvas => offcanvas-navbar}/offcanvas.js | 0 site/data/examples.yml | 4 ++-- .../{offcanvas.png => offcanvas-navbar.png} | Bin .../{offcanvas@2x.png => offcanvas-navbar@2x.png} | Bin 6 files changed, 3 insertions(+), 3 deletions(-) rename site/content/docs/5.0/examples/{offcanvas => offcanvas-navbar}/index.html (99%) rename site/content/docs/5.0/examples/{offcanvas => offcanvas-navbar}/offcanvas.css (100%) rename site/content/docs/5.0/examples/{offcanvas => offcanvas-navbar}/offcanvas.js (100%) rename site/static/docs/5.0/assets/img/examples/{offcanvas.png => offcanvas-navbar.png} (100%) rename site/static/docs/5.0/assets/img/examples/{offcanvas@2x.png => offcanvas-navbar@2x.png} (100%) diff --git a/site/content/docs/5.0/examples/offcanvas/index.html b/site/content/docs/5.0/examples/offcanvas-navbar/index.html similarity index 99% rename from site/content/docs/5.0/examples/offcanvas/index.html rename to site/content/docs/5.0/examples/offcanvas-navbar/index.html index 9aed0aed7dc7..12bb91e07930 100644 --- a/site/content/docs/5.0/examples/offcanvas/index.html +++ b/site/content/docs/5.0/examples/offcanvas-navbar/index.html @@ -1,6 +1,6 @@ --- layout: examples -title: Offcanvas template +title: Offcanvas navbar template extra_css: - "offcanvas.css" extra_js: diff --git a/site/content/docs/5.0/examples/offcanvas/offcanvas.css b/site/content/docs/5.0/examples/offcanvas-navbar/offcanvas.css similarity index 100% rename from site/content/docs/5.0/examples/offcanvas/offcanvas.css rename to site/content/docs/5.0/examples/offcanvas-navbar/offcanvas.css diff --git a/site/content/docs/5.0/examples/offcanvas/offcanvas.js b/site/content/docs/5.0/examples/offcanvas-navbar/offcanvas.js similarity index 100% rename from site/content/docs/5.0/examples/offcanvas/offcanvas.js rename to site/content/docs/5.0/examples/offcanvas-navbar/offcanvas.js diff --git a/site/data/examples.yml b/site/data/examples.yml index 9053c2dab371..d243bdad0dcc 100644 --- a/site/data/examples.yml +++ b/site/data/examples.yml @@ -31,8 +31,6 @@ description: "Nothing but the basics: compiled CSS and JavaScript." - name: Grid description: "Multiple examples of grid layouts with all four tiers, nesting, and more." - - name: Offcanvas - description: "Turn your expandable navbar into a sliding offcanvas menu." - name: Cheatsheet description: "Kitchen sink of Bootstrap components." - name: Cheatsheet RTL @@ -49,6 +47,8 @@ description: "Single navbar example with a fixed top navbar along with some additional content." - name: Navbar bottom description: "Single navbar example with a bottom navbar along with some additional content." + - name: Offcanvas navbar + description: "Turn your expandable navbar into a sliding offcanvas menu (doesn't use our offcanvas component)." - category: RTL description: "See Bootstrap's RTL version in action with these modified Custom Components examples." diff --git a/site/static/docs/5.0/assets/img/examples/offcanvas.png b/site/static/docs/5.0/assets/img/examples/offcanvas-navbar.png similarity index 100% rename from site/static/docs/5.0/assets/img/examples/offcanvas.png rename to site/static/docs/5.0/assets/img/examples/offcanvas-navbar.png diff --git a/site/static/docs/5.0/assets/img/examples/offcanvas@2x.png b/site/static/docs/5.0/assets/img/examples/offcanvas-navbar@2x.png similarity index 100% rename from site/static/docs/5.0/assets/img/examples/offcanvas@2x.png rename to site/static/docs/5.0/assets/img/examples/offcanvas-navbar@2x.png From 5c206490755d7966bed6c669fbccb7482b2e51a7 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 18 Feb 2021 15:32:03 -0800 Subject: [PATCH 26/44] labelledby references title and not header --- site/content/docs/5.0/components/offcanvas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 8bfb4a2ed71b..31cec7349762 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -137,7 +137,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible. U ## Accessibility -Be sure to add `aria-labelledby="..."`, referencing the offcanvas header, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. +Be sure to add `aria-labelledby="..."`, referencing the offcanvas title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. ## Sass From 6f4038551498d9b47bc7e52d33c3a194ea6bb989 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 18 Feb 2021 15:43:42 -0800 Subject: [PATCH 27/44] Add default shadow to offcanvas --- scss/_offcanvas.scss | 1 + scss/_variables.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index f6c574511212..33d1069afe37 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -10,6 +10,7 @@ background-color: $offcanvas-bg-color; background-clip: padding-box; outline: 0; + @include box-shadow($offcanvas-box-shadow); @include transition(transform $offcanvas-transition-duration ease-in-out); } diff --git a/scss/_variables.scss b/scss/_variables.scss index 6d66f5f4944b..2668a790cb77 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1446,6 +1446,7 @@ $offcanvas-title-line-height: $modal-title-line-height !default; $offcanvas-bg-color: $modal-content-bg !default; $offcanvas-color: $modal-content-color !default; $offcanvas-body-backdrop-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity) !default; +$offcanvas-box-shadow: $modal-content-box-shadow-xs !default; // scss-docs-end offcanvas-variables // Code From e25e9d17813047d4c6179d6e78752d7dc3c13bcb Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 19 Feb 2021 02:17:46 +0200 Subject: [PATCH 28/44] enable offcanvas-body to fill all the remaining wrapper area --- scss/_offcanvas.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/scss/_offcanvas.scss b/scss/_offcanvas.scss index 33d1069afe37..a0b3db56ba92 100644 --- a/scss/_offcanvas.scss +++ b/scss/_offcanvas.scss @@ -31,6 +31,7 @@ } .offcanvas-body { + flex-grow: 1; padding: $offcanvas-padding-y $offcanvas-padding-x; overflow-y: auto; } From e07cf43873218f6ec0b42c7c223294012a40736d Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 19 Feb 2021 02:19:46 +0200 Subject: [PATCH 29/44] Be more descriptive, on Accessibility area --- site/content/docs/5.0/components/offcanvas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 31cec7349762..3fa66ef0a10b 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -137,7 +137,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible. U ## Accessibility -Be sure to add `aria-labelledby="..."`, referencing the offcanvas title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. +The off-canvas panel is, conceptually, a modal dialog. So, be sure to add `aria-labelledby="..."`, referencing the offcanvas title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. ## Sass From 8445e3615b96c5cc1aa8c1ce6b33541a7f1bee7c Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 19 Feb 2021 02:21:04 +0200 Subject: [PATCH 30/44] remove redundant classes --- js/src/offcanvas.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 2964c53c30b7..1f523742ae56 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -33,8 +33,6 @@ const DATA_BODY_ACTIONS = 'data-bs-body' const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop' const CLASS_NAME_DISABLED = 'disabled' -const CLASS_NAME_OPEN = 'offcanvas-open' -const CLASS_NAME_TOGGLING = 'offcanvas-toggling' const CLASS_NAME_SHOW = 'show' const EVENT_SHOW = `show${EVENT_KEY}` @@ -95,8 +93,6 @@ class OffCanvas extends BaseComponent { this._element.classList.add(CLASS_NAME_SHOW) const completeCallBack = () => { - document.body.classList.add(CLASS_NAME_OPEN) - document.body.classList.remove(CLASS_NAME_TOGGLING) this._enforceFocusOnElement(this._element) EventHandler.trigger(this._element, EVENT_SHOWN, { relatedTarget }) } @@ -118,12 +114,9 @@ class OffCanvas extends BaseComponent { EventHandler.off(document, EVENT_FOCUSIN) this._element.blur() this._isShown = false - - document.body.classList.add(CLASS_NAME_TOGGLING) this._element.classList.remove(CLASS_NAME_SHOW) const completeCallback = () => { - document.body.classList.remove(CLASS_NAME_TOGGLING) this._element.setAttribute('aria-hidden', true) this._element.removeAttribute('aria-modal') this._element.removeAttribute('role') From 97db642c08a920a22147b4fe9b952b1c47399195 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 19 Feb 2021 02:21:36 +0200 Subject: [PATCH 31/44] ensure in case of an already open offcanvas, not to open another one --- js/src/offcanvas.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 1f523742ae56..44dd01c48a76 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -34,6 +34,7 @@ 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 OPEN_SELECTOR = '.offcanvas.show' const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` @@ -207,6 +208,12 @@ 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(OPEN_SELECTOR) + if (allReadyOpen && allReadyOpen !== target) { + return + } + const data = Data.getData(target, DATA_KEY) || new OffCanvas(target) data.toggle(this) }) From a06dbbba45ddc942f39f3a9cb247cfe7f1cbd8a8 Mon Sep 17 00:00:00 2001 From: GeoSot Date: Fri, 19 Feb 2021 02:22:30 +0200 Subject: [PATCH 32/44] bring back backdrop|scroll combinations --- js/src/offcanvas.js | 12 ++++++---- site/content/docs/5.0/components/offcanvas.md | 24 ++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 44dd01c48a76..0910ba72dd28 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -58,7 +58,7 @@ class OffCanvas extends BaseComponent { super(element) this._isShown = element.classList.contains(CLASS_NAME_SHOW) - this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) + this._bodyOptions = element.getAttribute(DATA_BODY_ACTIONS) || '' this._handleClosing() } @@ -81,10 +81,12 @@ class OffCanvas extends BaseComponent { this._isShown = true this._element.style.visibility = 'visible' - document.body.classList.add(CLASS_NAME_TOGGLING) - if (!this._bodyOptionsHas('scroll')) { + if (this._bodyOptionsHas('backdrop') || !this._bodyOptions.length) { document.body.classList.add(CLASS_NAME_BACKDROP_BODY) + } + + if (!this._bodyOptionsHas('scroll')) { scrollBarHide() } @@ -123,9 +125,9 @@ class OffCanvas extends BaseComponent { this._element.removeAttribute('role') this._element.style.visibility = 'hidden' + document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) if (!this._bodyOptionsHas('scroll')) { scrollBarReset() - document.body.classList.remove(CLASS_NAME_BACKDROP_BODY) } EventHandler.trigger(this._element, EVENT_HIDDEN) @@ -147,7 +149,7 @@ class OffCanvas extends BaseComponent { } _bodyOptionsHas(option) { - return this._bodyOptions === option + return this._bodyOptions.split('|').includes(option) } _handleClosing() { diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index 3fa66ef0a10b..a39d2bfae582 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -119,10 +119,12 @@ Try the right and bottom options out below. ## Options -By default, we disable scrolling on the `` when an offcanvas is visible. Use the `data-bs-body` attribute to enable `` scrolling. +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 {{< example >}} + +
@@ -133,6 +135,24 @@ By default, we disable scrolling on the `` when an offcanvas is visible. U

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

+
+
+
Offcanvas with backdrop
+ +
+
+

.....

+
+
+
+
+
Backdroped with scrolling
+ +
+
+

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

+
+
{{< /example >}} ## Accessibility @@ -154,6 +174,8 @@ The offcanvas plugin utilizes a few classes and attributes to handle the heavy l - `.offcanvas-right` 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"` is a combination of the above, enables `` scrolling and creates 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 ` @@ -56,7 +56,7 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ Button with data-bs-target -
+
Offcanvas
@@ -81,18 +81,18 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ ## Placement -There's no default placement for offcanvas components, so you'll always have to declare one by adding one of the modifier classes below. +There's no default placement for offcanvas components, so you must add one of the modifier classes below; -- `.offcanvas-left` places offcanvas on the left of the viewport (shown above) -- `.offcanvas-right` places offcanvas on the right of the viewport +- `.offcanvas-start` places offcanvas on the left of the viewport (shown above) +- `.offcanvas-end` places offcanvas on the right of the viewport - `.offcanvas-bottom` places offcanvas on the bottom of the viewport -Try the right and bottom options out below. +Try the right and bottom examples out below. {{< example >}} -
+
Offcanvas right
@@ -126,7 +126,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible an -
+
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
@@ -157,7 +157,7 @@ By default, we disable scrolling on the `` when an offcanvas is visible an ## Accessibility -The off-canvas panel is, conceptually, a modal dialog. So, be sure to add `aria-labelledby="..."`, referencing the offcanvas title, to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. +Since the offcanvas panel is conceptually a modal dialog, be sure to add `aria-labelledby="..."`—referencing the offcanvas title—to `.offcanvas`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript. ## Sass @@ -171,11 +171,12 @@ The offcanvas plugin utilizes a few classes and attributes to handle the heavy l - `.offcanvas` hides the content - `.offcanvas.show` shows the content -- `.offcanvas-right` hides the offcanvas on the right +- `.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"` is a combination of the above, enables `` scrolling and creates a backdrop over the `` when offcanvas is open +- `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 `
-
+
Backdroped with scrolling
@@ -176,7 +176,7 @@ The offcanvas plugin utilizes a few classes and attributes to handle the heavy l - `.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 +- `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 `