/**
* Mobile Menu JS - Topbar + Slide-in Panel
* BeShop - Accessible Mobile Navigation
*
* Features:
* - Hamburger toggle with icon swap
* - Slide-in panel with smooth animation
* - Overlay with backdrop blur
* - Dropdown toggles for submenus (injected via JS)
* - Full keyboard navigation (Tab, Escape, Arrow keys, Home/End)
* - Focus trap inside open panel
* - Automatic submenu open on toggle focus (Tab)
* - Focus + Click conflict prevention
* - Body scroll lock when panel is open
* - Resize listener to auto-close on desktop
*
* @package BeShop
*/
(function () {
'use strict';
/* ===========================================
Cache DOM elements
=========================================== */
const menuBar = document.getElementById('wsm-menu');
const openBtn = document.getElementById('mmenu-btn');
const closeBtn = document.getElementById('mmenu-close-btn');
const panel = document.getElementById('mobile-menu-panel');
const overlay = document.getElementById('mobile-menu-overlay');
const navContainer = document.getElementById('mobile-navigation');
const menuList = document.getElementById('wsm-menu-ul');
// Bail early if essential elements are missing
if (!menuBar || !openBtn || !panel || !overlay) {
return;
}
/* ===========================================
Inject Dropdown Toggle Buttons
=========================================== */
function injectDropdownToggles() {
if (!menuList) return;
const parents = menuList.querySelectorAll(
'.menu-item-has-children, .page_item_has_children'
);
parents.forEach(function (item) {
// Skip if toggle already exists
if (item.querySelector('.submenu-toggle')) return;
var toggle = document.createElement('button');
toggle.className = 'submenu-toggle';
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-label', 'Toggle submenu');
toggle.innerHTML =
'';
// Insert toggle after the link
var link = item.querySelector(':scope > a');
if (link && link.nextSibling) {
item.insertBefore(toggle, link.nextSibling);
} else {
item.appendChild(toggle);
}
});
}
/* ===========================================
Submenu Open / Close Helpers
=========================================== */
function openSubmenu(parentLi) {
parentLi.classList.add('submenu-open');
var toggle = parentLi.querySelector(':scope > .submenu-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', 'true');
}
}
function closeSubmenu(parentLi) {
parentLi.classList.remove('submenu-open');
var toggle = parentLi.querySelector(':scope > .submenu-toggle');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
// Also close any nested open submenus
var nested = parentLi.querySelectorAll('.submenu-open');
nested.forEach(function (n) {
n.classList.remove('submenu-open');
var t = n.querySelector(':scope > .submenu-toggle');
if (t) t.setAttribute('aria-expanded', 'false');
});
}
function closeSiblingSubmenus(currentLi) {
var parent = currentLi.parentElement;
if (!parent) return;
var siblings = parent.querySelectorAll(':scope > .submenu-open');
siblings.forEach(function (sib) {
if (sib !== currentLi) {
closeSubmenu(sib);
}
});
}
function closeAllSubmenus() {
if (!menuList) return;
var openItems = menuList.querySelectorAll('.submenu-open');
openItems.forEach(function (item) {
item.classList.remove('submenu-open');
var t = item.querySelector(':scope > .submenu-toggle');
if (t) t.setAttribute('aria-expanded', 'false');
});
}
/* ===========================================
Dropdown Toggle: Click Handler
=========================================== */
function handleToggleClick(e) {
var toggle = e.target.closest('.submenu-toggle');
if (!toggle) return;
e.preventDefault();
e.stopPropagation();
var parentLi = toggle.closest(
'.menu-item-has-children, .page_item_has_children'
);
if (!parentLi) return;
// If focus just opened this submenu, don't close it
if (parentLi._focusJustOpened) {
return;
}
closeSiblingSubmenus(parentLi);
if (parentLi.classList.contains('submenu-open')) {
closeSubmenu(parentLi);
} else {
openSubmenu(parentLi);
}
}
/* ===========================================
Dropdown Toggle: Focus Handler (Tab key)
=========================================== */
function handleToggleFocus(e) {
var toggle = e.target.closest('.submenu-toggle');
if (!toggle) return;
var parentLi = toggle.closest(
'.menu-item-has-children, .page_item_has_children'
);
if (!parentLi) return;
// Close sibling submenus
closeSiblingSubmenus(parentLi);
// Auto-open submenu on focus (Tab navigation)
if (!parentLi.classList.contains('submenu-open')) {
openSubmenu(parentLi);
// Set flag to prevent click from closing it
parentLi._focusJustOpened = true;
setTimeout(function () {
parentLi._focusJustOpened = false;
}, 300);
}
}
/* ===========================================
Open / Close Panel
=========================================== */
function openMenu() {
panel.classList.add('panel-open');
overlay.classList.add('active');
menuBar.classList.add('menu-open');
document.body.classList.add('mobile-menu-is-open');
openBtn.setAttribute('aria-expanded', 'true');
panel.setAttribute('aria-hidden', 'false');
// Focus the close button after panel opens
setTimeout(function () {
if (closeBtn) closeBtn.focus();
}, 100);
}
function closeMenu() {
panel.classList.remove('panel-open');
overlay.classList.remove('active');
menuBar.classList.remove('menu-open');
document.body.classList.remove('mobile-menu-is-open');
openBtn.setAttribute('aria-expanded', 'false');
panel.setAttribute('aria-hidden', 'true');
// Close all open submenus
closeAllSubmenus();
// Return focus to hamburger button
openBtn.focus();
}
/* ===========================================
Focus Trap
=========================================== */
function getFocusableElements() {
var selectors = [
'a[href]',
'button:not([disabled]):not([tabindex="-1"])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
var elements = panel.querySelectorAll(selectors);
return Array.from(elements).filter(function (el) {
return el.offsetParent !== null;
});
}
/* ===========================================
Keyboard Handler
=========================================== */
function handleKeydown(e) {
if (!panel.classList.contains('panel-open')) return;
var key = e.key;
// Escape → close menu
if (key === 'Escape') {
e.preventDefault();
closeMenu();
return;
}
var focusable = getFocusableElements();
if (focusable.length === 0) return;
var currentIndex = focusable.indexOf(document.activeElement);
var firstEl = focusable[0];
var lastEl = focusable[focusable.length - 1];
// Tab / Shift+Tab → focus trap
if (key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstEl || currentIndex <= 0) {
e.preventDefault();
lastEl.focus();
}
} else {
if (document.activeElement === lastEl || currentIndex >= focusable.length - 1) {
e.preventDefault();
firstEl.focus();
}
}
return;
}
// Arrow keys for menu navigation
if (key === 'ArrowDown') {
e.preventDefault();
if (currentIndex < focusable.length - 1) {
focusable[currentIndex + 1].focus();
} else {
firstEl.focus();
}
return;
}
if (key === 'ArrowUp') {
e.preventDefault();
if (currentIndex > 0) {
focusable[currentIndex - 1].focus();
} else {
lastEl.focus();
}
return;
}
// Home / End
if (key === 'Home') {
e.preventDefault();
firstEl.focus();
return;
}
if (key === 'End') {
e.preventDefault();
lastEl.focus();
return;
}
}
/* ===========================================
Event Listeners
=========================================== */
// Open button
openBtn.addEventListener('click', function (e) {
e.preventDefault();
if (panel.classList.contains('panel-open')) {
closeMenu();
} else {
openMenu();
}
});
// Close button
if (closeBtn) {
closeBtn.addEventListener('click', function (e) {
e.preventDefault();
closeMenu();
});
}
// Overlay click → close
overlay.addEventListener('click', function () {
closeMenu();
});
// Keyboard events
document.addEventListener('keydown', handleKeydown);
// Submenu toggle clicks
if (navContainer) {
navContainer.addEventListener('click', handleToggleClick);
navContainer.addEventListener('focusin', handleToggleFocus);
}
// Auto-close on resize to desktop
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
if (window.innerWidth > 992 && panel.classList.contains('panel-open')) {
closeMenu();
}
}, 150);
});
/* ===========================================
Initialize
=========================================== */
injectDropdownToggles();
})();