PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/com…/mobile-menu.js?ver=ab075ba976d9

js brotherssupply.com collected 2026-10-09 00:30:21 UTC 11,262 bytes, 211 lines download raw bytes

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.