Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 88 additions & 28 deletions assets/src/js/header/setupAccessibleNavMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ const NAV_DONATE_CLASS = '.nav-donate';
const NAV_SUBMENU_CLASS = '.nav-submenu';
const SITE_LOGO_CLASS = '.site-logo';
const NAV_MENU_CLOSE_CLASS = '.nav-menu-close';
const MOBILE_NAV_ID = '#nav-main';
const PAGE_WRAPPER_ID = '#content';
const NAV_MENU_TOGGLE_CLASS = '.nav-menu-toggle';

/**
* Function to handle keyboard accessibility in the navigation menu.
*/
export const setupAccessibleNavMenu = () => {
const mainNav = document.querySelector('#nav-main-desktop');
const mobileNav = document.querySelector('#nav-main');
const mobileNav = document.querySelector(MOBILE_NAV_ID);

if (!mainNav && !mobileNav) {
return;
Expand Down Expand Up @@ -121,26 +124,38 @@ export const setupAccessibleNavMenu = () => {
}

if (mobileNav) {
const doc = mobileNav.ownerDocument;
let lastFocusedElement = null;
const isMobileMenuOpen = () => mobileNav.classList.contains('open');
/**
* Adds event listeners to create a keyboard trap between the buttons.
*/
const addKeyboardTrap = () => {
const donateBtn = mobileNav.querySelector('.btn-donate');
const closeBtn = mobileNav.querySelector(NAV_MENU_CLOSE_CLASS);
const logo = mobileNav.querySelector(SITE_LOGO_CLASS);
const focusableSelectors =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a variable here? It's only used once

'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])';

closeBtn.addEventListener('keydown', event => {
if (event.key === 'Tab' && event.shiftKey) {
event.preventDefault();
setTimeout(() => donateBtn.focus(), 5);
const focusableElements = [...mobileNav.querySelectorAll(focusableSelectors)];

if (!focusableElements.length) {
return;
}

const firstEl = focusableElements[0];
const lastEl = focusableElements[focusableElements.length - 1];

mobileNav.addEventListener('keydown', event => {
if (event.key !== 'Tab') {
return;
}
if (event.key === 'Tab') {
setTimeout(() => logo.focus(), 0);

if (event.shiftKey && doc.activeElement === firstEl) {
event.preventDefault();
lastEl.focus({preventScroll: true});
}
});
logo.addEventListener('keydown', event => {
if (event.key === 'Tab' && event.shiftKey) {
setTimeout(() => closeBtn.focus(), 0);

if (!event.shiftKey && doc.activeElement === lastEl) {
event.preventDefault();
firstEl.focus({preventScroll: true});
}
});
};
Expand All @@ -156,10 +171,18 @@ export const setupAccessibleNavMenu = () => {
return;
}

hamburgerBtn.addEventListener('keydown', event => {
if (event.key === 'Enter') {
setTimeout(() => logo.focus(), 0);
}
hamburgerBtn.addEventListener('click', () => {
lastFocusedElement = doc.activeElement;

// Wait for CSS class to apply
requestAnimationFrame(() => {
if (!isMobileMenuOpen()) {
return;
}

syncMobileNavAria(true);
logo.focus();
});
});
};

Expand All @@ -168,21 +191,25 @@ export const setupAccessibleNavMenu = () => {
*/
const focusLogoOnMenuClose = () => {
const closeBtn = mobileNav.querySelector(NAV_MENU_CLOSE_CLASS);
const hamburgerLogo = mobileNav.querySelector(SITE_LOGO_CLASS);
const mainLogo = document.querySelector(`#header ${SITE_LOGO_CLASS}`);
const toggleBtn = document.querySelector(NAV_MENU_TOGGLE_CLASS);

if (!mainLogo || !hamburgerLogo || !closeBtn) {
if (!closeBtn || !toggleBtn) {
return;
}

closeBtn.addEventListener('keydown', event => {
if (event.key === 'Enter') {
setTimeout(() => mainLogo.focus(), 0);
}
if (event.key === 'Tab' && event.shiftKey) {
setTimeout(() => hamburgerLogo.focus(), 0);
}
closeBtn.addEventListener('click', () => {
const page = document.querySelector(PAGE_WRAPPER_ID);
page.removeAttribute('aria-hidden');
page.inert = false;


requestAnimationFrame(() => {
(lastFocusedElement || toggleBtn).focus();
mobileNav.setAttribute('aria-hidden', 'true');
toggleBtn.setAttribute('aria-expanded', 'false');
});
});

};

addKeyboardTrap();
Expand All @@ -209,3 +236,36 @@ export const updateNavMenuTabIndex = () => {
];
tabbingItems.forEach(item => item.setAttribute('tabindex', menu.classList.contains('open') ? 0 : -1));
};

/**
* Function to update aria attributes for mobile navigation.
* @param {boolean} isOpen - Whether the mobile navigation is open.
*/
const syncMobileNavAria = isOpen => {
const mobileNav = document.querySelector(MOBILE_NAV_ID);
const page = document.querySelector(PAGE_WRAPPER_ID);
const toggleBtn = document.querySelector(NAV_MENU_TOGGLE_CLASS);

if (!mobileNav || !page || !toggleBtn) {
return;
}

// Mobile menu
if (isOpen) {
mobileNav.removeAttribute('aria-hidden');
} else {
mobileNav.setAttribute('aria-hidden', 'true');
}

// Page behind
if (isOpen) {
page.setAttribute('aria-hidden', 'true');
page.inert = true;
} else {
page.removeAttribute('aria-hidden');
page.inert = false;
}

// Toggle button state
toggleBtn.setAttribute('aria-expanded', String(isOpen));
};
8 changes: 7 additions & 1 deletion templates/burger-menu.twig
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<div id="nav-main" class="burger-menu d-lg-none">
<div
id="nav-main"
class="burger-menu d-lg-none"
role="dialog"
aria-modal="true"
aria-label="{{ __( 'Main navigation menu', 'planet4-master-theme' ) }}"
>
<div class="burger-menu-header">
<a
tabindex="-1"
Expand Down