van11y-accessible-modal-window-aria
Version:
ES2015 accessible modal window system, using ARIA (compatible IE9+ when transpiled)
353 lines (332 loc) • 17.7 kB
JavaScript
/*
* ES2015 accessible modal window system, using ARIA
* Website: https://van11y.net/accessible-modal/
* License MIT: https://github.com/nico3333fr/van11y-accessible-modal-window-aria/blob/master/LICENSE
*/
(function (doc) {
'use strict';
var MODAL_JS_CLASS = 'js-modal';
var MODAL_ID_PREFIX = 'label_modal_';
var MODAL_CLASS_SUFFIX = 'modal';
var MODAL_DATA_BACKGROUND_ATTR = 'data-modal-background-click';
var MODAL_PREFIX_CLASS_ATTR = 'data-modal-prefix-class';
var MODAL_TEXT_ATTR = 'data-modal-text';
var MODAL_CONTENT_ID_ATTR = 'data-modal-content-id';
var MODAL_DESCRIBEDBY_ID_ATTR = 'data-modal-describedby-id';
var MODAL_TITLE_ATTR = 'data-modal-title';
var MODAL_FOCUS_TO_ATTR = 'data-modal-focus-toid';
var MODAL_CLOSE_TEXT_ATTR = 'data-modal-close-text';
var MODAL_CLOSE_TITLE_ATTR = 'data-modal-close-title';
var MODAL_CLOSE_IMG_ATTR = 'data-modal-close-img';
var MODAL_ROLE = 'dialog';
var MODAL_BUTTON_CLASS_SUFFIX = 'modal-close';
var MODAL_BUTTON_JS_ID = 'js-modal-close';
var MODAL_BUTTON_JS_CLASS = 'js-modal-close';
var MODAL_BUTTON_CONTENT_BACK_ID = 'data-content-back-id';
var MODAL_BUTTON_FOCUS_BACK_ID = 'data-focus-back';
var MODAL_WRAPPER_CLASS_SUFFIX = 'modal__wrapper';
var MODAL_CONTENT_CLASS_SUFFIX = 'modal__content';
var MODAL_CONTENT_JS_ID = 'js-modal-content';
var MODAL_CLOSE_IMG_CLASS_SUFFIX = 'modal__closeimg';
var MODAL_CLOSE_TEXT_CLASS_SUFFIX = 'modal-close__text';
var MODAL_TITLE_ID = 'modal-title';
var MODAL_TITLE_CLASS_SUFFIX = 'modal-title';
var FOCUSABLE_ELEMENTS_STRING = "a[href], area[href], input:not([type='hidden']):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";
var WRAPPER_PAGE_JS = 'js-modal-page';
var MODAL_JS_ID = 'js-modal';
var MODAL_OVERLAY_ID = 'js-modal-overlay';
var MODAL_OVERLAY_CLASS_SUFFIX = 'modal-overlay';
var MODAL_OVERLAY_TXT = 'Close modal';
var MODAL_OVERLAY_BG_ENABLED_ATTR = 'data-background-click';
var VISUALLY_HIDDEN_CLASS = 'invisible';
var NO_SCROLL_CLASS = 'no-scroll';
var ATTR_ROLE = 'role';
var ATTR_OPEN = 'open';
var ATTR_LABELLEDBY = 'aria-labelledby';
var ATTR_DESCRIBEDBY = 'aria-describedby';
var ATTR_HIDDEN = 'aria-hidden';
//const ATTR_MODAL = 'aria-modal="true"';
var ATTR_HASPOPUP = 'aria-haspopup';
var ATTR_HASPOPUP_VALUE = 'dialog';
var findById = function findById(id) {
return doc.getElementById(id);
};
var addClass = function addClass(el, className) {
if (el.classList) {
el.classList.add(className); // IE 10+
} else {
el.className += ' ' + className; // IE 8+
}
};
var removeClass = function removeClass(el, className) {
if (el.classList) {
el.classList.remove(className); // IE 10+
} else {
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); // IE 8+
}
};
var hasClass = function hasClass(el, className) {
if (el.classList) {
return el.classList.contains(className); // IE 10+
} else {
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); // IE 8+ ?
}
};
/*const wrapInner = (el, wrapper_el) => { // doesn't work on IE/Edge, f…
while (el.firstChild)
wrapper_el.append(el.firstChild);
el.append(wrapper_el);
}*/
function wrapInner(parent, wrapper) {
if (typeof wrapper === "string") wrapper = document.createElement(wrapper);
parent.appendChild(wrapper);
while (parent.firstChild !== wrapper) wrapper.appendChild(parent.firstChild);
}
function remove(el) {
/* node.remove() is too modern for IE≤11 */
el.parentNode.removeChild(el);
}
/* gets an element el, search if it is child of parent class, returns id of the parent */
var searchParent = function searchParent(el, parentClass) {
var found = false;
var parentElement = el.parentNode;
while (parentElement && found === false) {
if (hasClass(parentElement, parentClass) === true) {
found = true;
} else {
parentElement = parentElement.parentNode;
}
}
if (found === true) {
return parentElement.getAttribute('id');
} else {
return '';
}
};
/**
* Create the template for an overlay
* @param {Object} config
* @return {String}
*/
var createOverlay = function createOverlay(config) {
var id = MODAL_OVERLAY_ID;
var overlayText = config.text || MODAL_OVERLAY_TXT;
var overlayClass = config.prefixClass + MODAL_OVERLAY_CLASS_SUFFIX;
var overlayBackgroundEnabled = config.backgroundEnabled === 'disabled' ? 'disabled' : 'enabled';
return "<span\n id=\"".concat(id, "\"\n class=\"").concat(overlayClass, "\"\n ").concat(MODAL_OVERLAY_BG_ENABLED_ATTR, "=\"").concat(overlayBackgroundEnabled, "\"\n title=\"").concat(overlayText, "\"\n >\n <span class=\"").concat(VISUALLY_HIDDEN_CLASS, "\">").concat(overlayText, "</span>\n </span>");
};
/**
* Create the template for a modal
* @param {Object} config
* @return {String}
*/
var createModal = function createModal(config) {
var id = MODAL_JS_ID;
var modalClassName = config.modalPrefixClass + MODAL_CLASS_SUFFIX;
var modalClassWrapper = config.modalPrefixClass + MODAL_WRAPPER_CLASS_SUFFIX;
var buttonCloseClassName = config.modalPrefixClass + MODAL_BUTTON_CLASS_SUFFIX;
var buttonCloseInner = config.modalCloseImgPath ? "<img src=\"".concat(config.modalCloseImgPath, "\" alt=\"").concat(config.modalCloseText, "\" class=\"").concat(config.modalPrefixClass).concat(MODAL_CLOSE_IMG_CLASS_SUFFIX, "\" />") : "<span class=\"".concat(config.modalPrefixClass).concat(MODAL_CLOSE_TEXT_CLASS_SUFFIX, "\">\n ").concat(config.modalCloseText, "\n </span>");
var contentClassName = config.modalPrefixClass + MODAL_CONTENT_CLASS_SUFFIX;
var titleClassName = config.modalPrefixClass + MODAL_TITLE_CLASS_SUFFIX;
var title = config.modalTitle !== '' ? "<h1 id=\"".concat(MODAL_TITLE_ID, "\" class=\"").concat(titleClassName, "\">\n ").concat(config.modalTitle, "\n </h1>") : '';
var button_close = "<button type=\"button\" class=\"".concat(MODAL_BUTTON_JS_CLASS, " ").concat(buttonCloseClassName, "\" id=\"").concat(MODAL_BUTTON_JS_ID, "\" title=\"").concat(config.modalCloseTitle, "\" ").concat(MODAL_BUTTON_CONTENT_BACK_ID, "=\"").concat(config.modalContentId, "\" ").concat(MODAL_BUTTON_FOCUS_BACK_ID, "=\"").concat(config.modalFocusBackId, "\">\n ").concat(buttonCloseInner, "\n </button>");
var content = config.modalText;
var describedById = config.modalDescribedById !== '' ? "".concat(ATTR_DESCRIBEDBY, "=\"").concat(config.modalDescribedById, "\"") : '';
// If there is no content but an id we try to fetch content id
if (content === '' && config.modalContentId) {
var contentFromId = findById(config.modalContentId);
if (contentFromId) {
content = "<div id=\"".concat(MODAL_CONTENT_JS_ID, "\">\n ").concat(contentFromId.innerHTML, "\n </div");
// we remove content from its source to avoid id duplicates, etc.
contentFromId.innerHTML = '';
}
}
return "<dialog id=\"".concat(id, "\" class=\"").concat(modalClassName, "\" ").concat(ATTR_ROLE, "=\"").concat(MODAL_ROLE, "\" ").concat(describedById, " ").concat(ATTR_OPEN, " ").concat(ATTR_LABELLEDBY, "=\"").concat(MODAL_TITLE_ID, "\">\n <div role=\"document\" class=\"").concat(modalClassWrapper, "\">\n ").concat(button_close, "\n <div class=\"").concat(contentClassName, "\">\n ").concat(title, "\n ").concat(content, "\n </div>\n </div>\n </dialog>");
};
var closeModal = function closeModal(config) {
remove(config.modal);
remove(config.overlay);
if (config.contentBackId !== '') {
var contentBack = findById(config.contentBackId);
if (contentBack) {
contentBack.innerHTML = config.modalContent;
}
}
if (config.modalFocusBackId) {
var contentFocus = findById(config.modalFocusBackId);
if (contentFocus) {
contentFocus.focus();
}
}
};
/** Find all modals inside a container
* @param {Node} node Default document
* @return {Array}
*/
var $listModals = function $listModals() {
var node = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : doc;
return [].slice.call(node.querySelectorAll('.' + MODAL_JS_CLASS));
};
/**
* Build modals for a container
* @param {Node} node
*/
var attach = function attach(node) {
var addListeners = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
$listModals(node).forEach(function (modal_node) {
var iLisible = Math.random().toString(32).slice(2, 12);
var wrapperBody = findById(WRAPPER_PAGE_JS);
var body = doc.querySelector('body');
if (!modal_node.getAttribute('id')) {
modal_node.setAttribute('id', MODAL_ID_PREFIX + iLisible);
}
modal_node.setAttribute(ATTR_HASPOPUP, ATTR_HASPOPUP_VALUE);
if (wrapperBody === null || wrapperBody.length === 0) {
var wrapper = doc.createElement('DIV');
wrapper.setAttribute('id', WRAPPER_PAGE_JS);
wrapInner(body, wrapper);
}
});
if (addListeners) {
/* listeners */
['click', 'keydown'].forEach(function (eventName) {
doc.body.addEventListener(eventName, function (e) {
// click on link modal
var parentModalLauncher = searchParent(e.target, MODAL_JS_CLASS);
if ((hasClass(e.target, MODAL_JS_CLASS) === true || parentModalLauncher !== '') && eventName === 'click') {
var body = doc.querySelector('body');
var modalLauncher = parentModalLauncher !== '' ? findById(parentModalLauncher) : e.target;
var modalPrefixClass = modalLauncher.hasAttribute(MODAL_PREFIX_CLASS_ATTR) === true ? modalLauncher.getAttribute(MODAL_PREFIX_CLASS_ATTR) + '-' : '';
var modalText = modalLauncher.hasAttribute(MODAL_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_TEXT_ATTR) : '';
var modalContentId = modalLauncher.hasAttribute(MODAL_CONTENT_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_CONTENT_ID_ATTR) : '';
var modalDescribedById = modalLauncher.hasAttribute(MODAL_DESCRIBEDBY_ID_ATTR) === true ? modalLauncher.getAttribute(MODAL_DESCRIBEDBY_ID_ATTR) : '';
var modalTitle = modalLauncher.hasAttribute(MODAL_TITLE_ATTR) === true ? modalLauncher.getAttribute(MODAL_TITLE_ATTR) : '';
var modalCloseText = modalLauncher.hasAttribute(MODAL_CLOSE_TEXT_ATTR) === true ? modalLauncher.getAttribute(MODAL_CLOSE_TEXT_ATTR) : MODAL_OVERLAY_TXT;
var modalCloseTitle = modalLauncher.hasAttribute(MODAL_CLOSE_TITLE_ATTR) === true ? modalLauncher.getAttribute(MODAL_CLOSE_TITLE_ATTR) : modalCloseText;
var modalCloseImgPath = modalLauncher.hasAttribute(MODAL_CLOSE_IMG_ATTR) === true ? modalLauncher.getAttribute(MODAL_CLOSE_IMG_ATTR) : '';
var backgroundEnabled = modalLauncher.hasAttribute(MODAL_DATA_BACKGROUND_ATTR) === true ? modalLauncher.getAttribute(MODAL_DATA_BACKGROUND_ATTR) : '';
var modalGiveFocusToId = modalLauncher.hasAttribute(MODAL_FOCUS_TO_ATTR) === true ? modalLauncher.getAttribute(MODAL_FOCUS_TO_ATTR) : '';
var wrapperBody = findById(WRAPPER_PAGE_JS);
// insert overlay
body.insertAdjacentHTML('beforeEnd', createOverlay({
text: modalCloseTitle,
backgroundEnabled: backgroundEnabled,
prefixClass: modalPrefixClass
}));
// insert modal
body.insertAdjacentHTML('beforeEnd', createModal({
modalText: modalText,
modalPrefixClass: modalPrefixClass,
backgroundEnabled: modalContentId,
modalTitle: modalTitle,
modalCloseText: modalCloseText,
modalCloseTitle: modalCloseTitle,
modalCloseImgPath: modalCloseImgPath,
modalContentId: modalContentId,
modalDescribedById: modalDescribedById,
modalFocusBackId: modalLauncher.getAttribute('id')
}));
// hide page
wrapperBody.setAttribute(ATTR_HIDDEN, 'true');
// add class noscroll to body
addClass(body, NO_SCROLL_CLASS);
// give focus to close button or specified element
var closeButton = findById(MODAL_BUTTON_JS_ID);
if (modalGiveFocusToId !== '') {
var focusTo = findById(modalGiveFocusToId);
if (focusTo) {
focusTo.focus();
} else {
closeButton.focus();
}
} else {
closeButton.focus();
}
e.preventDefault();
}
// click on close button or on overlay not blocked
var parentButton = searchParent(e.target, MODAL_BUTTON_JS_CLASS);
if ((e.target.getAttribute('id') === MODAL_BUTTON_JS_ID || parentButton !== '' || e.target.getAttribute('id') === MODAL_OVERLAY_ID || hasClass(e.target, MODAL_BUTTON_JS_CLASS) === true) && eventName === 'click') {
var _body = doc.querySelector('body');
var _wrapperBody = findById(WRAPPER_PAGE_JS);
var modal = findById(MODAL_JS_ID);
var modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID).innerHTML : '';
var overlay = findById(MODAL_OVERLAY_ID);
var modalButtonClose = findById(MODAL_BUTTON_JS_ID);
var modalFocusBackId = modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID);
var contentBackId = modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID);
var _backgroundEnabled = overlay.getAttribute(MODAL_OVERLAY_BG_ENABLED_ATTR);
if (!(e.target.getAttribute('id') === MODAL_OVERLAY_ID && _backgroundEnabled === 'disabled')) {
closeModal({
modal: modal,
modalContent: modalContent,
overlay: overlay,
modalFocusBackId: modalFocusBackId,
contentBackId: contentBackId,
backgroundEnabled: _backgroundEnabled,
fromId: e.target.getAttribute('id')
});
// show back page
_wrapperBody.removeAttribute(ATTR_HIDDEN);
// remove class noscroll to body
removeClass(_body, NO_SCROLL_CLASS);
}
}
// strike a key when modal opened
if (findById(MODAL_JS_ID) && eventName === 'keydown') {
var _body2 = doc.querySelector('body');
var _wrapperBody2 = findById(WRAPPER_PAGE_JS);
var _modal = findById(MODAL_JS_ID);
var _modalContent = findById(MODAL_CONTENT_JS_ID) ? findById(MODAL_CONTENT_JS_ID).innerHTML : '';
var _overlay = findById(MODAL_OVERLAY_ID);
var _modalButtonClose = findById(MODAL_BUTTON_JS_ID);
var _modalFocusBackId = _modalButtonClose.getAttribute(MODAL_BUTTON_FOCUS_BACK_ID);
var _contentBackId = _modalButtonClose.getAttribute(MODAL_BUTTON_CONTENT_BACK_ID);
var $listFocusables = [].slice.call(_modal.querySelectorAll(FOCUSABLE_ELEMENTS_STRING));
// esc
if (e.keyCode === 27) {
closeModal({
modal: _modal,
modalContent: _modalContent,
overlay: _overlay,
modalFocusBackId: _modalFocusBackId,
contentBackId: _contentBackId
});
// show back page
_wrapperBody2.removeAttribute(ATTR_HIDDEN);
// remove class noscroll to body
removeClass(_body2, NO_SCROLL_CLASS);
}
// tab or Maj Tab in modal => capture focus
if (e.keyCode === 9 && $listFocusables.indexOf(e.target) >= 0) {
// maj-tab on first element focusable => focus on last
if (e.shiftKey) {
if (e.target === $listFocusables[0]) {
$listFocusables[$listFocusables.length - 1].focus();
e.preventDefault();
}
} else {
// tab on last element focusable => focus on first
if (e.target === $listFocusables[$listFocusables.length - 1]) {
$listFocusables[0].focus();
e.preventDefault();
}
}
}
// tab outside modal => put it in focus
if (e.keyCode === 9 && $listFocusables.indexOf(e.target) === -1) {
e.preventDefault();
$listFocusables[0].focus();
}
}
}, true);
});
}
};
var _onLoad = function onLoad() {
attach();
document.removeEventListener('DOMContentLoaded', _onLoad);
};
document.addEventListener('DOMContentLoaded', _onLoad);
window.van11yAccessibleModalWindowAria = attach;
})(document);