Skip to content

Instantly share code, notes, and snippets.

@AMSTKO
Created February 8, 2016 13:43
Show Gist options
  • Save AMSTKO/edafbd203ad7c5ab1757 to your computer and use it in GitHub Desktop.
Save AMSTKO/edafbd203ad7c5ab1757 to your computer and use it in GitHub Desktop.
Accessible modal. Heavily inspired by: https://github.com/gdkraus/accessible-modal-dialog.
(function (global) {
'use strict';
// Helper function to check if a node matches a selector
function matches (node, selector) {
var p = Element.prototype;
var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s) {
return [].indexOf.call(document.querySelectorAll(s), this) !== -1;
};
return f.call(node, selector);
}
// Helper function to check if a node is visible in the viewport
function isVisible (node) {
return !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length);
}
// Helper function to get all focusable children from a node
function getFocusableChildren (node) {
var focusableElements = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[tabindex]', '[contenteditable]'];
return $$(focusableElements.join(','), node).filter(function (child) {
return isVisible(child);
});
}
// Helper function to get first node in context matching selector
function $ (selector, context) {
return (context || document).querySelector(selector);
}
// Helper function to get all nodes in context matching selector as an array
function $$ (selector, context) {
var nodes = (context || document).querySelectorAll(selector);
return nodes.length ? Array.prototype.slice.call(nodes) : [];
}
// Helper function trapping the tab key inside a node
function trapTabKey (node, isShiftKeyPressed) {
var focusableChildren = getFocusableChildren(node);
var focusedItem = document.activeElement;
var focusedItemIndex = focusableChildren.indexOf(focusedItem);
if (isShiftKeyPressed && focusedItemIndex === 0) {
focusableChildren[focusableChildren.length - 1].focus();
event.preventDefault();
} else if (!isShiftKeyPressed && focusedItemIndex === focusableChildren.length - 1) {
focusableChildren[0].focus();
event.preventDefault();
}
}
var focusedElementBeforeModal;
var ESCAPE_KEY = 27;
var TAB_KEY = 9;
var $overlay = $('#modal-overlay');
var $main = $('#main');
/**
* Modal constructor
* @param {Node} node - Modal element
*/
var Modal = function (node) {
this.$modal = node;
this.$closers = $$('[data-hide-modal]', this.$modal);
this.$openers = $$('[data-show-modal="' + this.$modal.id + '"]');
this.shown = false;
this.bindListeners();
};
/**
* Method to display the modal
*/
Modal.prototype.show = function () {
this.shown = true;
$main.setAttribute('aria-hidden', 'true');
this.$modal.setAttribute('aria-hidden', 'false');
$overlay.style.display = 'block';
this.$modal.style.display = 'block';
focusedElementBeforeModal = document.activeElement;
this.setFocusToFirstItem();
};
/**
* Method to hide the modal
*/
Modal.prototype.hide = function () {
this.shown = false;
this.$modal.setAttribute('aria-hidden', 'true');
$main.setAttribute('aria-hidden', 'false');
$overlay.style.display = 'none';
this.$modal.style.display = 'none';
focusedElementBeforeModal.focus();
};
/**
* Method to focus first focusable item in modal
*/
Modal.prototype.setFocusToFirstItem = function () {
var focusableChildren = getFocusableChildren(this.$modal);
if (focusableChildren.length) focusableChildren[0].focus();
};
/**
* Method binding listeners (click, keydown, focus, etc.)
*/
Modal.prototype.bindListeners = function () {
var that = this;
this.$openers.forEach(function ($opener) {
$opener.addEventListener('click', function (event) {
event.stopPropagation();
that.show();
});
});
this.$closers.forEach(function ($closer) {
$closer.addEventListener('click', function (event) {
that.hide();
});
});
document.addEventListener('keydown', function (event) {
if (that.shown === false) return;
if (event.which === ESCAPE_KEY) {
event.preventDefault();
that.hide();
}
if (event.which === TAB_KEY) {
trapTabKey(that.$modal, event.shiftKey);
}
});
document.body.addEventListener('focus', function (event) {
if (this.shown === false) return;
if (event.target && event.target.id === 'main') {
that.setFocusToFirstItem();
}
});
document.addEventListener('click', function (event) {
if (this.shown === false) return;
that.hide();
});
this.$modal.addEventListener('click', function (event) {
event.stopPropagation();
});
};
global.Modal = Modal;
}(window));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment