@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.
- package/animations/text.js +202 -316
- package/autoInit/accessibility.js +64 -0
- package/autoInit/counter.js +54 -238
- package/autoInit/modal.js +0 -49
- package/autoInit/navbar.js +199 -497
- package/index.js +1 -1
- package/package.json +1 -1
- package/styles.css +0 -6
package/autoInit/navbar.js
CHANGED
|
@@ -1,98 +1,15 @@
|
|
|
1
|
-
// Global accessibility state
|
|
2
|
-
let supportsInert = null;
|
|
3
|
-
let screenReaderLiveRegion = null;
|
|
4
|
-
|
|
5
1
|
export const init = () => {
|
|
6
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
|
408
|
-
if (
|
|
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
|
|
434
|
-
if (
|
|
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
|
-
|
|
335
|
+
if (currentIndex < focusableElements.length - 1) {
|
|
336
|
+
const nextIndex = currentIndex + 1;
|
|
337
|
+
focusableElements[nextIndex].focus();
|
|
338
|
+
}
|
|
466
339
|
} else {
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
//
|
|
476
|
-
function
|
|
348
|
+
// Menu button system with modal-like functionality
|
|
349
|
+
function setupMenuButton() {
|
|
477
350
|
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
|
478
|
-
const
|
|
351
|
+
const menu = document.querySelector('[data-hs-nav="menu"]');
|
|
479
352
|
|
|
480
|
-
if (!menuButton || !
|
|
353
|
+
if (!menuButton || !menu) return;
|
|
481
354
|
|
|
482
|
-
const menuId = `
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
496
|
-
const menuHideElement = document.querySelector(".
|
|
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
|
|
504
|
-
|
|
505
|
-
isMenuOpen = true;
|
|
376
|
+
function createFocusTrap() {
|
|
377
|
+
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]');
|
|
506
378
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
563
|
-
if (
|
|
564
|
-
isMenuOpen =
|
|
416
|
+
function openMenu() {
|
|
417
|
+
if (isMenuOpen || shouldPreventMenu()) return;
|
|
418
|
+
isMenuOpen = true;
|
|
565
419
|
|
|
566
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
.
|
|
574
|
-
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
585
|
-
menuButton.setAttribute("aria-label", "Open navigation menu");
|
|
586
|
-
setElementInert(mobileMenu, true);
|
|
440
|
+
}, 100);
|
|
587
441
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
442
|
+
const clickEvent = new MouseEvent("click", {
|
|
443
|
+
bubbles: true,
|
|
444
|
+
cancelable: true,
|
|
445
|
+
view: window,
|
|
446
|
+
});
|
|
447
|
+
menuButton.dispatchEvent(clickEvent);
|
|
448
|
+
}
|
|
591
449
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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 (
|
|
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
|
-
|
|
752
|
-
|
|
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
|
|
757
|
-
|
|
758
|
-
if
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
//
|
|
802
|
-
function
|
|
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
|
|
551
|
+
const dropdownWrappers = menuContainer.querySelectorAll('[data-hs-nav="menu-dropdown"]');
|
|
807
552
|
const links = menuContainer.querySelectorAll("a");
|
|
808
553
|
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
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
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
867
|
-
if (!linkText) return;
|
|
868
|
-
|
|
582
|
+
const linkText = link.textContent?.trim();
|
|
869
583
|
const sanitizedText = sanitizeForID(linkText);
|
|
870
|
-
const linkId = `navbar-
|
|
584
|
+
const linkId = `navbar-menu-${sanitizedText}-link`;
|
|
871
585
|
link.id = linkId;
|
|
872
586
|
});
|
|
873
587
|
|
|
874
|
-
|
|
588
|
+
setupMenuArrowNavigation(menuContainer);
|
|
875
589
|
}
|
|
876
590
|
|
|
877
|
-
//
|
|
878
|
-
function
|
|
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
|
-
|
|
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
|
|
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
|
|
917
|
-
|
|
918
|
-
|
|
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
|
|
666
|
+
const menuButton = document.querySelector(
|
|
965
667
|
'[data-hs-nav="menubtn"]',
|
|
966
668
|
);
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
669
|
+
if (menuButton) {
|
|
670
|
+
menuButton.click();
|
|
671
|
+
menuButton.focus();
|
|
970
672
|
}
|
|
971
673
|
}
|
|
972
674
|
});
|