]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Add static backdrop to offcanvas (#35832)
authorJann Westermann <github@jann.bayern>
Wed, 2 Mar 2022 00:20:37 +0000 (01:20 +0100)
committerGitHub <noreply@github.com>
Wed, 2 Mar 2022 00:20:37 +0000 (02:20 +0200)
* Add static backdrop option,  to offcanvas
* Trigger prevented event on esc with keyboard=false
* Change offcanvas doc , moving backdrop examples to examples section

js/src/offcanvas.js
js/tests/unit/offcanvas.spec.js
site/content/docs/5.1/components/offcanvas.md

index 2735a9c2aeacf01269cc5eb1c291936e09f274f3..b5afc0c87b49f3d1773a694092363754180d2c0e 100644 (file)
@@ -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()
     })
   }
 
index 37f3397c791fdfb579c155105e2c385185ceddb4..a98a8c13e3d091e037a76e457ac969c4b46d553a 100644 (file)
@@ -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()
+      })
     })
   })
 
index 9f30f74e95bd4d35929421b387a814b8022e63a6..7a8cbc68a782edc3f6ecaf608bc2d87e708be368 100644 (file)
@@ -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