diff options
-rw-r--r-- | js/src/offcanvas.js | 30 | ||||
-rw-r--r-- | js/tests/unit/offcanvas.spec.js | 71 | ||||
-rw-r--r-- | site/content/docs/5.1/components/offcanvas.md | 97 |
3 files changed, 145 insertions, 53 deletions
diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 2735a9c2ae..b5afc0c87b 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -39,6 +39,7 @@ const OPEN_SELECTOR = '.offcanvas.show' const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` const EVENT_HIDE = `hide${EVENT_KEY}` +const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}` const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}` @@ -52,7 +53,7 @@ const Default = { } const DefaultType = { - backdrop: 'boolean', + backdrop: '(boolean|string)', keyboard: 'boolean', scroll: 'boolean' } @@ -164,12 +165,24 @@ class Offcanvas extends BaseComponent { // Private _initializeBackDrop() { + const clickCallback = () => { + if (this._config.backdrop === 'static') { + EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) + return + } + + this.hide() + } + + // 'static' option will be translated to true, and booleans will keep their value + const isVisible = Boolean(this._config.backdrop) + return new Backdrop({ className: CLASS_NAME_BACKDROP, - isVisible: this._config.backdrop, + isVisible, isAnimated: true, rootElement: this._element.parentNode, - clickCallback: () => this.hide() + clickCallback: isVisible ? clickCallback : null }) } @@ -181,9 +194,16 @@ class Offcanvas extends BaseComponent { _addEventListeners() { EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => { - if (this._config.keyboard && event.key === ESCAPE_KEY) { - this.hide() + if (event.key !== ESCAPE_KEY) { + return } + + if (!this._config.keyboard) { + EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED) + return + } + + this.hide() }) } diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 37f3397c79..a98a8c13e3 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -74,6 +74,21 @@ describe('Offcanvas', () => { expect(offCanvas.hide).toHaveBeenCalled() }) + it('should hide if esc is pressed and backdrop is static', () => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + spyOn(offCanvas, 'hide') + + offCanvasEl.dispatchEvent(keyDownEsc) + + expect(offCanvas.hide).toHaveBeenCalled() + }) + it('should not hide if esc is not pressed', () => { fixtureEl.innerHTML = '<div class="offcanvas"></div>' @@ -84,25 +99,61 @@ describe('Offcanvas', () => { spyOn(offCanvas, 'hide') - document.dispatchEvent(keydownTab) + offCanvasEl.dispatchEvent(keydownTab) expect(offCanvas.hide).not.toHaveBeenCalled() }) it('should not hide if esc is pressed but with keyboard = false', () => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) - const keyDownEsc = createEvent('keydown') - keyDownEsc.key = 'Escape' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' - spyOn(offCanvas, 'hide') + spyOn(offCanvas, 'hide') + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) - document.dispatchEvent(keyDownEsc) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._config.keyboard).toBeFalse() + offCanvasEl.dispatchEvent(keyDownEsc) - expect(offCanvas._config.keyboard).toBeFalse() - expect(offCanvas.hide).not.toHaveBeenCalled() + expect(hidePreventedSpy).toHaveBeenCalled() + expect(offCanvas.hide).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) + }) + + it('should not hide if user clicks on static backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) + + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + expect(hidePreventedSpy).toHaveBeenCalled() + expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) }) }) diff --git a/site/content/docs/5.1/components/offcanvas.md b/site/content/docs/5.1/components/offcanvas.md index 9f30f74e95..7a8cbc68a7 100644 --- a/site/content/docs/5.1/components/offcanvas.md +++ b/site/content/docs/5.1/components/offcanvas.md @@ -79,93 +79,113 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ </div> {{< /example >}} -## Placement - -There's no default placement for offcanvas components, so you must add one of the modifier classes below; - -- `.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 +### Body scrolling -Try the top, right, and bottom examples out below. +Scrolling the `<body>` element is disabled when an offcanvas and its backdrop are visible. Use the `data-bs-scroll` attribute to enable `<body>` scrolling. {{< example >}} -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasTop" aria-controls="offcanvasTop">Toggle top offcanvas</button> +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasScrolling" aria-controls="offcanvasScrolling">Enable body scrolling</button> -<div class="offcanvas offcanvas-top" tabindex="-1" id="offcanvasTop" aria-labelledby="offcanvasTopLabel"> +<div class="offcanvas offcanvas-start" data-bs-scroll="true" data-bs-backdrop="false" tabindex="-1" id="offcanvasScrolling" aria-labelledby="offcanvasScrollingLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasTopLabel">Offcanvas top</h5> + <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Offcanvas with body scrolling</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> - ... + <p>Try scrolling the rest of the page to see this option in action.</p> </div> </div> {{< /example >}} +### Body scrolling and backdrop + +You can also enable `<body>` scrolling with a visible backdrop. + {{< example >}} -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight">Toggle right offcanvas</button> +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasWithBothOptions" aria-controls="offcanvasWithBothOptions">Enable both scrolling & backdrop</button> -<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel"> +<div class="offcanvas offcanvas-start" data-bs-scroll="true" tabindex="-1" id="offcanvasWithBothOptions" aria-labelledby="offcanvasWithBothOptionsLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasRightLabel">Offcanvas right</h5> + <h5 class="offcanvas-title" id="offcanvasWithBothOptionsLabel">Backdrop with scrolling</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> - ... + <p>Try scrolling the rest of the page to see this option in action.</p> </div> </div> {{< /example >}} +### Static backdrop + +When backdrop is set to static, the offcanvas will not close when clicking outside of it. + {{< example >}} -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasBottom" aria-controls="offcanvasBottom">Toggle bottom offcanvas</button> +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#staticBackdrop" aria-controls="staticBackdrop"> + Toggle static offcanvas +</button> -<div class="offcanvas offcanvas-bottom" tabindex="-1" id="offcanvasBottom" aria-labelledby="offcanvasBottomLabel"> +<div class="offcanvas offcanvas-start" data-bs-backdrop="static" tabindex="-1" id="staticBackdrop" aria-labelledby="staticBackdropLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasBottomLabel">Offcanvas bottom</h5> + <h5 class="offcanvas-title" id="staticBackdropLabel">Offcanvas</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> - <div class="offcanvas-body small"> - ... + <div class="offcanvas-body"> + <div> + I will not close if you click outside of me. + </div> </div> </div> {{< /example >}} -## Backdrop +## Placement + +There's no default placement for offcanvas components, so you must add one of the modifier classes below; + +- `.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 -Scrolling the `<body>` element is disabled when an offcanvas and its backdrop are visible. Use the `data-bs-scroll` attribute to toggle `<body>` scrolling and `data-bs-backdrop` to toggle the backdrop. +Try the top, right, and bottom examples out below. {{< example >}} -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasScrolling" aria-controls="offcanvasScrolling">Enable body scrolling</button> -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasWithBackdrop" aria-controls="offcanvasWithBackdrop">Enable backdrop (default)</button> -<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasWithBothOptions" aria-controls="offcanvasWithBothOptions">Enable both scrolling & backdrop</button> +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasTop" aria-controls="offcanvasTop">Toggle top offcanvas</button> -<div class="offcanvas offcanvas-start" data-bs-scroll="true" data-bs-backdrop="false" tabindex="-1" id="offcanvasScrolling" aria-labelledby="offcanvasScrollingLabel"> +<div class="offcanvas offcanvas-top" tabindex="-1" id="offcanvasTop" aria-labelledby="offcanvasTopLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Offcanvas with body scrolling</h5> + <h5 class="offcanvas-title" id="offcanvasTopLabel">Offcanvas top</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> - <p>Try scrolling the rest of the page to see this option in action.</p> + ... </div> </div> -<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasWithBackdrop" aria-labelledby="offcanvasWithBackdropLabel"> +{{< /example >}} + +{{< example >}} +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" aria-controls="offcanvasRight">Toggle right offcanvas</button> + +<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasWithBackdropLabel">Offcanvas with backdrop</h5> + <h5 class="offcanvas-title" id="offcanvasRightLabel">Offcanvas right</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> <div class="offcanvas-body"> - <p>.....</p> + ... </div> </div> -<div class="offcanvas offcanvas-start" data-bs-scroll="true" tabindex="-1" id="offcanvasWithBothOptions" aria-labelledby="offcanvasWithBothOptionsLabel"> +{{< /example >}} + +{{< example >}} +<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasBottom" aria-controls="offcanvasBottom">Toggle bottom offcanvas</button> + +<div class="offcanvas offcanvas-bottom" tabindex="-1" id="offcanvasBottom" aria-labelledby="offcanvasBottomLabel"> <div class="offcanvas-header"> - <h5 class="offcanvas-title" id="offcanvasWithBothOptionsLabel">Backdrop with scrolling</h5> + <h5 class="offcanvas-title" id="offcanvasBottomLabel">Offcanvas bottom</h5> <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button> </div> - <div class="offcanvas-body"> - <p>Try scrolling the rest of the page to see this option in action.</p> + <div class="offcanvas-body small"> + ... </div> </div> {{< /example >}} @@ -225,7 +245,7 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap {{< bs-table "table" >}} | Name | Type | Default | Description | | --- | --- | --- | --- | -| `backdrop` | boolean | `true` | Apply a backdrop on body while offcanvas is open | +| `backdrop` | boolean or the string `static` | `true` | Apply a backdrop on body while offcanvas is open. Alternatively, specify `static` for a backdrop which doesn't close the offcanvas when clicked. | | `keyboard` | boolean | `true` | Closes the offcanvas when escape key is pressed | | `scroll` | boolean | `false` | Allow body scrolling while offcanvas is open | {{< /bs-table >}} @@ -266,6 +286,7 @@ Bootstrap's offcanvas class exposes a few events for hooking into offcanvas func | `shown.bs.offcanvas` | This event is fired when an offcanvas element has been made visible to the user (will wait for CSS transitions to complete). | | `hide.bs.offcanvas` | This event is fired immediately when the `hide` method has been called. | | `hidden.bs.offcanvas` | This event is fired when an offcanvas element has been hidden from the user (will wait for CSS transitions to complete). | +| `hidePrevented.bs.offcanvas` | This event is fired when the offcanvas is shown, its backdrop is `static` and a click outside of the offcanvas is performed. The event is also fired when the escape key is pressed and the `keyboard` option is set to `false`. | {{< /bs-table >}} ```js |