diff --git a/demo/views/demo-all.dust b/demo/views/demo-all.dust index a0eff8ddb568..1254be981c20 100644 --- a/demo/views/demo-all.dust +++ b/demo/views/demo-all.dust @@ -10,6 +10,15 @@ body { padding-top: 72px; } + + .offleft { + position: absolute; + top: 0; + left: -10000px; + width: 1px; + height: 1px; + overflow: hidden; + } @@ -33,6 +42,9 @@ {html|s} + + + diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index 7f3b13aa773d..95f32c449d00 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -3,6 +3,7 @@ import createComponent from '../../globals/js/mixins/create-component'; import initComponentByLauncher from '../../globals/js/mixins/init-component-by-launcher'; import eventedShowHideState from '../../globals/js/mixins/evented-show-hide-state'; import eventMatches from '../../globals/js/misc/event-matches'; +import on from '../../globals/js/misc/on'; class Modal extends mixin(createComponent, initComponentByLauncher, eventedShowHideState) { /** @@ -67,6 +68,16 @@ class Modal extends mixin(createComponent, initComponentByLauncher, eventedShowH callback(); }; + if (this._handleFocusinListener) { + this._handleFocusinListener = this._handleFocusinListener.release(); + } + + if (state === 'shown') { + const hasFocusin = 'onfocusin' in this.element.ownerDocument.defaultView; + const focusinEventName = hasFocusin ? 'focusin' : 'focus'; + this._handleFocusinListener = on(this.element.ownerDocument, focusinEventName, this._handleFocusin, !hasFocusin); + } + if (state === 'hidden') { this.element.classList.toggle(this.options.classVisible, false); } else if (state === 'shown') { @@ -100,11 +111,25 @@ class Modal extends mixin(createComponent, initComponentByLauncher, eventedShowH this.element.ownerDocument.body.addEventListener('keydown', this.keydownHandler); } + /** + * Handles `focusin` (or `focus` depending on browser support of `focusin`) event to do wrap-focus behavior. + * @param {Event} evt The event. + * @private + */ + _handleFocusin = (evt) => { + if (this.element.classList.contains(this.options.classVisible) && !this.element.contains(evt.target)) { + this.element.focus(); + } + }; + release() { if (this.keydownHandler) { this.element.ownerDocument.body.removeEventListener('keydown', this.keydownHandler); this.keydownHandler = null; } + if (this._handleFocusinListener) { + this._handleFocusinListener = this._handleFocusinListener.release(); + } super.release(); } diff --git a/tests/spec/modal_spec.js b/tests/spec/modal_spec.js index 065884f1882a..e0ea1432a50a 100644 --- a/tests/spec/modal_spec.js +++ b/tests/spec/modal_spec.js @@ -280,6 +280,49 @@ describe('Test modal', function () { }); }); + describe('Wrapping focus while modal is open', function () { + let container; + let modal; + let element; + let input; + + if (!document.hasFocus()) { + return; + } + + before(function () { + container = document.createElement('div'); + container.innerHTML = ModalHtml; + // Reset primary focus eleemnt for testing + delete container.querySelector('[data-modal-primary-focus]').dataset.modalPrimaryFocus; + document.body.appendChild(container); + element = container.querySelector('[data-modal]'); + input = document.createElement('input'); + input.type = 'text'; + document.body.appendChild(input); + modal = new Modal(element); + }); + + it('Should bring back focus when modal loses focus', async function () { + modal.show(); + modal.element.dispatchEvent(new CustomEvent('transitionend', { bubbles: true })); + input.focus(); + expect(element.contains(document.activeElement)).to.be.true; + }); + + after(function () { + if (modal) { + modal = modal.release(); + } + if (document.body.contains(input)) { + input.parentNode.removeChild(input); + } + if (document.body.contains(element)) { + element.parentNode.removeChild(element); + } + }); + }); + describe('Init Component by Launch functionality', function () { let container; let modal;