@hortonstudio/main 1.2.34 → 1.4.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/.claude/settings.local.json +22 -1
- package/TEMP-before-after-attributes.md +158 -0
- package/animations/hero.js +741 -611
- package/animations/text.js +505 -317
- package/animations/transition.js +36 -21
- package/autoInit/accessibility.js +7 -67
- package/autoInit/counter.js +338 -0
- package/autoInit/custom-values.js +266 -0
- package/autoInit/form.js +471 -0
- package/autoInit/modal.js +43 -38
- package/autoInit/navbar.js +550 -338
- package/autoInit/smooth-scroll.js +86 -84
- package/index.js +140 -88
- package/package.json +1 -1
- package/utils/before-after.js +279 -146
- package/utils/scroll-progress.js +26 -21
- package/utils/toc.js +73 -66
- package/CLAUDE.md +0 -45
- package/debug-version.html +0 -37
package/autoInit/navbar.js
CHANGED
@@ -1,81 +1,177 @@
|
|
1
|
+
// Global accessibility state
|
2
|
+
let supportsInert = null;
|
3
|
+
let screenReaderLiveRegion = null;
|
4
|
+
|
1
5
|
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
|
+
}
|
12
|
+
return { result: "navbar initialized" };
|
13
|
+
};
|
14
|
+
|
15
|
+
function initializeNavbar() {
|
16
|
+
setupAccessibilityFeatures();
|
2
17
|
setupDynamicDropdowns();
|
3
18
|
setupMobileMenuButton();
|
4
19
|
setupMobileMenuARIA();
|
5
20
|
setupMobileMenuBreakpointHandler();
|
6
|
-
|
7
|
-
|
21
|
+
}
|
22
|
+
|
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
|
+
}
|
8
96
|
|
9
97
|
// Desktop dropdown system
|
10
98
|
function setupDynamicDropdowns() {
|
11
|
-
const dropdownWrappers = document.querySelectorAll(
|
99
|
+
const dropdownWrappers = document.querySelectorAll(
|
100
|
+
'[data-hs-nav="dropdown"]',
|
101
|
+
);
|
12
102
|
const allDropdowns = [];
|
13
|
-
|
103
|
+
|
14
104
|
const closeAllDropdowns = (exceptWrapper = null) => {
|
15
|
-
allDropdowns.forEach(dropdown => {
|
105
|
+
allDropdowns.forEach((dropdown) => {
|
16
106
|
if (dropdown.wrapper !== exceptWrapper && dropdown.isOpen) {
|
17
107
|
dropdown.closeDropdown();
|
18
108
|
}
|
19
109
|
});
|
20
110
|
};
|
21
|
-
|
22
|
-
dropdownWrappers.forEach(wrapper => {
|
23
|
-
const toggle = wrapper.querySelector(
|
111
|
+
|
112
|
+
dropdownWrappers.forEach((wrapper) => {
|
113
|
+
const toggle = wrapper.querySelector("a");
|
24
114
|
if (!toggle) return;
|
25
|
-
|
26
|
-
const allElements = wrapper.querySelectorAll(
|
115
|
+
|
116
|
+
const allElements = wrapper.querySelectorAll("*");
|
27
117
|
let dropdownList = null;
|
28
|
-
|
118
|
+
|
29
119
|
for (const element of allElements) {
|
30
|
-
const links = element.querySelectorAll(
|
120
|
+
const links = element.querySelectorAll("a");
|
31
121
|
if (links.length >= 2 && !element.contains(toggle)) {
|
32
122
|
dropdownList = element;
|
33
123
|
break;
|
34
124
|
}
|
35
125
|
}
|
36
|
-
|
126
|
+
|
37
127
|
if (!dropdownList) return;
|
38
|
-
|
39
|
-
const toggleText =
|
128
|
+
|
129
|
+
const toggleText =
|
130
|
+
(toggle.textContent && toggle.textContent.trim()) || "dropdown";
|
40
131
|
const sanitizedText = sanitizeForID(toggleText);
|
41
132
|
const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
|
42
133
|
const listId = `navbar-dropdown-${sanitizedText}-list`;
|
43
|
-
|
134
|
+
|
44
135
|
toggle.id = toggleId;
|
45
|
-
toggle.setAttribute(
|
46
|
-
toggle.setAttribute(
|
47
|
-
toggle.setAttribute(
|
48
|
-
|
136
|
+
toggle.setAttribute("aria-haspopup", "menu");
|
137
|
+
toggle.setAttribute("aria-expanded", "false");
|
138
|
+
toggle.setAttribute("aria-controls", listId);
|
139
|
+
|
49
140
|
dropdownList.id = listId;
|
50
|
-
dropdownList.setAttribute(
|
51
|
-
dropdownList.setAttribute(
|
52
|
-
|
53
|
-
const menuItems = dropdownList.querySelectorAll(
|
54
|
-
menuItems.forEach(item => {
|
55
|
-
item.setAttribute(
|
56
|
-
item.setAttribute(
|
141
|
+
dropdownList.setAttribute("role", "menu");
|
142
|
+
dropdownList.setAttribute("aria-hidden", "true");
|
143
|
+
|
144
|
+
const menuItems = dropdownList.querySelectorAll("a");
|
145
|
+
menuItems.forEach((item) => {
|
146
|
+
item.setAttribute("role", "menuitem");
|
147
|
+
item.setAttribute("tabindex", "-1");
|
57
148
|
});
|
58
|
-
|
149
|
+
|
59
150
|
let isOpen = false;
|
60
151
|
let currentMenuItemIndex = -1;
|
61
|
-
|
152
|
+
|
62
153
|
function openDropdown() {
|
63
154
|
if (isOpen) return;
|
64
155
|
closeAllDropdowns(wrapper);
|
65
156
|
isOpen = true;
|
66
|
-
toggle.setAttribute(
|
67
|
-
dropdownList.setAttribute(
|
68
|
-
menuItems.forEach(item => {
|
69
|
-
item.setAttribute(
|
157
|
+
toggle.setAttribute("aria-expanded", "true");
|
158
|
+
dropdownList.setAttribute("aria-hidden", "false");
|
159
|
+
menuItems.forEach((item) => {
|
160
|
+
item.setAttribute("tabindex", "0");
|
70
161
|
});
|
71
|
-
|
162
|
+
|
163
|
+
// Announce to screen readers
|
164
|
+
const menuName = getMenuName(toggle);
|
165
|
+
announceToScreenReader(`${menuName} menu opened`);
|
166
|
+
|
167
|
+
const clickEvent = new MouseEvent("click", {
|
72
168
|
bubbles: true,
|
73
169
|
cancelable: true,
|
74
|
-
view: window
|
170
|
+
view: window,
|
75
171
|
});
|
76
172
|
wrapper.dispatchEvent(clickEvent);
|
77
173
|
}
|
78
|
-
|
174
|
+
|
79
175
|
function closeDropdown() {
|
80
176
|
if (!isOpen) return;
|
81
177
|
const shouldRestoreFocus = dropdownList.contains(document.activeElement);
|
@@ -84,71 +180,80 @@ function setupDynamicDropdowns() {
|
|
84
180
|
if (shouldRestoreFocus) {
|
85
181
|
toggle.focus();
|
86
182
|
}
|
87
|
-
toggle.setAttribute(
|
88
|
-
dropdownList.setAttribute(
|
89
|
-
menuItems.forEach(item => {
|
90
|
-
item.setAttribute(
|
183
|
+
toggle.setAttribute("aria-expanded", "false");
|
184
|
+
dropdownList.setAttribute("aria-hidden", "true");
|
185
|
+
menuItems.forEach((item) => {
|
186
|
+
item.setAttribute("tabindex", "-1");
|
91
187
|
});
|
92
|
-
|
188
|
+
|
189
|
+
// Announce to screen readers
|
190
|
+
const menuName = getMenuName(toggle);
|
191
|
+
announceToScreenReader(`${menuName} menu closed`);
|
192
|
+
|
193
|
+
const clickEvent = new MouseEvent("click", {
|
93
194
|
bubbles: true,
|
94
195
|
cancelable: true,
|
95
|
-
view: window
|
196
|
+
view: window,
|
96
197
|
});
|
97
198
|
wrapper.dispatchEvent(clickEvent);
|
98
199
|
}
|
99
|
-
|
100
|
-
wrapper.addEventListener(
|
200
|
+
|
201
|
+
wrapper.addEventListener("mouseenter", () => {
|
101
202
|
if (!isOpen) {
|
102
|
-
const clickEvent = new MouseEvent(
|
203
|
+
const clickEvent = new MouseEvent("click", {
|
103
204
|
bubbles: true,
|
104
205
|
cancelable: true,
|
105
|
-
view: window
|
206
|
+
view: window,
|
106
207
|
});
|
107
208
|
wrapper.dispatchEvent(clickEvent);
|
108
209
|
closeAllDropdowns(wrapper);
|
109
210
|
isOpen = true;
|
110
|
-
toggle.setAttribute(
|
111
|
-
dropdownList.setAttribute(
|
112
|
-
menuItems.forEach(item => {
|
113
|
-
item.setAttribute(
|
211
|
+
toggle.setAttribute("aria-expanded", "true");
|
212
|
+
dropdownList.setAttribute("aria-hidden", "false");
|
213
|
+
menuItems.forEach((item) => {
|
214
|
+
item.setAttribute("tabindex", "0");
|
114
215
|
});
|
115
216
|
}
|
116
217
|
});
|
117
|
-
|
118
|
-
wrapper.addEventListener(
|
218
|
+
|
219
|
+
wrapper.addEventListener("mouseleave", () => {
|
119
220
|
if (isOpen) {
|
120
221
|
if (dropdownList.contains(document.activeElement)) {
|
121
222
|
toggle.focus();
|
122
223
|
}
|
123
|
-
const clickEvent = new MouseEvent(
|
224
|
+
const clickEvent = new MouseEvent("click", {
|
124
225
|
bubbles: true,
|
125
226
|
cancelable: true,
|
126
|
-
view: window
|
227
|
+
view: window,
|
127
228
|
});
|
128
229
|
wrapper.dispatchEvent(clickEvent);
|
129
230
|
isOpen = false;
|
130
|
-
toggle.setAttribute(
|
131
|
-
dropdownList.setAttribute(
|
132
|
-
menuItems.forEach(item => {
|
133
|
-
item.setAttribute(
|
231
|
+
toggle.setAttribute("aria-expanded", "false");
|
232
|
+
dropdownList.setAttribute("aria-hidden", "true");
|
233
|
+
menuItems.forEach((item) => {
|
234
|
+
item.setAttribute("tabindex", "-1");
|
134
235
|
});
|
135
236
|
currentMenuItemIndex = -1;
|
136
237
|
}
|
137
238
|
});
|
138
|
-
|
139
|
-
document.addEventListener(
|
239
|
+
|
240
|
+
document.addEventListener("keydown", function (e) {
|
140
241
|
if (!isOpen) return;
|
141
242
|
if (!wrapper.contains(document.activeElement)) return;
|
142
|
-
|
143
|
-
if (e.key ===
|
243
|
+
|
244
|
+
if (e.key === "ArrowDown") {
|
144
245
|
e.preventDefault();
|
145
246
|
if (document.activeElement === toggle) {
|
146
247
|
currentMenuItemIndex = 0;
|
147
248
|
menuItems[currentMenuItemIndex].focus();
|
148
249
|
} else {
|
149
250
|
if (currentMenuItemIndex === menuItems.length - 1) {
|
150
|
-
const nextElement =
|
151
|
-
|
251
|
+
const nextElement =
|
252
|
+
(wrapper.nextElementSibling &&
|
253
|
+
wrapper.nextElementSibling.querySelector("a, button")) ||
|
254
|
+
document.querySelector(
|
255
|
+
".navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button",
|
256
|
+
);
|
152
257
|
if (nextElement) {
|
153
258
|
closeDropdown();
|
154
259
|
nextElement.focus();
|
@@ -158,14 +263,16 @@ function setupDynamicDropdowns() {
|
|
158
263
|
currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
|
159
264
|
menuItems[currentMenuItemIndex].focus();
|
160
265
|
}
|
161
|
-
} else if (e.key ===
|
266
|
+
} else if (e.key === "ArrowUp") {
|
162
267
|
e.preventDefault();
|
163
268
|
if (document.activeElement === toggle) {
|
164
269
|
currentMenuItemIndex = menuItems.length - 1;
|
165
270
|
menuItems[currentMenuItemIndex].focus();
|
166
271
|
} else {
|
167
272
|
if (currentMenuItemIndex === 0) {
|
168
|
-
const prevElement =
|
273
|
+
const prevElement =
|
274
|
+
wrapper.previousElementSibling &&
|
275
|
+
wrapper.previousElementSibling.querySelector("a, button");
|
169
276
|
if (prevElement) {
|
170
277
|
closeDropdown();
|
171
278
|
prevElement.focus();
|
@@ -176,10 +283,13 @@ function setupDynamicDropdowns() {
|
|
176
283
|
return;
|
177
284
|
}
|
178
285
|
}
|
179
|
-
currentMenuItemIndex =
|
286
|
+
currentMenuItemIndex =
|
287
|
+
currentMenuItemIndex <= 0
|
288
|
+
? menuItems.length - 1
|
289
|
+
: currentMenuItemIndex - 1;
|
180
290
|
menuItems[currentMenuItemIndex].focus();
|
181
291
|
}
|
182
|
-
} else if (e.key ===
|
292
|
+
} else if (e.key === "Tab") {
|
183
293
|
if (e.shiftKey) {
|
184
294
|
if (document.activeElement === menuItems[0]) {
|
185
295
|
e.preventDefault();
|
@@ -189,8 +299,12 @@ function setupDynamicDropdowns() {
|
|
189
299
|
} else {
|
190
300
|
if (document.activeElement === menuItems[menuItems.length - 1]) {
|
191
301
|
e.preventDefault();
|
192
|
-
const nextElement =
|
193
|
-
|
302
|
+
const nextElement =
|
303
|
+
(wrapper.nextElementSibling &&
|
304
|
+
wrapper.nextElementSibling.querySelector("a, button")) ||
|
305
|
+
document.querySelector(
|
306
|
+
".navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button",
|
307
|
+
);
|
194
308
|
closeDropdown();
|
195
309
|
if (nextElement) {
|
196
310
|
setTimeout(() => {
|
@@ -199,32 +313,32 @@ function setupDynamicDropdowns() {
|
|
199
313
|
}
|
200
314
|
}
|
201
315
|
}
|
202
|
-
} else if (e.key ===
|
316
|
+
} else if (e.key === "Escape") {
|
203
317
|
e.preventDefault();
|
204
318
|
closeDropdown();
|
205
319
|
toggle.focus();
|
206
|
-
} else if (e.key ===
|
320
|
+
} else if (e.key === "Home") {
|
207
321
|
e.preventDefault();
|
208
322
|
currentMenuItemIndex = 0;
|
209
323
|
menuItems[0].focus();
|
210
|
-
} else if (e.key ===
|
324
|
+
} else if (e.key === "End") {
|
211
325
|
e.preventDefault();
|
212
326
|
currentMenuItemIndex = menuItems.length - 1;
|
213
327
|
menuItems[menuItems.length - 1].focus();
|
214
|
-
} else if (e.key ===
|
328
|
+
} else if (e.key === " ") {
|
215
329
|
e.preventDefault();
|
216
330
|
}
|
217
331
|
});
|
218
|
-
|
219
|
-
toggle.addEventListener(
|
220
|
-
if (e.key ===
|
332
|
+
|
333
|
+
toggle.addEventListener("keydown", function (e) {
|
334
|
+
if (e.key === "ArrowDown") {
|
221
335
|
e.preventDefault();
|
222
336
|
openDropdown();
|
223
337
|
if (menuItems.length > 0) {
|
224
338
|
currentMenuItemIndex = 0;
|
225
339
|
setTimeout(() => menuItems[0].focus(), 100);
|
226
340
|
}
|
227
|
-
} else if (e.key ===
|
341
|
+
} else if (e.key === " ") {
|
228
342
|
e.preventDefault();
|
229
343
|
if (isOpen) {
|
230
344
|
closeDropdown();
|
@@ -235,7 +349,7 @@ function setupDynamicDropdowns() {
|
|
235
349
|
setTimeout(() => menuItems[0].focus(), 100);
|
236
350
|
}
|
237
351
|
}
|
238
|
-
} else if (e.key ===
|
352
|
+
} else if (e.key === "ArrowUp") {
|
239
353
|
e.preventDefault();
|
240
354
|
if (isOpen) {
|
241
355
|
currentMenuItemIndex = menuItems.length - 1;
|
@@ -243,29 +357,29 @@ function setupDynamicDropdowns() {
|
|
243
357
|
} else {
|
244
358
|
closeDropdown();
|
245
359
|
}
|
246
|
-
} else if (e.key ===
|
360
|
+
} else if (e.key === "Escape") {
|
247
361
|
e.preventDefault();
|
248
362
|
closeDropdown();
|
249
363
|
}
|
250
364
|
});
|
251
|
-
|
252
|
-
document.addEventListener(
|
365
|
+
|
366
|
+
document.addEventListener("click", function (e) {
|
253
367
|
if (!wrapper.contains(e.target) && isOpen) {
|
254
368
|
closeDropdown();
|
255
369
|
}
|
256
370
|
});
|
257
|
-
|
371
|
+
|
258
372
|
allDropdowns.push({
|
259
373
|
wrapper,
|
260
374
|
isOpen: () => isOpen,
|
261
375
|
closeDropdown,
|
262
376
|
toggle,
|
263
|
-
dropdownList
|
377
|
+
dropdownList,
|
264
378
|
});
|
265
379
|
});
|
266
|
-
|
267
|
-
document.addEventListener(
|
268
|
-
allDropdowns.forEach(dropdown => {
|
380
|
+
|
381
|
+
document.addEventListener("focusin", function (e) {
|
382
|
+
allDropdowns.forEach((dropdown) => {
|
269
383
|
if (dropdown.isOpen() && !dropdown.wrapper.contains(e.target)) {
|
270
384
|
dropdown.closeDropdown();
|
271
385
|
}
|
@@ -277,66 +391,73 @@ function setupDynamicDropdowns() {
|
|
277
391
|
|
278
392
|
// Desktop left/right arrow navigation
|
279
393
|
function addDesktopArrowNavigation() {
|
280
|
-
document.addEventListener(
|
281
|
-
if (e.key !==
|
282
|
-
|
394
|
+
document.addEventListener("keydown", function (e) {
|
395
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
396
|
+
|
283
397
|
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
284
398
|
if (mobileMenu && mobileMenu.contains(document.activeElement)) return;
|
285
|
-
|
286
|
-
const navbar =
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
399
|
+
|
400
|
+
const navbar =
|
401
|
+
document.querySelector('[data-hs-nav="wrapper"]') ||
|
402
|
+
document.querySelector(".navbar_component") ||
|
403
|
+
document.querySelector('nav[role="navigation"]') ||
|
404
|
+
document.querySelector("nav");
|
405
|
+
|
291
406
|
if (!navbar || !navbar.contains(document.activeElement)) return;
|
292
|
-
|
293
|
-
const openDropdownList = navbar.querySelector(
|
294
|
-
|
295
|
-
|
407
|
+
|
408
|
+
const openDropdownList = navbar.querySelector(
|
409
|
+
'[aria-hidden="false"][role="menu"]',
|
410
|
+
);
|
411
|
+
if (openDropdownList && openDropdownList.contains(document.activeElement))
|
412
|
+
return;
|
413
|
+
|
296
414
|
e.preventDefault();
|
297
|
-
|
298
|
-
const allNavbarElements = navbar.querySelectorAll(
|
299
|
-
const focusableElements = Array.from(allNavbarElements).filter(el => {
|
300
|
-
if (el.getAttribute(
|
301
|
-
|
415
|
+
|
416
|
+
const allNavbarElements = navbar.querySelectorAll("a, button");
|
417
|
+
const focusableElements = Array.from(allNavbarElements).filter((el) => {
|
418
|
+
if (el.getAttribute("tabindex") === "-1") return false;
|
419
|
+
|
302
420
|
const isInDropdownList = el.closest('[role="menu"]');
|
303
421
|
if (isInDropdownList) return false;
|
304
|
-
|
422
|
+
|
305
423
|
const isInMobileMenu = el.closest('[data-hs-nav="menu"]');
|
306
424
|
if (isInMobileMenu) return false;
|
307
|
-
|
425
|
+
|
308
426
|
const computedStyle = window.getComputedStyle(el);
|
309
|
-
const isHidden =
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
427
|
+
const isHidden =
|
428
|
+
computedStyle.display === "none" ||
|
429
|
+
computedStyle.visibility === "hidden" ||
|
430
|
+
computedStyle.opacity === "0" ||
|
431
|
+
el.offsetWidth === 0 ||
|
432
|
+
el.offsetHeight === 0;
|
314
433
|
if (isHidden) return false;
|
315
|
-
|
434
|
+
|
316
435
|
let parent = el.parentElement;
|
317
436
|
while (parent && parent !== navbar) {
|
318
437
|
const parentStyle = window.getComputedStyle(parent);
|
319
|
-
const parentHidden =
|
320
|
-
|
321
|
-
|
322
|
-
|
438
|
+
const parentHidden =
|
439
|
+
parentStyle.display === "none" ||
|
440
|
+
parentStyle.visibility === "hidden" ||
|
441
|
+
parent.offsetWidth === 0 ||
|
442
|
+
parent.offsetHeight === 0;
|
323
443
|
if (parentHidden) return false;
|
324
444
|
parent = parent.parentElement;
|
325
445
|
}
|
326
|
-
|
446
|
+
|
327
447
|
return true;
|
328
448
|
});
|
329
|
-
|
449
|
+
|
330
450
|
const currentIndex = focusableElements.indexOf(document.activeElement);
|
331
451
|
if (currentIndex === -1) return;
|
332
|
-
|
452
|
+
|
333
453
|
let nextIndex;
|
334
|
-
if (e.key ===
|
454
|
+
if (e.key === "ArrowRight") {
|
335
455
|
nextIndex = (currentIndex + 1) % focusableElements.length;
|
336
456
|
} else {
|
337
|
-
nextIndex =
|
457
|
+
nextIndex =
|
458
|
+
currentIndex === 0 ? focusableElements.length - 1 : currentIndex - 1;
|
338
459
|
}
|
339
|
-
|
460
|
+
|
340
461
|
focusableElements[nextIndex].focus();
|
341
462
|
});
|
342
463
|
}
|
@@ -345,233 +466,287 @@ function addDesktopArrowNavigation() {
|
|
345
466
|
function setupMobileMenuButton() {
|
346
467
|
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
347
468
|
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
348
|
-
|
469
|
+
|
349
470
|
if (!menuButton || !mobileMenu) return;
|
350
|
-
|
471
|
+
|
351
472
|
const menuId = `mobile-menu-${Date.now()}`;
|
352
|
-
|
353
|
-
menuButton.setAttribute(
|
354
|
-
menuButton.setAttribute(
|
355
|
-
menuButton.setAttribute(
|
356
|
-
|
473
|
+
|
474
|
+
menuButton.setAttribute("aria-expanded", "false");
|
475
|
+
menuButton.setAttribute("aria-controls", menuId);
|
476
|
+
menuButton.setAttribute("aria-label", "Open navigation menu");
|
477
|
+
|
357
478
|
mobileMenu.id = menuId;
|
358
|
-
mobileMenu.setAttribute(
|
359
|
-
mobileMenu.setAttribute(
|
360
|
-
mobileMenu
|
361
|
-
|
479
|
+
mobileMenu.setAttribute("role", "dialog");
|
480
|
+
mobileMenu.setAttribute("aria-modal", "true");
|
481
|
+
setElementInert(mobileMenu, true);
|
482
|
+
|
362
483
|
let isMenuOpen = false;
|
363
|
-
|
484
|
+
|
364
485
|
function shouldPreventMobileMenu() {
|
365
|
-
const
|
366
|
-
if (!
|
367
|
-
|
368
|
-
const computedStyle = window.getComputedStyle(
|
369
|
-
return computedStyle.display ===
|
486
|
+
const menuHideElement = document.querySelector(".menu-hide.is-mobile");
|
487
|
+
if (!menuHideElement) return false;
|
488
|
+
|
489
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
490
|
+
return computedStyle.display === "none";
|
370
491
|
}
|
371
|
-
|
492
|
+
|
372
493
|
function openMenu() {
|
373
494
|
if (isMenuOpen || shouldPreventMobileMenu()) return;
|
374
495
|
isMenuOpen = true;
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
496
|
+
|
497
|
+
try {
|
498
|
+
// Add body overflow hidden class
|
499
|
+
document.body.classList.add("u-overflow-hidden");
|
500
|
+
|
501
|
+
// Add blur effect to modal blur elements
|
502
|
+
document
|
503
|
+
.querySelectorAll('[data-hs-nav="modal-blur"]')
|
504
|
+
.forEach((element) => {
|
505
|
+
element.style.display = "block";
|
506
|
+
element.style.opacity = "0.5";
|
507
|
+
element.style.transition = "opacity 0.3s ease";
|
508
|
+
});
|
509
|
+
|
510
|
+
menuButton.setAttribute("aria-expanded", "true");
|
511
|
+
menuButton.setAttribute("aria-label", "Close navigation menu");
|
512
|
+
setElementInert(mobileMenu, false);
|
513
|
+
|
514
|
+
// Announce to screen readers
|
515
|
+
const menuName = getMenuName(menuButton);
|
516
|
+
announceToScreenReader(`${menuName} opened`);
|
517
|
+
|
518
|
+
// Prevent tabbing outside navbar using tabindex management
|
519
|
+
const navbarWrapper =
|
520
|
+
document.querySelector('[data-hs-nav="wrapper"]') ||
|
521
|
+
document.querySelector(".navbar_component") ||
|
522
|
+
document.querySelector('nav[role="navigation"]') ||
|
523
|
+
document.querySelector("nav");
|
524
|
+
|
525
|
+
const allFocusableElements = document.querySelectorAll(
|
526
|
+
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
527
|
+
);
|
528
|
+
allFocusableElements.forEach((el) => {
|
529
|
+
if (navbarWrapper && !navbarWrapper.contains(el)) {
|
530
|
+
el.setAttribute(
|
531
|
+
"data-mobile-menu-tabindex",
|
532
|
+
el.getAttribute("tabindex") || "0",
|
533
|
+
);
|
534
|
+
el.setAttribute("tabindex", "-1");
|
535
|
+
}
|
536
|
+
});
|
537
|
+
|
538
|
+
const clickEvent = new MouseEvent("click", {
|
539
|
+
bubbles: true,
|
540
|
+
cancelable: true,
|
541
|
+
view: window,
|
542
|
+
});
|
543
|
+
menuButton.dispatchEvent(clickEvent);
|
544
|
+
} catch (e) {
|
545
|
+
// DOM operation failed, restore state
|
546
|
+
isMenuOpen = false;
|
547
|
+
menuButton.setAttribute("aria-expanded", "false");
|
548
|
+
menuButton.setAttribute("aria-label", "Open navigation menu");
|
549
|
+
}
|
410
550
|
}
|
411
|
-
|
551
|
+
|
412
552
|
function closeMenu() {
|
413
553
|
if (!isMenuOpen) return;
|
414
554
|
isMenuOpen = false;
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
mobileMenu.inert = true;
|
434
|
-
|
435
|
-
// Restore tabbing to entire page using tabindex management
|
436
|
-
const elementsToRestore = document.querySelectorAll('[data-mobile-menu-tabindex]');
|
437
|
-
elementsToRestore.forEach(el => {
|
438
|
-
const originalTabindex = el.getAttribute('data-mobile-menu-tabindex');
|
439
|
-
if (originalTabindex === '0') {
|
440
|
-
el.removeAttribute('tabindex');
|
441
|
-
} else {
|
442
|
-
el.setAttribute('tabindex', originalTabindex);
|
555
|
+
|
556
|
+
try {
|
557
|
+
// Remove body overflow hidden class
|
558
|
+
document.body.classList.remove("u-overflow-hidden");
|
559
|
+
|
560
|
+
// Remove blur effect from modal blur elements
|
561
|
+
document
|
562
|
+
.querySelectorAll('[data-hs-nav="modal-blur"]')
|
563
|
+
.forEach((element) => {
|
564
|
+
element.style.opacity = "0";
|
565
|
+
element.style.transition = "opacity 0.3s ease";
|
566
|
+
setTimeout(() => {
|
567
|
+
element.style.display = "none";
|
568
|
+
}, 300);
|
569
|
+
});
|
570
|
+
|
571
|
+
if (mobileMenu.contains(document.activeElement)) {
|
572
|
+
menuButton.focus();
|
443
573
|
}
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
574
|
+
menuButton.setAttribute("aria-expanded", "false");
|
575
|
+
menuButton.setAttribute("aria-label", "Open navigation menu");
|
576
|
+
setElementInert(mobileMenu, true);
|
577
|
+
|
578
|
+
// Announce to screen readers
|
579
|
+
const menuName = getMenuName(menuButton);
|
580
|
+
announceToScreenReader(`${menuName} closed`);
|
581
|
+
|
582
|
+
// Restore tabbing to entire page using tabindex management
|
583
|
+
const elementsToRestore = document.querySelectorAll(
|
584
|
+
"[data-mobile-menu-tabindex]",
|
585
|
+
);
|
586
|
+
elementsToRestore.forEach((el) => {
|
587
|
+
const originalTabindex = el.getAttribute("data-mobile-menu-tabindex");
|
588
|
+
if (originalTabindex === "0") {
|
589
|
+
el.removeAttribute("tabindex");
|
590
|
+
} else {
|
591
|
+
el.setAttribute("tabindex", originalTabindex);
|
592
|
+
}
|
593
|
+
el.removeAttribute("data-mobile-menu-tabindex");
|
594
|
+
});
|
595
|
+
|
596
|
+
const clickEvent = new MouseEvent("click", {
|
597
|
+
bubbles: true,
|
598
|
+
cancelable: true,
|
599
|
+
view: window,
|
600
|
+
});
|
601
|
+
menuButton.dispatchEvent(clickEvent);
|
602
|
+
} catch (e) {
|
603
|
+
// DOM operation failed, ensure consistent state
|
604
|
+
isMenuOpen = false;
|
605
|
+
menuButton.setAttribute("aria-expanded", "false");
|
606
|
+
menuButton.setAttribute("aria-label", "Open navigation menu");
|
607
|
+
}
|
453
608
|
}
|
454
|
-
|
609
|
+
|
455
610
|
function toggleMenu() {
|
456
611
|
if (shouldPreventMobileMenu()) return;
|
457
|
-
|
612
|
+
|
458
613
|
if (isMenuOpen) {
|
459
614
|
closeMenu();
|
460
615
|
} else {
|
461
616
|
openMenu();
|
462
617
|
}
|
463
618
|
}
|
464
|
-
|
465
|
-
menuButton.addEventListener(
|
466
|
-
if (e.key ===
|
619
|
+
|
620
|
+
menuButton.addEventListener("keydown", function (e) {
|
621
|
+
if (e.key === "Enter" || e.key === " ") {
|
467
622
|
e.preventDefault();
|
468
623
|
toggleMenu();
|
469
|
-
} else if (e.key ===
|
624
|
+
} else if (e.key === "ArrowDown") {
|
470
625
|
e.preventDefault();
|
471
626
|
if (!isMenuOpen) {
|
472
627
|
openMenu();
|
473
628
|
}
|
474
|
-
const firstElement = mobileMenu.querySelector(
|
629
|
+
const firstElement = mobileMenu.querySelector("button, a");
|
475
630
|
if (firstElement) {
|
476
631
|
firstElement.focus();
|
477
632
|
}
|
478
|
-
} else if (e.key ===
|
633
|
+
} else if (e.key === "ArrowUp") {
|
479
634
|
e.preventDefault();
|
480
635
|
if (isMenuOpen) {
|
481
636
|
closeMenu();
|
482
637
|
}
|
483
638
|
}
|
484
639
|
});
|
485
|
-
|
486
|
-
menuButton.addEventListener(
|
640
|
+
|
641
|
+
menuButton.addEventListener("click", function (e) {
|
487
642
|
if (!e.isTrusted) return;
|
488
|
-
|
643
|
+
|
489
644
|
if (shouldPreventMobileMenu()) return;
|
490
|
-
|
645
|
+
|
491
646
|
if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
|
492
647
|
menuButton.focus();
|
493
648
|
}
|
494
|
-
|
649
|
+
|
495
650
|
const newMenuState = !isMenuOpen;
|
496
651
|
isMenuOpen = newMenuState;
|
497
|
-
|
652
|
+
|
498
653
|
// Handle body overflow class
|
499
654
|
if (isMenuOpen) {
|
500
|
-
document.body.classList.add(
|
655
|
+
document.body.classList.add("u-overflow-hidden");
|
501
656
|
} else {
|
502
|
-
document.body.classList.remove(
|
657
|
+
document.body.classList.remove("u-overflow-hidden");
|
503
658
|
}
|
504
|
-
|
659
|
+
|
505
660
|
// Handle blur effect
|
506
|
-
document
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
element.style.
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
661
|
+
document
|
662
|
+
.querySelectorAll('[data-hs-nav="modal-blur"]')
|
663
|
+
.forEach((element) => {
|
664
|
+
if (isMenuOpen) {
|
665
|
+
element.style.display = "block";
|
666
|
+
element.style.opacity = "0.5";
|
667
|
+
element.style.transition = "opacity 0.3s ease";
|
668
|
+
} else {
|
669
|
+
element.style.opacity = "0";
|
670
|
+
element.style.transition = "opacity 0.3s ease";
|
671
|
+
setTimeout(() => {
|
672
|
+
element.style.display = "none";
|
673
|
+
}, 300);
|
674
|
+
}
|
675
|
+
});
|
676
|
+
|
677
|
+
menuButton.setAttribute("aria-expanded", isMenuOpen);
|
678
|
+
menuButton.setAttribute(
|
679
|
+
"aria-label",
|
680
|
+
isMenuOpen ? "Close navigation menu" : "Open navigation menu",
|
523
681
|
);
|
524
|
-
mobileMenu
|
525
|
-
|
682
|
+
setElementInert(mobileMenu, !isMenuOpen);
|
683
|
+
|
526
684
|
// Handle tabindex management for external clicks
|
527
685
|
if (isMenuOpen) {
|
528
|
-
const navbarWrapper =
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
allFocusableElements.
|
686
|
+
const navbarWrapper =
|
687
|
+
document.querySelector('[data-hs-nav="wrapper"]') ||
|
688
|
+
document.querySelector(".navbar_component") ||
|
689
|
+
document.querySelector('nav[role="navigation"]') ||
|
690
|
+
document.querySelector("nav");
|
691
|
+
|
692
|
+
const allFocusableElements = document.querySelectorAll(
|
693
|
+
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
694
|
+
);
|
695
|
+
allFocusableElements.forEach((el) => {
|
535
696
|
if (navbarWrapper && !navbarWrapper.contains(el)) {
|
536
|
-
el.setAttribute(
|
537
|
-
|
697
|
+
el.setAttribute(
|
698
|
+
"data-mobile-menu-tabindex",
|
699
|
+
el.getAttribute("tabindex") || "0",
|
700
|
+
);
|
701
|
+
el.setAttribute("tabindex", "-1");
|
538
702
|
}
|
539
703
|
});
|
540
704
|
} else {
|
541
|
-
const elementsToRestore = document.querySelectorAll(
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
705
|
+
const elementsToRestore = document.querySelectorAll(
|
706
|
+
"[data-mobile-menu-tabindex]",
|
707
|
+
);
|
708
|
+
elementsToRestore.forEach((el) => {
|
709
|
+
const originalTabindex = el.getAttribute("data-mobile-menu-tabindex");
|
710
|
+
if (originalTabindex === "0") {
|
711
|
+
el.removeAttribute("tabindex");
|
546
712
|
} else {
|
547
|
-
el.setAttribute(
|
713
|
+
el.setAttribute("tabindex", originalTabindex);
|
548
714
|
}
|
549
|
-
el.removeAttribute(
|
715
|
+
el.removeAttribute("data-mobile-menu-tabindex");
|
550
716
|
});
|
551
717
|
}
|
552
718
|
});
|
553
|
-
|
719
|
+
|
554
720
|
// Store the menu state and functions for breakpoint handler
|
555
721
|
window.mobileMenuState = {
|
556
722
|
isMenuOpen: () => isMenuOpen,
|
557
723
|
closeMenu: closeMenu,
|
558
|
-
openMenu: openMenu
|
724
|
+
openMenu: openMenu,
|
559
725
|
};
|
726
|
+
|
727
|
+
// Cleanup function for window.mobileMenuState
|
728
|
+
if (typeof window !== "undefined") {
|
729
|
+
window.addEventListener("unload", () => {
|
730
|
+
if (window.mobileMenuState) {
|
731
|
+
delete window.mobileMenuState;
|
732
|
+
}
|
733
|
+
});
|
734
|
+
}
|
560
735
|
}
|
561
736
|
|
562
737
|
// Mobile menu breakpoint handler
|
563
738
|
function setupMobileMenuBreakpointHandler() {
|
564
739
|
let preventedMenuState = false;
|
565
|
-
|
740
|
+
|
566
741
|
function handleBreakpointChange() {
|
567
|
-
const
|
568
|
-
if (!
|
569
|
-
|
570
|
-
const computedStyle = window.getComputedStyle(
|
571
|
-
const shouldPrevent = computedStyle.display ===
|
572
|
-
|
742
|
+
const menuHideElement = document.querySelector(".menu-hide.is-mobile");
|
743
|
+
if (!menuHideElement) return;
|
744
|
+
|
745
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
746
|
+
const shouldPrevent = computedStyle.display === "none";
|
747
|
+
|
573
748
|
if (!window.mobileMenuState) return;
|
574
|
-
|
749
|
+
|
575
750
|
if (shouldPrevent && window.mobileMenuState.isMenuOpen()) {
|
576
751
|
// Store that the menu was open before being prevented
|
577
752
|
preventedMenuState = true;
|
@@ -582,19 +757,24 @@ function setupMobileMenuBreakpointHandler() {
|
|
582
757
|
window.mobileMenuState.openMenu();
|
583
758
|
}
|
584
759
|
}
|
585
|
-
|
760
|
+
|
586
761
|
// Use ResizeObserver for more accurate detection
|
587
|
-
if (
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
762
|
+
if (typeof ResizeObserver !== "undefined") {
|
763
|
+
try {
|
764
|
+
const resizeObserver = new ResizeObserver(handleBreakpointChange);
|
765
|
+
const menuHideElement = document.querySelector(".menu-hide.is-mobile");
|
766
|
+
if (menuHideElement) {
|
767
|
+
resizeObserver.observe(menuHideElement);
|
768
|
+
}
|
769
|
+
} catch (e) {
|
770
|
+
// ResizeObserver not supported or error occurred
|
771
|
+
// Silently fall back to resize event
|
592
772
|
}
|
593
773
|
}
|
594
|
-
|
774
|
+
|
595
775
|
// Fallback to resize event
|
596
|
-
window.addEventListener(
|
597
|
-
|
776
|
+
window.addEventListener("resize", handleBreakpointChange);
|
777
|
+
|
598
778
|
// Initial check
|
599
779
|
handleBreakpointChange();
|
600
780
|
}
|
@@ -602,9 +782,9 @@ function setupMobileMenuBreakpointHandler() {
|
|
602
782
|
function sanitizeForID(text) {
|
603
783
|
return text
|
604
784
|
.toLowerCase()
|
605
|
-
.replace(/[^a-z0-9\s]/g,
|
606
|
-
.replace(/\s+/g,
|
607
|
-
.replace(/^-+|-+$/g,
|
785
|
+
.replace(/[^a-z0-9\s]/g, "")
|
786
|
+
.replace(/\s+/g, "-")
|
787
|
+
.replace(/^-+|-+$/g, "")
|
608
788
|
.substring(0, 50);
|
609
789
|
}
|
610
790
|
|
@@ -613,11 +793,11 @@ function setupMobileMenuARIA() {
|
|
613
793
|
const menuContainer = document.querySelector('[data-hs-nav="menu"]');
|
614
794
|
if (!menuContainer) return;
|
615
795
|
|
616
|
-
const buttons = menuContainer.querySelectorAll(
|
617
|
-
const links = menuContainer.querySelectorAll(
|
796
|
+
const buttons = menuContainer.querySelectorAll("button");
|
797
|
+
const links = menuContainer.querySelectorAll("a");
|
618
798
|
|
619
|
-
buttons.forEach(button => {
|
620
|
-
const buttonText = button.textContent
|
799
|
+
buttons.forEach((button) => {
|
800
|
+
const buttonText = button.textContent && button.textContent.trim();
|
621
801
|
if (!buttonText) return;
|
622
802
|
|
623
803
|
const sanitizedText = sanitizeForID(buttonText);
|
@@ -625,57 +805,77 @@ function setupMobileMenuARIA() {
|
|
625
805
|
const listId = `navbar-mobile-${sanitizedText}-list`;
|
626
806
|
|
627
807
|
button.id = buttonId;
|
628
|
-
button.setAttribute(
|
629
|
-
button.setAttribute(
|
808
|
+
button.setAttribute("aria-expanded", "false");
|
809
|
+
button.setAttribute("aria-controls", listId);
|
630
810
|
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
if (
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
el.querySelectorAll('a').length > 1 &&
|
642
|
-
!el.contains(button)
|
811
|
+
// Look for dropdown list in the same container as the button
|
812
|
+
let dropdownList = null;
|
813
|
+
const buttonContainer = button.closest(
|
814
|
+
'.menu-card_dropdown, .menu_contain, [data-hs-nav="menu"]',
|
815
|
+
);
|
816
|
+
|
817
|
+
if (buttonContainer) {
|
818
|
+
// First try to find a list element within the same container
|
819
|
+
dropdownList = buttonContainer.querySelector(
|
820
|
+
'.menu-card_list, .dropdown-list, [role="menu"]',
|
643
821
|
);
|
822
|
+
|
823
|
+
// If not found, look for any element with multiple links that's not the button itself
|
824
|
+
if (!dropdownList || !dropdownList.querySelector("a")) {
|
825
|
+
const allListElements =
|
826
|
+
buttonContainer.querySelectorAll("div, ul, nav");
|
827
|
+
dropdownList = Array.from(allListElements).find(
|
828
|
+
(el) =>
|
829
|
+
el.querySelectorAll("a").length > 1 &&
|
830
|
+
!el.contains(button) &&
|
831
|
+
el !== button,
|
832
|
+
);
|
833
|
+
}
|
644
834
|
}
|
645
835
|
|
646
|
-
if (dropdownList && dropdownList.querySelector(
|
836
|
+
if (dropdownList && dropdownList.querySelector("a")) {
|
647
837
|
dropdownList.id = listId;
|
648
|
-
dropdownList
|
838
|
+
setElementInert(dropdownList, true);
|
649
839
|
|
650
|
-
button.addEventListener(
|
651
|
-
const isExpanded = button.getAttribute(
|
840
|
+
button.addEventListener("click", function () {
|
841
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
652
842
|
const newState = !isExpanded;
|
653
|
-
button.setAttribute(
|
654
|
-
dropdownList
|
843
|
+
button.setAttribute("aria-expanded", newState);
|
844
|
+
setElementInert(dropdownList, !newState);
|
845
|
+
|
846
|
+
// Announce to screen readers
|
847
|
+
const menuName = getMenuName(button);
|
848
|
+
announceToScreenReader(
|
849
|
+
`${menuName} submenu ${newState ? "opened" : "closed"}`,
|
850
|
+
);
|
655
851
|
});
|
656
852
|
}
|
657
853
|
});
|
658
854
|
|
659
|
-
links.forEach(link => {
|
660
|
-
const linkText = link.textContent
|
855
|
+
links.forEach((link) => {
|
856
|
+
const linkText = link.textContent && link.textContent.trim();
|
661
857
|
if (!linkText) return;
|
662
858
|
|
663
859
|
const sanitizedText = sanitizeForID(linkText);
|
664
860
|
const linkId = `navbar-mobile-${sanitizedText}-link`;
|
665
861
|
link.id = linkId;
|
666
862
|
});
|
667
|
-
|
863
|
+
|
668
864
|
setupMobileMenuArrowNavigation(menuContainer);
|
669
865
|
}
|
670
866
|
|
671
867
|
// Mobile menu arrow navigation
|
672
868
|
function setupMobileMenuArrowNavigation(menuContainer) {
|
673
869
|
function getFocusableElements() {
|
674
|
-
const allElements = menuContainer.querySelectorAll(
|
675
|
-
return Array.from(allElements).filter(el => {
|
870
|
+
const allElements = menuContainer.querySelectorAll("button, a");
|
871
|
+
return Array.from(allElements).filter((el) => {
|
676
872
|
let current = el;
|
677
873
|
while (current && current !== menuContainer) {
|
678
|
-
|
874
|
+
// Check both native inert and polyfill inert
|
875
|
+
if (
|
876
|
+
current.inert === true ||
|
877
|
+
current.getAttribute("data-inert") === "true"
|
878
|
+
) {
|
679
879
|
return false;
|
680
880
|
}
|
681
881
|
current = current.parentElement;
|
@@ -683,17 +883,17 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
683
883
|
return true;
|
684
884
|
});
|
685
885
|
}
|
686
|
-
|
886
|
+
|
687
887
|
let currentFocusIndex = -1;
|
688
|
-
|
689
|
-
menuContainer.addEventListener(
|
888
|
+
|
889
|
+
menuContainer.addEventListener("keydown", function (e) {
|
690
890
|
const focusableElements = getFocusableElements();
|
691
891
|
if (focusableElements.length === 0) return;
|
692
|
-
|
892
|
+
|
693
893
|
const activeElement = document.activeElement;
|
694
894
|
currentFocusIndex = focusableElements.indexOf(activeElement);
|
695
|
-
|
696
|
-
if (e.key ===
|
895
|
+
|
896
|
+
if (e.key === "ArrowDown") {
|
697
897
|
e.preventDefault();
|
698
898
|
if (currentFocusIndex >= focusableElements.length - 1) {
|
699
899
|
currentFocusIndex = 0;
|
@@ -701,10 +901,12 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
701
901
|
currentFocusIndex = currentFocusIndex + 1;
|
702
902
|
}
|
703
903
|
focusableElements[currentFocusIndex].focus();
|
704
|
-
} else if (e.key ===
|
904
|
+
} else if (e.key === "ArrowUp") {
|
705
905
|
e.preventDefault();
|
706
906
|
if (currentFocusIndex <= 0) {
|
707
|
-
const mobileMenuButton = document.querySelector(
|
907
|
+
const mobileMenuButton = document.querySelector(
|
908
|
+
'[data-hs-nav="menubtn"]',
|
909
|
+
);
|
708
910
|
if (mobileMenuButton) {
|
709
911
|
mobileMenuButton.focus();
|
710
912
|
return;
|
@@ -712,40 +914,50 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
712
914
|
}
|
713
915
|
currentFocusIndex = currentFocusIndex - 1;
|
714
916
|
focusableElements[currentFocusIndex].focus();
|
715
|
-
} else if (e.key ===
|
917
|
+
} else if (e.key === "ArrowRight") {
|
716
918
|
e.preventDefault();
|
717
|
-
if (
|
718
|
-
|
919
|
+
if (
|
920
|
+
activeElement.tagName === "BUTTON" &&
|
921
|
+
activeElement.hasAttribute("aria-controls")
|
922
|
+
) {
|
923
|
+
const isExpanded =
|
924
|
+
activeElement.getAttribute("aria-expanded") === "true";
|
719
925
|
if (!isExpanded) {
|
720
926
|
activeElement.click();
|
721
927
|
}
|
722
928
|
return;
|
723
929
|
}
|
724
|
-
} else if (e.key ===
|
930
|
+
} else if (e.key === "ArrowLeft") {
|
725
931
|
e.preventDefault();
|
726
|
-
if (
|
727
|
-
|
932
|
+
if (
|
933
|
+
activeElement.tagName === "BUTTON" &&
|
934
|
+
activeElement.hasAttribute("aria-controls")
|
935
|
+
) {
|
936
|
+
const isExpanded =
|
937
|
+
activeElement.getAttribute("aria-expanded") === "true";
|
728
938
|
if (isExpanded) {
|
729
939
|
activeElement.click();
|
730
940
|
}
|
731
941
|
return;
|
732
942
|
}
|
733
|
-
} else if (e.key ===
|
943
|
+
} else if (e.key === "Home") {
|
734
944
|
e.preventDefault();
|
735
945
|
currentFocusIndex = 0;
|
736
946
|
focusableElements[0].focus();
|
737
|
-
} else if (e.key ===
|
947
|
+
} else if (e.key === "End") {
|
738
948
|
e.preventDefault();
|
739
949
|
currentFocusIndex = focusableElements.length - 1;
|
740
950
|
focusableElements[focusableElements.length - 1].focus();
|
741
|
-
} else if (e.key ===
|
951
|
+
} else if (e.key === " " && activeElement.tagName === "A") {
|
742
952
|
e.preventDefault();
|
743
|
-
} else if (e.key ===
|
744
|
-
const mobileMenuButton = document.querySelector(
|
953
|
+
} else if (e.key === "Escape") {
|
954
|
+
const mobileMenuButton = document.querySelector(
|
955
|
+
'[data-hs-nav="menubtn"]',
|
956
|
+
);
|
745
957
|
if (mobileMenuButton) {
|
746
958
|
mobileMenuButton.click();
|
747
959
|
mobileMenuButton.focus();
|
748
960
|
}
|
749
961
|
}
|
750
962
|
});
|
751
|
-
}
|
963
|
+
}
|