var blogoralite = blogoralite || {}; blogoralite.toggleMenu = function() { const hamburger = document.querySelector('.hamburger'); const menu = document.querySelector('#header ul'); if(hamburger && menu){ const isActive = hamburger.classList.contains('active'); hamburger.classList.toggle('active'); // keep aria-expanded in sync for accessibility hamburger.setAttribute('aria-expanded', String(!isActive)); menu.classList.toggle('active'); if (!isActive) { // Menu is opening setTimeout(function() { // Collect visible and focusable menu items and make them programmatically focusable const items = Array.from(menu.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')); function isVisible(el) { try { if (!el) return false; // Check if the element itself or any parent is hidden let current = el; while (current) { const style = window.getComputedStyle(current); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false; } current = current.parentElement; } // If element has any client rects, it's visible in layout return el.getClientRects().length > 0; } catch (e) { return false; } } const visibleItems = items.filter(el => isVisible(el) && !el.disabled); // Ensure each visible item is programmatically focusable visibleItems.forEach(el => { if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0'); }); // Always make hamburger focusable when menu is open hamburger.setAttribute('tabindex', '0'); // Focus the first visible one if (visibleItems.length) visibleItems[0].focus(); // Call focus trap after menu is visible so it can detect visible items blogoralite.focusTrap(menu); }, 100); } else { // Menu is closing hamburger.focus(); // remove temporary tabindex we added when menu opened if (hamburger && hamburger.getAttribute('tabindex') === '0') { hamburger.removeAttribute('tabindex'); } // Collapse any open submenus when the menu closes try { const openParents = menu.querySelectorAll('li.active'); openParents.forEach(function(li) { li.classList.remove('active'); const sub = li.querySelector('ul'); if (sub) sub.classList.remove('active'); const parentLink = li.querySelector('a'); if (parentLink) parentLink.setAttribute('aria-expanded', 'false'); }); } catch (e) { // ignore if menu no longer exists } } } }; blogoralite.focusTrap = function(menu) { // Helper to collect visible focusable elements at the moment of keypress function getFocusable() { // Helper to check if element is truly visible function isItemVisible(el) { if (!el || el.disabled) return false; let current = el; while (current) { const style = window.getComputedStyle(current); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || current.offsetParent === null) { return false; } if (current === menu) break; current = current.parentElement; } return true; } // Get all top-level menu items first const topLevelItems = Array.from(menu.children) .map(li => li.querySelector('a[href], button')) .filter(el => el && isItemVisible(el)); // Get all visible submenu items from active submenus const activeSubmenus = menu.querySelectorAll('li.active > ul'); const submenuItems = Array.from(activeSubmenus) .map(submenu => Array.from(submenu.querySelectorAll('a[href], button'))) .flat() .filter(el => el && isItemVisible(el)); // Combine all items in correct order: top-level items, then submenu items const allItems = [...topLevelItems, ...submenuItems]; // Add hamburger button as the last focusable item const hamburger = document.querySelector('.hamburger'); if (hamburger && menu.classList.contains('active')) { hamburger.setAttribute('tabindex', '0'); allItems.push(hamburger); } // Ensure all items are focusable allItems.forEach(el => { if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '0'); } }); return allItems; } function handleKeyDown(e) { if (!menu.classList.contains('active')) return; if (e.key === 'Escape') { e.preventDefault(); blogoralite.toggleMenu(); return; } if (e.key === 'Tab') { const focusableElements = getFocusable(); if (!focusableElements.length) return; const currentIndex = focusableElements.indexOf(document.activeElement); if (currentIndex !== -1) { e.preventDefault(); const nextIndex = e.shiftKey ? (currentIndex - 1 + focusableElements.length) % focusableElements.length : (currentIndex + 1) % focusableElements.length; focusableElements[nextIndex].focus(); } else { // If not in focusable elements, focus the first one e.preventDefault(); focusableElements[0].focus(); } } } document.addEventListener('keydown', handleKeyDown); // Remove event listener when menu closes const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (!menu.classList.contains('active')) { document.removeEventListener('keydown', handleKeyDown); observer.disconnect(); } } }); }); observer.observe(menu, { attributes: true }); }; document.addEventListener('DOMContentLoaded', function() { const hamburger = document.querySelector('.hamburger'); if(hamburger){ hamburger.addEventListener('click', blogoralite.toggleMenu); hamburger.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); blogoralite.toggleMenu(); } }); } // Allow Enter/Space on parent menu items to open/close their submenu const menu = document.querySelector('#header ul'); if (menu) { // Mark parent items and add a delegated keydown handler on the menu for Enter/Space (desktop only) const parentItems = Array.from(menu.querySelectorAll('li')).filter(li => li.querySelector('ul')); parentItems.forEach(function(li) { // find the direct child link/button (trigger) const children = Array.from(li.children); const trigger = children.find(c => c.tagName === 'A' || c.tagName === 'BUTTON'); if (!trigger) return; trigger.setAttribute('aria-haspopup', 'true'); if (!trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false'); // Add a click handler on desktop to toggle submenu (prevents navigation on parent links) trigger.addEventListener('click', function(ev) { if (!isDesktop()) return; // only override on desktop const submenu = li.querySelector('ul'); if (!submenu) return; ev.preventDefault(); ev.stopPropagation(); toggleSubmenuForTrigger(trigger); }); }); function isDesktop() { try { // primary: matchMedia breakpoint if (window.matchMedia) { if (window.matchMedia('(min-width: 768px)').matches) return true; } // fallback: use viewport width threshold if (typeof window.innerWidth === 'number') { if (window.innerWidth >= 768) return true; // treat >=768px as desktop/tablet } // final fallback: hamburger visibility const hb = document.querySelector('.hamburger'); return !(hb && hb.offsetParent !== null); } catch (err) { return false; } } // helper to toggle submenu for a trigger element function toggleSubmenuForTrigger(trigger) { const parentLi = trigger.closest('li'); if (!parentLi) return; const submenu = parentLi.querySelector('ul'); if (!submenu) return; const isOpen = parentLi.classList.contains('active') || trigger.getAttribute('aria-expanded') === 'true' || submenu.classList.contains('active'); if (!isOpen) { parentLi.classList.add('active'); submenu.classList.add('active'); trigger.setAttribute('aria-expanded', 'true'); // Make all submenu items focusable const submenuItems = submenu.querySelectorAll('a[href], button, [tabindex]:not([tabindex="-1"])'); submenuItems.forEach(item => { if (!item.hasAttribute('tabindex')) { item.setAttribute('tabindex', '0'); } }); // Focus the first item if (submenuItems.length) { submenuItems[0].focus(); } } else { parentLi.classList.remove('active'); submenu.classList.remove('active'); trigger.setAttribute('aria-expanded', 'false'); } } menu.addEventListener('keydown', function(e) { console.debug('[menu keydown]', e.key, 'target=', e.target && e.target.outerHTML && e.target.outerHTML.slice(0,120)); const key = e.key || e.keyCode; const isActivate = (key === 'Enter' || key === ' ' || key === 'Spacebar' || key === 13 || key === 32); if (!isActivate) return; // target may be a child element inside the trigger (e.g., inside ) let target = e.target; // find the nearest trigger (a or button) up the tree but still within the menu const trigger = target.closest && target.closest('a, button'); if (!trigger || !menu.contains(trigger)) return; // find parent li and its submenu const parentLi = trigger.closest('li'); if (!parentLi) return; const submenu = parentLi.querySelector('ul'); if (!submenu) return; // prevent default here to stop link navigation on Enter e.preventDefault(); // toggle on keydown for desktop if (isDesktop()) { toggleSubmenuForTrigger(trigger); } }); } });