@hortonstudio/main 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,98 +1,15 @@
1
- // Global accessibility state
2
- let supportsInert = null;
3
- let screenReaderLiveRegion = null;
4
-
5
1
  export const init = () => {
6
- // Ensure DOM is ready before initializing
7
- if (document.readyState === "loading") {
8
- document.addEventListener("DOMContentLoaded", initializeNavbar);
9
- } else {
10
- initializeNavbar();
11
- }
2
+ initializeNavbar();
12
3
  return { result: "navbar initialized" };
13
4
  };
14
5
 
15
6
  function initializeNavbar() {
16
- setupAccessibilityFeatures();
17
7
  setupDynamicDropdowns();
18
- setupMobileMenuButton();
19
- setupMobileMenuARIA();
20
- setupMobileMenuBreakpointHandler();
8
+ setupMenuButton();
9
+ setupMenuARIA();
10
+ setupMenuDisplayObserver();
21
11
  }
22
12
 
23
- // Accessibility features setup
24
- function setupAccessibilityFeatures() {
25
- // Check inert support once
26
- supportsInert = "inert" in HTMLElement.prototype;
27
-
28
- // Create screen reader live region only if body exists
29
- if (document.body) {
30
- screenReaderLiveRegion = document.createElement("div");
31
- screenReaderLiveRegion.setAttribute("aria-live", "polite");
32
- screenReaderLiveRegion.setAttribute("aria-atomic", "true");
33
- screenReaderLiveRegion.className = "u-sr-only";
34
- screenReaderLiveRegion.style.cssText =
35
- "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;";
36
- document.body.appendChild(screenReaderLiveRegion);
37
- }
38
- }
39
-
40
- // Inert polyfill for browsers that don't support it
41
- function setElementInert(element, isInert) {
42
- if (supportsInert) {
43
- element.inert = isInert;
44
- } else {
45
- // Polyfill: manage tabindex for all focusable elements
46
- const focusableSelectors =
47
- 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
48
- const focusableElements = element.querySelectorAll(focusableSelectors);
49
-
50
- if (isInert) {
51
- // Store original tabindex values and disable
52
- focusableElements.forEach((el) => {
53
- const currentTabindex = el.getAttribute("tabindex");
54
- el.setAttribute("data-inert-tabindex", currentTabindex || "0");
55
- el.setAttribute("tabindex", "-1");
56
- });
57
- element.setAttribute("data-inert", "true");
58
- } else {
59
- // Restore original tabindex values
60
- focusableElements.forEach((el) => {
61
- const originalTabindex = el.getAttribute("data-inert-tabindex");
62
- if (originalTabindex === "0") {
63
- el.removeAttribute("tabindex");
64
- } else if (originalTabindex) {
65
- el.setAttribute("tabindex", originalTabindex);
66
- }
67
- el.removeAttribute("data-inert-tabindex");
68
- });
69
- element.removeAttribute("data-inert");
70
- }
71
- }
72
- }
73
-
74
- // Screen reader announcements
75
- function announceToScreenReader(message) {
76
- if (screenReaderLiveRegion) {
77
- screenReaderLiveRegion.textContent = message;
78
-
79
- // Clear after a delay to allow for repeat announcements
80
- setTimeout(() => {
81
- screenReaderLiveRegion.textContent = "";
82
- }, 1000);
83
- }
84
- }
85
-
86
- // Extract menu name from element text or aria-label
87
- function getMenuName(element) {
88
- const text =
89
- (element.textContent && element.textContent.trim()) ||
90
- element.getAttribute("aria-label") ||
91
- "menu";
92
- return text
93
- .replace(/^(Open|Close)\s+/i, "")
94
- .replace(/\s+(menu|navigation)$/i, "");
95
- }
96
13
 
97
14
  // Desktop dropdown system
98
15
  function setupDynamicDropdowns() {
@@ -111,8 +28,6 @@ function setupDynamicDropdowns() {
111
28
 
112
29
  dropdownWrappers.forEach((wrapper) => {
113
30
  const toggle = wrapper.querySelector("a");
114
- if (!toggle) return;
115
-
116
31
  const allElements = wrapper.querySelectorAll("*");
117
32
  let dropdownList = null;
118
33
 
@@ -124,10 +39,7 @@ function setupDynamicDropdowns() {
124
39
  }
125
40
  }
126
41
 
127
- if (!dropdownList) return;
128
-
129
- const toggleText =
130
- (toggle.textContent && toggle.textContent.trim()) || "dropdown";
42
+ const toggleText = toggle.textContent?.trim() || "dropdown";
131
43
  const sanitizedText = sanitizeForID(toggleText);
132
44
  const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
133
45
  const listId = `navbar-dropdown-${sanitizedText}-list`;
@@ -160,10 +72,6 @@ function setupDynamicDropdowns() {
160
72
  item.setAttribute("tabindex", "0");
161
73
  });
162
74
 
163
- // Announce to screen readers
164
- const menuName = getMenuName(toggle);
165
- announceToScreenReader(`${menuName} menu opened`);
166
-
167
75
  const clickEvent = new MouseEvent("click", {
168
76
  bubbles: true,
169
77
  cancelable: true,
@@ -186,10 +94,6 @@ function setupDynamicDropdowns() {
186
94
  item.setAttribute("tabindex", "-1");
187
95
  });
188
96
 
189
- // Announce to screen readers
190
- const menuName = getMenuName(toggle);
191
- announceToScreenReader(`${menuName} menu closed`);
192
-
193
97
  const clickEvent = new MouseEvent("click", {
194
98
  bubbles: true,
195
99
  cancelable: true,
@@ -200,40 +104,13 @@ function setupDynamicDropdowns() {
200
104
 
201
105
  wrapper.addEventListener("mouseenter", () => {
202
106
  if (!isOpen) {
203
- const clickEvent = new MouseEvent("click", {
204
- bubbles: true,
205
- cancelable: true,
206
- view: window,
207
- });
208
- wrapper.dispatchEvent(clickEvent);
209
- closeAllDropdowns(wrapper);
210
- isOpen = true;
211
- toggle.setAttribute("aria-expanded", "true");
212
- dropdownList.setAttribute("aria-hidden", "false");
213
- menuItems.forEach((item) => {
214
- item.setAttribute("tabindex", "0");
215
- });
107
+ openDropdown();
216
108
  }
217
109
  });
218
110
 
219
111
  wrapper.addEventListener("mouseleave", () => {
220
112
  if (isOpen) {
221
- if (dropdownList.contains(document.activeElement)) {
222
- toggle.focus();
223
- }
224
- const clickEvent = new MouseEvent("click", {
225
- bubbles: true,
226
- cancelable: true,
227
- view: window,
228
- });
229
- wrapper.dispatchEvent(clickEvent);
230
- isOpen = false;
231
- toggle.setAttribute("aria-expanded", "false");
232
- dropdownList.setAttribute("aria-hidden", "true");
233
- menuItems.forEach((item) => {
234
- item.setAttribute("tabindex", "-1");
235
- });
236
- currentMenuItemIndex = -1;
113
+ closeDropdown();
237
114
  }
238
115
  });
239
116
 
@@ -249,19 +126,18 @@ function setupDynamicDropdowns() {
249
126
  } else {
250
127
  if (currentMenuItemIndex === menuItems.length - 1) {
251
128
  const nextElement =
252
- (wrapper.nextElementSibling &&
253
- wrapper.nextElementSibling.querySelector("a, button")) ||
254
- document.querySelector(
255
- ".navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button",
256
- );
129
+ wrapper.nextElementSibling &&
130
+ wrapper.nextElementSibling.querySelector("a, button");
257
131
  if (nextElement) {
258
132
  closeDropdown();
259
133
  nextElement.focus();
260
134
  return;
261
135
  }
262
136
  }
263
- currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
264
- menuItems[currentMenuItemIndex].focus();
137
+ if (currentMenuItemIndex < menuItems.length - 1) {
138
+ currentMenuItemIndex = currentMenuItemIndex + 1;
139
+ menuItems[currentMenuItemIndex].focus();
140
+ }
265
141
  }
266
142
  } else if (e.key === "ArrowUp") {
267
143
  e.preventDefault();
@@ -283,11 +159,10 @@ function setupDynamicDropdowns() {
283
159
  return;
284
160
  }
285
161
  }
286
- currentMenuItemIndex =
287
- currentMenuItemIndex <= 0
288
- ? menuItems.length - 1
289
- : currentMenuItemIndex - 1;
290
- menuItems[currentMenuItemIndex].focus();
162
+ if (currentMenuItemIndex > 0) {
163
+ currentMenuItemIndex = currentMenuItemIndex - 1;
164
+ menuItems[currentMenuItemIndex].focus();
165
+ }
291
166
  }
292
167
  } else if (e.key === "Tab") {
293
168
  if (e.shiftKey) {
@@ -300,16 +175,11 @@ function setupDynamicDropdowns() {
300
175
  if (document.activeElement === menuItems[menuItems.length - 1]) {
301
176
  e.preventDefault();
302
177
  const nextElement =
303
- (wrapper.nextElementSibling &&
304
- wrapper.nextElementSibling.querySelector("a, button")) ||
305
- document.querySelector(
306
- ".navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button",
307
- );
178
+ wrapper.nextElementSibling &&
179
+ wrapper.nextElementSibling.querySelector("a, button");
308
180
  closeDropdown();
309
181
  if (nextElement) {
310
- setTimeout(() => {
311
- nextElement.focus();
312
- }, 10);
182
+ nextElement.focus();
313
183
  }
314
184
  }
315
185
  }
@@ -335,8 +205,10 @@ function setupDynamicDropdowns() {
335
205
  e.preventDefault();
336
206
  openDropdown();
337
207
  if (menuItems.length > 0) {
338
- currentMenuItemIndex = 0;
339
- setTimeout(() => menuItems[0].focus(), 100);
208
+ setTimeout(() => {
209
+ currentMenuItemIndex = 0;
210
+ menuItems[0].focus();
211
+ }, 100);
340
212
  }
341
213
  } else if (e.key === " ") {
342
214
  e.preventDefault();
@@ -346,7 +218,7 @@ function setupDynamicDropdowns() {
346
218
  openDropdown();
347
219
  if (menuItems.length > 0) {
348
220
  currentMenuItemIndex = 0;
349
- setTimeout(() => menuItems[0].focus(), 100);
221
+ menuItems[0].focus();
350
222
  }
351
223
  }
352
224
  } else if (e.key === "ArrowUp") {
@@ -404,14 +276,10 @@ function addDesktopArrowNavigation() {
404
276
  document.addEventListener("keydown", function (e) {
405
277
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
406
278
 
407
- const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
408
- if (mobileMenu && mobileMenu.contains(document.activeElement)) return;
279
+ const menu = document.querySelector('[data-hs-nav="menu"]');
280
+ if (menu && menu.contains(document.activeElement)) return;
409
281
 
410
- const navbar =
411
- document.querySelector('[data-hs-nav="wrapper"]') ||
412
- document.querySelector(".navbar_component") ||
413
- document.querySelector('nav[role="navigation"]') ||
414
- document.querySelector("nav");
282
+ const navbar = document.querySelector('[data-hs-nav="wrapper"]');
415
283
 
416
284
  if (!navbar || !navbar.contains(document.activeElement)) return;
417
285
 
@@ -430,8 +298,11 @@ function addDesktopArrowNavigation() {
430
298
  const isInDropdownList = el.closest('[role="menu"]');
431
299
  if (isInDropdownList) return false;
432
300
 
433
- const isInMobileMenu = el.closest('[data-hs-nav="menu"]');
434
- if (isInMobileMenu) return false;
301
+ const isInMenu = el.closest('[data-hs-nav="menu"]');
302
+ if (isInMenu) return false;
303
+
304
+ const isInSkipLink = el.closest('[data-hs-nav="skip-link"]');
305
+ if (isInSkipLink) return false;
435
306
 
436
307
  const computedStyle = window.getComputedStyle(el);
437
308
  const isHidden =
@@ -460,166 +331,154 @@ function addDesktopArrowNavigation() {
460
331
  const currentIndex = focusableElements.indexOf(document.activeElement);
461
332
  if (currentIndex === -1) return;
462
333
 
463
- let nextIndex;
464
334
  if (e.key === "ArrowRight") {
465
- nextIndex = (currentIndex + 1) % focusableElements.length;
335
+ if (currentIndex < focusableElements.length - 1) {
336
+ const nextIndex = currentIndex + 1;
337
+ focusableElements[nextIndex].focus();
338
+ }
466
339
  } else {
467
- nextIndex =
468
- currentIndex === 0 ? focusableElements.length - 1 : currentIndex - 1;
340
+ if (currentIndex > 0) {
341
+ const nextIndex = currentIndex - 1;
342
+ focusableElements[nextIndex].focus();
343
+ }
469
344
  }
470
-
471
- focusableElements[nextIndex].focus();
472
345
  });
473
346
  }
474
347
 
475
- // Mobile menu button system with modal-like functionality
476
- function setupMobileMenuButton() {
348
+ // Menu button system with modal-like functionality
349
+ function setupMenuButton() {
477
350
  const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
478
- const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
351
+ const menu = document.querySelector('[data-hs-nav="menu"]');
479
352
 
480
- if (!menuButton || !mobileMenu) return;
353
+ if (!menuButton || !menu) return;
481
354
 
482
- const menuId = `mobile-menu-${Date.now()}`;
355
+ const menuId = `menu-${Date.now()}`;
483
356
 
484
357
  menuButton.setAttribute("aria-expanded", "false");
485
358
  menuButton.setAttribute("aria-controls", menuId);
486
359
  menuButton.setAttribute("aria-label", "Open navigation menu");
487
360
 
488
- mobileMenu.id = menuId;
489
- mobileMenu.setAttribute("role", "dialog");
490
- mobileMenu.setAttribute("aria-modal", "true");
491
- setElementInert(mobileMenu, true);
361
+ menu.id = menuId;
362
+ menu.setAttribute("role", "dialog");
363
+ menu.setAttribute("aria-modal", "true");
492
364
 
493
365
  let isMenuOpen = false;
366
+ let focusTrapHandler = null;
494
367
 
495
- function shouldPreventMobileMenu() {
496
- const menuHideElement = document.querySelector(".menu-hide.is-mobile");
368
+ function shouldPreventMenu() {
369
+ const menuHideElement = document.querySelector(".menu_hide");
497
370
  if (!menuHideElement) return false;
498
371
 
499
372
  const computedStyle = window.getComputedStyle(menuHideElement);
500
373
  return computedStyle.display === "none";
501
374
  }
502
375
 
503
- function openMenu() {
504
- if (isMenuOpen || shouldPreventMobileMenu()) return;
505
- isMenuOpen = true;
376
+ function createFocusTrap() {
377
+ const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]');
506
378
 
507
- try {
508
- // Add body overflow hidden class
509
- document.body.classList.add("u-overflow-hidden");
510
-
511
- // Add blur effect to modal blur elements
512
- document
513
- .querySelectorAll('[data-hs-nav="modal-blur"]')
514
- .forEach((element) => {
515
- element.style.display = "block";
516
- element.style.opacity = "0.5";
517
- element.style.transition = "opacity 0.3s ease";
518
- });
519
-
520
- menuButton.setAttribute("aria-expanded", "true");
521
- menuButton.setAttribute("aria-label", "Close navigation menu");
522
- setElementInert(mobileMenu, false);
523
-
524
- // Announce to screen readers
525
- const menuName = getMenuName(menuButton);
526
- announceToScreenReader(`${menuName} opened`);
527
-
528
- // Prevent tabbing outside navbar using tabindex management
529
- const navbarWrapper =
530
- document.querySelector('[data-hs-nav="wrapper"]') ||
531
- document.querySelector(".navbar_component") ||
532
- document.querySelector('nav[role="navigation"]') ||
533
- document.querySelector("nav");
534
-
535
- const allFocusableElements = document.querySelectorAll(
536
- 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
537
- );
538
- allFocusableElements.forEach((el) => {
539
- if (navbarWrapper && !navbarWrapper.contains(el)) {
540
- el.setAttribute(
541
- "data-mobile-menu-tabindex",
542
- el.getAttribute("tabindex") || "0",
543
- );
544
- el.setAttribute("tabindex", "-1");
379
+ if (!navbarWrapper) return;
380
+
381
+ focusTrapHandler = (e) => {
382
+ if (e.key === 'Tab') {
383
+ const focusableElements = navbarWrapper.querySelectorAll(
384
+ 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
385
+ );
386
+ const focusableArray = Array.from(focusableElements);
387
+ const firstElement = focusableArray[0];
388
+ const lastElement = focusableArray[focusableArray.length - 1];
389
+
390
+ if (e.shiftKey) {
391
+ // Shift+Tab: moving backwards
392
+ if (document.activeElement === firstElement) {
393
+ e.preventDefault();
394
+ lastElement.focus();
395
+ }
396
+ } else {
397
+ // Tab: moving forwards
398
+ if (document.activeElement === lastElement) {
399
+ e.preventDefault();
400
+ firstElement.focus();
401
+ }
545
402
  }
546
- });
403
+ }
404
+ };
547
405
 
548
- const clickEvent = new MouseEvent("click", {
549
- bubbles: true,
550
- cancelable: true,
551
- view: window,
552
- });
553
- menuButton.dispatchEvent(clickEvent);
554
- } catch (e) {
555
- // DOM operation failed, restore state
556
- isMenuOpen = false;
557
- menuButton.setAttribute("aria-expanded", "false");
558
- menuButton.setAttribute("aria-label", "Open navigation menu");
406
+ document.addEventListener('keydown', focusTrapHandler);
407
+ }
408
+
409
+ function removeFocusTrap() {
410
+ if (focusTrapHandler) {
411
+ document.removeEventListener('keydown', focusTrapHandler);
412
+ focusTrapHandler = null;
559
413
  }
560
414
  }
561
415
 
562
- function closeMenu() {
563
- if (!isMenuOpen) return;
564
- isMenuOpen = false;
416
+ function openMenu() {
417
+ if (isMenuOpen || shouldPreventMenu()) return;
418
+ isMenuOpen = true;
565
419
 
566
- try {
567
- // Remove body overflow hidden class
568
- document.body.classList.remove("u-overflow-hidden");
420
+ document.body.classList.add("u-overflow-hidden");
569
421
 
570
- // Remove blur effect from modal blur elements
571
- document
572
- .querySelectorAll('[data-hs-nav="modal-blur"]')
573
- .forEach((element) => {
574
- element.style.opacity = "0";
575
- element.style.transition = "opacity 0.3s ease";
576
- setTimeout(() => {
577
- element.style.display = "none";
578
- }, 300);
579
- });
422
+ document
423
+ .querySelectorAll('[data-hs-nav="modal-blur"]')
424
+ .forEach((element) => {
425
+ element.classList.add('is-active');
426
+ });
580
427
 
581
- if (mobileMenu.contains(document.activeElement)) {
582
- menuButton.focus();
428
+ menuButton.setAttribute("aria-expanded", "true");
429
+ menuButton.setAttribute("aria-label", "Close navigation menu");
430
+
431
+ // Create focus trap for navbar
432
+ createFocusTrap();
433
+
434
+ // Focus first menu item after menu opens
435
+ setTimeout(() => {
436
+ const firstElement = menu.querySelector("button, a");
437
+ if (firstElement) {
438
+ firstElement.focus();
583
439
  }
584
- menuButton.setAttribute("aria-expanded", "false");
585
- menuButton.setAttribute("aria-label", "Open navigation menu");
586
- setElementInert(mobileMenu, true);
440
+ }, 100);
587
441
 
588
- // Announce to screen readers
589
- const menuName = getMenuName(menuButton);
590
- announceToScreenReader(`${menuName} closed`);
442
+ const clickEvent = new MouseEvent("click", {
443
+ bubbles: true,
444
+ cancelable: true,
445
+ view: window,
446
+ });
447
+ menuButton.dispatchEvent(clickEvent);
448
+ }
591
449
 
592
- // Restore tabbing to entire page using tabindex management
593
- const elementsToRestore = document.querySelectorAll(
594
- "[data-mobile-menu-tabindex]",
595
- );
596
- elementsToRestore.forEach((el) => {
597
- const originalTabindex = el.getAttribute("data-mobile-menu-tabindex");
598
- if (originalTabindex === "0") {
599
- el.removeAttribute("tabindex");
600
- } else {
601
- el.setAttribute("tabindex", originalTabindex);
602
- }
603
- el.removeAttribute("data-mobile-menu-tabindex");
604
- });
450
+ function closeMenu() {
451
+ if (!isMenuOpen) return;
452
+ isMenuOpen = false;
605
453
 
606
- const clickEvent = new MouseEvent("click", {
607
- bubbles: true,
608
- cancelable: true,
609
- view: window,
454
+ document.body.classList.remove("u-overflow-hidden");
455
+
456
+ document
457
+ .querySelectorAll('[data-hs-nav="modal-blur"]')
458
+ .forEach((element) => {
459
+ element.classList.remove('is-active');
610
460
  });
611
- menuButton.dispatchEvent(clickEvent);
612
- } catch (e) {
613
- // DOM operation failed, ensure consistent state
614
- isMenuOpen = false;
615
- menuButton.setAttribute("aria-expanded", "false");
616
- menuButton.setAttribute("aria-label", "Open navigation menu");
461
+
462
+ if (menu.contains(document.activeElement)) {
463
+ menuButton.focus();
617
464
  }
465
+ menuButton.setAttribute("aria-expanded", "false");
466
+ menuButton.setAttribute("aria-label", "Open navigation menu");
467
+
468
+ // Remove focus trap
469
+ removeFocusTrap();
470
+
471
+ const clickEvent = new MouseEvent("click", {
472
+ bubbles: true,
473
+ cancelable: true,
474
+ view: window,
475
+ });
476
+ menuButton.dispatchEvent(clickEvent);
618
477
  }
619
478
 
620
479
  function toggleMenu() {
621
- if (shouldPreventMobileMenu()) return;
622
-
480
+ if (shouldPreventMenu()) return;
481
+
623
482
  if (isMenuOpen) {
624
483
  closeMenu();
625
484
  } else {
@@ -631,162 +490,48 @@ function setupMobileMenuButton() {
631
490
  if (e.key === "Enter" || e.key === " ") {
632
491
  e.preventDefault();
633
492
  toggleMenu();
634
- } else if (e.key === "ArrowDown") {
635
- e.preventDefault();
636
- if (!isMenuOpen) {
637
- openMenu();
638
- }
639
- const firstElement = mobileMenu.querySelector("button, a");
640
- if (firstElement) {
641
- firstElement.focus();
642
- }
643
- } else if (e.key === "ArrowUp") {
644
- e.preventDefault();
645
- if (isMenuOpen) {
646
- closeMenu();
647
- }
648
493
  }
649
494
  });
650
495
 
651
496
  menuButton.addEventListener("click", function (e) {
652
497
  if (!e.isTrusted) return;
653
-
654
- if (shouldPreventMobileMenu()) return;
655
-
656
- if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
657
- menuButton.focus();
658
- }
659
-
660
- const newMenuState = !isMenuOpen;
661
- isMenuOpen = newMenuState;
662
-
663
- // Handle body overflow class
664
- if (isMenuOpen) {
665
- document.body.classList.add("u-overflow-hidden");
666
- } else {
667
- document.body.classList.remove("u-overflow-hidden");
668
- }
669
-
670
- // Handle blur effect
671
- document
672
- .querySelectorAll('[data-hs-nav="modal-blur"]')
673
- .forEach((element) => {
674
- if (isMenuOpen) {
675
- element.style.display = "block";
676
- element.style.opacity = "0.5";
677
- element.style.transition = "opacity 0.3s ease";
678
- } else {
679
- element.style.opacity = "0";
680
- element.style.transition = "opacity 0.3s ease";
681
- setTimeout(() => {
682
- element.style.display = "none";
683
- }, 300);
684
- }
685
- });
686
-
687
- menuButton.setAttribute("aria-expanded", isMenuOpen);
688
- menuButton.setAttribute(
689
- "aria-label",
690
- isMenuOpen ? "Close navigation menu" : "Open navigation menu",
691
- );
692
- setElementInert(mobileMenu, !isMenuOpen);
693
-
694
- // Handle tabindex management for external clicks
695
- if (isMenuOpen) {
696
- const navbarWrapper =
697
- document.querySelector('[data-hs-nav="wrapper"]') ||
698
- document.querySelector(".navbar_component") ||
699
- document.querySelector('nav[role="navigation"]') ||
700
- document.querySelector("nav");
701
-
702
- const allFocusableElements = document.querySelectorAll(
703
- 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
704
- );
705
- allFocusableElements.forEach((el) => {
706
- if (navbarWrapper && !navbarWrapper.contains(el)) {
707
- el.setAttribute(
708
- "data-mobile-menu-tabindex",
709
- el.getAttribute("tabindex") || "0",
710
- );
711
- el.setAttribute("tabindex", "-1");
712
- }
713
- });
714
- } else {
715
- const elementsToRestore = document.querySelectorAll(
716
- "[data-mobile-menu-tabindex]",
717
- );
718
- elementsToRestore.forEach((el) => {
719
- const originalTabindex = el.getAttribute("data-mobile-menu-tabindex");
720
- if (originalTabindex === "0") {
721
- el.removeAttribute("tabindex");
722
- } else {
723
- el.setAttribute("tabindex", originalTabindex);
724
- }
725
- el.removeAttribute("data-mobile-menu-tabindex");
726
- });
727
- }
498
+ toggleMenu();
728
499
  });
729
500
 
730
- // Store the menu state and functions for breakpoint handler
731
- window.mobileMenuState = {
732
- isMenuOpen: () => isMenuOpen,
733
- closeMenu: closeMenu,
734
- openMenu: openMenu,
735
- };
736
-
737
- // Cleanup function for window.mobileMenuState
738
- if (typeof window !== "undefined") {
739
- window.addEventListener("pagehide", () => {
740
- if (window.mobileMenuState) {
741
- delete window.mobileMenuState;
742
- }
743
- });
744
- }
745
501
  }
746
502
 
747
- // Mobile menu breakpoint handler
748
- function setupMobileMenuBreakpointHandler() {
749
- let preventedMenuState = false;
750
503
 
751
- function handleBreakpointChange() {
752
- const menuHideElement = document.querySelector(".menu-hide.is-mobile");
504
+ function setupMenuDisplayObserver() {
505
+ function handleDisplayChange() {
506
+ const menuHideElement = document.querySelector(".menu_hide");
753
507
  if (!menuHideElement) return;
754
-
508
+
755
509
  const computedStyle = window.getComputedStyle(menuHideElement);
756
- const shouldPrevent = computedStyle.display === "none";
757
-
758
- if (!window.mobileMenuState) return;
759
-
760
- if (shouldPrevent && window.mobileMenuState.isMenuOpen()) {
761
- // Store that the menu was open before being prevented
762
- preventedMenuState = true;
763
- window.mobileMenuState.closeMenu();
764
- } else if (!shouldPrevent && preventedMenuState) {
765
- // Restore menu state if it was open before being prevented
766
- preventedMenuState = false;
767
- window.mobileMenuState.openMenu();
768
- }
510
+ const isMenuVisible = computedStyle.display !== "none";
511
+
512
+ // Get menu button to check if menu is open
513
+ const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
514
+ const isMenuOpen = menuButton && menuButton.getAttribute("aria-expanded") === "true";
515
+
516
+ const shouldShowModal = isMenuVisible && isMenuOpen;
517
+
518
+ // Toggle modal effects only when menu is visible AND menu is open
519
+ document.body.classList.toggle("u-overflow-hidden", shouldShowModal);
520
+
521
+ document
522
+ .querySelectorAll('[data-hs-nav="modal-blur"]')
523
+ .forEach((element) => {
524
+ element.classList.toggle('is-active', shouldShowModal);
525
+ });
769
526
  }
770
527
 
771
- // Use ResizeObserver for more accurate detection
772
- if (typeof ResizeObserver !== "undefined") {
773
- try {
774
- const resizeObserver = new ResizeObserver(handleBreakpointChange);
775
- const menuHideElement = document.querySelector(".menu-hide.is-mobile");
776
- if (menuHideElement) {
777
- resizeObserver.observe(menuHideElement);
778
- }
779
- } catch (e) {
780
- // ResizeObserver not supported or error occurred
781
- // Silently fall back to resize event
782
- }
528
+ const displayObserver = new ResizeObserver(handleDisplayChange);
529
+ const menuHideElement = document.querySelector(".menu_hide");
530
+ if (menuHideElement) {
531
+ displayObserver.observe(menuHideElement);
532
+ // Initial check
533
+ handleDisplayChange();
783
534
  }
784
-
785
- // Fallback to resize event
786
- window.addEventListener("resize", handleBreakpointChange);
787
-
788
- // Initial check
789
- handleBreakpointChange();
790
535
  }
791
536
 
792
537
  function sanitizeForID(text) {
@@ -798,94 +543,60 @@ function sanitizeForID(text) {
798
543
  .substring(0, 50);
799
544
  }
800
545
 
801
- // Mobile menu ARIA setup
802
- function setupMobileMenuARIA() {
546
+ // Menu ARIA setup
547
+ function setupMenuARIA() {
803
548
  const menuContainer = document.querySelector('[data-hs-nav="menu"]');
804
549
  if (!menuContainer) return;
805
550
 
806
- const buttons = menuContainer.querySelectorAll("button");
551
+ const dropdownWrappers = menuContainer.querySelectorAll('[data-hs-nav="menu-dropdown"]');
807
552
  const links = menuContainer.querySelectorAll("a");
808
553
 
809
- buttons.forEach((button) => {
810
- const buttonText = button.textContent && button.textContent.trim();
811
- if (!buttonText) return;
812
-
813
- const sanitizedText = sanitizeForID(buttonText);
814
- const buttonId = `navbar-mobile-${sanitizedText}-toggle`;
815
- const listId = `navbar-mobile-${sanitizedText}-list`;
554
+ dropdownWrappers.forEach((wrapper) => {
555
+ const button = wrapper.querySelector('[data-hs-nav="menu-dropdown-btn"]');
556
+ const dropdownList = wrapper.querySelector('[data-hs-nav="menu-dropdown-list"]');
816
557
 
817
- button.id = buttonId;
818
- button.setAttribute("aria-expanded", "false");
819
- button.setAttribute("aria-controls", listId);
558
+ if (button && dropdownList) {
559
+ const buttonText = button.textContent?.trim();
560
+ const sanitizedText = sanitizeForID(buttonText);
561
+ const buttonId = `navbar-menu-${sanitizedText}-toggle`;
562
+ const listId = `navbar-menu-${sanitizedText}-list`;
820
563
 
821
- // Look for dropdown list in the same container as the button
822
- let dropdownList = null;
823
- const buttonContainer = button.closest(
824
- '.menu-card_dropdown, .menu_contain, [data-hs-nav="menu"]',
825
- );
826
-
827
- if (buttonContainer) {
828
- // First try to find a list element within the same container
829
- dropdownList = buttonContainer.querySelector(
830
- '.menu-card_list, .dropdown-list, [role="menu"]',
831
- );
564
+ button.id = buttonId;
565
+ button.setAttribute("aria-expanded", "false");
566
+ button.setAttribute("aria-controls", listId);
832
567
 
833
- // If not found, look for any element with multiple links that's not the button itself
834
- if (!dropdownList || !dropdownList.querySelector("a")) {
835
- const allListElements =
836
- buttonContainer.querySelectorAll("div, ul, nav");
837
- dropdownList = Array.from(allListElements).find(
838
- (el) =>
839
- el.querySelectorAll("a").length > 1 &&
840
- !el.contains(button) &&
841
- el !== button,
842
- );
843
- }
844
- }
845
-
846
- if (dropdownList && dropdownList.querySelector("a")) {
847
568
  dropdownList.id = listId;
848
- setElementInert(dropdownList, true);
569
+ dropdownList.setAttribute("aria-hidden", "true");
849
570
 
850
571
  button.addEventListener("click", function () {
851
572
  const isExpanded = button.getAttribute("aria-expanded") === "true";
852
573
  const newState = !isExpanded;
853
574
  button.setAttribute("aria-expanded", newState);
854
- setElementInert(dropdownList, !newState);
575
+ dropdownList.setAttribute("aria-hidden", !newState);
855
576
 
856
- // Announce to screen readers
857
- const menuName = getMenuName(button);
858
- announceToScreenReader(
859
- `${menuName} submenu ${newState ? "opened" : "closed"}`,
860
- );
861
577
  });
862
578
  }
863
579
  });
864
580
 
865
581
  links.forEach((link) => {
866
- const linkText = link.textContent && link.textContent.trim();
867
- if (!linkText) return;
868
-
582
+ const linkText = link.textContent?.trim();
869
583
  const sanitizedText = sanitizeForID(linkText);
870
- const linkId = `navbar-mobile-${sanitizedText}-link`;
584
+ const linkId = `navbar-menu-${sanitizedText}-link`;
871
585
  link.id = linkId;
872
586
  });
873
587
 
874
- setupMobileMenuArrowNavigation(menuContainer);
588
+ setupMenuArrowNavigation(menuContainer);
875
589
  }
876
590
 
877
- // Mobile menu arrow navigation
878
- function setupMobileMenuArrowNavigation(menuContainer) {
591
+ // Menu arrow navigation
592
+ function setupMenuArrowNavigation(menuContainer) {
879
593
  function getFocusableElements() {
880
594
  const allElements = menuContainer.querySelectorAll("button, a");
881
595
  return Array.from(allElements).filter((el) => {
596
+ // Check if element or any ancestor has aria-hidden="true"
882
597
  let current = el;
883
598
  while (current && current !== menuContainer) {
884
- // Check both native inert and polyfill inert
885
- if (
886
- current.inert === true ||
887
- current.getAttribute("data-inert") === "true"
888
- ) {
599
+ if (current.getAttribute("aria-hidden") === "true") {
889
600
  return false;
890
601
  }
891
602
  current = current.parentElement;
@@ -905,25 +616,16 @@ function setupMobileMenuArrowNavigation(menuContainer) {
905
616
 
906
617
  if (e.key === "ArrowDown") {
907
618
  e.preventDefault();
908
- if (currentFocusIndex >= focusableElements.length - 1) {
909
- currentFocusIndex = 0;
910
- } else {
619
+ if (currentFocusIndex < focusableElements.length - 1) {
911
620
  currentFocusIndex = currentFocusIndex + 1;
621
+ focusableElements[currentFocusIndex].focus();
912
622
  }
913
- focusableElements[currentFocusIndex].focus();
914
623
  } else if (e.key === "ArrowUp") {
915
624
  e.preventDefault();
916
- if (currentFocusIndex <= 0) {
917
- const mobileMenuButton = document.querySelector(
918
- '[data-hs-nav="menubtn"]',
919
- );
920
- if (mobileMenuButton) {
921
- mobileMenuButton.focus();
922
- return;
923
- }
625
+ if (currentFocusIndex > 0) {
626
+ currentFocusIndex = currentFocusIndex - 1;
627
+ focusableElements[currentFocusIndex].focus();
924
628
  }
925
- currentFocusIndex = currentFocusIndex - 1;
926
- focusableElements[currentFocusIndex].focus();
927
629
  } else if (e.key === "ArrowRight") {
928
630
  e.preventDefault();
929
631
  if (
@@ -961,12 +663,12 @@ function setupMobileMenuArrowNavigation(menuContainer) {
961
663
  } else if (e.key === " " && activeElement.tagName === "A") {
962
664
  e.preventDefault();
963
665
  } else if (e.key === "Escape") {
964
- const mobileMenuButton = document.querySelector(
666
+ const menuButton = document.querySelector(
965
667
  '[data-hs-nav="menubtn"]',
966
668
  );
967
- if (mobileMenuButton) {
968
- mobileMenuButton.click();
969
- mobileMenuButton.focus();
669
+ if (menuButton) {
670
+ menuButton.click();
671
+ menuButton.focus();
970
672
  }
971
673
  }
972
674
  });