Last active
August 24, 2018 19:56
-
-
Save baerkins/4dee92b6d7b0afc7fa346fb2aedcfa88 to your computer and use it in GitHub Desktop.
Accessible Modal with JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body.is-locked { | |
height: 100%; | |
overflow: hidden; | |
} | |
.modal { | |
display: none; | |
} | |
.modal--is-open { | |
display: block; | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: #fff; | |
z-index: 2; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Example Header --> | |
<header role="banner"> | |
<div class="container"> | |
<a href="/" class="logo">Some Website Logo</a> | |
<nav class="primary-nav" role="navigation"> | |
<span id="primary-nav-title" class="sr-only"> | |
<ul aria-labelledby="primary-nav-title"> | |
<li><a href="/about" role="menuitem">About</a></li> | |
</ul> | |
</nav> | |
<!-- | |
Modal Toggle Button | |
Match `data-modalid` to the ID of the modal element to toggle | |
--> | |
<button class="more-nav--toggle _modal-toggle" data-modalid="more-menu">Open Nav</button> | |
</div> | |
</header> | |
<!-- /Example Header --> | |
<!-- | |
Modal | |
Use div (section can behave strangely for some reason with screen readers) | |
Give the modal it's own H1 - modals should be treated as though they are their own page | |
Close button should be the first focusable element | |
--> | |
<div id="more-modal" class="more-menu modal" role="dialog" aria-modal="false" tabindex="-1"> | |
<div class="container"> | |
<h1 class="sr-only">Modal Title</h1> | |
<button class="more-nav--close _modal-toggle" data-modalid="more-modal">Close Nav</button> | |
<!-- Add modal content stuff --> | |
</div> | |
</div> | |
<!-- /Modal --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Configurable Classes | |
// | |
const modalOpenClass = 'modal--is-open'; | |
const modalReturnFocus = 'modal--will-focus'; | |
// | |
// Placeholders for modal focusable elements | |
// | |
let focusableEls = null, | |
firstFocusableEl = null, | |
lastFocusableEl = null; | |
window.siteHasOpenModal = false; | |
/** | |
* Open modal | |
* Add modal--open class to modal, set aria-modal to true, set focus on modal | |
* add modal--return-focus class to trigger | |
* | |
* @param {modalID} modalID ID of target modal | |
* @param {object} trigger The javascript object that triggered the event | |
* | |
*/ | |
function openModal(modalID, trigger) { | |
if ( window.siteHasOpenModal ) { | |
return false; | |
} | |
window.siteHasOpenModal = true; | |
const modal = document.getElementById(modalID); | |
modal.classList.add(modalOpenClass); | |
modal.setAttribute('aria-modal', true); | |
document.body.classList.add('is-locked'); | |
trigger.classList.add(modalReturnFocus); | |
// Determine focusable elements | |
focusableEls = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); | |
firstFocusableEl = focusableEls[0]; | |
lastFocusableEl = focusableEls[focusableEls.length-1]; | |
firstFocusableEl.focus(); | |
}; | |
/** | |
* Close modal | |
* Remove modal--open class to modal, set aria-modal to false | |
* remove modal--return-focus class to first element, focus it | |
* | |
* @param {string} modalID ID of target modal | |
* | |
*/ | |
function closeModal(modalID) { | |
const modal = document.getElementById(modalID); | |
modal.classList.remove(modalOpenClass); | |
modal.setAttribute('aria-modal', false); | |
const returnFocus = document.getElementsByClassName(modalReturnFocus)[0]; | |
returnFocus.focus(); | |
returnFocus.classList.remove(modalReturnFocus); | |
window.siteHasOpenModal = false; | |
document.body.classList.remove('is-locked'); | |
}; | |
/** | |
* Handle Keydown events when a modal is open | |
* | |
* @param {event} e keydown event | |
* | |
*/ | |
function keydownModal(e) { | |
// Return if a modal is not open | |
if ( !window.siteHasOpenModal ) { | |
return false; | |
} | |
// Grab the first modal | |
const modal = document.getElementsByClassName('modal ' + modalOpenClass)[0]; | |
// Allow Escape to close window modal | |
if (e.keyCode === 27 ) { | |
closeModal( modal.getAttribute('id') ); | |
// Tab Trap on open modals | |
} else if (e.key === 'Tab' || e.keyCode === 9) { | |
// shift + tab | |
if ( e.shiftKey ) { | |
if (document.activeElement === firstFocusableEl) { | |
lastFocusableEl.focus(); | |
e.preventDefault(); | |
} | |
// tab | |
} else { | |
if (document.activeElement === lastFocusableEl) { | |
firstFocusableEl.focus(); | |
e.preventDefault(); | |
} | |
} | |
} | |
} | |
/** | |
* modalInit | |
* Generalized modal mechanics providing accessible features | |
* | |
* Example Markup: | |
* <button class="_modal-toggle" data-modalid="some-modal">Toggle</button> | |
* <div id="some-modal" class="modal" role="dialog" aria-modal="false"><!-- Stuff --></div> | |
* | |
* <!-- Open State --> | |
* <button class="_modal-toggle modal--return-focus" data-modalid="some-modal">Toggle</button> | |
* <div id="some-modal" class="modal modal-open" role="dialog" aria-modal="true"><!-- Stuff --></div> | |
* | |
*/ | |
export const modalInit = () => { | |
const modalButtons = document.getElementsByClassName('_modal-toggle'); | |
[].forEach.call(modalButtons, (btn) => { | |
btn.addEventListener('click', () => { | |
const modalID = btn.dataset.modalid; | |
const modal = document.getElementById(modalID); | |
if (modal.classList.contains(modalOpenClass)) { | |
closeModal(modalID); | |
} else { | |
openModal(modalID, btn); | |
} | |
}); | |
}); | |
// Handle keydown events for open modals | |
window.addEventListener('keydown', keydownModal, true); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment