Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow offcanvas to be initialized in open state #33382

Merged
merged 8 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions js/src/offcanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const NAME = 'offcanvas'
const DATA_KEY = 'bs.offcanvas'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const ESCAPE_KEY = 'Escape'

const Default = {
Expand All @@ -48,7 +49,8 @@ const DefaultType = {
const CLASS_NAME_BACKDROP_BODY = 'offcanvas-backdrop'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_TOGGLING = 'offcanvas-toggling'
const ACTIVE_SELECTOR = `.offcanvas.show, .${CLASS_NAME_TOGGLING}`
const OPEN_SELECTOR = '.offcanvas.show'
const ACTIVE_SELECTOR = `${OPEN_SELECTOR}, .${CLASS_NAME_TOGGLING}`

const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
Expand All @@ -72,7 +74,7 @@ class Offcanvas extends BaseComponent {
super(element)

this._config = this._getConfig(config)
this._isShown = element.classList.contains(CLASS_NAME_SHOW)
this._isShown = false
this._addEventListeners()
}

Expand Down Expand Up @@ -262,6 +264,10 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
data.toggle(this)
})

EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
SelectorEngine.find(OPEN_SELECTOR).forEach(el => (Data.get(el, DATA_KEY) || new Offcanvas(el)).show())
})

/**
* ------------------------------------------------------------------------
* jQuery
Expand Down
138 changes: 131 additions & 7 deletions js/tests/unit/offcanvas.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,44 @@ describe('Offcanvas', () => {
expect(offCanvas._config.scroll).toEqual(false)
})
})
describe('options', () => {
it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: true })
const initialOverFlow = document.body.style.overflow

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(document.body.style.overflow).toEqual(initialOverFlow)

offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(document.body.style.overflow).toEqual(initialOverFlow)
done()
})
offCanvas.show()
})

it('if scroll is disabled, should not allow body to scroll while offcanvas is open', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl, { scroll: false })

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(document.body.style.overflow).toEqual('hidden')

offCanvas.hide()
})
offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(document.body.style.overflow).toEqual('auto')
done()
})
offCanvas.show()
})
})

describe('toggle', () => {
it('should call show method if show class is not present', () => {
Expand All @@ -161,10 +199,12 @@ describe('Offcanvas', () => {
})

it('should call hide method if show class is present', () => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('.show')
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()
expect(offCanvasEl.classList.contains('show')).toBe(true)

spyOn(offCanvas, 'hide')

Expand All @@ -178,11 +218,13 @@ describe('Offcanvas', () => {
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'

spyOn(EventHandler, 'trigger')

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)

offCanvas.show()
expect(offCanvasEl.classList.contains('show')).toBe(true)

spyOn(EventHandler, 'trigger').and.callThrough()
offCanvas.show()

expect(EventHandler.trigger).not.toHaveBeenCalled()
Expand Down Expand Up @@ -226,13 +268,30 @@ describe('Offcanvas', () => {

offCanvas.show()
})

it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
spyOn(Offcanvas.prototype, 'show').and.callThrough()

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
done()
})

window.dispatchEvent(createEvent('load'))

const instance = Offcanvas.getInstance(offCanvasEl)
expect(instance).not.toBeNull()
expect(Offcanvas.prototype.show).toHaveBeenCalled()
})
})

describe('hide', () => {
it('should do nothing if already shown', () => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

spyOn(EventHandler, 'trigger')
spyOn(EventHandler, 'trigger').and.callThrough()

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
Expand All @@ -243,10 +302,11 @@ describe('Offcanvas', () => {
})

it('should hide a shown element', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()

offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
expect(offCanvasEl.classList.contains('show')).toEqual(false)
Expand All @@ -257,10 +317,11 @@ describe('Offcanvas', () => {
})

it('should not fire hidden when hide is prevented', done => {
fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl)
offCanvas.show()

const expectEnd = () => {
setTimeout(() => {
Expand Down Expand Up @@ -315,6 +376,52 @@ describe('Offcanvas', () => {

expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled()
})

it('should not call toggle if another offcanvas is open', done => {
fixtureEl.innerHTML = [
'<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2" ></button>',
'<div id="offcanvas1" class="offcanvas"></div>',
'<div id="offcanvas2" class="offcanvas"></div>'
].join('')

const trigger2 = fixtureEl.querySelector('#btn2')
const offcanvasEl1 = document.querySelector('#offcanvas1')
const offcanvasEl2 = document.querySelector('#offcanvas2')
const offcanvas1 = new Offcanvas(offcanvasEl1)

offcanvasEl1.addEventListener('shown.bs.offcanvas', () => {
trigger2.click()
})
offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => {
expect(Offcanvas.getInstance(offcanvasEl2)).toEqual(null)
done()
})
offcanvas1.show()
})

it('should focus on trigger element after closing offcanvas', done => {
fixtureEl.innerHTML = [
'<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas" ></button>',
'<div id="offcanvas" class="offcanvas"></div>'
].join('')

const trigger = fixtureEl.querySelector('#btn')
const offcanvasEl = fixtureEl.querySelector('#offcanvas')
const offcanvas = new Offcanvas(offcanvasEl)
spyOn(trigger, 'focus')

offcanvasEl.addEventListener('shown.bs.offcanvas', () => {
offcanvas.hide()
})
offcanvasEl.addEventListener('hidden.bs.offcanvas', () => {
setTimeout(() => {
expect(trigger.focus).toHaveBeenCalled()
done()
}, 5)
})

trigger.click()
})
})

describe('jQueryInterface', () => {
Expand Down Expand Up @@ -432,6 +539,23 @@ describe('Offcanvas', () => {
jQueryMock.fn.offcanvas.call(jQueryMock, 'show')
expect(Offcanvas.prototype.show).toHaveBeenCalled()
})

it('should create a offcanvas with given config', () => {
fixtureEl.innerHTML = '<div></div>'

const div = fixtureEl.querySelector('div')

jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
jQueryMock.elements = [div]

jQueryMock.fn.offcanvas.call(jQueryMock, { scroll: true })
spyOn(Offcanvas.prototype, 'constructor')
expect(Offcanvas.prototype.constructor).not.toHaveBeenCalledWith(div, { scroll: true })

const offcanvas = Offcanvas.getInstance(div)
expect(offcanvas).toBeDefined()
expect(offcanvas._config.scroll).toBe(true)
})
})

describe('getInstance', () => {
Expand Down
3 changes: 0 additions & 3 deletions site/assets/scss/_component-examples.scss
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,7 @@

.offcanvas {
position: static;
display: block;
height: 200px;
visibility: visible;
transform: translateX(0);
}
}

Expand Down
6 changes: 3 additions & 3 deletions site/content/docs/5.0/components/offcanvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Offcanvas is a sidebar component that can be toggled via JavaScript to appear fr

### Offcanvas components

Below is a _static_ offcanvas example (meaning its `position`, `display`, and `visibility` have been overridden). Offcanvas includes support for a header with a close button and an optional body class for some initial `padding`. We suggest that you include offcanvas headers with dismiss actions whenever possible, or provide an explicit dismiss action.
Below is an offcanvas example that is shown by default (via `.show` on `.offcanvas`). Offcanvas includes support for a header with a close button and an optional body class for some initial `padding`. We suggest that you include offcanvas headers with dismiss actions whenever possible, or provide an explicit dismiss action.

{{< example class="bd-example-offcanvas p-0 bg-light" >}}
<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvas" aria-labelledby="offcanvasLabel">
{{< example class="bd-example-offcanvas p-0 bg-light overflow-hidden" >}}
<div class="offcanvas offcanvas-start show" tabindex="-1" id="offcanvas" aria-labelledby="offcanvasLabel" data-bs-backdrop="false" data-bs-scroll="true">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasLabel">Offcanvas</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
Expand Down