Last active
November 10, 2020 14:55
-
-
Save jenswittmann/4645794452bc2ddaf14129e493f533fa to your computer and use it in GitHub Desktop.
HTML A11Y ARIA helpers
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
// hide tab focus if not needed | |
* { | |
&:focus, | |
&:-moz-focusring { | |
outline: 0 !important; | |
} | |
} | |
.focus-outline { | |
* { | |
&:focus { | |
outline: 1px solid; | |
} | |
} | |
} |
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
<!-- overlay nav --> | |
<button id="navopener" aria-haspopup="true" aria-expanded="false" aria-controls="nav" class="bg-transparent bn" data-modal-open> | |
<span class="clip">Menü öffnen</span> | |
<span class="icon icon2"> | |
<svg focusable="false"><use href="#icon-nav"></use></svg> | |
</span> | |
</button> | |
<nav id="nav" aria-modal="true" aria-labeledby="navopener" class="fixed top-0 right-0 bottom-0 w-75 w-50-ns white bg-black" hidden> | |
<div class="pa4"> | |
<ul role="menu"> | |
<li role="menuitem"><a href="/">Projekte</a></li> | |
<li role="menuitem"><a href="/">Profil</a></li> | |
<li role="menuitem"><a href="/">Team</a></li> | |
<li role="menuitem"><a href="/">Kontakt</a></li> | |
</ul> | |
</div> | |
<div class="fixed top-0 right-0"> | |
<button aria-expanded="false" aria-controls="nav" class="ma4" data-modal-close> | |
<span class="clip">Menü schließen</span> | |
<span class="icon icon2"> | |
<svg focusable="false"><use href="#icon-close"></use></svg> | |
</span> | |
</button> | |
</div> | |
</nav> |
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
export default class ariaHelper { | |
constructor(config) { | |
this.config = { | |
...{ | |
autoload: true, | |
dataNames: { | |
ariaControlsElSelector: 'aria-controls', | |
modalCloseSelector: 'data-modal-close', | |
modalOpener: 'data-modal-open', | |
preventScrollingOnBackground: | |
'data-modal-prevent-scrolling', | |
modalOnopenCloseGroup: 'data-modal-onopen-closegroup', | |
}, | |
}, | |
...config, | |
} | |
this.data = { | |
ariaHistory: [], | |
watchModalTabsOptions: null, | |
interactionEls: {}, | |
eventlistenerPreventCache: [], | |
} | |
if (!this.config.autoload) return | |
this.load() | |
} | |
load() { | |
this.watchModalOnopenCloseGroup() | |
this.watchAriaControlsToggle() | |
this.watchAriaHistoryEl() | |
this.watchModalClose() | |
this.watchTabsOnlyInActiveModalAllowed() | |
this.exitOnEsc() | |
this.showTabsFocus() | |
this.preventScrollingOnBackground() | |
} | |
reload() { | |
this.load() | |
} | |
getSlave(el) { | |
let ariaSlaveEl = el.getAttribute('aria-controls') | |
return document.getElementById(ariaSlaveEl) | |
} | |
getStatus(el) { | |
return JSON.parse(el.getAttribute('aria-expanded')) | |
} | |
setStatus(el, mode) { | |
if (!el.hasAttribute('data-aria-expanded-freeze')) { | |
el.setAttribute('aria-expanded', mode) | |
} | |
} | |
watchAriaHistoryEl() { | |
const historyEls = document.querySelectorAll( | |
'[' + this.config.dataNames.ariaControlsElSelector + ']' | |
) | |
if (!historyEls) return | |
historyEls.forEach((el, i) => { | |
if (this.checkIfEventListenerSetted(el, 'watchAriaHistoryEl')) | |
return | |
el.addEventListener('click', (e) => { | |
this.data.ariaHistory.push(el) | |
}) | |
}) | |
} | |
getPreviousHistoryEl(pos) { | |
if (!pos) pos = -1 | |
return this.data.ariaHistory[this.data.ariaHistory.length + pos] | |
} | |
setPreviousHistoryElFocus() { | |
let el = this.getPreviousHistoryEl(-1) | |
if (!el) return | |
el.focus() | |
this.data.ariaHistory.splice(-1, 1) | |
} | |
watchModalClose() { | |
let els = document.querySelectorAll( | |
'[' + this.config.dataNames.modalCloseSelector + ']' | |
) | |
if (!els) return | |
els.forEach((el, i) => { | |
if (this.checkIfEventListenerSetted(el, 'watchModalClose')) return | |
el.addEventListener('click', (e) => { | |
this.watchModalTabsOptions = null | |
this.setPreviousHistoryElFocus() | |
}) | |
}) | |
} | |
closeModal(el) { | |
if (!el) return | |
let slave = el.getAttribute('aria-controls'), | |
elSlave = document.getElementById(slave) | |
el.setAttribute('aria-expanded', false) | |
elSlave.hidden = true | |
this.watchModalTabsOptions = null | |
} | |
exitOnEsc() { | |
document.addEventListener('keydown', (e) => { | |
if (e.key == 'Escape') { | |
let el = this.getPreviousHistoryEl(), | |
elBefore = this.getPreviousHistoryEl(-2) | |
if (el) { | |
this.closeModal(el) | |
this.setPreviousHistoryElFocus() | |
if ( | |
el.hasAttribute( | |
this.config.dataNames.preventScrollingOnBackground | |
) | |
) { | |
this.setBodyScrollState(false) | |
} | |
if ( | |
elBefore && | |
elBefore.hasAttribute(this.config.dataNames.modalOpener) | |
) { | |
this.setOptionsForTabsOnlyInActiveModalAllowed(elBefore) | |
} | |
} | |
} | |
}) | |
} | |
setOptionsForTabsOnlyInActiveModalAllowed(el) { | |
let ariaControls = el.getAttribute('aria-controls'), | |
ariaControlsEl = document.getElementById(ariaControls), | |
ariaControlsElFirstFocusableEl = ariaControlsEl.querySelectorAll( | |
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' | |
) | |
this.watchModalTabsOptions = { | |
parent: ariaControlsEl, | |
focusTo: ariaControlsElFirstFocusableEl[0], | |
} | |
} | |
watchTabsOnlyInActiveModalAllowed() { | |
const els = document.querySelectorAll( | |
'[' + this.config.dataNames.modalOpener + ']' | |
) | |
if (!els) return | |
els.forEach((el, i) => { | |
if ( | |
this.checkIfEventListenerSetted( | |
el, | |
'watchTabsOnlyInActiveModalAllowed' | |
) | |
) | |
return | |
el.addEventListener('click', (e) => { | |
this.setOptionsForTabsOnlyInActiveModalAllowed(el) | |
setTimeout(() => { | |
this.watchModalTabsOptions.focusTo.focus() | |
}, 100) | |
}) | |
}) | |
document.body.addEventListener( | |
'focus', | |
(e) => { | |
let options = this.watchModalTabsOptions | |
if (!options) return | |
if ( | |
!options.parent.hidden && | |
!options.parent.contains(e.target) | |
) { | |
e.stopPropagation() | |
options.focusTo.focus() | |
} | |
}, | |
true | |
) | |
} | |
ariaControlsToggle(el) { | |
if (!el) return | |
let slaveName = el.getAttribute('aria-controls'), | |
slaveEl = this.getSlave(el), | |
slaveStatus = this.getStatus(el), | |
connectedAriaControlsEls = document.querySelectorAll( | |
'[aria-controls="' + slaveName + '"]' | |
) | |
connectedAriaControlsEls.forEach((el, i) => { | |
this.setStatus(el, !slaveStatus) | |
}) | |
slaveEl.hidden = slaveStatus | |
if (!slaveStatus) return | |
this.setPreviousHistoryElFocus() | |
} | |
watchAriaControlsToggle() { | |
let els = document.querySelectorAll( | |
'[' + this.config.dataNames.ariaControlsElSelector + ']' | |
) | |
if (!els) return | |
els.forEach((el, i) => { | |
if (this.checkIfEventListenerSetted(el, 'watchAriaControlsToggle')) | |
return | |
el.addEventListener('click', (e) => { | |
this.ariaControlsToggle(el) | |
}) | |
}) | |
} | |
showTabsFocus() { | |
document.addEventListener('keyup', (e) => { | |
if (e.which === 9) { | |
document.documentElement.classList.add('focus-outline') | |
} | |
}) | |
} | |
watchModalOnopenCloseGroup() { | |
let els = document.querySelectorAll( | |
'[' + this.config.dataNames.modalOnopenCloseGroup + ']' | |
) | |
if (!els) return | |
els.forEach((el, i) => { | |
if ( | |
this.checkIfEventListenerSetted( | |
el, | |
'watchModalOnopenCloseGroup' | |
) | |
) | |
return | |
el.addEventListener('click', (e) => { | |
let groupName = el.getAttribute( | |
this.config.dataNames.modalOnopenCloseGroup | |
), | |
groupConnectedEls = document.querySelectorAll( | |
'[' + | |
this.config.dataNames.modalOnopenCloseGroup + | |
'="' + | |
groupName + | |
'"]' | |
) | |
groupConnectedEls.forEach((groupConnectedEl, i) => { | |
let isExpanded = this.getStatus(groupConnectedEl) | |
if (!isExpanded || groupConnectedEl == el) return | |
this.closeModal(groupConnectedEl) | |
}) | |
}) | |
}) | |
} | |
setBodyScrollState(bodyScrollState) { | |
if (bodyScrollState == null) { | |
bodyScrollState = JSON.parse( | |
document.body.getAttribute('data-scrolling') | |
) | |
} | |
document.body.setAttribute('data-scrolling', !bodyScrollState) | |
} | |
preventScrollingOnBackground() { | |
let els = document.querySelectorAll( | |
'[' + this.config.dataNames.preventScrollingOnBackground + ']' | |
) | |
if (!els) return | |
els.forEach((el, i) => { | |
if ( | |
this.checkIfEventListenerSetted( | |
el, | |
'preventScrollingOnBackground' | |
) | |
) | |
return | |
el.addEventListener('click', (e) => { | |
this.setBodyScrollState() | |
}) | |
}) | |
} | |
checkIfEventListenerSetted(el, cacheKey) { | |
if (!this.data.eventlistenerPreventCache[cacheKey]) { | |
this.data.eventlistenerPreventCache[cacheKey] = [] | |
} | |
if (!this.data.eventlistenerPreventCache[cacheKey].includes(el)) { | |
this.data.eventlistenerPreventCache[cacheKey].push(el) | |
return false | |
} | |
return true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment