diff --git a/config.yml b/config.yml index f1d7a2a23f15..f1a25cb57a0c 100644 --- a/config.yml +++ b/config.yml @@ -75,5 +75,5 @@ params: js_hash: "sha384-j0CNLUeiqtyaRmlzUHCPZ+Gy5fQu0dQ6eZ/xAww941Ai1SxSY+0EQqNXNE6DZiVc" js_bundle: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" js_bundle_hash: "sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" - popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.1/dist/umd/popper.min.js" - popper_hash: "sha384-SR1sx49pcuLnqZUnnPwx6FCym0wLsk5JZuNx2bPPENzswTNFaQU1RDvt3wT4gWFG" + popper: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" + popper_hash: "sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" diff --git a/js/src/carousel.js b/js/src/carousel.js index 76581ca5d42c..e336abb1eda2 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -336,10 +336,10 @@ class Carousel extends BaseComponent { if (event.key === ARROW_LEFT_KEY) { event.preventDefault() - this._slide(DIRECTION_LEFT) + this._slide(DIRECTION_RIGHT) } else if (event.key === ARROW_RIGHT_KEY) { event.preventDefault() - this._slide(DIRECTION_RIGHT) + this._slide(DIRECTION_LEFT) } } @@ -509,10 +509,10 @@ class Carousel extends BaseComponent { } if (isRTL()) { - return direction === DIRECTION_RIGHT ? ORDER_PREV : ORDER_NEXT + return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT } - return direction === DIRECTION_RIGHT ? ORDER_NEXT : ORDER_PREV + return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV } _orderToDirection(order) { @@ -521,10 +521,10 @@ class Carousel extends BaseComponent { } if (isRTL()) { - return order === ORDER_NEXT ? DIRECTION_LEFT : DIRECTION_RIGHT + return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT } - return order === ORDER_NEXT ? DIRECTION_RIGHT : DIRECTION_LEFT + return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT } // Static diff --git a/js/src/dropdown.js b/js/src/dropdown.js index fd16e912676b..e3cbb3321a27 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -10,6 +10,7 @@ import * as Popper from '@popperjs/core' import { defineJQueryPlugin, getElementFromSelector, + isDisabled, isElement, isVisible, isRTL, @@ -51,7 +52,6 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}` -const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_SHOW = 'show' const CLASS_NAME_DROPUP = 'dropup' const CLASS_NAME_DROPEND = 'dropend' @@ -121,7 +121,7 @@ class Dropdown extends BaseComponent { // Public toggle() { - if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED)) { + if (isDisabled(this._element)) { return } @@ -137,7 +137,7 @@ class Dropdown extends BaseComponent { } show() { - if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || this._menu.classList.contains(CLASS_NAME_SHOW)) { + if (isDisabled(this._element) || this._menu.classList.contains(CLASS_NAME_SHOW)) { return } @@ -204,7 +204,7 @@ class Dropdown extends BaseComponent { } hide() { - if (this._element.disabled || this._element.classList.contains(CLASS_NAME_DISABLED) || !this._menu.classList.contains(CLASS_NAME_SHOW)) { + if (isDisabled(this._element) || !this._menu.classList.contains(CLASS_NAME_SHOW)) { return } @@ -218,12 +218,20 @@ class Dropdown extends BaseComponent { return } + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + if ('ontouchstart' in document.documentElement) { + [].concat(...document.body.children) + .forEach(elem => EventHandler.off(elem, 'mouseover', null, noop())) + } + if (this._popper) { this._popper.destroy() } this._menu.classList.toggle(CLASS_NAME_SHOW) this._element.classList.toggle(CLASS_NAME_SHOW) + this._element.setAttribute('aria-expanded', 'false') Manipulator.removeDataAttribute(this._menu, 'popper') EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget) } @@ -430,19 +438,43 @@ class Dropdown extends BaseComponent { .forEach(elem => EventHandler.off(elem, 'mouseover', noop)) } - toggles[i].setAttribute('aria-expanded', 'false') - if (context._popper) { context._popper.destroy() } dropdownMenu.classList.remove(CLASS_NAME_SHOW) toggles[i].classList.remove(CLASS_NAME_SHOW) + toggles[i].setAttribute('aria-expanded', 'false') Manipulator.removeDataAttribute(dropdownMenu, 'popper') EventHandler.trigger(toggles[i], EVENT_HIDDEN, relatedTarget) } } + static selectMenuItem(parent, event) { + const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible) + + if (!items.length) { + return + } + + let index = items.indexOf(event.target) + + // Up + if (event.key === ARROW_UP_KEY && index > 0) { + index-- + } + + // Down + if (event.key === ARROW_DOWN_KEY && index < items.length - 1) { + index++ + } + + // index is -1 if the first keydown is an ArrowUp + index = index === -1 ? 0 : index + + items[index].focus() + } + static getParentFromElement(element) { return getElementFromSelector(element) || element.parentNode } @@ -463,26 +495,29 @@ class Dropdown extends BaseComponent { return } + const isActive = this.classList.contains(CLASS_NAME_SHOW) + + if (!isActive && event.key === ESCAPE_KEY) { + return + } + event.preventDefault() event.stopPropagation() - if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) { + if (isDisabled(this)) { return } - const parent = Dropdown.getParentFromElement(this) - const isActive = this.classList.contains(CLASS_NAME_SHOW) + const getToggleButton = () => this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] if (event.key === ESCAPE_KEY) { - const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] - button.focus() + getToggleButton().focus() Dropdown.clearMenus() return } if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) { - const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] - button.click() + getToggleButton().click() return } @@ -491,28 +526,7 @@ class Dropdown extends BaseComponent { return } - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible) - - if (!items.length) { - return - } - - let index = items.indexOf(event.target) - - // Up - if (event.key === ARROW_UP_KEY && index > 0) { - index-- - } - - // Down - if (event.key === ARROW_DOWN_KEY && index < items.length - 1) { - index++ - } - - // index is -1 if the first keydown is an ArrowUp - index = index === -1 ? 0 : index - - items[index].focus() + Dropdown.selectMenuItem(Dropdown.getParentFromElement(this), event) } } diff --git a/js/src/modal.js b/js/src/modal.js index 2966f03fb5d1..b2a2e80ebc04 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -10,12 +10,11 @@ import { emulateTransitionEnd, getElementFromSelector, getTransitionDurationFromElement, - isVisible, isRTL, + isVisible, reflow, typeCheckConfig } from './util/index' -import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' @@ -222,6 +221,7 @@ class Modal extends BaseComponent { _getConfig(config) { config = { ...Default, + ...Manipulator.getDataAttributes(this._element), ...config } typeCheckConfig(NAME, config, DefaultType) @@ -474,7 +474,7 @@ class Modal extends BaseComponent { const actualValue = element.style[styleProp] const calculatedValue = window.getComputedStyle(element)[styleProp] Manipulator.setDataAttribute(element, styleProp, actualValue) - element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px' + element.style[styleProp] = `${callback(Number.parseFloat(calculatedValue))}px` }) } @@ -509,24 +509,17 @@ class Modal extends BaseComponent { static jQueryInterface(config, relatedTarget) { return this.each(function () { - let data = Data.get(this, DATA_KEY) - const _config = { - ...Default, - ...Manipulator.getDataAttributes(this), - ...(typeof config === 'object' && config ? config : {}) - } + const data = Modal.getInstance(this) || new Modal(this, typeof config === 'object' ? config : {}) - if (!data) { - data = new Modal(this, _config) + if (typeof config !== 'string') { + return } - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config](relatedTarget) + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) } + + data[config](relatedTarget) }) } } @@ -540,7 +533,7 @@ class Modal extends BaseComponent { EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) { const target = getElementFromSelector(this) - if (this.tagName === 'A' || this.tagName === 'AREA') { + if (['A', 'AREA'].includes(this.tagName)) { event.preventDefault() } @@ -557,15 +550,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( }) }) - let data = Data.get(target, DATA_KEY) - if (!data) { - const config = { - ...Manipulator.getDataAttributes(target), - ...Manipulator.getDataAttributes(this) - } - - data = new Modal(target, config) - } + const data = Modal.getInstance(target) || new Modal(target) data.toggle(this) }) diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js index 3667b9a12dd0..4e830b530b14 100644 --- a/js/src/scrollspy.js +++ b/js/src/scrollspy.js @@ -12,7 +12,6 @@ import { isElement, typeCheckConfig } from './util/index' -import Data from './dom/data' import EventHandler from './dom/event-handler' import Manipulator from './dom/manipulator' import SelectorEngine from './dom/selector-engine' @@ -155,6 +154,7 @@ class ScrollSpy extends BaseComponent { _getConfig(config) { config = { ...Default, + ...Manipulator.getDataAttributes(this._element), ...(typeof config === 'object' && config ? config : {}) } @@ -278,20 +278,17 @@ class ScrollSpy extends BaseComponent { static jQueryInterface(config) { return this.each(function () { - let data = Data.get(this, DATA_KEY) - const _config = typeof config === 'object' && config + const data = ScrollSpy.getInstance(this) || new ScrollSpy(this, typeof config === 'object' ? config : {}) - if (!data) { - data = new ScrollSpy(this, _config) + if (typeof config !== 'string') { + return } - if (typeof config === 'string') { - if (typeof data[config] === 'undefined') { - throw new TypeError(`No method named "${config}"`) - } - - data[config]() + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) } + + data[config]() }) } } @@ -304,7 +301,7 @@ class ScrollSpy extends BaseComponent { EventHandler.on(window, EVENT_LOAD_DATA_API, () => { SelectorEngine.find(SELECTOR_DATA_SPY) - .forEach(spy => new ScrollSpy(spy, Manipulator.getDataAttributes(spy))) + .forEach(spy => new ScrollSpy(spy)) }) /** diff --git a/js/src/util/index.js b/js/src/util/index.js index 77583aee3b9a..f19d76e036e9 100644 --- a/js/src/util/index.js +++ b/js/src/util/index.js @@ -48,7 +48,7 @@ const getSelector = element => { // Just in case some CMS puts out a full URL with the anchor appended if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) { - hrefAttr = '#' + hrefAttr.split('#')[1] + hrefAttr = `#${hrefAttr.split('#')[1]}` } selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null @@ -128,9 +128,7 @@ const typeCheckConfig = (componentName, config, configTypes) => { if (!new RegExp(expectedTypes).test(valueType)) { throw new TypeError( - `${componentName.toUpperCase()}: ` + - `Option "${property}" provided type "${valueType}" ` + - `but expected type "${expectedTypes}".` + `${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".` ) } }) diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index 3e619ef51e43..e63a66bf218b 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -35,7 +35,7 @@ const _setElementAttributes = (selector, styleProp, callback) => { const actualValue = element.style[styleProp] const calculatedValue = window.getComputedStyle(element)[styleProp] Manipulator.setDataAttribute(element, styleProp, actualValue) - element.style[styleProp] = callback(Number.parseFloat(calculatedValue)) + 'px' + element.style[styleProp] = `${callback(Number.parseFloat(calculatedValue))}px` }) } diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index dc3006fffd2b..75c3bbd6d8dc 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -317,7 +317,7 @@ describe('Carousel', () => { expect(carousel._addTouchEventListeners).toHaveBeenCalled() }) - it('should allow swiperight and call _slide with pointer events', done => { + it('should allow swiperight and call _slide (prev) with pointer events', done => { if (!supportPointerEvent) { expect().nothing() done() @@ -362,7 +362,7 @@ describe('Carousel', () => { }) }) - it('should allow swipeleft and call previous with pointer events', done => { + it('should allow swipeleft and call next with pointer events', done => { if (!supportPointerEvent) { expect().nothing() done() @@ -408,7 +408,7 @@ describe('Carousel', () => { }) }) - it('should allow swiperight and call _slide with touch events', done => { + it('should allow swiperight and call _slide (prev) with touch events', done => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = () => {} @@ -447,7 +447,7 @@ describe('Carousel', () => { }) }) - it('should allow swipeleft and call _slide with touch events', done => { + it('should allow swipeleft and call _slide (next) with touch events', done => { Simulator.setType('touch') clearPointerEvents() document.documentElement.ontouchstart = () => {} @@ -601,7 +601,7 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl, {}) const onSlide = e => { - expect(e.direction).toEqual('right') + expect(e.direction).toEqual('left') expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) expect(e.from).toEqual(0) expect(e.to).toEqual(1) @@ -613,7 +613,7 @@ describe('Carousel', () => { } const onSlide2 = e => { - expect(e.direction).toEqual('left') + expect(e.direction).toEqual('right') done() } @@ -636,7 +636,7 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl, {}) const onSlid = e => { - expect(e.direction).toEqual('right') + expect(e.direction).toEqual('left') expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) expect(e.from).toEqual(0) expect(e.to).toEqual(1) @@ -648,7 +648,7 @@ describe('Carousel', () => { } const onSlid2 = e => { - expect(e.direction).toEqual('left') + expect(e.direction).toEqual('right') done() } @@ -1069,13 +1069,13 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - expect(carousel._directionToOrder('left')).toEqual('prev') + expect(carousel._directionToOrder('left')).toEqual('next') expect(carousel._directionToOrder('prev')).toEqual('prev') - expect(carousel._directionToOrder('right')).toEqual('next') + expect(carousel._directionToOrder('right')).toEqual('prev') expect(carousel._directionToOrder('next')).toEqual('next') - expect(carousel._orderToDirection('next')).toEqual('right') - expect(carousel._orderToDirection('prev')).toEqual('left') + expect(carousel._orderToDirection('next')).toEqual('left') + expect(carousel._orderToDirection('prev')).toEqual('right') }) it('"_directionToOrder" and "_orderToDirection" must return the right results when rtl=true', () => { @@ -1086,13 +1086,13 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl, {}) expect(util.isRTL()).toEqual(true, 'rtl has to be true') - expect(carousel._directionToOrder('left')).toEqual('next') + expect(carousel._directionToOrder('left')).toEqual('prev') expect(carousel._directionToOrder('prev')).toEqual('prev') - expect(carousel._directionToOrder('right')).toEqual('prev') + expect(carousel._directionToOrder('right')).toEqual('next') expect(carousel._directionToOrder('next')).toEqual('next') - expect(carousel._orderToDirection('next')).toEqual('left') - expect(carousel._orderToDirection('prev')).toEqual('right') + expect(carousel._orderToDirection('next')).toEqual('right') + expect(carousel._orderToDirection('prev')).toEqual('left') document.documentElement.dir = 'ltl' }) @@ -1106,11 +1106,11 @@ describe('Carousel', () => { carousel._slide('left') expect(spy).toHaveBeenCalledWith('left') - expect(spy2).toHaveBeenCalledWith('prev') + expect(spy2).toHaveBeenCalledWith('next') carousel._slide('right') expect(spy).toHaveBeenCalledWith('right') - expect(spy2).toHaveBeenCalledWith('next') + expect(spy2).toHaveBeenCalledWith('prev') }) it('"_slide" has to call "_directionToOrder" and "_orderToDirection" when rtl=true', () => { @@ -1124,11 +1124,11 @@ describe('Carousel', () => { carousel._slide('left') expect(spy).toHaveBeenCalledWith('left') - expect(spy2).toHaveBeenCalledWith('next') + expect(spy2).toHaveBeenCalledWith('prev') carousel._slide('right') expect(spy).toHaveBeenCalledWith('right') - expect(spy2).toHaveBeenCalledWith('prev') + expect(spy2).toHaveBeenCalledWith('next') document.documentElement.dir = 'ltl' }) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index ad51d487bf12..b8969be7c7d7 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -743,7 +743,7 @@ describe('Dropdown', () => { it('should hide a dropdown', done => { fixtureEl.innerHTML = [ ' diff --git a/site/content/docs/5.0/components/offcanvas.md b/site/content/docs/5.0/components/offcanvas.md index a2b5ed3fa577..b9dbd7ce8da2 100644 --- a/site/content/docs/5.0/components/offcanvas.md +++ b/site/content/docs/5.0/components/offcanvas.md @@ -85,9 +85,24 @@ There's no default placement for offcanvas components, so you must add one of th - `.offcanvas-start` places offcanvas on the left of the viewport (shown above) - `.offcanvas-end` places offcanvas on the right of the viewport +- `.offcanvas-top` places offcanvas on the top of the viewport - `.offcanvas-bottom` places offcanvas on the bottom of the viewport -Try the right and bottom examples out below. +Try the top, right, and bottom examples out below. + +{{< example >}} + + +
+
+
Offcanvas top
+ +
+
+ ... +
+
+{{< /example >}} {{< example >}} @@ -225,7 +240,7 @@ var bsOffcanvas = new bootstrap.Offcanvas(myOffcanvas) | `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 | +| `getInstance` | *Static* method which allows you to get the offcanvas instance associated with a DOM element | {{< /bs-table >}} ### Events diff --git a/site/content/docs/5.0/examples/features/index.html b/site/content/docs/5.0/examples/features/index.html index de4e00f6d266..3c174a0100f5 100644 --- a/site/content/docs/5.0/examples/features/index.html +++ b/site/content/docs/5.0/examples/features/index.html @@ -65,10 +65,10 @@ -