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