From 35bd0f625aea578bc3bc86213825e107111265f4 Mon Sep 17 00:00:00 2001 From: Osong Agberndifor Date: Fri, 16 Jan 2026 11:07:43 +0100 Subject: [PATCH 1/3] PLANET-8026 Trapped focus within mobile nav menu when open - Updated codebase to trap focus with mobile menu when open --- .../src/js/header/setupAccessibleNavMenu.js | 77 +++++++++++++++---- templates/burger-menu.twig | 8 +- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/assets/src/js/header/setupAccessibleNavMenu.js b/assets/src/js/header/setupAccessibleNavMenu.js index 2126d84d0b..17bf9bcb12 100644 --- a/assets/src/js/header/setupAccessibleNavMenu.js +++ b/assets/src/js/header/setupAccessibleNavMenu.js @@ -5,6 +5,9 @@ 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. @@ -121,6 +124,9 @@ 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. */ @@ -156,10 +162,16 @@ 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()) { + syncMobileNavAria(true); + logo.focus(); + } + }); }); }; @@ -168,21 +180,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(); @@ -209,3 +225,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)); +}; diff --git a/templates/burger-menu.twig b/templates/burger-menu.twig index f52b7d0eef..e9b888dadb 100644 --- a/templates/burger-menu.twig +++ b/templates/burger-menu.twig @@ -1,4 +1,10 @@ -