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;