-
-
Save efrigoli/e483462d9581503706b9536f9725a0e6 to your computer and use it in GitHub Desktop.
This code provides a starting point for enhancing keyboard navigation accessibility within a megamenu. It adds arrow key navigation capabilities and enhances Escape and Tab key behavior to maintain proper focus when navigating via keyboard.
This file contains hidden or 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
| /* | |
| * All code is intended as learning examples only and will require modification to function properly based on the HTML structure of the menu itself. | |
| */ | |
| /** | |
| * Attaches a keyboard event listener to the given item and handles keyboard interactions. | |
| * | |
| * @param {HTMLElement} item - The menu item to attach the event listener to. | |
| * | |
| */ | |
| function processKeyboardInput(item) { | |
| // Add event listener for keydown events when the menu item is focused. | |
| item.addEventListener('keydown', function (event) { | |
| // Prevent default behavior for arrow keys, ESC key, and tab. | |
| event.stopImmediatePropagation(); | |
| // Setting up variables for the next and previous items in the menu. | |
| let nextListItem; | |
| let prevListItem; | |
| // Check if the current menu item in focus is the mobile menu toggle button. | |
| if (this.classList.contains('mobile-menu-toggle')) { | |
| // If the current item is the mobile menu toggle button, there are no previous sibling items to target. | |
| nextListItem = this.nextElementSibling; | |
| prevListItem = null; | |
| } else { | |
| // If the current item is a submenu toggle button, target the next and previous sibling items. | |
| nextListItem = this.closest('li').nextElementSibling; | |
| prevListItem = this.closest('li').previousElementSibling; | |
| } | |
| // Switch statement to handle different key events. | |
| switch (event.key) { | |
| // ESC key closes the current menu or submenu and returns focus to the parent button. | |
| case "Escape": | |
| // Setting up a target button variable that will be used to receive the focus. | |
| let targetButton; | |
| // If the current item is a mobile menu toggle button with an open mobile menu, set the target button value to the mobile menu toggle button. | |
| if (this.classList.contains('mobile-menu-toggle') && this.getAttribute('aria-expanded') == 'true') { | |
| targetButton = this; | |
| } else if (this.classList.contains('megamenu-button')) { | |
| // If the current item is a submenu toggle button with an open submenu, set the target button value to the submenu toggle button. | |
| if (this.getAttribute('aria-expanded') == 'true') { | |
| targetButton = this; | |
| } | |
| // If the current item is nested within an open submenu, set the target button value to the parent submenu toggle button. | |
| else if (this.closest('.submenu-expanded')) { | |
| targetButton = this.closest('.submenu-expanded'); | |
| } | |
| } | |
| // If the target button's menu is expanded, close the menu. | |
| if (targetButton.getAttribute('aria-expanded') == 'true') { | |
| toggleMenu(targetButton); | |
| } | |
| // Focus on the target button. | |
| targetButton.focus(); | |
| break; | |
| // Right arrow key moves focus to the next menu item moving from left to right, looping back to the first item in the menu if at the end. | |
| case "ArrowRight": | |
| // Prevent the default right arrow key behavior. | |
| event.preventDefault(); | |
| // If the current item is a submenu toggle button and the submenu is open, close the open submenu. | |
| if (this.classList.contains('megamenu-button') && (this.getAttribute('aria-expanded') == 'true')) { | |
| toggleMenu(this); | |
| } | |
| // If the current item is not nested within an open submenu, move focus to the next item. | |
| if (!this.closest('.submenu-expanded')) { | |
| // If there is a next sibling item in the menu, move focus to it. | |
| if (nextListItem) { | |
| nextListItem.querySelector('a, button').focus(); | |
| } else { | |
| // If this menu item is at the end of the menu, return focus to the first item in the menu. | |
| this.closest('.megamenu').querySelector('li').querySelector('a, button').focus(); | |
| } | |
| } else { | |
| // If the current item is nested within an open submenu, close the submenu and move focus to the next main-level menu item. | |
| let openMenuButton = this.closest('.submenu-expanded').previousElementSibling; | |
| // If there is an open submenu, close it. | |
| if (openMenuButton) { | |
| toggleMenu(openMenuButton); | |
| } | |
| // If there is a next top-level sibling item in the menu, move focus to it. | |
| if (this.closest('.menu-item-level-0').nextElementSibling) { | |
| this.closest('.menu-item-level-0').nextElementSibling.querySelector('a, button').focus(); | |
| } else { | |
| // If this menu item is at the end of the top-level menu, return focus to the first top-level item in the menu. | |
| this.closest('.megamenu').querySelector('li').querySelector('a, button').focus(); | |
| } | |
| } | |
| break; | |
| // Left arrow key moves focus to the previous menu item, looping back to the last item if at the beginning. | |
| case "ArrowLeft": | |
| // Prevent the default left arrow key behavior. | |
| event.preventDefault(); | |
| // If the current item is a submenu toggle button and the submenu is open, close the open submenu. | |
| if (this.classList.contains('megamenu-button') && (this.getAttribute('aria-expanded') == 'true')) { | |
| toggleMenu(this); | |
| } | |
| // If the current item is not nested within an open submenu, move focus to the previous item. | |
| if (!this.closest('.submenu-expanded')) { | |
| // If there is a previous sibling item in the menu, move focus to it. | |
| if (prevListItem) { | |
| prevListItem.querySelector('a, button').focus(); | |
| } else { | |
| // If this menu item is at the start of the menu, return focus to the last item in the menu. | |
| this.closest('.megamenu').lastElementChild.querySelector('a, button').focus(); | |
| } | |
| } else { | |
| // If the current item is nested within an open submenu, close the submenu and move focus to the previous main-level menu item. | |
| let openMenuButton = this.closest('.submenu-expanded').previousElementSibling; | |
| // If there is an open submenu, close it. | |
| if (openMenuButton) { | |
| toggleMenu(openMenuButton); | |
| } | |
| // If there is a previous top-level sibling item in the menu, move focus to it. | |
| if (this.closest('.menu-item-level-0').previousElementSibling) { | |
| this.closest('.menu-item-level-0').previousElementSibling.querySelector('a, button').focus(); | |
| } else { | |
| // If this menu item is at the start of the top-level menu, return focus to the last top-level item in the menu. | |
| this.closest('.megamenu').lastElementChild.querySelector('a, button').focus(); | |
| } | |
| } | |
| break; | |
| // Up arrow key moves focus to the previous menu item if in a submenu, looping back to the last item if at the beginning. | |
| case "ArrowUp": | |
| // Prevent the default up arrow key behavior. | |
| event.preventDefault(); | |
| // If the current item is within a submenu dropdown and there is a previous sibling item, move focus to it. | |
| if (this.closest('.submenu-expanded') && prevListItem) { | |
| prevListItem.querySelector('a, button').focus(); | |
| } else if(this.closest('.submenu-expanded') && prevListItem) { | |
| // If the current item is the first item within a submenu dropdown, move focus to the last item in the submenu. | |
| this.closest('.submenu-expanded').lastElementChild.querySelector('a, button').focus(); | |
| } | |
| break; | |
| // Down arrow key opens submenus and mobile menus, focusing on the first menu item. | |
| // If in a submenu, down arrow key moves focus to the next menu item, looping back to the first item if at the end. | |
| case "ArrowDown": | |
| // Prevent the default down arrow key behavior. | |
| event.preventDefault(); | |
| // If the current item is the mobile menu toggle button, open the menu and focus on the first item. | |
| if (this.classList.contains('mobile-menu-toggle')) { | |
| // Check that the mobile menu is not already open before toggling. | |
| if (this.getAttribute('aria-expanded') == 'false') { | |
| toggleMenu(this); | |
| } | |
| // Focus on the first item in the mobile menu. | |
| this.nextElementSibling.querySelector('li').querySelector('a, button').focus(); | |
| } else if (this.classList.contains('megamenu-button')) { | |
| // If the current item is a submenu toggle button, open the menu and focus on the first item. | |
| if (this.getAttribute('aria-expanded') == 'false') { | |
| toggleMenu(this); | |
| } | |
| this.nextElementSibling.querySelector('li').querySelector('a, button').focus(); | |
| } else { | |
| // If the current item is within a submenu dropdown and there is a next sibling item, move focus to it. | |
| if (this.closest('.submenu-expanded') && nextListItem) { | |
| nextListItem.querySelector('a, button').focus(); | |
| } else if (this.closest('.submenu-expanded') && !nextListItem) { | |
| // If the current item is the last item within a submenu dropdown, move focus to the first item in the submenu. | |
| this.closest('.submenu-expanded').querySelector('li:first-child').querySelector('a, button').focus(); | |
| } | |
| } | |
| break; | |
| // Tab key closes the currently open submenu and moves focus to the next menu item. | |
| case "Tab": | |
| // If reverse-tabbing out of a submenu, close the submenu and move focus to the parent menu item. | |
| if (event.shiftKey) { | |
| if (!prevListItem && this.closest('li').classList.contains('menu-item-level-1')) { | |
| toggleMenu(this.closest('.menu-item-level-0').querySelector('a, button')); | |
| } | |
| } else if (!nextListItem && !event.shiftKey && !this.classList.contains('megamenu-button')) { | |
| // If tabbing past the last item of a submenu, close the submenu and move focus to the next main-level menu item. | |
| toggleMenu(this.closest('.menu-item-level-0').querySelector('a, button')); | |
| } | |
| break; | |
| } | |
| }); | |
| } | |
| /** | |
| * Toggles the menu open or closed based on the provided trigger button. | |
| * @param {HTMLElement} triggerButton - The button element that triggers the menu toggle. | |
| */ | |
| function toggleMenu(triggerButton) { | |
| // If the trigger button is the mobile menu toggle button, toggle the mobile menu. | |
| if (triggerButton.classList.contains('mobile-menu-toggle')) { | |
| // If the mobile menu is already expanded, close it. | |
| if (triggerButton.getAttribute('aria-expanded') == 'true') { | |
| triggerButton.nextElementSibling.classList.remove('menu-expanded'); | |
| triggerButton.setAttribute('aria-expanded', 'false'); | |
| } else { | |
| // If the mobile menu is closed, expand it. | |
| triggerButton.nextElementSibling.classList.add('menu-expanded'); | |
| triggerButton.setAttribute('aria-expanded', 'true'); | |
| } | |
| } else { | |
| // Submenu toggle actions. | |
| var targetSubMenu = triggerButton.nextElementSibling; | |
| // If the submenu is already expanded, close it. | |
| if (triggerButton.getAttribute('aria-expanded') == 'true') { | |
| triggerButton.setAttribute('aria-expanded', 'false'); | |
| targetSubMenu.classList.remove('submenu-expanded'); | |
| } else { | |
| // If the submenu is closed, close any other open submenus and expand the target submenu. | |
| if (!triggerButton.closest('.submenu-expanded')) { | |
| // Target all submenu buttons and close their submenus. | |
| document.querySelectorAll('.megamenu-button').forEach(function (button) { | |
| button.setAttribute('aria-expanded', 'false'); | |
| button.parentElement.querySelectorAll('.sub-menu').forEach(function (sibling) { | |
| sibling.classList.remove('submenu-expanded'); | |
| }); | |
| }); | |
| } | |
| // Expand the target submenu. | |
| triggerButton.setAttribute('aria-expanded', 'true'); | |
| targetSubMenu.classList.add('submenu-expanded'); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment