-
-
Save SteveJonesDev/ce8bf0219e4ebe5582454022e429ef07 to your computer and use it in GitHub Desktop.
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; | |
} | |
}); | |
}); | |
}); |
.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; | |
} | |
} |
@ksascomm, the javascript has been updated to account for multi-level fly-outs.
On line 63 of your CSS, there appears to be a SASS variable reference, $neutral_200
Thanks, @jsitesMM, I've fixed that.
Javascript updated to fix ESLint issues.
I believe .focus()
is deprecated: https://api.jquery.com/focus-shorthand/#focus-handler
Instead you're supposed to use .trigger( "focus" )
: https://api.jquery.com/focus/#trigger2
@BFTrick, .focus()
is a method in vanilla JavaScript. This JavaScript doesn't use jQuery.
🤦
This looks great, but I'm running in to an issue where the aria-expanded is never set to true - it looks as though it's being quickly set and then reset when I'm checking in dev tools. Any ideas?
@joe-deltaechovictor, Yes I’ve fixed this bug in another code base. I’ll update this code in a bit.
Thanks so much for the quick reply. Looks like exactly what I need. Far better accessibility than the new Gutenberg navigation block.
Just giving this a gentle nudge.
@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
If you have a menu with two levels of fly-outs (ie a Child page with Grandchild pages), the Parent fly-out disappears when the Child's Flyout toggle button is clicked, as the ".toggled-on" class is transferred from the Parent to the Child fly-out. Any solutions for dealing with two levels of fly-outs so the Parent fly-out remains visible?