Skip to content

Commit

Permalink
Add static backdrop to offcanvas (#35832)
Browse files Browse the repository at this point in the history
* Add static backdrop option,  to offcanvas
* Trigger prevented event on esc with keyboard=false
* Change offcanvas doc , moving backdrop examples to examples section
  • Loading branch information
chefarbeiter committed Mar 2, 2022
1 parent d788d2e commit 8d7358f
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 53 deletions.
30 changes: 25 additions & 5 deletions js/src/offcanvas.js
Expand Up @@ -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}`
Expand All @@ -52,7 +53,7 @@ const Default = {
}

const DefaultType = {
backdrop: 'boolean',
backdrop: '(boolean|string)',
keyboard: 'boolean',
scroll: 'boolean'
}
Expand Down Expand Up @@ -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
})
}

Expand All @@ -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()
})
}

Expand Down
71 changes: 61 additions & 10 deletions js/tests/unit/offcanvas.spec.js
Expand Up @@ -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>'

Expand All @@ -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()
})
})
})

Expand Down
97 changes: 59 additions & 38 deletions site/content/docs/5.1/components/offcanvas.md
Expand Up @@ -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 >}}
Expand Down Expand Up @@ -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 >}}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8d7358f

Please sign in to comment.