-
-
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') === 'true' | |
| ? '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; | |
| } | |
| } |
@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
Fixed issue with the aria-expanded always returning false on the dropdown toggle.
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?