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'); }); // Also collapse third-level submenus const openThirdLevel = menu.querySelectorAll('li ul li.active'); openThirdLevel.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'); }); // Also collapse fourth-level submenus const openFourthLevel = menu.querySelectorAll('li ul li ul li.active'); openFourthLevel.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 blogoralite_getFocusable() { // Helper to check if element is truly visible function blogoralite_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; } const allItems = []; function blogoralite_traverseMenu(ul) { Array.from(ul.children).forEach(li => { const link = li.querySelector('a[href], button'); if (link && blogoralite_isItemVisible(link)) { allItems.push(link); } // If submenu is open, traverse it const blogoralite_submenu = li.querySelector('ul'); if (blogoralite_submenu && li.classList.contains('active')) { blogoralite_traverseMenu(blogoralite_submenu); } }); } blogoralite_traverseMenu(menu); // Add hamburger button as the last focusable item const blogoralite_hamburger = document.querySelector('.hamburger'); if (blogoralite_hamburger && menu.classList.contains('active')) { blogoralite_hamburger.setAttribute('tabindex', '0'); allItems.push(blogoralite_hamburger); } // Ensure all items are focusable allItems.forEach(el => { if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '0'); } }); return allItems; } function blogoralite_handleKeyDown(e) { if (!menu.classList.contains('active')) return; if (e.key === 'Escape') { e.preventDefault(); blogoralite.toggleMenu(); return; } if (e.key === 'Tab') { const focusableElements = blogoralite_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', blogoralite_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', blogoralite_handleKeyDown); observer.disconnect(); } } }); }); observer.observe(menu, { attributes: true }); }; blogoralite.desktopMenuKeyboardNav = function(menu) { function blogoralite_getFocusable() { // Helper to check if element is visible function blogoralite_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; } const allItems = []; function blogoralite_traverseMenu(ul) { Array.from(ul.children).forEach(li => { const link = li.querySelector('a[href], button'); if (link && blogoralite_isItemVisible(link)) { allItems.push(link); } // If submenu is open, traverse it const submenu = li.querySelector('ul'); if (submenu && (li.classList.contains('active') || li.classList.contains('hover'))) { blogoralite_traverseMenu(submenu); } }); } blogoralite_traverseMenu(menu); return allItems; } function blogoralite_NextFocusableMenu() { const allFocusable = Array.from(document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')).filter(el => !el.disabled && el.offsetParent !== null); const menuFocusable = blogoralite_getFocusable(); if (!menuFocusable.length) return null; const lastMenuIndex = allFocusable.indexOf(menuFocusable[menuFocusable.length - 1]); if (lastMenuIndex !== -1 && lastMenuIndex + 1 < allFocusable.length) { return allFocusable[lastMenuIndex + 1]; } return null; } function blogoralite_handleKeyDown(e) { if (e.key === 'Tab') { const focusableElements = blogoralite_getFocusable(); if (!focusableElements.length) return; // Handle Tab within the menu if (menu.contains(document.activeElement)) { const currentIndex = focusableElements.indexOf(document.activeElement); if (currentIndex !== -1) { const isLastItem = currentIndex === focusableElements.length - 1; const isFirstItem = currentIndex === 0; if (e.shiftKey && isFirstItem) { // Shift+Tab on first item: allow focus to move to previous element outside menu return; // Do not prevent default, let natural tab order take over } else if (!e.shiftKey && isLastItem) { // Tab on last item: allow focus to move to next element outside menu return; // Do not prevent default, let natural tab order take over } else { // Cycle within menu e.preventDefault(); const nextIndex = e.shiftKey ? (currentIndex - 1 + focusableElements.length) % focusableElements.length : (currentIndex + 1) % focusableElements.length; focusableElements[nextIndex].focus(); } } } // Handle Shift+Tab from the element immediately after the menu else if (e.shiftKey && document.activeElement === blogoralite_NextFocusableMenu()) { e.preventDefault(); focusableElements[focusableElements.length - 1].focus(); } } } document.addEventListener('keydown', blogoralite_handleKeyDown); }; 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 to toggle submenu (prevents navigation on parent links) trigger.addEventListener('click', function(ev) { const submenu = li.querySelector('ul'); if (!submenu) return; ev.preventDefault(); ev.stopPropagation(); blogoralite_toggleSubmenuForTrigger(trigger); }); // Also handle third-level submenus const subParentItems = Array.from(li.querySelectorAll('li')).filter(subLi => subLi.querySelector('ul')); subParentItems.forEach(function(subLi) { const subChildren = Array.from(subLi.children); const subTrigger = subChildren.find(c => c.tagName === 'A' || c.tagName === 'BUTTON'); if (!subTrigger) return; subTrigger.setAttribute('aria-haspopup', 'true'); if (!subTrigger.hasAttribute('aria-expanded')) subTrigger.setAttribute('aria-expanded', 'false'); subTrigger.addEventListener('click', function(ev) { if (!isDesktop()) return; const subSubmenu = subLi.querySelector('ul'); if (!subSubmenu) return; ev.preventDefault(); ev.stopPropagation(); blogoralite_toggleSubmenuForTrigger(subTrigger); }); // Also handle fourth-level submenus const subSubParentItems = Array.from(subLi.querySelectorAll('li')).filter(subSubLi => subSubLi.querySelector('ul')); subSubParentItems.forEach(function(subSubLi) { const subSubChildren = Array.from(subSubLi.children); const subSubTrigger = subSubChildren.find(c => c.tagName === 'A' || c.tagName === 'BUTTON'); if (!subSubTrigger) return; subSubTrigger.setAttribute('aria-haspopup', 'true'); if (!subSubTrigger.hasAttribute('aria-expanded')) subSubTrigger.setAttribute('aria-expanded', 'false'); subSubTrigger.addEventListener('click', function(ev) { const subSubSubmenu = subSubLi.querySelector('ul'); if (!subSubSubmenu) return; ev.preventDefault(); ev.stopPropagation(); blogoralite_toggleSubmenuForTrigger(subSubTrigger); }); }); }); }); function blogoralite_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 blogoralite_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) { // Close any open nested submenus const nestedLis = submenu.querySelectorAll('li.active'); nestedLis.forEach(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'); }); 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 and mobile if (blogoralite_isDesktop()) { blogoralite_toggleSubmenuForTrigger(trigger); } else { blogoralite_toggleSubmenuForTrigger(trigger); } }); // Add desktop keyboard navigation if hamburger is hidden (desktop/tablet) if (hamburger && hamburger.offsetParent === null) { blogoralite.desktopMenuKeyboardNav(menu); } } });