Last active
May 21, 2024 13:05
-
-
Save SteveJonesDev/ce8bf0219e4ebe5582454022e429ef07 to your computer and use it in GitHub Desktop.
Accessible WordPress Navigation Menu
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
document.addEventListener('DOMContentLoaded', function () { | |
// Getting main menu elements | |
const menuContainer = document.querySelector('.menu-container'); | |
const menuToggle = menuContainer.querySelector('.menu-button'); | |
const siteHeaderMenu = menuContainer.querySelector('#site-header-menu'); | |
const siteNavigation = menuContainer.querySelector('#site-navigation'); | |
// If the menu toggle button exists, set up its behaviors | |
if (menuToggle) { | |
// Initial ARIA attribute setup for accessibility | |
menuToggle.setAttribute('aria-expanded', 'false'); | |
siteNavigation.setAttribute('aria-expanded', 'false'); | |
// Event listener for main menu toggle button | |
menuToggle.addEventListener('click', function () { | |
// Toggle visual states for the button and menu | |
this.classList.toggle('toggled-on'); | |
siteHeaderMenu.classList.toggle('toggled-on'); | |
// Determine and set the new expanded state for ARIA | |
const isExpanded = this.getAttribute('aria-expanded') === 'true'; | |
const newExpandedState = isExpanded ? 'false' : 'true'; | |
// Update ARIA attributes | |
this.setAttribute('aria-expanded', newExpandedState); | |
siteNavigation.setAttribute('aria-expanded', newExpandedState); | |
}); | |
} | |
// Set up dropdown toggle buttons for menu items with children | |
const menuItemsWithChildren = document.querySelectorAll( | |
'.menu-item-has-children > a' | |
); | |
menuItemsWithChildren.forEach(function (item) { | |
const linkText = item.textContent; | |
// Create the dropdown toggle button | |
const dropdownToggle = document.createElement('button'); | |
dropdownToggle.className = 'dropdown-toggle'; | |
dropdownToggle.setAttribute('aria-expanded', 'false'); | |
// Set ARIA label for accessibility | |
dropdownToggle.setAttribute('aria-label', linkText + ' submenu'); | |
// Insert the dropdown button after the menu item | |
item.insertAdjacentElement('afterend', dropdownToggle); | |
// Set up behavior when the dropdown button is clicked | |
dropdownToggle.addEventListener('click', function () { | |
// Determine the expanded state of the dropdown | |
const isExpanded = this.getAttribute('aria-expanded'); | |
// Toggle the dropdown's expanded state | |
if (isExpanded === 'true') { | |
this.setAttribute('aria-expanded', 'false'); | |
} else { | |
this.setAttribute('aria-expanded', 'true'); | |
} | |
}); | |
}); | |
// Toggle dropdowns behavior | |
const dropdownToggles = siteHeaderMenu.querySelectorAll('.dropdown-toggle'); | |
dropdownToggles.forEach(function (toggle) { | |
toggle.addEventListener('click', function (e) { | |
e.preventDefault(); | |
e.stopPropagation(); // Prevent event from bubbling | |
// Toggle the clicked dropdown | |
this.classList.toggle('toggled-on'); | |
const nextSubMenu = this.nextElementSibling; | |
if (nextSubMenu && nextSubMenu.classList.contains('sub-menu')) { | |
nextSubMenu.classList.toggle('toggled-on'); | |
} | |
// Update the ARIA expanded state of the dropdown | |
const isExpanded = | |
this.getAttribute('aria-expanded') === 'false' | |
? 'true' | |
: 'false'; | |
this.setAttribute('aria-expanded', isExpanded); | |
// Close other dropdowns on the same level to avoid multiple open dropdowns | |
const siblingToggles = Array.from( | |
this.parentElement.parentElement.children | |
) | |
.map((el) => el.querySelector('.dropdown-toggle')) | |
.filter((el) => el !== null && el !== this); | |
siblingToggles.forEach((sibToggle) => { | |
sibToggle.classList.remove('toggled-on'); | |
const sibSubMenu = sibToggle.nextElementSibling; | |
if (sibSubMenu && sibSubMenu.classList.contains('sub-menu')) { | |
sibSubMenu.classList.remove('toggled-on'); | |
} | |
sibToggle.setAttribute('aria-expanded', 'false'); | |
}); | |
}); | |
}); | |
// Indicate that a menu has a sub-menu | |
const subMenus = document.querySelectorAll( | |
'.sub-menu .menu-item-has-children' | |
); | |
subMenus.forEach(function (subMenu) { | |
subMenu.parentElement.classList.add('has-sub-menu'); | |
}); | |
// Keyboard navigation setup for menu | |
const menuLinksAndDropdownToggles = document.querySelectorAll( | |
'.menu-item a, button.dropdown-toggle' | |
); | |
menuLinksAndDropdownToggles.forEach(function (element) { | |
element.addEventListener('keydown', function (e) { | |
const key = e.keyCode; | |
// Key handling for improved keyboard navigation | |
if (![27, 37, 38, 39, 40].includes(key)) { | |
return; | |
} | |
// Handle different keys for navigation | |
switch (key) { | |
case 27: // Escape: Close dropdown or main menu | |
e.preventDefault(); | |
e.stopPropagation(); | |
const parentDropdown = | |
this.closest('ul').previousElementSibling; | |
if ( | |
parentDropdown && | |
parentDropdown.classList.contains('dropdown-toggle') && | |
parentDropdown.classList.contains('toggled-on') | |
) { | |
parentDropdown.focus(); | |
parentDropdown.click(); | |
} else if (!parentDropdown) { | |
// If no parent dropdown found, close the main menu. | |
if ( | |
menuToggle && | |
menuToggle.classList.contains('toggled-on') | |
) { | |
menuToggle.click(); | |
menuToggle.focus(); | |
} | |
} | |
break; | |
case 37: // Left arrow: Move focus to the previous item | |
e.preventDefault(); | |
if (this.classList.contains('dropdown-toggle')) { | |
this.previousElementSibling.focus(); | |
} else { | |
const prevSibling = | |
this.parentElement.previousElementSibling; | |
if ( | |
prevSibling && | |
prevSibling.querySelector('button.dropdown-toggle') | |
) { | |
prevSibling | |
.querySelector('button.dropdown-toggle') | |
.focus(); | |
} else if ( | |
prevSibling && | |
prevSibling.querySelector('a') | |
) { | |
prevSibling.querySelector('a').focus(); | |
} | |
} | |
break; | |
case 39: // Right arrow: Move focus to the next item or enter a submenu | |
e.preventDefault(); | |
if ( | |
this.nextElementSibling && | |
this.nextElementSibling.matches( | |
'button.dropdown-toggle' | |
) | |
) { | |
this.nextElementSibling.focus(); | |
} else { | |
const nextSibling = | |
this.parentElement.nextElementSibling; | |
if (nextSibling) { | |
nextSibling.querySelector('a').focus(); | |
} | |
} | |
if ( | |
this.matches('ul.sub-menu .dropdown-toggle.toggled-on') | |
) { | |
this.parentElement | |
.querySelector('ul.sub-menu li:first-child a') | |
.focus(); | |
} | |
break; | |
case 40: // Down arrow: Move focus to the next item or submenu | |
e.preventDefault(); | |
if (this.nextElementSibling) { | |
const firstChildLink = | |
this.nextElementSibling.querySelector( | |
'li:first-child a' | |
); | |
if (firstChildLink) { | |
firstChildLink.focus(); | |
} | |
} else { | |
const nextElem = this.parentElement.nextElementSibling; | |
if (nextElem) { | |
nextElem.querySelector('a').focus(); | |
} | |
} | |
break; | |
case 38: // Up arrow: Move focus to the previous item or exit a submenu | |
e.preventDefault(); | |
const prevElem = this.parentElement.previousElementSibling; | |
if (prevElem) { | |
prevElem.querySelector('a').focus(); | |
} else { | |
const closestUl = this.closest('ul'); | |
if ( | |
closestUl && | |
closestUl.previousElementSibling.matches( | |
'.dropdown-toggle.toggled-on' | |
) | |
) { | |
closestUl.previousElementSibling.focus(); | |
} | |
} | |
break; | |
} | |
}); | |
}); | |
}); |
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
.menu-container:after { | |
display: table; | |
clear: both; | |
content: ""; | |
} | |
.site-header-menu { | |
display: none; | |
font-size: 1rem; | |
clear: both; | |
} | |
.main-navigation ul { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
} | |
.main-navigation ul li { | |
margin-right: 40px; | |
margin-bottom: 10px; | |
min-height: 30px; | |
} | |
.main-navigation ul a, | |
.main-navigation ul a:visited { | |
border: none; | |
color: $black; | |
font-size: 16px; | |
font-weight: 400; | |
line-height: 22px; | |
position: relative; | |
text-decoration: none; | |
text-transform: uppercase; | |
} | |
.main-navigation ul a:hover { | |
border-bottom: 3px solid; | |
} | |
.main-navigation ul ul { | |
display: none; | |
margin-top: 0px; | |
margin-left: 25px; | |
} | |
.main-navigation ul ul li{ | |
a{ | |
font-size: 14px; | |
text-transform: none; | |
padding: 0; | |
margin-bottom: 10px; | |
border-bottom: solid 4px transparent; | |
padding-bottom: 2px; | |
} | |
&:last-child{ | |
padding-bottom: 0; | |
a{ | |
margin-bottom: 0; | |
} | |
} | |
&:hover > a { | |
background: #EEEEEE; | |
} | |
} | |
.main-navigation ul ul ul { | |
display: none; | |
margin-left: 25px; | |
} | |
.no-js .site-header-menu, | |
.site-header-menu.toggled-on { | |
display: block; | |
} | |
.site-header-menu.toggled-on { | |
margin-top: 10px; | |
} | |
.no-js .main-navigation ul, | |
.main-navigation ul .sub-menu.toggled-on { | |
display: block; | |
} | |
button.dropdown-toggle, | |
button.menu-button { | |
display: inline; | |
background-color: transparent; | |
border: 0; | |
-webkit-appearance: none; | |
-moz-appearance: none; | |
cursor: pointer; | |
content: ""; | |
} | |
button.dropdown-toggle { | |
width: 25px; | |
height: 25px; | |
position: absolute; | |
right: 15px; | |
margin-left: 10px; | |
padding: 2px; | |
} | |
.dropdown-toggle:after, | |
.dropdown-toggle.toggled-on:after { | |
font-size: 1rem; | |
} | |
.menu-button { | |
width: 25px; | |
height: 25px; | |
float: right; | |
padding: 0 !important; | |
font-size: 1.35rem; | |
margin-top: 25px; | |
padding: 5px 5px 5px 5px; | |
} | |
.main-navigation a:focus, | |
button.dropdown-toggle:focus, | |
button.menu-button:focus { | |
outline: 1px solid $black; | |
outline-offset: 2px; | |
} | |
.no-js .menu-button { | |
display: none; | |
} | |
/* Plus symbol to expand sub-menu on mobile */ | |
.dropdown-toggle:after { | |
content: "\002B"; | |
} | |
/* Minus symbol to collapse sub-menu on mobile */ | |
.dropdown-toggle.toggled-on:after { | |
content: "\2212"; | |
} | |
/* 'Hamburger' or bars to expand menu on mobile*/ | |
.menu-button:before { | |
content: "\f0c9"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
} | |
/* Times (x) to collapse menu on mobile*/ | |
.menu-button.toggled-on:before { | |
content: "\f00d"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
} | |
.dropdown-toggle:after, | |
.dropdown-toggle.toggled-on:after, | |
.menu-button:before, | |
.menu-button.toggled-on:before { | |
font-weight: bold; | |
} | |
/* Screen readers */ | |
.screen-readers { | |
position: absolute !important; | |
width: 1px; | |
height: 1px; | |
margin: -1px; | |
padding: 0; | |
border: 0; | |
word-break: normal !important; | |
overflow: hidden; | |
clip: rect(0 0 0 0); | |
} | |
/* Desktop media query */ | |
@media only screen and (min-width: 768px) { | |
button.menu-button { | |
display: none; | |
} | |
.menu-container { | |
padding-top: 0; | |
padding-bottom: 0; | |
padding-left: 0; | |
} | |
.site-header-menu { | |
display: block; | |
margin-left: 10px; | |
float: right; | |
//margin-top: 30px; | |
clear: none; | |
} | |
.main-navigation ul { | |
position: relative; | |
float: left; | |
} | |
.main-navigation ul li { | |
position: relative; | |
float: left; | |
margin: 0; | |
padding: 10px; | |
min-height: 0px; | |
} | |
.no-js .main-navigation ul ul, | |
.main-navigation ul ul { | |
position: absolute; | |
display: none; | |
top: 100%; | |
left: 0; | |
padding: 0; | |
z-index: 999; | |
background: $white; | |
border-radius: 15px; | |
padding: 25px; | |
} | |
.no-js .main-navigation ul ul li, | |
.main-navigation ul ul li { | |
float: none; | |
width: 175px; | |
padding: 0; | |
padding-bottom: 15px; | |
} | |
.main-navigation ul .has-sub-menu > li { | |
padding-right: 40px; | |
} | |
.no-js .main-navigation ul ul ul, | |
.main-navigation ul ul ul { | |
top: -1px; | |
left: 100%; | |
margin-left: 0; | |
margin-top: -5px; | |
} | |
ul.sub-menu .dropdown-toggle { | |
position: absolute; | |
right: 10px; | |
top: 4px; | |
} | |
/* Arrow down */ | |
.main-navigation ul .dropdown-toggle:after { | |
content: "\f078"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
font-size: .75rem; | |
} | |
/* Arrow right */ | |
.main-navigation ul ul .dropdown-toggle:after { | |
content: "\f054"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
font-size: .75rem; | |
} | |
/* Arrow up */ | |
.main-navigation ul .dropdown-toggle.toggled-on:after { | |
content: "\f077"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
} | |
/* Arrow left */ | |
.main-navigation ul ul .dropdown-toggle.toggled-on:after { | |
content: "\f053"; | |
font-family: "Font Awesome 6 Free"; font-weight: 400; | |
} | |
.main-navigation ul .dropdown-toggle:after, | |
.main-navigation ul ul .dropdown-toggle:after, | |
.main-navigation ul .dropdown-toggle.toggled-on:after, | |
.main-navigation ul ul .dropdown-toggle.toggled-on:after { | |
font-weight: bold; | |
} | |
button.dropdown-toggle { | |
width: auto; | |
height: auto; | |
position: inherit; | |
right: auto; | |
} | |
.main-navigation ul li:hover > ul { | |
display: block; | |
} | |
} |
@joe-deltaechovictor the code has been updated.
@SteveJonesDev Legend - will give it a go shortly.
@SteveJonesDev Sorry to be a pain, but I'm still getting the same issue. I've diff'd the code and the JS looks the same as before?
Good tutorial !
Can you insert all Accessibility Features from
https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/#accessibilityfeatures
and keyboard Support (for example with the right arrow,left arrow, home, end keys when in the submenus)
https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/#kbd_label
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just giving this a gentle nudge.