Skip to content

Commit

Permalink
feat(modal): introduce wrap-focus behavior for a11y (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
asudoh authored and iangfleming committed Jun 9, 2017
1 parent 2612b2d commit 1a2d80f
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 0 deletions.
12 changes: 12 additions & 0 deletions demo/views/demo-all.dust
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
body {
padding-top: 72px;
}

.offleft {
position: absolute;
top: 0;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
}
</style>
</head>

Expand All @@ -33,6 +42,9 @@
{html|s}
</div>

<!-- Pseudo element to demonstrate focus-wrap behavior (focus trap) -->
<input type="text" class="offleft">

<!-- Scripts -->
<!-- <script src="../../dist/bluemix-components.min.js"></script> -->
<script src="/demo.js"></script>
Expand Down
25 changes: 25 additions & 0 deletions src/components/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
/**
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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();
}

Expand Down
43 changes: 43 additions & 0 deletions tests/spec/modal_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 1a2d80f

Please sign in to comment.