chunk-003-mobile-menu.js

I’ll build this as a strong standalone mobile-menu chunk for RX Theme: accessible drawer, dropdown submenus, focus trap, ESC close, overlay, scroll lock, resize handling, reduced-motion support, and safe WordPress-friendly initialization.

Below is a complete advanced-level code for:

assets/static-js/static-chunks-js/chunk-003-mobile-menu.js

It is written as a safe standalone JavaScript chunk for RX Theme. It does not require jQuery. It supports accessibility, mobile drawer menu, submenu toggles, overlay, body scroll lock, ESC close, click-outside close, focus trap, keyboard navigation, active menu detection, resize reset, and WordPress admin-bar compatibility.

/*!
 * RX Theme - Mobile Menu Chunk
 * File: assets/static-js/static-chunks-js/chunk-003-mobile-menu.js
 * Version: 1.0.0
 * Author: RxHarun
 *
 * Purpose:
 * Advanced mobile menu functionality for RX Theme.
 *
 * Features:
 * - Mobile drawer open/close
 * - Overlay support
 * - Body scroll lock
 * - ESC key close
 * - Click outside close
 * - Focus trap for accessibility
 * - ARIA attributes
 * - Submenu accordion
 * - Keyboard navigation
 * - Current page active detection
 * - Resize reset for desktop
 * - WordPress admin bar offset support
 * - Reduced motion support
 * - Safe multiple-init protection
 */

(function () {
  'use strict';

  /**
   * Prevent double initialization.
   */
  if (window.RX_MOBILE_MENU_INITIALIZED) {
    return;
  }

  window.RX_MOBILE_MENU_INITIALIZED = true;

  /**
   * Main RX namespace.
   */
  window.RXTheme = window.RXTheme || {};

  /**
   * Mobile menu module.
   */
  window.RXTheme.MobileMenu = (function () {
    /**
     * Default settings.
     */
    var settings = {
      breakpoint: 1024,
      bodyOpenClass: 'rx-mobile-menu-open',
      bodyLockClass: 'rx-scroll-locked',
      menuOpenClass: 'is-open',
      overlayOpenClass: 'is-active',
      submenuOpenClass: 'is-submenu-open',
      submenuParentClass: 'menu-item-has-children',
      currentClass: 'rx-current-menu-item',
      initializedClass: 'rx-mobile-menu-ready',
      animationDuration: 300,
      focusableSelectors: [
        'a[href]',
        'button:not([disabled])',
        'input:not([disabled])',
        'select:not([disabled])',
        'textarea:not([disabled])',
        '[tabindex]:not([tabindex="-1"])'
      ].join(',')
    };

    /**
     * DOM cache.
     */
    var dom = {
      html: document.documentElement,
      body: document.body,
      toggle: null,
      menu: null,
      overlay: null,
      closeButtons: [],
      submenuToggles: [],
      firstFocusable: null,
      lastFocusable: null
    };

    /**
     * State.
     */
    var state = {
      isOpen: false,
      lastFocusedElement: null,
      scrollY: 0,
      resizeTimer: null,
      prefersReducedMotion: false
    };

    /**
     * Select first matching element from multiple selectors.
     */
    function selectOne(selectors) {
      for (var i = 0; i < selectors.length; i++) {
        var element = document.querySelector(selectors[i]);
        if (element) {
          return element;
        }
      }

      return null;
    }

    /**
     * Select all matching elements from multiple selectors.
     */
    function selectAll(selectors) {
      var results = [];

      selectors.forEach(function (selector) {
        var nodes = document.querySelectorAll(selector);

        nodes.forEach(function (node) {
          if (results.indexOf(node) === -1) {
            results.push(node);
          }
        });
      });

      return results;
    }

    /**
     * Check if current screen is mobile.
     */
    function isMobileView() {
      return window.innerWidth < settings.breakpoint;
    }

    /**
     * Check reduced motion preference.
     */
    function detectReducedMotion() {
      if (!window.matchMedia) {
        return false;
      }

      return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    }

    /**
     * Get WordPress admin bar height.
     */
    function getAdminBarHeight() {
      var adminBar = document.getElementById('wpadminbar');

      if (!adminBar) {
        return 0;
      }

      return adminBar.offsetHeight || 0;
    }

    /**
     * Apply admin bar offset to mobile menu.
     */
    function applyAdminBarOffset() {
      if (!dom.menu) {
        return;
      }

      var offset = getAdminBarHeight();

      if (offset > 0) {
        dom.menu.style.setProperty('--rx-admin-bar-offset', offset + 'px');
      } else {
        dom.menu.style.removeProperty('--rx-admin-bar-offset');
      }
    }

    /**
     * Return all focusable elements inside menu.
     */
    function getFocusableElements() {
      if (!dom.menu) {
        return [];
      }

      var elements = Array.prototype.slice.call(
        dom.menu.querySelectorAll(settings.focusableSelectors)
      );

      return elements.filter(function (element) {
        return (
          element.offsetWidth > 0 ||
          element.offsetHeight > 0 ||
          element === document.activeElement
        );
      });
    }

    /**
     * Update first and last focusable elements.
     */
    function updateFocusableElements() {
      var focusable = getFocusableElements();

      dom.firstFocusable = focusable[0] || null;
      dom.lastFocusable = focusable[focusable.length - 1] || null;
    }

    /**
     * Safely focus an element.
     */
    function safeFocus(element) {
      if (!element || typeof element.focus !== 'function') {
        return;
      }

      try {
        element.focus({ preventScroll: true });
      } catch (error) {
        element.focus();
      }
    }

    /**
     * Lock body scroll.
     */
    function lockScroll() {
      state.scrollY = window.scrollY || window.pageYOffset || 0;

      dom.body.classList.add(settings.bodyLockClass);
      dom.body.style.position = 'fixed';
      dom.body.style.top = '-' + state.scrollY + 'px';
      dom.body.style.left = '0';
      dom.body.style.right = '0';
      dom.body.style.width = '100%';
    }

    /**
     * Unlock body scroll.
     */
    function unlockScroll() {
      dom.body.classList.remove(settings.bodyLockClass);
      dom.body.style.position = '';
      dom.body.style.top = '';
      dom.body.style.left = '';
      dom.body.style.right = '';
      dom.body.style.width = '';

      window.scrollTo(0, state.scrollY || 0);
    }

    /**
     * Set ARIA states.
     */
    function setMenuAria(isOpen) {
      if (dom.toggle) {
        dom.toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
      }

      if (dom.menu) {
        dom.menu.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
      }

      if (dom.overlay) {
        dom.overlay.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
      }
    }

    /**
     * Open mobile menu.
     */
    function openMenu() {
      if (state.isOpen || !dom.menu) {
        return;
      }

      state.isOpen = true;
      state.lastFocusedElement = document.activeElement;

      applyAdminBarOffset();
      updateFocusableElements();

      dom.body.classList.add(settings.bodyOpenClass);
      dom.menu.classList.add(settings.menuOpenClass);

      if (dom.overlay) {
        dom.overlay.classList.add(settings.overlayOpenClass);
      }

      setMenuAria(true);
      lockScroll();

      window.setTimeout(function () {
        updateFocusableElements();

        if (dom.firstFocusable) {
          safeFocus(dom.firstFocusable);
        } else {
          safeFocus(dom.menu);
        }
      }, state.prefersReducedMotion ? 0 : 50);

      document.dispatchEvent(
        new CustomEvent('rxMobileMenuOpened', {
          detail: {
            menu: dom.menu
          }
        })
      );
    }

    /**
     * Close mobile menu.
     */
    function closeMenu() {
      if (!state.isOpen || !dom.menu) {
        return;
      }

      state.isOpen = false;

      dom.body.classList.remove(settings.bodyOpenClass);
      dom.menu.classList.remove(settings.menuOpenClass);

      if (dom.overlay) {
        dom.overlay.classList.remove(settings.overlayOpenClass);
      }

      setMenuAria(false);
      unlockScroll();

      if (state.lastFocusedElement) {
        safeFocus(state.lastFocusedElement);
      } else if (dom.toggle) {
        safeFocus(dom.toggle);
      }

      document.dispatchEvent(
        new CustomEvent('rxMobileMenuClosed', {
          detail: {
            menu: dom.menu
          }
        })
      );
    }

    /**
     * Toggle menu.
     */
    function toggleMenu(event) {
      if (event) {
        event.preventDefault();
      }

      if (state.isOpen) {
        closeMenu();
      } else {
        openMenu();
      }
    }

    /**
     * Close all submenus.
     */
    function closeAllSubmenus(exceptToggle) {
      dom.submenuToggles.forEach(function (toggle) {
        if (exceptToggle && toggle === exceptToggle) {
          return;
        }

        var parent = toggle.closest('.' + settings.submenuParentClass);
        var submenu = getSubmenuFromToggle(toggle);

        toggle.setAttribute('aria-expanded', 'false');

        if (parent) {
          parent.classList.remove(settings.submenuOpenClass);
        }

        if (submenu) {
          submenu.hidden = true;
          submenu.setAttribute('aria-hidden', 'true');
        }
      });
    }

    /**
     * Get submenu from toggle button.
     */
    function getSubmenuFromToggle(toggle) {
      if (!toggle) {
        return null;
      }

      var controlledId = toggle.getAttribute('aria-controls');

      if (controlledId) {
        return document.getElementById(controlledId);
      }

      var parent = toggle.closest('.' + settings.submenuParentClass);

      if (!parent) {
        return null;
      }

      return parent.querySelector(':scope > ul, :scope > .sub-menu, :scope > .children');
    }

    /**
     * Toggle submenu.
     */
    function toggleSubmenu(event) {
      event.preventDefault();

      var toggle = event.currentTarget;
      var parent = toggle.closest('.' + settings.submenuParentClass);
      var submenu = getSubmenuFromToggle(toggle);

      if (!submenu || !parent) {
        return;
      }

      var isExpanded = toggle.getAttribute('aria-expanded') === 'true';

      if (!isExpanded) {
        closeAllSubmenus(toggle);
      }

      toggle.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
      parent.classList.toggle(settings.submenuOpenClass, !isExpanded);

      submenu.hidden = isExpanded;
      submenu.setAttribute('aria-hidden', isExpanded ? 'true' : 'false');

      updateFocusableElements();

      document.dispatchEvent(
        new CustomEvent('rxMobileSubmenuToggled', {
          detail: {
            toggle: toggle,
            submenu: submenu,
            isOpen: !isExpanded
          }
        })
      );
    }

    /**
     * Create submenu toggle buttons if missing.
     */
    function enhanceSubmenus() {
      if (!dom.menu) {
        return;
      }

      var parents = dom.menu.querySelectorAll(
        '.' + settings.submenuParentClass + ', .page_item_has_children'
      );

      parents.forEach(function (parent, index) {
        var submenu = parent.querySelector(':scope > ul, :scope > .sub-menu, :scope > .children');

        if (!submenu) {
          return;
        }

        var link = parent.querySelector(':scope > a');
        var existingToggle = parent.querySelector(':scope > .rx-submenu-toggle');

        if (!submenu.id) {
          submenu.id = 'rx-mobile-submenu-' + index + '-' + Math.random().toString(36).slice(2, 8);
        }

        submenu.hidden = true;
        submenu.setAttribute('aria-hidden', 'true');

        if (existingToggle) {
          existingToggle.setAttribute('aria-expanded', 'false');
          existingToggle.setAttribute('aria-controls', submenu.id);
          return;
        }

        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'rx-submenu-toggle';
        button.setAttribute('aria-expanded', 'false');
        button.setAttribute('aria-controls', submenu.id);

        var labelText = link ? link.textContent.trim() : 'submenu';
        button.setAttribute('aria-label', 'Open submenu for ' + labelText);

        button.innerHTML =
          '<span class="rx-submenu-toggle-icon" aria-hidden="true"></span>' +
          '<span class="screen-reader-text">Open submenu</span>';

        if (link) {
          link.insertAdjacentElement('afterend', button);
        } else {
          parent.insertBefore(button, submenu);
        }
      });

      dom.submenuToggles = Array.prototype.slice.call(
        dom.menu.querySelectorAll('.rx-submenu-toggle')
      );

      dom.submenuToggles.forEach(function (toggle) {
        toggle.removeEventListener('click', toggleSubmenu);
        toggle.addEventListener('click', toggleSubmenu);
      });
    }

    /**
     * Mark active/current links.
     */
    function markCurrentLinks() {
      if (!dom.menu) {
        return;
      }

      var currentUrl = normalizeUrl(window.location.href);
      var links = dom.menu.querySelectorAll('a[href]');

      links.forEach(function (link) {
        var linkUrl = normalizeUrl(link.href);

        if (linkUrl === currentUrl) {
          link.classList.add(settings.currentClass);

          var parent = link.closest('li');

          if (parent) {
            parent.classList.add(settings.currentClass);
          }
        }
      });
    }

    /**
     * Normalize URL.
     */
    function normalizeUrl(url) {
      try {
        var parsed = new URL(url, window.location.origin);

        parsed.hash = '';

        var finalUrl = parsed.href;

        if (finalUrl.endsWith('/')) {
          finalUrl = finalUrl.slice(0, -1);
        }

        return finalUrl;
      } catch (error) {
        return url;
      }
    }

    /**
     * Trap focus inside menu.
     */
    function trapFocus(event) {
      if (!state.isOpen || event.key !== 'Tab') {
        return;
      }

      updateFocusableElements();

      if (!dom.firstFocusable || !dom.lastFocusable) {
        event.preventDefault();
        safeFocus(dom.menu);
        return;
      }

      if (event.shiftKey && document.activeElement === dom.firstFocusable) {
        event.preventDefault();
        safeFocus(dom.lastFocusable);
      } else if (!event.shiftKey && document.activeElement === dom.lastFocusable) {
        event.preventDefault();
        safeFocus(dom.firstFocusable);
      }
    }

    /**
     * Handle keyboard events.
     */
    function handleKeydown(event) {
      if (event.key === 'Escape' && state.isOpen) {
        closeMenu();
        return;
      }

      trapFocus(event);
    }

    /**
     * Handle overlay click.
     */
    function handleOverlayClick(event) {
      event.preventDefault();
      closeMenu();
    }

    /**
     * Handle outside click.
     */
    function handleDocumentClick(event) {
      if (!state.isOpen) {
        return;
      }

      var target = event.target;

      if (
        dom.menu &&
        !dom.menu.contains(target) &&
        dom.toggle &&
        !dom.toggle.contains(target)
      ) {
        closeMenu();
      }
    }

    /**
     * Close menu when a normal menu link is clicked.
     */
    function handleMenuLinkClick(event) {
      var link = event.target.closest('a[href]');

      if (!link || !dom.menu.contains(link)) {
        return;
      }

      var href = link.getAttribute('href');

      if (!href || href === '#' || href.indexOf('javascript:') === 0) {
        return;
      }

      if (isMobileView()) {
        closeMenu();
      }
    }

    /**
     * Handle resize.
     */
    function handleResize() {
      clearTimeout(state.resizeTimer);

      state.resizeTimer = window.setTimeout(function () {
        applyAdminBarOffset();

        if (!isMobileView() && state.isOpen) {
          closeMenu();
        }
      }, 150);
    }

    /**
     * Setup menu ARIA attributes.
     */
    function setupAria() {
      if (!dom.toggle || !dom.menu) {
        return;
      }

      if (!dom.menu.id) {
        dom.menu.id = 'rx-mobile-menu';
      }

      dom.toggle.setAttribute('aria-controls', dom.menu.id);
      dom.toggle.setAttribute('aria-expanded', 'false');

      if (!dom.toggle.hasAttribute('aria-label')) {
        dom.toggle.setAttribute('aria-label', 'Open mobile menu');
      }

      dom.menu.setAttribute('aria-hidden', 'true');

      if (!dom.menu.hasAttribute('tabindex')) {
        dom.menu.setAttribute('tabindex', '-1');
      }

      if (dom.overlay) {
        dom.overlay.setAttribute('aria-hidden', 'true');
      }
    }

    /**
     * Bind events.
     */
    function bindEvents() {
      if (dom.toggle) {
        dom.toggle.removeEventListener('click', toggleMenu);
        dom.toggle.addEventListener('click', toggleMenu);
      }

      dom.closeButtons.forEach(function (button) {
        button.removeEventListener('click', closeMenu);
        button.addEventListener('click', closeMenu);
      });

      if (dom.overlay) {
        dom.overlay.removeEventListener('click', handleOverlayClick);
        dom.overlay.addEventListener('click', handleOverlayClick);
      }

      if (dom.menu) {
        dom.menu.removeEventListener('click', handleMenuLinkClick);
        dom.menu.addEventListener('click', handleMenuLinkClick);
      }

      document.removeEventListener('keydown', handleKeydown);
      document.addEventListener('keydown', handleKeydown);

      document.removeEventListener('click', handleDocumentClick);
      document.addEventListener('click', handleDocumentClick);

      window.removeEventListener('resize', handleResize);
      window.addEventListener('resize', handleResize, { passive: true });

      window.removeEventListener('orientationchange', handleResize);
      window.addEventListener('orientationchange', handleResize, { passive: true });
    }

    /**
     * Cache DOM elements.
     */
    function cacheDom() {
      dom.html = document.documentElement;
      dom.body = document.body;

      dom.toggle = selectOne([
        '[data-rx-mobile-menu-toggle]',
        '.rx-mobile-menu-toggle',
        '.menu-toggle',
        '.navbar-toggle'
      ]);

      dom.menu = selectOne([
        '[data-rx-mobile-menu]',
        '.rx-mobile-menu',
        '#rx-mobile-menu',
        '.main-navigation',
        '.site-navigation'
      ]);

      dom.overlay = selectOne([
        '[data-rx-mobile-menu-overlay]',
        '.rx-mobile-menu-overlay',
        '.rx-menu-overlay'
      ]);

      dom.closeButtons = selectAll([
        '[data-rx-mobile-menu-close]',
        '.rx-mobile-menu-close',
        '.rx-menu-close'
      ]);
    }

    /**
     * Create overlay if theme does not provide one.
     */
    function maybeCreateOverlay() {
      if (dom.overlay || !dom.body) {
        return;
      }

      var overlay = document.createElement('div');
      overlay.className = 'rx-mobile-menu-overlay';
      overlay.setAttribute('data-rx-mobile-menu-overlay', '');
      overlay.setAttribute('aria-hidden', 'true');

      dom.body.appendChild(overlay);
      dom.overlay = overlay;
    }

    /**
     * Public init.
     */
    function init(customSettings) {
      if (customSettings && typeof customSettings === 'object') {
        Object.keys(customSettings).forEach(function (key) {
          settings[key] = customSettings[key];
        });
      }

      state.prefersReducedMotion = detectReducedMotion();

      cacheDom();

      if (!dom.toggle || !dom.menu) {
        return;
      }

      maybeCreateOverlay();
      setupAria();
      enhanceSubmenus();
      markCurrentLinks();
      applyAdminBarOffset();
      bindEvents();

      dom.body.classList.add(settings.initializedClass);

      document.dispatchEvent(
        new CustomEvent('rxMobileMenuReady', {
          detail: {
            toggle: dom.toggle,
            menu: dom.menu,
            overlay: dom.overlay
          }
        })
      );
    }

    /**
     * Public destroy.
     */
    function destroy() {
      if (state.isOpen) {
        closeMenu();
      }

      if (dom.toggle) {
        dom.toggle.removeEventListener('click', toggleMenu);
      }

      dom.closeButtons.forEach(function (button) {
        button.removeEventListener('click', closeMenu);
      });

      if (dom.overlay) {
        dom.overlay.removeEventListener('click', handleOverlayClick);
      }

      if (dom.menu) {
        dom.menu.removeEventListener('click', handleMenuLinkClick);
      }

      dom.submenuToggles.forEach(function (toggle) {
        toggle.removeEventListener('click', toggleSubmenu);
      });

      document.removeEventListener('keydown', handleKeydown);
      document.removeEventListener('click', handleDocumentClick);
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('orientationchange', handleResize);

      dom.body.classList.remove(settings.initializedClass);
    }

    return {
      init: init,
      open: openMenu,
      close: closeMenu,
      toggle: toggleMenu,
      destroy: destroy
    };
  })();

  /**
   * Auto init on DOM ready.
   */
  function domReady(callback) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', callback);
    } else {
      callback();
    }
  }

  domReady(function () {
    window.RXTheme.MobileMenu.init();
  });
})();

For this JavaScript to work beautifully, your theme HTML should have names like this:

<button class="rx-mobile-menu-toggle" type="button">
  Menu
</button>

<nav id="rx-mobile-menu" class="rx-mobile-menu">
  <button class="rx-mobile-menu-close" type="button">Close</button>

  <ul>
    <li><a href="/">Home</a></li>
    <li class="menu-item-has-children">
      <a href="/health/">Health</a>
      <ul class="sub-menu">
        <li><a href="/health/bone/">Bone Health</a></li>
        <li><a href="/health/eye/">Eye Health</a></li>
      </ul>
    </li>
  </ul>
</nav>

And add this basic CSS later in your mobile-menu CSS file:

.rx-mobile-menu {
  position: fixed;
  top: var(--rx-admin-bar-offset, 0);
  left: 0;
  bottom: 0;
  width: min(88vw, 360px);
  transform: translateX(-100%);
  visibility: hidden;
  overflow-y: auto;
  z-index: 99999;
  background: #fff;
  transition: transform 0.3s ease, visibility 0.3s ease;
}

.rx-mobile-menu.is-open {
  transform: translateX(0);
  visibility: visible;
}

.rx-mobile-menu-overlay {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 45%);
  opacity: 0;
  visibility: hidden;
  z-index: 99998;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.rx-mobile-menu-overlay.is-active {
  opacity: 1;
  visibility: visible;
}

.rx-scroll-locked {
  overflow: hidden;
}

.rx-submenu-toggle {
  cursor: pointer;
}

.sub-menu[hidden],
.children[hidden] {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .rx-mobile-menu,
  .rx-mobile-menu-overlay {
    transition: none;
  }
}

Best enqueue name for this file:

wp_enqueue_script(
    'rx-chunk-mobile-menu',
    get_template_directory_uri() . '/assets/static-js/static-chunks-js/chunk-003-mobile-menu.js',
    array(),
    RX_THEME_VERSION,
    true
);

Leave a Reply

Your email address will not be published. Required fields are marked *