1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 MOBILE MENU â widget behavior (Phase-2 widget system; moved out of main.js) 3 4 Wave-8 template/nav chrome. The markup is ENGINE-RENDERED and FROZEN: 5 [hzo_mobile_trigger] emits the hamburger (.hzo-mobile-trigger, in the header) 6 and [hzo_mobile_menu] emits the slide-in overlay (.hzo-mobile-menu#hzo-mobile-menu, 7 role="dialog" aria-modal, with L1 .hzo-mobile-accordion / L2 .hzo-mobile-subaccordion 8 accordions) â see includes/structural-shortcodes.php + parts/header.html. This 9 file only carries the BEHAVIOR that used to live in the nav-chrome IIFE of 10 assets/js/main.js (open/close, accordion toggles, inert sync, blur-after-touch). 11 12 Behavior is byte-for-byte the legacy behavior: this is header-critical chrome 13 on every page in the fleet. The default (attribute-free) markup runs exactly 14 as it did in main.js. The one tunable legacy constant â the delay before 15 accordions reset after the slide-out â is exposed as an opt-in data-attribute 16 that DEFAULTS to the legacy value: 17 18 data-hzo-mobile-menu-reset-delay ms to wait after closing before 19 collapsing expanded accordions (so the 20 user doesn't see them snap shut mid 21 slide-out). Absent/invalid = 300, the 22 legacy hardcoded value. 23 24 MOTION: this component introduces NO animation of its own â the slide/fade 25 transitions live in mobile-menu.css (unchanged). So there is nothing here for 26 prefers-reduced-motion to suppress; the default path stays identical. 27 28 A11y (WAI-ARIA APG Dialog): the overlay carries role="dialog" + aria-modal in 29 the markup; while open, focus is trapped (hzo.trapFocus), Esc closes, focus 30 returns to the hamburger on close, and collapsed accordion panels are marked 31 `inert` so their clipped links leave the tab order (WCAG 2.4.3). The hamburger 32 reflects state via aria-expanded. 33 34 SHARED PRIMITIVE, GUARDED AT EVENT TIME: hzo.trapFocus lives in main.js, which 35 is enqueued conditionally â so this script NEVER hard-depends on 36 hozio-studio-main. If trapFocus is missing at open time the menu still opens 37 (navigation must not break); it simply opens without the Tab trap, matching the 38 legacy effective outcome (which threw only AFTER the overlay was already shown). 39 40 Public API (the old main.js IIFE exposed no window.hzo names â nothing 41 historical to preserve): 42 hzo.mobileMenu = { init, setup } (init(scope) for dynamically-injected DOM) 43 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 44(function () { 45 'use strict'; 46 47 /* Per-root wiring â idempotent via the ready guard. All handlers close over 48 this root, so multiple menus (should the engine ever render more than one) 49 stay independent; the single-instance default path is identical to legacy. */ 50 function setup(overlay) { 51 if (!overlay || overlay.getAttribute('data-hzo-mobile-menu-ready') === 'true') { return; } 52 overlay.setAttribute('data-hzo-mobile-menu-ready', 'true'); 53 54 // The trigger lives OUTSIDE the overlay (in the header) and points back 55 // via aria-controls; fall back to the lone .hzo-mobile-trigger (legacy 56 // selector) when there's no id to match. 57 var hamburger = null; 58 if (overlay.id) { 59 hamburger = document.querySelector('.hzo-mobile-trigger[aria-controls="' + overlay.id + '"]'); 60 } 61 if (!hamburger) { hamburger = document.querySelector('.hzo-mobile-trigger'); } 62 63 var closeBtn = overlay.querySelector('.hzo-mobile-close'); 64 var mobileTrap = null; 65 66 var resetDelay = parseInt(overlay.getAttribute('data-hzo-mobile-menu-reset-delay'), 10); 67 if (isNaN(resetDelay) || resetDelay < 0) { resetDelay = 300; } 68 69 // Collapsed accordion panels are clipped (max-height:0;
69 overflow:hidden) 70 // but their links stay in the DOM and would otherwise be Tab-reachable 71 // inside the focus trap. Mark collapsed panels `inert` so only visible 72 // controls are tabbable. (inert degrades to a harmless no-op where 73 // unsupported.) 74 function syncMobileInert() { 75 var panels = overlay.querySelectorAll('.hzo-mobile-panel'); 76 for (var p = 0; p < panels.length; p++) { 77 var acc = panels[p].closest('.hzo-mobile-accordion, .hzo-mobile-subaccordion'); 78 panels[p].inert = !(acc && acc.classList.contains('is-expanded')); 79 } 80 } 81 82 function openMobile() { 83 overlay.classList.add('is-open'); 84 document.body.classList.add('hzo-menu-open'); 85 if (hamburger) { hamburger.setAttribute('aria-expanded', 'true'); } 86 syncMobileInert(); 87 // role="dialog" + aria-modal in the markup don't trap Tab on their 88 // own â this closes the quality-rules §2.7 focus-management gap. 89 // Guarded: trapFocus (main.js) is enqueued conditionally, so never a 90 // hard dep â the menu still opens if it's missing (nav must not break). 91 if (window.hzo && typeof window.hzo.trapFocus === 'function') { 92 mobileTrap = window.hzo.trapFocus(overlay, { 93 onEscape: closeMobile, 94 initialFocus: closeBtn || overlay, 95 returnFocus: hamburger 96 }); 97 } 98 } 99 100 function resetMobileAccordions() { 101 var expanded = overlay.querySelectorAll('.hzo-mobile-accordion.is-expanded, .hzo-mobile-subaccordion.is-expanded'); 102 for (var i = 0; i < expanded.length; i++) { 103 expanded[i].classList.remove('is-expanded'); 104 var btn = expanded[i].querySelector('.hzo-mobile-toggle, .hzo-mobile-subtoggle'); 105 if (btn) { btn.setAttribute('aria-expanded', 'false'); } 106 } 107 syncMobileInert(); 108 } 109 110 function closeMobile() { 111 overlay.classList.remove('is-open'); 112 document.body.classList.remove('hzo-menu-open'); 113 if (hamburger) { hamburger.setAttribute('aria-expanded', 'false'); } 114 if (mobileTrap) { mobileTrap(); mobileTrap = null; } // release() returns focus to the trigger 115 // Reset accordions after the slide-out animation. 116 setTimeout(resetMobileAccordions, resetDelay); 117 } 118 119 if (hamburger) { hamburger.addEventListener('click', openMobile); } 120 if (closeBtn) { closeBtn.addEventListener('click', closeMobile); } 121 122 // Close on overlay backdrop click. 123 overlay.addEventListener('click', function (e) { 124 if (e.target === overlay) { closeMobile(); } 125 }); 126 127 /* âââ Mobile Accordions (Level 1) âââââââââââââââââââââââââââââââââââ */ 128 var toggles = overlay.querySelectorAll('.hzo-mobile-toggle'); 129 for (var i = 0; i < toggles.length; i++) { 130 toggles[i].addEventListener('click', function () { 131 var accordion = this.closest('.hzo-mobile-accordion'); 132 if (!accordion) { return; } 133 var isExpanded = accordion.classList.contains('is-expanded'); 134 135 // Close siblings and their sub-accordions. 136 var siblings = accordion.parentElement.querySelectorAll('.hzo-mobile-accordion.is-expanded'); 137 for (var j = 0; j < siblings.length; j++) { 138 siblings[j].classList.remove('is-expanded'); 139 var sibBtn = siblings[j].querySelector('.hzo-mobile-toggle'); 140 if (sibBtn) { sibBtn.setAttribute('aria-expanded', 'false'); } 141 var subExpanded = siblings[j].querySelectorAll('.hzo-mobile-subaccordion.is-expanded'); 142 for (var s = 0; s < subExpanded.length; s++) { 143 subExpanded[s].classList.remove('is-expanded'); 144 var subBtn = subExpanded[s].querySelector('.hzo-mobile-subtoggle'); 145 if (subBtn) { subBtn.setAttribute('aria-expanded', 'false'); } 146 } 147 } 148 149 // Toggle current. 150 if (!isExpanded) { 151 accordion.classList.add('is-expanded'); 152 this.setAttribute('aria-expanded', 'true'); 153 } 154 syncMobileInert(); 155 }); 156 } 157 158 /* âââ Mobile Sub-Accordions (Level 2) âââââââââââââââââââââââââââââââ */ 159 var subToggles = overlay.querySelectorAll('.hzo-mobile-subtoggle'); 160 for (var k = 0; k < subToggles.length; k++) { 161 subToggles[k].addEventListener('click', function () { 162 var subAccordion = this.closest('.hzo-mobile-subaccordion'); 163 if (!subAccordion) { return; } 164 var isExpanded = subAccordion.classList.contains('is-expanded'); 165 166 var siblings = subAccordion.parentElement.querySelectorAll('.hzo-mobile-subaccordion.is-expanded'); 167 for (var l = 0; l < siblings.length; l++) { 168 siblings[l].classList.remove('is-expanded'); 169 var sibBtn = siblings[l].querySelector('.hzo-mobile-subtoggle'); 170 if (sibBtn) { sibBtn.setAttribute('aria-expanded', 'false'); } 171 } 172 173 if (!isExpanded) { 174 subAccordion.classList.add('is-expanded'); 175 this.setAttribute('aria-expanded', 'true'); 176 } 177 syncMobileInert(); 178 }); 179 } 180 181 /* âââ Blur after every touch to prevent sticky :focus âââââââââââââââ */ 182 var mobileNav = overlay.querySelector('.hzo-mobile-nav'); 183 if (mobileNav) { 184 mobileNav.addEventListener('touchend', function () { 185 setTimeout(function () { 186 if (document.activeElement && document.activeElement !== document.body) { 187 document.activeElement.blur(); 188 } 189 }, 50); 190 }); 191 } 192 193 // Initial inert state (every accordion panel starts collapsed). 194 syncMobileInert(); 195 } 196 197 function init(scope) { 198 var menus = (scope || document).querySelectorAll('.hzo-mobile-menu'); 199 for (var m = 0; m < menus.length; m++) { setup(menus[m]); } 200 } 201 202 if (document.readyState === 'loading') { 203 document.addEventListener('DOMContentLoaded', function () { init(); }); 204 } else { 205 init(); 206 } 207 208 // Shared engine namespace (never a new top-level global). 209 window.hzo = window.hzo || {}; 210 window.hzo.mobileMenu = { init: init, setup: setup }; 211})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.