@hortonstudio/main 1.2.28 → 1.2.29
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 +2 -1
- package/animations/hero.js +83 -32
- package/index.js +1 -1
- package/package.json +1 -1
- package/utils/navbar.js +192 -253
package/animations/hero.js
CHANGED
@@ -45,11 +45,11 @@ const config = {
|
|
45
45
|
},
|
46
46
|
appear: {
|
47
47
|
y: 50,
|
48
|
-
duration: 1
|
48
|
+
duration: 1,
|
49
49
|
ease: "power3.out"
|
50
50
|
},
|
51
51
|
navStagger: {
|
52
|
-
duration: 1
|
52
|
+
duration: 1,
|
53
53
|
stagger: 0.1,
|
54
54
|
ease: "power3.out"
|
55
55
|
},
|
@@ -409,7 +409,30 @@ export async function init() {
|
|
409
409
|
|
410
410
|
if (navElement) {
|
411
411
|
heroTimeline.to(navElement,
|
412
|
-
{
|
412
|
+
{
|
413
|
+
opacity: 1,
|
414
|
+
y: 0,
|
415
|
+
duration: config.nav.duration,
|
416
|
+
ease: config.nav.ease,
|
417
|
+
onComplete: () => {
|
418
|
+
// If no advanced nav, restore interactions here
|
419
|
+
if (!hasAdvancedNav) {
|
420
|
+
const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
|
421
|
+
allFocusableElements.forEach(el => {
|
422
|
+
el.style.pointerEvents = '';
|
423
|
+
const originalTabindex = el.getAttribute('data-original-tabindex');
|
424
|
+
if (originalTabindex === '0') {
|
425
|
+
el.removeAttribute('tabindex');
|
426
|
+
} else {
|
427
|
+
el.setAttribute('tabindex', originalTabindex);
|
428
|
+
}
|
429
|
+
el.removeAttribute('data-original-tabindex');
|
430
|
+
});
|
431
|
+
// Restore nav pointer events
|
432
|
+
navElement.style.pointerEvents = '';
|
433
|
+
}
|
434
|
+
}
|
435
|
+
},
|
413
436
|
timing.nav
|
414
437
|
);
|
415
438
|
}
|
@@ -453,7 +476,29 @@ export async function init() {
|
|
453
476
|
|
454
477
|
if (hasAdvancedNav && navButton.length > 0) {
|
455
478
|
heroTimeline.to(navButton,
|
456
|
-
{
|
479
|
+
{
|
480
|
+
opacity: 1,
|
481
|
+
duration: config.nav.duration,
|
482
|
+
ease: config.nav.ease,
|
483
|
+
onComplete: () => {
|
484
|
+
// Restore page-wide tabbing and interactions after navbar animations complete
|
485
|
+
const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
|
486
|
+
allFocusableElements.forEach(el => {
|
487
|
+
el.style.pointerEvents = '';
|
488
|
+
const originalTabindex = el.getAttribute('data-original-tabindex');
|
489
|
+
if (originalTabindex === '0') {
|
490
|
+
el.removeAttribute('tabindex');
|
491
|
+
} else {
|
492
|
+
el.setAttribute('tabindex', originalTabindex);
|
493
|
+
}
|
494
|
+
el.removeAttribute('data-original-tabindex');
|
495
|
+
});
|
496
|
+
// Restore nav pointer events
|
497
|
+
if (navElement) {
|
498
|
+
navElement.style.pointerEvents = '';
|
499
|
+
}
|
500
|
+
}
|
501
|
+
},
|
457
502
|
timing.navButton
|
458
503
|
);
|
459
504
|
}
|
@@ -539,43 +584,49 @@ export async function init() {
|
|
539
584
|
duration: config.appear.duration,
|
540
585
|
ease: config.appear.ease,
|
541
586
|
onComplete: () => {
|
542
|
-
//
|
543
|
-
const
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
el.
|
549
|
-
|
550
|
-
|
587
|
+
// Check if interactions haven't been restored yet
|
588
|
+
const stillDisabled = document.querySelector('[data-original-tabindex]');
|
589
|
+
if (stillDisabled) {
|
590
|
+
const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
|
591
|
+
allFocusableElements.forEach(el => {
|
592
|
+
el.style.pointerEvents = '';
|
593
|
+
const originalTabindex = el.getAttribute('data-original-tabindex');
|
594
|
+
if (originalTabindex === '0') {
|
595
|
+
el.removeAttribute('tabindex');
|
596
|
+
} else {
|
597
|
+
el.setAttribute('tabindex', originalTabindex);
|
598
|
+
}
|
599
|
+
el.removeAttribute('data-original-tabindex');
|
600
|
+
});
|
601
|
+
// Restore nav pointer events
|
602
|
+
if (navElement) {
|
603
|
+
navElement.style.pointerEvents = '';
|
551
604
|
}
|
552
|
-
el.removeAttribute('data-original-tabindex');
|
553
|
-
});
|
554
|
-
// Restore nav pointer events
|
555
|
-
if (navElement) {
|
556
|
-
navElement.style.pointerEvents = '';
|
557
605
|
}
|
558
606
|
}
|
559
607
|
},
|
560
608
|
timing.appear
|
561
609
|
);
|
562
610
|
} else {
|
563
|
-
// If no appear elements,
|
611
|
+
// If no appear elements, check if interactions need restoring when timeline completes
|
564
612
|
heroTimeline.call(() => {
|
565
|
-
const
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
el.
|
571
|
-
|
572
|
-
|
613
|
+
const stillDisabled = document.querySelector('[data-original-tabindex]');
|
614
|
+
if (stillDisabled) {
|
615
|
+
const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
|
616
|
+
allFocusableElements.forEach(el => {
|
617
|
+
el.style.pointerEvents = '';
|
618
|
+
const originalTabindex = el.getAttribute('data-original-tabindex');
|
619
|
+
if (originalTabindex === '0') {
|
620
|
+
el.removeAttribute('tabindex');
|
621
|
+
} else {
|
622
|
+
el.setAttribute('tabindex', originalTabindex);
|
623
|
+
}
|
624
|
+
el.removeAttribute('data-original-tabindex');
|
625
|
+
});
|
626
|
+
// Restore nav pointer events
|
627
|
+
if (navElement) {
|
628
|
+
navElement.style.pointerEvents = '';
|
573
629
|
}
|
574
|
-
el.removeAttribute('data-original-tabindex');
|
575
|
-
});
|
576
|
-
// Restore nav pointer events
|
577
|
-
if (navElement) {
|
578
|
-
navElement.style.pointerEvents = '';
|
579
630
|
}
|
580
631
|
});
|
581
632
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
package/utils/navbar.js
CHANGED
@@ -1,77 +1,15 @@
|
|
1
1
|
export const init = () => {
|
2
|
-
// Mobile menu accessibility
|
3
|
-
const mobileMenuButton = document.querySelector('[data-hs-hero="nav-menu"]');
|
4
|
-
const mobileMenu = document.getElementById('mobile-navigation-menu');
|
5
|
-
|
6
|
-
if (mobileMenuButton && mobileMenu) {
|
7
|
-
let mobileMenuOpen = false;
|
8
|
-
|
9
|
-
// Initialize mobile menu button ARIA attributes
|
10
|
-
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
11
|
-
mobileMenuButton.setAttribute('aria-controls', 'mobile-navigation-menu');
|
12
|
-
mobileMenuButton.setAttribute('aria-label', 'Open navigation menu');
|
13
|
-
|
14
|
-
// Function to toggle mobile menu
|
15
|
-
function toggleMobileMenu() {
|
16
|
-
mobileMenuOpen = !mobileMenuOpen;
|
17
|
-
|
18
|
-
// Update ARIA attributes
|
19
|
-
mobileMenuButton.setAttribute('aria-expanded', mobileMenuOpen);
|
20
|
-
mobileMenuButton.setAttribute('aria-label',
|
21
|
-
mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
22
|
-
);
|
23
|
-
}
|
24
|
-
|
25
|
-
mobileMenuButton.addEventListener('click', toggleMobileMenu);
|
26
|
-
|
27
|
-
mobileMenuButton.addEventListener('keydown', function(e) {
|
28
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
29
|
-
e.preventDefault();
|
30
|
-
toggleMobileMenu();
|
31
|
-
}
|
32
|
-
});
|
33
|
-
}
|
34
|
-
|
35
|
-
// Setup dynamic dropdown system
|
36
2
|
setupDynamicDropdowns();
|
37
|
-
|
38
|
-
// Setup dynamic mobile menu button
|
39
3
|
setupMobileMenuButton();
|
40
|
-
|
41
|
-
// Dynamic mobile menu keyboard navigation
|
42
|
-
const menuContainer = document.querySelector('[data-hs-nav="menu"]');
|
43
|
-
if (menuContainer) {
|
44
|
-
const allInteractiveElements = menuContainer.querySelectorAll('button, a');
|
45
|
-
|
46
|
-
allInteractiveElements.forEach(element => {
|
47
|
-
element.addEventListener('keydown', function(e) {
|
48
|
-
if (e.key === 'Escape') {
|
49
|
-
// Close mobile menu and focus button
|
50
|
-
const mobileMenuButton = document.querySelector('[data-hs-hero="nav-menu"]');
|
51
|
-
if (mobileMenuButton) {
|
52
|
-
mobileMenuButton.click();
|
53
|
-
mobileMenuButton.focus();
|
54
|
-
}
|
55
|
-
}
|
56
|
-
});
|
57
|
-
});
|
58
|
-
}
|
59
|
-
|
60
|
-
// Setup mobile menu ARIA attributes
|
61
4
|
setupMobileMenuARIA();
|
62
|
-
|
63
5
|
return { result: 'navbar initialized' };
|
64
6
|
};
|
65
7
|
|
66
|
-
//
|
8
|
+
// Desktop dropdown system
|
67
9
|
function setupDynamicDropdowns() {
|
68
|
-
// Find all dropdown wrappers
|
69
10
|
const dropdownWrappers = document.querySelectorAll('[data-hs-nav="dropdown"]');
|
70
|
-
|
71
|
-
// Global array to track all dropdown instances
|
72
11
|
const allDropdowns = [];
|
73
12
|
|
74
|
-
// Function to close all dropdowns except the specified one
|
75
13
|
const closeAllDropdowns = (exceptWrapper = null) => {
|
76
14
|
allDropdowns.forEach(dropdown => {
|
77
15
|
if (dropdown.wrapper !== exceptWrapper && dropdown.isOpen) {
|
@@ -81,11 +19,9 @@ function setupDynamicDropdowns() {
|
|
81
19
|
};
|
82
20
|
|
83
21
|
dropdownWrappers.forEach(wrapper => {
|
84
|
-
|
85
|
-
|
86
|
-
if (!toggle) return; // Skip if no toggle found
|
22
|
+
const toggle = wrapper.querySelector('a');
|
23
|
+
if (!toggle) return;
|
87
24
|
|
88
|
-
// Find dropdown list: element with 2+ <a> tags that doesn't contain the toggle
|
89
25
|
const allElements = wrapper.querySelectorAll('*');
|
90
26
|
let dropdownList = null;
|
91
27
|
|
@@ -97,49 +33,40 @@ function setupDynamicDropdowns() {
|
|
97
33
|
}
|
98
34
|
}
|
99
35
|
|
100
|
-
if (!dropdownList) return;
|
36
|
+
if (!dropdownList) return;
|
101
37
|
|
102
|
-
// Generate unique IDs for ARIA attributes
|
103
38
|
const toggleText = toggle.textContent?.trim() || 'dropdown';
|
104
39
|
const sanitizedText = sanitizeForID(toggleText);
|
105
40
|
const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
|
106
41
|
const listId = `navbar-dropdown-${sanitizedText}-list`;
|
107
42
|
|
108
|
-
// Set up ARIA attributes for toggle
|
109
43
|
toggle.id = toggleId;
|
110
44
|
toggle.setAttribute('aria-haspopup', 'menu');
|
111
45
|
toggle.setAttribute('aria-expanded', 'false');
|
112
46
|
toggle.setAttribute('aria-controls', listId);
|
113
47
|
|
114
|
-
// Set up ARIA attributes for dropdown list
|
115
48
|
dropdownList.id = listId;
|
116
49
|
dropdownList.setAttribute('role', 'menu');
|
117
|
-
dropdownList.
|
50
|
+
dropdownList.setAttribute('aria-hidden', 'true');
|
118
51
|
|
119
|
-
// Set up ARIA attributes for menu items
|
120
52
|
const menuItems = dropdownList.querySelectorAll('a');
|
121
53
|
menuItems.forEach(item => {
|
122
54
|
item.setAttribute('role', 'menuitem');
|
55
|
+
item.setAttribute('tabindex', '-1');
|
123
56
|
});
|
124
57
|
|
125
|
-
// Track dropdown state
|
126
58
|
let isOpen = false;
|
127
59
|
let currentMenuItemIndex = -1;
|
128
60
|
|
129
|
-
// Function to open dropdown (simulate click)
|
130
61
|
function openDropdown() {
|
131
62
|
if (isOpen) return;
|
132
|
-
|
133
|
-
// Close all other dropdowns first
|
134
63
|
closeAllDropdowns(wrapper);
|
135
|
-
|
136
64
|
isOpen = true;
|
137
|
-
|
138
|
-
// Update ARIA states
|
139
65
|
toggle.setAttribute('aria-expanded', 'true');
|
140
|
-
dropdownList.
|
141
|
-
|
142
|
-
|
66
|
+
dropdownList.setAttribute('aria-hidden', 'false');
|
67
|
+
menuItems.forEach(item => {
|
68
|
+
item.setAttribute('tabindex', '0');
|
69
|
+
});
|
143
70
|
const clickEvent = new MouseEvent('click', {
|
144
71
|
bubbles: true,
|
145
72
|
cancelable: true,
|
@@ -148,26 +75,19 @@ function setupDynamicDropdowns() {
|
|
148
75
|
wrapper.dispatchEvent(clickEvent);
|
149
76
|
}
|
150
77
|
|
151
|
-
// Function to close dropdown (simulate second click)
|
152
78
|
function closeDropdown() {
|
153
79
|
if (!isOpen) return;
|
154
|
-
|
155
|
-
// Check if focus should be restored to toggle
|
156
80
|
const shouldRestoreFocus = dropdownList.contains(document.activeElement);
|
157
|
-
|
158
81
|
isOpen = false;
|
159
82
|
currentMenuItemIndex = -1;
|
160
|
-
|
161
|
-
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
162
83
|
if (shouldRestoreFocus) {
|
163
84
|
toggle.focus();
|
164
85
|
}
|
165
|
-
|
166
|
-
// Update ARIA states after focus is moved
|
167
86
|
toggle.setAttribute('aria-expanded', 'false');
|
168
|
-
dropdownList.
|
169
|
-
|
170
|
-
|
87
|
+
dropdownList.setAttribute('aria-hidden', 'true');
|
88
|
+
menuItems.forEach(item => {
|
89
|
+
item.setAttribute('tabindex', '-1');
|
90
|
+
});
|
171
91
|
const clickEvent = new MouseEvent('click', {
|
172
92
|
bubbles: true,
|
173
93
|
cancelable: true,
|
@@ -176,65 +96,56 @@ function setupDynamicDropdowns() {
|
|
176
96
|
wrapper.dispatchEvent(clickEvent);
|
177
97
|
}
|
178
98
|
|
179
|
-
// Hover events (native) - trigger click events for Webflow
|
180
99
|
wrapper.addEventListener('mouseenter', () => {
|
181
100
|
if (!isOpen) {
|
182
|
-
// Trigger click to open
|
183
101
|
const clickEvent = new MouseEvent('click', {
|
184
102
|
bubbles: true,
|
185
103
|
cancelable: true,
|
186
104
|
view: window
|
187
105
|
});
|
188
106
|
wrapper.dispatchEvent(clickEvent);
|
189
|
-
|
190
|
-
// Update our state
|
191
107
|
closeAllDropdowns(wrapper);
|
192
108
|
isOpen = true;
|
193
109
|
toggle.setAttribute('aria-expanded', 'true');
|
194
|
-
dropdownList.
|
110
|
+
dropdownList.setAttribute('aria-hidden', 'false');
|
111
|
+
menuItems.forEach(item => {
|
112
|
+
item.setAttribute('tabindex', '0');
|
113
|
+
});
|
195
114
|
}
|
196
115
|
});
|
197
116
|
|
198
117
|
wrapper.addEventListener('mouseleave', () => {
|
199
118
|
if (isOpen) {
|
200
|
-
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
201
119
|
if (dropdownList.contains(document.activeElement)) {
|
202
120
|
toggle.focus();
|
203
121
|
}
|
204
|
-
|
205
|
-
// Trigger click to close
|
206
122
|
const clickEvent = new MouseEvent('click', {
|
207
123
|
bubbles: true,
|
208
124
|
cancelable: true,
|
209
125
|
view: window
|
210
126
|
});
|
211
127
|
wrapper.dispatchEvent(clickEvent);
|
212
|
-
|
213
|
-
// Update our state after focus is moved
|
214
128
|
isOpen = false;
|
215
129
|
toggle.setAttribute('aria-expanded', 'false');
|
216
|
-
dropdownList.
|
130
|
+
dropdownList.setAttribute('aria-hidden', 'true');
|
131
|
+
menuItems.forEach(item => {
|
132
|
+
item.setAttribute('tabindex', '-1');
|
133
|
+
});
|
217
134
|
currentMenuItemIndex = -1;
|
218
135
|
}
|
219
136
|
});
|
220
137
|
|
221
|
-
// Keyboard navigation within dropdown - attach to document to catch all events
|
222
138
|
document.addEventListener('keydown', function(e) {
|
223
139
|
if (!isOpen) return;
|
224
|
-
|
225
|
-
// Only handle if focus is on toggle or within wrapper
|
226
140
|
if (!wrapper.contains(document.activeElement)) return;
|
227
141
|
|
228
142
|
if (e.key === 'ArrowDown') {
|
229
143
|
e.preventDefault();
|
230
|
-
// If currently on toggle, start at first item
|
231
144
|
if (document.activeElement === toggle) {
|
232
145
|
currentMenuItemIndex = 0;
|
233
146
|
menuItems[currentMenuItemIndex].focus();
|
234
147
|
} else {
|
235
|
-
// If at last item in dropdown, move to next sibling element
|
236
148
|
if (currentMenuItemIndex === menuItems.length - 1) {
|
237
|
-
// Find next focusable element after this dropdown
|
238
149
|
const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
|
239
150
|
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
240
151
|
if (nextElement) {
|
@@ -248,21 +159,17 @@ function setupDynamicDropdowns() {
|
|
248
159
|
}
|
249
160
|
} else if (e.key === 'ArrowUp') {
|
250
161
|
e.preventDefault();
|
251
|
-
// If currently on toggle, start at last item
|
252
162
|
if (document.activeElement === toggle) {
|
253
163
|
currentMenuItemIndex = menuItems.length - 1;
|
254
164
|
menuItems[currentMenuItemIndex].focus();
|
255
165
|
} else {
|
256
|
-
// If at first item in dropdown, move to previous sibling element
|
257
166
|
if (currentMenuItemIndex === 0) {
|
258
|
-
// Find previous focusable element before this dropdown
|
259
167
|
const prevElement = wrapper.previousElementSibling?.querySelector('a, button');
|
260
168
|
if (prevElement) {
|
261
169
|
closeDropdown();
|
262
170
|
prevElement.focus();
|
263
171
|
return;
|
264
172
|
} else {
|
265
|
-
// If no previous sibling, go back to toggle
|
266
173
|
closeDropdown();
|
267
174
|
toggle.focus();
|
268
175
|
return;
|
@@ -271,47 +178,19 @@ function setupDynamicDropdowns() {
|
|
271
178
|
currentMenuItemIndex = currentMenuItemIndex <= 0 ? menuItems.length - 1 : currentMenuItemIndex - 1;
|
272
179
|
menuItems[currentMenuItemIndex].focus();
|
273
180
|
}
|
274
|
-
} else if (e.key === 'ArrowLeft') {
|
275
|
-
e.preventDefault();
|
276
|
-
// Navigate to previous dropdown/element
|
277
|
-
const prevElement = wrapper.previousElementSibling?.querySelector('a, button');
|
278
|
-
if (prevElement) {
|
279
|
-
closeDropdown();
|
280
|
-
prevElement.focus();
|
281
|
-
} else {
|
282
|
-
closeDropdown();
|
283
|
-
toggle.focus();
|
284
|
-
}
|
285
|
-
} else if (e.key === 'ArrowRight') {
|
286
|
-
e.preventDefault();
|
287
|
-
// Navigate to next dropdown/element
|
288
|
-
const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
|
289
|
-
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
290
|
-
if (nextElement) {
|
291
|
-
closeDropdown();
|
292
|
-
nextElement.focus();
|
293
|
-
}
|
294
181
|
} else if (e.key === 'Tab') {
|
295
|
-
// Handle Tab navigation within dropdown
|
296
182
|
if (e.shiftKey) {
|
297
|
-
// Shift+Tab going backwards
|
298
183
|
if (document.activeElement === menuItems[0]) {
|
299
184
|
e.preventDefault();
|
300
185
|
closeDropdown();
|
301
186
|
toggle.focus();
|
302
187
|
}
|
303
188
|
} else {
|
304
|
-
// Tab going forwards
|
305
189
|
if (document.activeElement === menuItems[menuItems.length - 1]) {
|
306
190
|
e.preventDefault();
|
307
|
-
// Find next element BEFORE closing dropdown to avoid focus issues
|
308
191
|
const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
|
309
192
|
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
310
|
-
|
311
|
-
// Close dropdown first
|
312
193
|
closeDropdown();
|
313
|
-
|
314
|
-
// Then move focus to next element
|
315
194
|
if (nextElement) {
|
316
195
|
setTimeout(() => {
|
317
196
|
nextElement.focus();
|
@@ -333,16 +212,13 @@ function setupDynamicDropdowns() {
|
|
333
212
|
menuItems[menuItems.length - 1].focus();
|
334
213
|
} else if (e.key === ' ') {
|
335
214
|
e.preventDefault();
|
336
|
-
// Just prevent space from scrolling page - Enter activates links
|
337
215
|
}
|
338
216
|
});
|
339
217
|
|
340
|
-
// Keyboard events for toggle
|
341
218
|
toggle.addEventListener('keydown', function(e) {
|
342
219
|
if (e.key === 'ArrowDown') {
|
343
220
|
e.preventDefault();
|
344
221
|
openDropdown();
|
345
|
-
// Focus first menu item after opening
|
346
222
|
if (menuItems.length > 0) {
|
347
223
|
currentMenuItemIndex = 0;
|
348
224
|
setTimeout(() => menuItems[0].focus(), 100);
|
@@ -353,7 +229,6 @@ function setupDynamicDropdowns() {
|
|
353
229
|
closeDropdown();
|
354
230
|
} else {
|
355
231
|
openDropdown();
|
356
|
-
// Focus first item when opening with spacebar
|
357
232
|
if (menuItems.length > 0) {
|
358
233
|
currentMenuItemIndex = 0;
|
359
234
|
setTimeout(() => menuItems[0].focus(), 100);
|
@@ -362,7 +237,6 @@ function setupDynamicDropdowns() {
|
|
362
237
|
} else if (e.key === 'ArrowUp') {
|
363
238
|
e.preventDefault();
|
364
239
|
if (isOpen) {
|
365
|
-
// Focus last menu item
|
366
240
|
currentMenuItemIndex = menuItems.length - 1;
|
367
241
|
menuItems[currentMenuItemIndex].focus();
|
368
242
|
} else {
|
@@ -372,17 +246,14 @@ function setupDynamicDropdowns() {
|
|
372
246
|
e.preventDefault();
|
373
247
|
closeDropdown();
|
374
248
|
}
|
375
|
-
// Note: Enter key naturally follows the link - no preventDefault needed
|
376
249
|
});
|
377
250
|
|
378
|
-
// Close dropdown when clicking outside
|
379
251
|
document.addEventListener('click', function(e) {
|
380
252
|
if (!wrapper.contains(e.target) && isOpen) {
|
381
253
|
closeDropdown();
|
382
254
|
}
|
383
255
|
});
|
384
256
|
|
385
|
-
// Add this dropdown instance to the global array
|
386
257
|
allDropdowns.push({
|
387
258
|
wrapper,
|
388
259
|
isOpen: () => isOpen,
|
@@ -392,7 +263,6 @@ function setupDynamicDropdowns() {
|
|
392
263
|
});
|
393
264
|
});
|
394
265
|
|
395
|
-
// Global focus management - close dropdown when tab focus moves outside
|
396
266
|
document.addEventListener('focusin', function(e) {
|
397
267
|
allDropdowns.forEach(dropdown => {
|
398
268
|
if (dropdown.isOpen() && !dropdown.wrapper.contains(e.target)) {
|
@@ -401,99 +271,116 @@ function setupDynamicDropdowns() {
|
|
401
271
|
});
|
402
272
|
});
|
403
273
|
|
404
|
-
|
274
|
+
addDesktopArrowNavigation();
|
275
|
+
}
|
276
|
+
|
277
|
+
// Desktop left/right arrow navigation
|
278
|
+
function addDesktopArrowNavigation() {
|
405
279
|
document.addEventListener('keydown', function(e) {
|
406
|
-
// Only handle arrow keys
|
407
280
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
408
281
|
|
409
|
-
|
410
|
-
|
282
|
+
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
283
|
+
if (mobileMenu && mobileMenu.contains(document.activeElement)) return;
|
284
|
+
|
285
|
+
const navbar = document.querySelector('[data-hs-nav="wrapper"]') ||
|
286
|
+
document.querySelector('.navbar_component') ||
|
287
|
+
document.querySelector('nav[role="navigation"]') ||
|
288
|
+
document.querySelector('nav');
|
289
|
+
|
411
290
|
if (!navbar || !navbar.contains(document.activeElement)) return;
|
412
291
|
|
413
|
-
|
414
|
-
|
292
|
+
const openDropdownList = navbar.querySelector('[aria-hidden="false"][role="menu"]');
|
293
|
+
if (openDropdownList && openDropdownList.contains(document.activeElement)) return;
|
415
294
|
|
416
295
|
e.preventDefault();
|
417
296
|
|
418
|
-
|
419
|
-
|
420
|
-
if (
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
if (nextElement) {
|
425
|
-
activeDropdown.closeDropdown();
|
426
|
-
nextElement.focus();
|
427
|
-
}
|
428
|
-
} else { // ArrowLeft
|
429
|
-
// Find previous focusable element before this dropdown
|
430
|
-
const prevElement = activeDropdown.wrapper.previousElementSibling?.querySelector('a, button');
|
431
|
-
if (prevElement) {
|
432
|
-
activeDropdown.closeDropdown();
|
433
|
-
prevElement.focus();
|
434
|
-
} else {
|
435
|
-
// If no previous sibling, go to toggle of this dropdown
|
436
|
-
activeDropdown.closeDropdown();
|
437
|
-
activeDropdown.toggle.focus();
|
438
|
-
}
|
439
|
-
}
|
440
|
-
} else {
|
441
|
-
// Normal navigation for non-dropdown elements
|
442
|
-
const focusableElements = navbar.querySelectorAll('a:not([inert] a), button:not([inert] button)');
|
443
|
-
const focusableArray = Array.from(focusableElements);
|
297
|
+
const allNavbarElements = navbar.querySelectorAll('a, button');
|
298
|
+
const focusableElements = Array.from(allNavbarElements).filter(el => {
|
299
|
+
if (el.getAttribute('tabindex') === '-1') return false;
|
300
|
+
|
301
|
+
const isInDropdownList = el.closest('[role="menu"]');
|
302
|
+
if (isInDropdownList) return false;
|
444
303
|
|
445
|
-
const
|
446
|
-
if (
|
304
|
+
const isInMobileMenu = el.closest('[data-hs-nav="menu"]');
|
305
|
+
if (isInMobileMenu) return false;
|
447
306
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
307
|
+
const computedStyle = window.getComputedStyle(el);
|
308
|
+
const isHidden = computedStyle.display === 'none' ||
|
309
|
+
computedStyle.visibility === 'hidden' ||
|
310
|
+
computedStyle.opacity === '0' ||
|
311
|
+
el.offsetWidth === 0 ||
|
312
|
+
el.offsetHeight === 0;
|
313
|
+
if (isHidden) return false;
|
314
|
+
|
315
|
+
let parent = el.parentElement;
|
316
|
+
while (parent && parent !== navbar) {
|
317
|
+
const parentStyle = window.getComputedStyle(parent);
|
318
|
+
const parentHidden = parentStyle.display === 'none' ||
|
319
|
+
parentStyle.visibility === 'hidden' ||
|
320
|
+
parent.offsetWidth === 0 ||
|
321
|
+
parent.offsetHeight === 0;
|
322
|
+
if (parentHidden) return false;
|
323
|
+
parent = parent.parentElement;
|
453
324
|
}
|
454
325
|
|
455
|
-
|
326
|
+
return true;
|
327
|
+
});
|
328
|
+
|
329
|
+
const currentIndex = focusableElements.indexOf(document.activeElement);
|
330
|
+
if (currentIndex === -1) return;
|
331
|
+
|
332
|
+
let nextIndex;
|
333
|
+
if (e.key === 'ArrowRight') {
|
334
|
+
nextIndex = (currentIndex + 1) % focusableElements.length;
|
335
|
+
} else {
|
336
|
+
nextIndex = currentIndex === 0 ? focusableElements.length - 1 : currentIndex - 1;
|
456
337
|
}
|
338
|
+
|
339
|
+
focusableElements[nextIndex].focus();
|
457
340
|
});
|
458
341
|
}
|
459
342
|
|
460
|
-
//
|
343
|
+
// Mobile menu button system
|
461
344
|
function setupMobileMenuButton() {
|
462
|
-
// Find mobile menu button and menu
|
463
345
|
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
464
346
|
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
465
347
|
|
466
348
|
if (!menuButton || !mobileMenu) return;
|
467
349
|
|
468
|
-
// Generate unique ID for the menu
|
469
350
|
const menuId = `mobile-menu-${Date.now()}`;
|
470
351
|
|
471
|
-
// Set up ARIA attributes for button
|
472
352
|
menuButton.setAttribute('aria-expanded', 'false');
|
473
353
|
menuButton.setAttribute('aria-controls', menuId);
|
474
354
|
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
475
355
|
|
476
|
-
// Set up ARIA attributes for menu
|
477
356
|
mobileMenu.id = menuId;
|
478
357
|
mobileMenu.setAttribute('role', 'dialog');
|
479
358
|
mobileMenu.setAttribute('aria-modal', 'true');
|
480
359
|
mobileMenu.inert = true;
|
481
360
|
|
482
|
-
// Track menu state
|
483
361
|
let isMenuOpen = false;
|
484
362
|
|
485
|
-
// Function to open menu (simulate click)
|
486
363
|
function openMenu() {
|
487
364
|
if (isMenuOpen) return;
|
488
|
-
|
489
365
|
isMenuOpen = true;
|
490
|
-
|
491
|
-
// Update ARIA states
|
492
366
|
menuButton.setAttribute('aria-expanded', 'true');
|
493
367
|
menuButton.setAttribute('aria-label', 'Close navigation menu');
|
494
368
|
mobileMenu.inert = false;
|
495
369
|
|
496
|
-
//
|
370
|
+
// Prevent tabbing outside navbar using tabindex management
|
371
|
+
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]') ||
|
372
|
+
document.querySelector('.navbar_component') ||
|
373
|
+
document.querySelector('nav[role="navigation"]') ||
|
374
|
+
document.querySelector('nav');
|
375
|
+
|
376
|
+
const allFocusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
377
|
+
allFocusableElements.forEach(el => {
|
378
|
+
if (navbarWrapper && !navbarWrapper.contains(el)) {
|
379
|
+
el.setAttribute('data-mobile-menu-tabindex', el.getAttribute('tabindex') || '0');
|
380
|
+
el.setAttribute('tabindex', '-1');
|
381
|
+
}
|
382
|
+
});
|
383
|
+
|
497
384
|
const clickEvent = new MouseEvent('click', {
|
498
385
|
bubbles: true,
|
499
386
|
cancelable: true,
|
@@ -502,23 +389,28 @@ function setupMobileMenuButton() {
|
|
502
389
|
menuButton.dispatchEvent(clickEvent);
|
503
390
|
}
|
504
391
|
|
505
|
-
// Function to close menu (simulate second click)
|
506
392
|
function closeMenu() {
|
507
393
|
if (!isMenuOpen) return;
|
508
|
-
|
509
394
|
isMenuOpen = false;
|
510
|
-
|
511
|
-
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
512
395
|
if (mobileMenu.contains(document.activeElement)) {
|
513
396
|
menuButton.focus();
|
514
397
|
}
|
515
|
-
|
516
|
-
// Update ARIA states after focus is moved
|
517
398
|
menuButton.setAttribute('aria-expanded', 'false');
|
518
399
|
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
519
400
|
mobileMenu.inert = true;
|
520
401
|
|
521
|
-
//
|
402
|
+
// Restore tabbing to entire page using tabindex management
|
403
|
+
const elementsToRestore = document.querySelectorAll('[data-mobile-menu-tabindex]');
|
404
|
+
elementsToRestore.forEach(el => {
|
405
|
+
const originalTabindex = el.getAttribute('data-mobile-menu-tabindex');
|
406
|
+
if (originalTabindex === '0') {
|
407
|
+
el.removeAttribute('tabindex');
|
408
|
+
} else {
|
409
|
+
el.setAttribute('tabindex', originalTabindex);
|
410
|
+
}
|
411
|
+
el.removeAttribute('data-mobile-menu-tabindex');
|
412
|
+
});
|
413
|
+
|
522
414
|
const clickEvent = new MouseEvent('click', {
|
523
415
|
bubbles: true,
|
524
416
|
cancelable: true,
|
@@ -527,7 +419,6 @@ function setupMobileMenuButton() {
|
|
527
419
|
menuButton.dispatchEvent(clickEvent);
|
528
420
|
}
|
529
421
|
|
530
|
-
// Function to toggle menu state
|
531
422
|
function toggleMenu() {
|
532
423
|
if (isMenuOpen) {
|
533
424
|
closeMenu();
|
@@ -536,56 +427,85 @@ function setupMobileMenuButton() {
|
|
536
427
|
}
|
537
428
|
}
|
538
429
|
|
539
|
-
// Keyboard events for button
|
540
430
|
menuButton.addEventListener('keydown', function(e) {
|
541
431
|
if (e.key === 'Enter' || e.key === ' ') {
|
542
432
|
e.preventDefault();
|
543
433
|
toggleMenu();
|
434
|
+
} else if (e.key === 'ArrowDown') {
|
435
|
+
e.preventDefault();
|
436
|
+
if (!isMenuOpen) {
|
437
|
+
openMenu();
|
438
|
+
}
|
439
|
+
const firstElement = mobileMenu.querySelector('button, a');
|
440
|
+
if (firstElement) {
|
441
|
+
firstElement.focus();
|
442
|
+
}
|
443
|
+
} else if (e.key === 'ArrowUp') {
|
444
|
+
e.preventDefault();
|
445
|
+
if (isMenuOpen) {
|
446
|
+
closeMenu();
|
447
|
+
}
|
544
448
|
}
|
545
449
|
});
|
546
450
|
|
547
|
-
// Listen for actual clicks to update state (for cases where Webflow triggers clicks)
|
548
451
|
menuButton.addEventListener('click', function(e) {
|
549
|
-
// Only update state if this wasn't triggered by our own events
|
550
452
|
if (!e.isTrusted) return;
|
551
|
-
|
552
|
-
// If closing and focus is inside menu, move it out first
|
553
453
|
if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
|
554
454
|
menuButton.focus();
|
555
455
|
}
|
556
|
-
|
557
|
-
// Toggle our internal state
|
558
456
|
isMenuOpen = !isMenuOpen;
|
559
|
-
|
560
|
-
// Update ARIA attributes after focus is handled
|
561
457
|
menuButton.setAttribute('aria-expanded', isMenuOpen);
|
562
458
|
menuButton.setAttribute('aria-label',
|
563
459
|
isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
564
460
|
);
|
565
461
|
mobileMenu.inert = !isMenuOpen;
|
462
|
+
|
463
|
+
// Handle tabindex management for external clicks
|
464
|
+
if (isMenuOpen) {
|
465
|
+
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]') ||
|
466
|
+
document.querySelector('.navbar_component') ||
|
467
|
+
document.querySelector('nav[role="navigation"]') ||
|
468
|
+
document.querySelector('nav');
|
469
|
+
|
470
|
+
const allFocusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
471
|
+
allFocusableElements.forEach(el => {
|
472
|
+
if (navbarWrapper && !navbarWrapper.contains(el)) {
|
473
|
+
el.setAttribute('data-mobile-menu-tabindex', el.getAttribute('tabindex') || '0');
|
474
|
+
el.setAttribute('tabindex', '-1');
|
475
|
+
}
|
476
|
+
});
|
477
|
+
} else {
|
478
|
+
const elementsToRestore = document.querySelectorAll('[data-mobile-menu-tabindex]');
|
479
|
+
elementsToRestore.forEach(el => {
|
480
|
+
const originalTabindex = el.getAttribute('data-mobile-menu-tabindex');
|
481
|
+
if (originalTabindex === '0') {
|
482
|
+
el.removeAttribute('tabindex');
|
483
|
+
} else {
|
484
|
+
el.setAttribute('tabindex', originalTabindex);
|
485
|
+
}
|
486
|
+
el.removeAttribute('data-mobile-menu-tabindex');
|
487
|
+
});
|
488
|
+
}
|
566
489
|
});
|
567
490
|
}
|
568
491
|
|
569
|
-
// Helper function to sanitize text for HTML IDs
|
570
492
|
function sanitizeForID(text) {
|
571
493
|
return text
|
572
494
|
.toLowerCase()
|
573
|
-
.replace(/[^a-z0-9\s]/g, '')
|
574
|
-
.replace(/\s+/g, '-')
|
575
|
-
.replace(/^-+|-+$/g, '')
|
576
|
-
.substring(0, 50);
|
495
|
+
.replace(/[^a-z0-9\s]/g, '')
|
496
|
+
.replace(/\s+/g, '-')
|
497
|
+
.replace(/^-+|-+$/g, '')
|
498
|
+
.substring(0, 50);
|
577
499
|
}
|
578
500
|
|
579
|
-
//
|
501
|
+
// Mobile menu ARIA setup
|
580
502
|
function setupMobileMenuARIA() {
|
581
503
|
const menuContainer = document.querySelector('[data-hs-nav="menu"]');
|
582
504
|
if (!menuContainer) return;
|
583
505
|
|
584
|
-
// Find all buttons and links inside the menu container
|
585
506
|
const buttons = menuContainer.querySelectorAll('button');
|
586
507
|
const links = menuContainer.querySelectorAll('a');
|
587
508
|
|
588
|
-
// Process each button (dropdown toggles)
|
589
509
|
buttons.forEach(button => {
|
590
510
|
const buttonText = button.textContent?.trim();
|
591
511
|
if (!buttonText) return;
|
@@ -594,20 +514,16 @@ function setupMobileMenuARIA() {
|
|
594
514
|
const buttonId = `navbar-mobile-${sanitizedText}-toggle`;
|
595
515
|
const listId = `navbar-mobile-${sanitizedText}-list`;
|
596
516
|
|
597
|
-
// Set button ID and ARIA attributes
|
598
517
|
button.id = buttonId;
|
599
518
|
button.setAttribute('aria-expanded', 'false');
|
600
519
|
button.setAttribute('aria-controls', listId);
|
601
520
|
|
602
|
-
// Find associated dropdown list (look for next sibling or nearby element with links)
|
603
521
|
let dropdownList = button.nextElementSibling;
|
604
522
|
|
605
|
-
// If next sibling doesn't contain links, look for parent's next sibling
|
606
523
|
if (!dropdownList || !dropdownList.querySelector('a')) {
|
607
524
|
dropdownList = button.parentElement?.nextElementSibling;
|
608
525
|
}
|
609
526
|
|
610
|
-
// If still not found, look for a nearby element that contains multiple links
|
611
527
|
if (!dropdownList || !dropdownList.querySelector('a')) {
|
612
528
|
const parent = button.closest('[data-hs-nav="menu"]');
|
613
529
|
const allListElements = parent?.querySelectorAll('div, ul, nav');
|
@@ -617,68 +533,93 @@ function setupMobileMenuARIA() {
|
|
617
533
|
);
|
618
534
|
}
|
619
535
|
|
620
|
-
// Set dropdown list ID and initial hidden state if found
|
621
536
|
if (dropdownList && dropdownList.querySelector('a')) {
|
622
537
|
dropdownList.id = listId;
|
623
538
|
dropdownList.inert = true;
|
624
539
|
|
625
|
-
// Add click listener to button to manage dropdown state
|
626
540
|
button.addEventListener('click', function() {
|
627
541
|
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
628
542
|
const newState = !isExpanded;
|
629
|
-
|
630
|
-
// Update button state
|
631
543
|
button.setAttribute('aria-expanded', newState);
|
632
|
-
|
633
|
-
// Update dropdown state
|
634
544
|
dropdownList.inert = !newState;
|
635
545
|
});
|
636
546
|
}
|
637
547
|
});
|
638
548
|
|
639
|
-
// Process each link (navigation items) - but don't override dropdown links
|
640
549
|
links.forEach(link => {
|
641
550
|
const linkText = link.textContent?.trim();
|
642
551
|
if (!linkText) return;
|
643
552
|
|
644
553
|
const sanitizedText = sanitizeForID(linkText);
|
645
554
|
const linkId = `navbar-mobile-${sanitizedText}-link`;
|
646
|
-
|
647
|
-
// Set link ID
|
648
555
|
link.id = linkId;
|
649
|
-
|
650
|
-
// Links inside inert containers are automatically non-focusable
|
651
556
|
});
|
652
557
|
|
653
|
-
// Add arrow key navigation for mobile menu
|
654
558
|
setupMobileMenuArrowNavigation(menuContainer);
|
655
559
|
}
|
656
560
|
|
657
|
-
//
|
561
|
+
// Mobile menu arrow navigation
|
658
562
|
function setupMobileMenuArrowNavigation(menuContainer) {
|
659
|
-
// Get all focusable elements in the mobile menu
|
660
563
|
function getFocusableElements() {
|
661
|
-
|
564
|
+
const allElements = menuContainer.querySelectorAll('button, a');
|
565
|
+
return Array.from(allElements).filter(el => {
|
566
|
+
let current = el;
|
567
|
+
while (current && current !== menuContainer) {
|
568
|
+
if (current.inert === true) {
|
569
|
+
return false;
|
570
|
+
}
|
571
|
+
current = current.parentElement;
|
572
|
+
}
|
573
|
+
return true;
|
574
|
+
});
|
662
575
|
}
|
663
576
|
|
664
577
|
let currentFocusIndex = -1;
|
665
578
|
|
666
579
|
menuContainer.addEventListener('keydown', function(e) {
|
667
|
-
const focusableElements =
|
580
|
+
const focusableElements = getFocusableElements();
|
668
581
|
if (focusableElements.length === 0) return;
|
669
582
|
|
670
|
-
// Find current focus index
|
671
583
|
const activeElement = document.activeElement;
|
672
584
|
currentFocusIndex = focusableElements.indexOf(activeElement);
|
673
585
|
|
674
586
|
if (e.key === 'ArrowDown') {
|
675
587
|
e.preventDefault();
|
676
|
-
|
588
|
+
if (currentFocusIndex >= focusableElements.length - 1) {
|
589
|
+
currentFocusIndex = 0;
|
590
|
+
} else {
|
591
|
+
currentFocusIndex = currentFocusIndex + 1;
|
592
|
+
}
|
677
593
|
focusableElements[currentFocusIndex].focus();
|
678
594
|
} else if (e.key === 'ArrowUp') {
|
679
595
|
e.preventDefault();
|
680
|
-
|
596
|
+
if (currentFocusIndex <= 0) {
|
597
|
+
const mobileMenuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
598
|
+
if (mobileMenuButton) {
|
599
|
+
mobileMenuButton.focus();
|
600
|
+
return;
|
601
|
+
}
|
602
|
+
}
|
603
|
+
currentFocusIndex = currentFocusIndex - 1;
|
681
604
|
focusableElements[currentFocusIndex].focus();
|
605
|
+
} else if (e.key === 'ArrowRight') {
|
606
|
+
e.preventDefault();
|
607
|
+
if (activeElement.tagName === 'BUTTON' && activeElement.hasAttribute('aria-controls')) {
|
608
|
+
const isExpanded = activeElement.getAttribute('aria-expanded') === 'true';
|
609
|
+
if (!isExpanded) {
|
610
|
+
activeElement.click();
|
611
|
+
}
|
612
|
+
return;
|
613
|
+
}
|
614
|
+
} else if (e.key === 'ArrowLeft') {
|
615
|
+
e.preventDefault();
|
616
|
+
if (activeElement.tagName === 'BUTTON' && activeElement.hasAttribute('aria-controls')) {
|
617
|
+
const isExpanded = activeElement.getAttribute('aria-expanded') === 'true';
|
618
|
+
if (isExpanded) {
|
619
|
+
activeElement.click();
|
620
|
+
}
|
621
|
+
return;
|
622
|
+
}
|
682
623
|
} else if (e.key === 'Home') {
|
683
624
|
e.preventDefault();
|
684
625
|
currentFocusIndex = 0;
|
@@ -689,9 +630,7 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
689
630
|
focusableElements[focusableElements.length - 1].focus();
|
690
631
|
} else if (e.key === ' ' && activeElement.tagName === 'A') {
|
691
632
|
e.preventDefault();
|
692
|
-
// Just prevent space from scrolling page - Enter activates links
|
693
633
|
} else if (e.key === 'Escape') {
|
694
|
-
// Close mobile menu
|
695
634
|
const mobileMenuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
696
635
|
if (mobileMenuButton) {
|
697
636
|
mobileMenuButton.click();
|