Skip to content

Instantly share code, notes, and snippets.

@efrigoli
Last active January 9, 2025 19:52
Show Gist options
  • Select an option

  • Save efrigoli/e483462d9581503706b9536f9725a0e6 to your computer and use it in GitHub Desktop.

Select an option

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.
/*
* 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