Skip to content

Add keydown listener to backdrop to enable static offcanvas to close on Esc #37968

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 8 additions & 1 deletion js/src/offcanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ class Offcanvas extends BaseComponent {
this.hide()
}

const keydownCallback = event => {
if (event.key === ESCAPE_KEY) {
this.hide()
}
}

// 'static' option will be translated to true, and booleans will keep their value
const isVisible = Boolean(this._config.backdrop)

Expand All @@ -182,7 +188,8 @@ class Offcanvas extends BaseComponent {
isVisible,
isAnimated: true,
rootElement: this._element.parentNode,
clickCallback: isVisible ? clickCallback : null
clickCallback: isVisible ? clickCallback : null,
keydownCallback: this._config.keyboard ? keydownCallback : null
})
}

Expand Down
12 changes: 11 additions & 1 deletion js/src/util/backdrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import Config from './config.js'
const NAME = 'backdrop'
const CLASS_NAME_FADE = 'fade'
const CLASS_NAME_SHOW = 'show'
const EVENT_KEY = `.bs.${NAME}`
const EVENT_KEYDOWN = `keydown.bs.${NAME}`
const EVENT_MOUSEDOWN = `mousedown.bs.${NAME}`

const Default = {
className: 'modal-backdrop',
clickCallback: null,
isAnimated: false,
isVisible: true, // if false, we use the backdrop helper without adding any element to the dom
keydownCallback: null,
rootElement: 'body' // give the choice to place backdrop under different elements
}

Expand All @@ -31,6 +34,7 @@ const DefaultType = {
clickCallback: '(function|null)',
isAnimated: 'boolean',
isVisible: 'boolean',
keydownCallback: '(function|null)',
rootElement: '(element|string)'
}

Expand Down Expand Up @@ -99,7 +103,7 @@ class Backdrop extends Config {
return
}

EventHandler.off(this._element, EVENT_MOUSEDOWN)
EventHandler.off(this._element, EVENT_KEY)

this._element.remove()
this._isAppended = false
Expand Down Expand Up @@ -138,6 +142,12 @@ class Backdrop extends Config {
execute(this._config.clickCallback)
})

if (this._config.keydownCallback) {
EventHandler.on(document, EVENT_KEYDOWN, event => {
execute(this._config.keydownCallback, [event])
})
}

this._isAppended = true
}

Expand Down
68 changes: 68 additions & 0 deletions js/tests/unit/offcanvas.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,74 @@ describe('Offcanvas', () => {
})
})

it('should hide if backdrop is static and esc key is pressed on document', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' })

const keydownEscEvent = createEvent('keydown')
keydownEscEvent.key = 'Escape'

const spyHide = spyOn(offCanvas, 'hide')

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
document.dispatchEvent(keydownEscEvent)
expect(spyHide).toHaveBeenCalled()
resolve()
})

offCanvas.show()
})
})

it('should not hide if backdrop is static and esc key is pressed on document but keyboard = false', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static', keyboard: false })

const keydownEscEvent = createEvent('keydown')
keydownEscEvent.key = 'Escape'

const spyHide = spyOn(offCanvas, 'hide')

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
expect(offCanvas._config.keyboard).toBeFalse()

document.dispatchEvent(keydownEscEvent)
expect(spyHide).not.toHaveBeenCalled()
resolve()
})

offCanvas.show()
})
})

it('should not hide if backdrop is static but key other than esc is pressed on document', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'

const offCanvasEl = fixtureEl.querySelector('div')
const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' })

const keydownEvent = createEvent('keydown')
keydownEvent.key = 'Tab'

const spyHide = spyOn(offCanvas, 'hide')

offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
document.dispatchEvent(keydownEvent)
expect(spyHide).not.toHaveBeenCalled()
resolve()
})

offCanvas.show()
})
})

it('should call `hide` on resize, if element\'s position is not fixed any more', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="offcanvas-lg"></div>'
Expand Down
26 changes: 26 additions & 0 deletions js/tests/unit/util/backdrop.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,32 @@ describe('Backdrop', () => {
})
})

describe('keydown callback', () => {
it('should execute keydown callback on keydown event', () => {
return new Promise(resolve => {
const spy = jasmine.createSpy('spy')

const instance = new Backdrop({
isVisible: true,
isAnimated: false,
keydownCallback: () => spy()
})
const endTest = () => {
setTimeout(() => {
expect(spy).toHaveBeenCalled()
resolve()
}, 10)
}

instance.show(() => {
const keydownEvent = new Event('keydown', { bubbles: true, cancelable: true })
document.querySelector(CLASS_BACKDROP).dispatchEvent(keydownEvent)
endTest()
})
})
})
})

describe('animation callbacks', () => {
it('should show and hide backdrop after counting transition duration if it is animated', () => {
return new Promise(resolve => {
Expand Down