Created
September 8, 2023 07:00
-
-
Save stefanprobst/50f9aa409a4d0cd4fdaa1099ffea0819 to your computer and use it in GitHub Desktop.
yLGgxdK
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
<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 & 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 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
/* | |
* 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 | |
); |
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
<script src="https://www.w3.orgcontent/shared/js/utils.js"></script> |
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
.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; | |
} |
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
<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