Skip to content

Instantly share code, notes, and snippets.

@stefanprobst
Created September 8, 2023 07:00
Show Gist options
  • Save stefanprobst/50f9aa409a4d0cd4fdaa1099ffea0819 to your computer and use it in GitHub Desktop.
Save stefanprobst/50f9aa409a4d0cd4fdaa1099ffea0819 to your computer and use it in GitHub Desktop.
yLGgxdK
<nav aria-label="Mythical University">
<ul id="exTest" class="disclosure-nav">
<li>
<button type="button" aria-expanded="true" aria-controls="id_about_menu">About</button>
<ul id="id_about_menu">
<li>
<a href="#mythical-page-content">Overview</a>
</li>
<li>
<a href="#mythical-page-content">Administration</a>
</li>
<li>
<a href="#mythical-page-content">Facts</a>
</li>
<li>
<a href="#mythical-page-content">Campus Tours</a>
</li>
</ul>
</li>
<li>
<button type="button" aria-expanded="true" aria-controls="id_admissions_menu">Admissions</button>
<ul id="id_admissions_menu">
<li>
<a href="#mythical-page-content">Apply</a>
</li>
<li>
<a href="#mythical-page-content">Tuition</a>
</li>
<li>
<a href="#mythical-page-content">Sign Up</a>
</li>
<li>
<a href="#mythical-page-content">Visit</a>
</li>
<li>
<a href="#mythical-page-content">Photo Tour</a>
</li>
<li>
<a href="#mythical-page-content">Connect</a>
</li>
</ul>
</li>
<li>
<button type="button" aria-expanded="true" aria-controls="id_academics_menu">Academics</button>
<ul id="id_academics_menu">
<li>
<a href="#mythical-page-content">Colleges &amp; Schools</a>
</li>
<li>
<a href="#mythical-page-content">Programs of Study</a>
</li>
<li>
<a href="#mythical-page-content">Honors Programs</a>
</li>
<li>
<a href="#mythical-page-content">Online Courses</a>
</li>
<li>
<a href="#mythical-page-content">Course Explorer</a>
</li>
<li>
<a href="#mythical-page-content">Register for Class</a>
</li>
<li>
<a href="#mythical-page-content">Academic Calendar</a>
</li>
<li>
<a href="#mythical-page-content">Transcripts</a>
</li>
</ul>
</li>
</ul>
</nav>
<div id="mythical-page-content" class="disclosure-page-content" tabindex="-1" role="region" aria-label="Mythical University sample page content">
<h3 id="mythical-page-heading">Mythical University</h3>
<p>
Sample content section.
Activating a link above will update and navigate to this region.
</p>
</div>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* Supplemental JS for the disclosure menu keyboard behavior
*/
'use strict';
class DisclosureNav {
constructor(domNode) {
this.rootNode = domNode;
this.controlledNodes = [];
this.openIndex = null;
this.useArrowKeys = true;
this.topLevelNodes = [
...this.rootNode.querySelectorAll(
'.main-link, button[aria-expanded][aria-controls]'
),
];
this.topLevelNodes.forEach((node) => {
// handle button + menu
if (
node.tagName.toLowerCase() === 'button' &&
node.hasAttribute('aria-controls')
) {
const menu = node.parentNode.querySelector('ul');
if (menu) {
// save ref controlled menu
this.controlledNodes.push(menu);
// collapse menus
node.setAttribute('aria-expanded', 'false');
this.toggleMenu(menu, false);
// attach event listeners
menu.addEventListener('keydown', this.onMenuKeyDown.bind(this));
node.addEventListener('click', this.onButtonClick.bind(this));
node.addEventListener('keydown', this.onButtonKeyDown.bind(this));
}
}
// handle links
else {
this.controlledNodes.push(null);
node.addEventListener('keydown', this.onLinkKeyDown.bind(this));
}
});
this.rootNode.addEventListener('focusout', this.onBlur.bind(this));
}
controlFocusByKey(keyboardEvent, nodeList, currentIndex) {
switch (keyboardEvent.key) {
case 'ArrowUp':
case 'ArrowLeft':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var prevIndex = Math.max(0, currentIndex - 1);
nodeList[prevIndex].focus();
}
break;
case 'ArrowDown':
case 'ArrowRight':
keyboardEvent.preventDefault();
if (currentIndex > -1) {
var nextIndex = Math.min(nodeList.length - 1, currentIndex + 1);
nodeList[nextIndex].focus();
}
break;
case 'Home':
keyboardEvent.preventDefault();
nodeList[0].focus();
break;
case 'End':
keyboardEvent.preventDefault();
nodeList[nodeList.length - 1].focus();
break;
}
}
// public function to close open menu
close() {
this.toggleExpand(this.openIndex, false);
}
onBlur(event) {
var menuContainsFocus = this.rootNode.contains(event.relatedTarget);
if (!menuContainsFocus && this.openIndex !== null) {
this.toggleExpand(this.openIndex, false);
}
}
onButtonClick(event) {
var button = event.target;
var buttonIndex = this.topLevelNodes.indexOf(button);
var buttonExpanded = button.getAttribute('aria-expanded') === 'true';
this.toggleExpand(buttonIndex, !buttonExpanded);
}
onButtonKeyDown(event) {
var targetButtonIndex = this.topLevelNodes.indexOf(document.activeElement);
// close on escape
if (event.key === 'Escape') {
this.toggleExpand(this.openIndex, false);
}
// move focus into the open menu if the current menu is open
else if (
this.useArrowKeys &&
this.openIndex === targetButtonIndex &&
event.key === 'ArrowDown'
) {
event.preventDefault();
this.controlledNodes[this.openIndex].querySelector('a').focus();
}
// handle arrow key navigation between top-level buttons, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetButtonIndex);
}
}
onLinkKeyDown(event) {
var targetLinkIndex = this.topLevelNodes.indexOf(document.activeElement);
// handle arrow key navigation between top-level buttons, if set
if (this.useArrowKeys) {
this.controlFocusByKey(event, this.topLevelNodes, targetLinkIndex);
}
}
onMenuKeyDown(event) {
if (this.openIndex === null) {
return;
}
var menuLinks = Array.prototype.slice.call(
this.controlledNodes[this.openIndex].querySelectorAll('a')
);
var currentIndex = menuLinks.indexOf(document.activeElement);
// close on escape
if (event.key === 'Escape') {
this.topLevelNodes[this.openIndex].focus();
this.toggleExpand(this.openIndex, false);
}
// handle arrow key navigation within menu links, if set
else if (this.useArrowKeys) {
this.controlFocusByKey(event, menuLinks, currentIndex);
}
}
toggleExpand(index, expanded) {
// close open menu, if applicable
if (this.openIndex !== index) {
this.toggleExpand(this.openIndex, false);
}
// handle menu at called index
if (this.topLevelNodes[index]) {
this.openIndex = expanded ? index : null;
this.topLevelNodes[index].setAttribute('aria-expanded', expanded);
this.toggleMenu(this.controlledNodes[index], expanded);
}
}
toggleMenu(domNode, show) {
if (domNode) {
domNode.style.display = show ? 'block' : 'none';
}
}
updateKeyControls(useArrowKeys) {
this.useArrowKeys = useArrowKeys;
}
}
/* Initialize Disclosure Menus */
window.addEventListener(
'load',
function () {
var menus = document.querySelectorAll('.disclosure-nav');
var disclosureMenus = [];
for (var i = 0; i < menus.length; i++) {
disclosureMenus[i] = new DisclosureNav(menus[i]);
}
// listen to arrow key checkbox
var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
if (arrowKeySwitch) {
arrowKeySwitch.addEventListener('change', function () {
var checked = arrowKeySwitch.checked;
for (var i = 0; i < disclosureMenus.length; i++) {
disclosureMenus[i].updateKeyControls(checked);
}
});
}
// fake link behavior
disclosureMenus.forEach((disclosureNav, i) => {
var links = menus[i].querySelectorAll('[href="#mythical-page-content"]');
var examplePageHeading = document.getElementById('mythical-page-heading');
for (var k = 0; k < links.length; k++) {
// The codepen export script updates the internal link href with a full URL
// we're just manually fixing that behavior here
links[k].href = '#mythical-page-content';
links[k].addEventListener('click', (event) => {
// change the heading text to fake a page change
var pageTitle = event.target.innerText;
examplePageHeading.innerText = pageTitle;
// handle aria-current
for (var n = 0; n < links.length; n++) {
links[n].removeAttribute('aria-current');
}
event.target.setAttribute('aria-current', 'page');
});
}
});
},
false
);
<script src="https://www.w3.orgcontent/shared/js/utils.js"></script>
.disclosure-nav {
background-color: #eee;
display: flex;
list-style-type: none;
padding: 0;
margin: 0;
}
.disclosure-nav ul {
background-color: #eee;
border: 1px solid #005a9c;
border-top-width: 5px;
border-radius: 0 0 4px 4px;
display: block;
list-style-type: none;
margin: 0;
min-width: 200px;
padding: 0;
position: absolute;
top: 100%;
}
.disclosure-nav li {
margin: 0;
}
.disclosure-nav > li {
display: flex;
position: relative;
}
.disclosure-nav ul a {
border: 0;
color: #000;
display: block;
margin: 0;
padding: 0.5em 1em;
text-decoration: underline;
}
.disclosure-nav ul a:hover,
.disclosure-nav ul a:focus {
background-color: #ddd;
margin-bottom: 0;
text-decoration: none;
}
.disclosure-nav ul a:focus {
outline: 5px solid rgb(0 90 156 / 75%);
position: relative;
}
.disclosure-nav button,
.disclosure-nav .main-link {
align-items: center;
background-color: transparent;
border: 1px solid transparent;
border-right-color: #ccc;
display: flex;
padding: 1em;
}
.disclosure-nav .main-link {
border-right-color: transparent;
}
.disclosure-nav button::after {
content: "";
border-bottom: 1px solid #000;
border-right: 1px solid #000;
height: 0.5em;
margin-left: 0.75em;
width: 0.5em;
transform: rotate(45deg);
}
.disclosure-nav .main-link + button::after {
margin-left: 0;
}
.disclosure-nav button:focus,
.disclosure-nav .main-link:focus {
border-color: #005a9c;
outline: 5px solid rgb(0 90 156 / 75%);
position: relative;
}
.disclosure-nav button:hover,
.disclosure-nav button[aria-expanded="true"] {
background-color: #005a9c;
color: #fff;
}
.disclosure-nav button:hover::after,
.disclosure-nav button[aria-expanded="true"]::after {
border-color: #fff;
}
/* Styles for example page content section */
.disclosure-page-content {
border: 1px solid #ccc;
padding: 1em;
}
.disclosure-page-content h3 {
margin-top: 0.5em;
}
.sample-header {
border: #005a9c solid 2px;
background: #005a9c;
color: white;
text-align: center;
}
.sample-header .title {
font-size: 2.5em;
font-weight: bold;
font-family: serif;
}
.sample-header .tagline {
font-style: italic;
}
.sample-footer {
border: #005a9c solid 2px;
background: #005a9c;
font-family: serif;
color: white;
font-style: italic;
padding-left: 1em;
}
<link href="https://www.w3.org/content/shared/css/core.css" rel="stylesheet" />
<link href="https://www.w3.org/StyleSheets/TR/2016/base.css" rel="stylesheet" />
<link href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment