diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 6f680664ca..3c54310a50 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -34,7 +34,7 @@ }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "43.0 kB" + "maxSize": "43.25 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index 8d1feb13bb..5ad973daaa 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -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) @@ -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 }) } diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 82b54900e4..db60cccf2e 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -18,6 +18,8 @@ import { 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 = { @@ -25,6 +27,7 @@ const Default = { 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 } @@ -33,6 +36,7 @@ const DefaultType = { clickCallback: '(function|null)', isAnimated: 'boolean', isVisible: 'boolean', + keydownCallback: '(function|null)', rootElement: '(element|string)' } @@ -101,7 +105,7 @@ class Backdrop extends Config { return } - EventHandler.off(this._element, EVENT_MOUSEDOWN) + EventHandler.off(this._element, EVENT_KEY) this._element.remove() this._isAppended = false @@ -140,6 +144,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 } diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 3b6c98c100..cca001c345 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -158,6 +158,74 @@ describe('Offcanvas', () => { }) }) + it('should hide if backdrop is static and esc key is pressed on document', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + 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 = '' + + 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 = '' + + 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 = '' diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index 0faaac6a5c..05c5fab6e0 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -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 => {