@hortonstudio/main 1.2.26 → 1.2.28
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 +15 -67
- package/animations/text.js +25 -25
- package/animations/transition.js +20 -10
- package/autoInit/modal.js +75 -0
- package/index.js +5 -3
- package/package.json +1 -1
- package/utils/navbar.js +600 -112
- package/configure-example.js +0 -32
package/animations/hero.js
CHANGED
|
@@ -468,7 +468,9 @@ export async function init() {
|
|
|
468
468
|
ease: config.headingSplit.ease,
|
|
469
469
|
onComplete: () => {
|
|
470
470
|
if (split && split.revert) {
|
|
471
|
-
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
split.revert();
|
|
473
|
+
}, 100);
|
|
472
474
|
}
|
|
473
475
|
}
|
|
474
476
|
},
|
|
@@ -487,7 +489,9 @@ export async function init() {
|
|
|
487
489
|
ease: config.subheadingSplit.ease,
|
|
488
490
|
onComplete: () => {
|
|
489
491
|
if (split && split.revert) {
|
|
490
|
-
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
split.revert();
|
|
494
|
+
}, 100);
|
|
491
495
|
}
|
|
492
496
|
}
|
|
493
497
|
},
|
|
@@ -597,72 +601,16 @@ export async function init() {
|
|
|
597
601
|
|
|
598
602
|
// Add resize listener for responsive line splits
|
|
599
603
|
let heroResizeTimeout;
|
|
604
|
+
let lastWidth = window.innerWidth;
|
|
600
605
|
window.addEventListener('resize', () => {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
headingSplits.forEach((split, index) => {
|
|
610
|
-
if (split.elementsClass === 'lines' && split.revert) {
|
|
611
|
-
split.revert();
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
subheadingSplits.forEach((split, index) => {
|
|
616
|
-
if (split.elementsClass === 'lines' && split.revert) {
|
|
617
|
-
split.revert();
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
// Re-initialize line splits after resize
|
|
622
|
-
document.fonts.ready.then(() => {
|
|
623
|
-
// Re-split line elements
|
|
624
|
-
headingSplitElements.forEach((parent, index) => {
|
|
625
|
-
const textElement = heading[index];
|
|
626
|
-
const splitType = parent.getAttribute('data-hs-heroconfig') || 'line';
|
|
627
|
-
|
|
628
|
-
if (splitType === 'line') {
|
|
629
|
-
const splitConfig = {
|
|
630
|
-
type: "lines",
|
|
631
|
-
mask: "lines",
|
|
632
|
-
linesClass: "line"
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
const split = new SplitText(textElement, splitConfig);
|
|
636
|
-
split.elementsClass = 'lines';
|
|
637
|
-
headingSplits[index] = split;
|
|
638
|
-
|
|
639
|
-
gsap.set(split.lines, { yPercent: 0 });
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
subheadingSplitElements.forEach((parent, index) => {
|
|
644
|
-
const textElement = subheading[index];
|
|
645
|
-
const splitType = parent.getAttribute('data-hs-heroconfig') || 'word';
|
|
646
|
-
|
|
647
|
-
if (splitType === 'line') {
|
|
648
|
-
const splitConfig = {
|
|
649
|
-
type: "lines",
|
|
650
|
-
mask: "lines",
|
|
651
|
-
linesClass: "line"
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
const split = new SplitText(textElement, splitConfig);
|
|
655
|
-
split.elementsClass = 'lines';
|
|
656
|
-
subheadingSplits[index] = split;
|
|
657
|
-
|
|
658
|
-
gsap.set(split.lines, { yPercent: 0 });
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
ScrollTrigger.refresh();
|
|
665
|
-
}, 100);
|
|
606
|
+
const currentWidth = window.innerWidth;
|
|
607
|
+
if (currentWidth !== lastWidth) {
|
|
608
|
+
lastWidth = currentWidth;
|
|
609
|
+
clearTimeout(heroResizeTimeout);
|
|
610
|
+
heroResizeTimeout = setTimeout(() => {
|
|
611
|
+
ScrollTrigger.refresh();
|
|
612
|
+
}, 300);
|
|
613
|
+
}
|
|
666
614
|
});
|
|
667
615
|
|
|
668
616
|
return { result: 'anim-hero initialized' };
|
package/animations/text.js
CHANGED
|
@@ -141,10 +141,13 @@ const CharSplitAnimations = {
|
|
|
141
141
|
scrollTrigger: {
|
|
142
142
|
trigger: textElement,
|
|
143
143
|
start: config.charSplit.start,
|
|
144
|
-
invalidateOnRefresh:
|
|
144
|
+
invalidateOnRefresh: false,
|
|
145
145
|
},
|
|
146
146
|
onComplete: () => {
|
|
147
|
-
|
|
147
|
+
if (textElement.splitTextInstance) {
|
|
148
|
+
textElement.splitTextInstance.revert();
|
|
149
|
+
delete textElement.splitTextInstance;
|
|
150
|
+
}
|
|
148
151
|
}
|
|
149
152
|
});
|
|
150
153
|
|
|
@@ -198,10 +201,13 @@ const WordSplitAnimations = {
|
|
|
198
201
|
scrollTrigger: {
|
|
199
202
|
trigger: textElement,
|
|
200
203
|
start: config.wordSplit.start,
|
|
201
|
-
invalidateOnRefresh:
|
|
204
|
+
invalidateOnRefresh: false,
|
|
202
205
|
},
|
|
203
206
|
onComplete: () => {
|
|
204
|
-
|
|
207
|
+
if (textElement.splitTextInstance) {
|
|
208
|
+
textElement.splitTextInstance.revert();
|
|
209
|
+
delete textElement.splitTextInstance;
|
|
210
|
+
}
|
|
205
211
|
}
|
|
206
212
|
});
|
|
207
213
|
|
|
@@ -255,10 +261,13 @@ const LineSplitAnimations = {
|
|
|
255
261
|
scrollTrigger: {
|
|
256
262
|
trigger: textElement,
|
|
257
263
|
start: config.lineSplit.start,
|
|
258
|
-
invalidateOnRefresh:
|
|
264
|
+
invalidateOnRefresh: false,
|
|
259
265
|
},
|
|
260
266
|
onComplete: () => {
|
|
261
|
-
|
|
267
|
+
if (textElement.splitTextInstance) {
|
|
268
|
+
textElement.splitTextInstance.revert();
|
|
269
|
+
delete textElement.splitTextInstance;
|
|
270
|
+
}
|
|
262
271
|
}
|
|
263
272
|
});
|
|
264
273
|
|
|
@@ -303,7 +312,7 @@ const AppearAnimations = {
|
|
|
303
312
|
scrollTrigger: {
|
|
304
313
|
trigger: element,
|
|
305
314
|
start: config.appear.start,
|
|
306
|
-
invalidateOnRefresh:
|
|
315
|
+
invalidateOnRefresh: false,
|
|
307
316
|
}
|
|
308
317
|
});
|
|
309
318
|
|
|
@@ -352,25 +361,16 @@ export async function init() {
|
|
|
352
361
|
}
|
|
353
362
|
|
|
354
363
|
let resizeTimeout;
|
|
364
|
+
let lastWidth = window.innerWidth;
|
|
355
365
|
window.addEventListener('resize', () => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Re-initialize line splits after resize
|
|
368
|
-
LineSplitAnimations.initial().then(() => {
|
|
369
|
-
LineSplitAnimations.animate();
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
ScrollTrigger.refresh();
|
|
373
|
-
}, 100);
|
|
366
|
+
const currentWidth = window.innerWidth;
|
|
367
|
+
if (currentWidth !== lastWidth) {
|
|
368
|
+
lastWidth = currentWidth;
|
|
369
|
+
clearTimeout(resizeTimeout);
|
|
370
|
+
resizeTimeout = setTimeout(() => {
|
|
371
|
+
ScrollTrigger.refresh();
|
|
372
|
+
}, 300);
|
|
373
|
+
}
|
|
374
374
|
});
|
|
375
375
|
|
|
376
376
|
const api = window[API_NAME] || {};
|
package/animations/transition.js
CHANGED
|
@@ -21,18 +21,28 @@ function initTransitions() {
|
|
|
21
21
|
|
|
22
22
|
// On Page Load
|
|
23
23
|
if (transitionTrigger.length > 0) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
|
|
25
|
+
function triggerTransition() {
|
|
26
|
+
if (window.Webflow && window.Webflow.push) {
|
|
27
|
+
Webflow.push(function () {
|
|
28
|
+
transitionTrigger.click();
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
// Non-Webflow initialization
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
transitionTrigger.click();
|
|
34
|
+
}, 100);
|
|
35
|
+
}
|
|
36
|
+
$("body").addClass("no-scroll-transition");
|
|
37
|
+
setTimeout(() => {$("body").removeClass("no-scroll-transition");}, introDurationMS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Wait for full page load (images, fonts, etc.) for Safari
|
|
41
|
+
if (document.readyState === 'complete') {
|
|
42
|
+
triggerTransition();
|
|
28
43
|
} else {
|
|
29
|
-
|
|
30
|
-
setTimeout(() => {
|
|
31
|
-
transitionTrigger.click();
|
|
32
|
-
}, 100);
|
|
44
|
+
window.addEventListener('load', triggerTransition, { once: true });
|
|
33
45
|
}
|
|
34
|
-
$("body").addClass("no-scroll-transition");
|
|
35
|
-
setTimeout(() => {$("body").removeClass("no-scroll-transition");}, introDurationMS);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
// On Link Click
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function initModal() {
|
|
2
|
+
const config = {
|
|
3
|
+
transitionDuration: 0.3,
|
|
4
|
+
blurOpacity: 0.5
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function openModal(element) {
|
|
8
|
+
document.body.classList.add('u-overflow-clip');
|
|
9
|
+
|
|
10
|
+
// Add blur to all other modals
|
|
11
|
+
document.querySelectorAll('[data-hs-modal]').forEach(modal => {
|
|
12
|
+
if (modal !== element) {
|
|
13
|
+
modal.style.display = 'block';
|
|
14
|
+
modal.style.opacity = config.blurOpacity;
|
|
15
|
+
modal.style.transition = `opacity ${config.transitionDuration}s ease`;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function closeModal(element) {
|
|
21
|
+
document.body.classList.remove('u-overflow-clip');
|
|
22
|
+
|
|
23
|
+
// Remove blur from all other modals
|
|
24
|
+
document.querySelectorAll('[data-hs-modal]').forEach(modal => {
|
|
25
|
+
if (modal !== element) {
|
|
26
|
+
modal.style.display = 'none';
|
|
27
|
+
modal.style.opacity = '0';
|
|
28
|
+
modal.style.transition = `opacity ${config.transitionDuration}s ease`;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toggleModal(element) {
|
|
34
|
+
element.x = ((element.x || 0) + 1) % 2;
|
|
35
|
+
|
|
36
|
+
if (element.x) {
|
|
37
|
+
openModal(element);
|
|
38
|
+
} else {
|
|
39
|
+
closeModal(element);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Initialize openclose functionality
|
|
44
|
+
document.querySelectorAll('[data-hs-modal="openclose"]').forEach(trigger => {
|
|
45
|
+
trigger.addEventListener('click', function() {
|
|
46
|
+
toggleModal(this);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Initialize open functionality
|
|
51
|
+
document.querySelectorAll('[data-hs-modal="open"]').forEach(trigger => {
|
|
52
|
+
trigger.addEventListener('click', function() {
|
|
53
|
+
openModal(this);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Initialize close functionality
|
|
58
|
+
document.querySelectorAll('[data-hs-modal="close"]').forEach(trigger => {
|
|
59
|
+
trigger.addEventListener('click', function() {
|
|
60
|
+
closeModal(this);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { result: 'modal initialized' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function init() {
|
|
68
|
+
if (document.readyState === 'loading') {
|
|
69
|
+
document.addEventListener('DOMContentLoaded', initModal);
|
|
70
|
+
} else {
|
|
71
|
+
initModal();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { result: 'modal initialized' };
|
|
75
|
+
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Version:1.2.
|
|
1
|
+
// Version:1.2.28
|
|
2
2
|
|
|
3
3
|
const API_NAME = 'hsmain';
|
|
4
4
|
|
|
@@ -22,7 +22,8 @@ const initializeHsMain = async () => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
const autoInitModules = {
|
|
25
|
-
'smooth-scroll': true
|
|
25
|
+
'smooth-scroll': true,
|
|
26
|
+
'modal': true
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const allDataAttributes = { ...animationModules, ...utilityModules };
|
|
@@ -47,7 +48,8 @@ const initializeHsMain = async () => {
|
|
|
47
48
|
'data-hs-util-toc': () => import('./utils/toc.js'),
|
|
48
49
|
'data-hs-util-progress': () => import('./utils/scroll-progress.js'),
|
|
49
50
|
'data-hs-util-navbar': () => import('./utils/navbar.js'),
|
|
50
|
-
'smooth-scroll': () => import('./autoInit/smooth-scroll.js')
|
|
51
|
+
'smooth-scroll': () => import('./autoInit/smooth-scroll.js'),
|
|
52
|
+
'modal': () => import('./autoInit/modal.js')
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
let scripts = [...document.querySelectorAll(`script[type="module"][src="${import.meta.url}"]`)];
|
package/package.json
CHANGED
package/utils/navbar.js
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
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
|
+
setupDynamicDropdowns();
|
|
37
|
+
|
|
38
|
+
// Setup dynamic mobile menu button
|
|
39
|
+
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
|
+
setupMobileMenuARIA();
|
|
62
|
+
|
|
63
|
+
return { result: 'navbar initialized' };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// New dynamic dropdown system for Webflow interactions
|
|
67
|
+
function setupDynamicDropdowns() {
|
|
2
68
|
// Find all dropdown wrappers
|
|
3
|
-
const dropdownWrappers = document.querySelectorAll('[data-hs-nav
|
|
69
|
+
const dropdownWrappers = document.querySelectorAll('[data-hs-nav="dropdown"]');
|
|
4
70
|
|
|
5
71
|
// Global array to track all dropdown instances
|
|
6
72
|
const allDropdowns = [];
|
|
@@ -15,34 +81,55 @@ export const init = () => {
|
|
|
15
81
|
};
|
|
16
82
|
|
|
17
83
|
dropdownWrappers.forEach(wrapper => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
84
|
+
// Auto-detect toggle and dropdown list
|
|
85
|
+
const toggle = wrapper.querySelector('a'); // First <a> element
|
|
86
|
+
if (!toggle) return; // Skip if no toggle found
|
|
87
|
+
|
|
88
|
+
// Find dropdown list: element with 2+ <a> tags that doesn't contain the toggle
|
|
89
|
+
const allElements = wrapper.querySelectorAll('*');
|
|
90
|
+
let dropdownList = null;
|
|
91
|
+
|
|
92
|
+
for (const element of allElements) {
|
|
93
|
+
const links = element.querySelectorAll('a');
|
|
94
|
+
if (links.length >= 2 && !element.contains(toggle)) {
|
|
95
|
+
dropdownList = element;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!dropdownList) return; // Skip if no dropdown list found
|
|
101
|
+
|
|
102
|
+
// Generate unique IDs for ARIA attributes
|
|
103
|
+
const toggleText = toggle.textContent?.trim() || 'dropdown';
|
|
104
|
+
const sanitizedText = sanitizeForID(toggleText);
|
|
105
|
+
const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
|
|
106
|
+
const listId = `navbar-dropdown-${sanitizedText}-list`;
|
|
107
|
+
|
|
108
|
+
// Set up ARIA attributes for toggle
|
|
109
|
+
toggle.id = toggleId;
|
|
110
|
+
toggle.setAttribute('aria-haspopup', 'menu');
|
|
111
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
112
|
+
toggle.setAttribute('aria-controls', listId);
|
|
113
|
+
|
|
114
|
+
// Set up ARIA attributes for dropdown list
|
|
115
|
+
dropdownList.id = listId;
|
|
116
|
+
dropdownList.setAttribute('role', 'menu');
|
|
117
|
+
dropdownList.inert = true;
|
|
118
|
+
|
|
119
|
+
// Set up ARIA attributes for menu items
|
|
120
|
+
const menuItems = dropdownList.querySelectorAll('a');
|
|
121
|
+
menuItems.forEach(item => {
|
|
122
|
+
item.setAttribute('role', 'menuitem');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Track dropdown state
|
|
34
126
|
let isOpen = false;
|
|
35
|
-
let
|
|
127
|
+
let currentMenuItemIndex = -1;
|
|
36
128
|
|
|
37
|
-
//
|
|
129
|
+
// Function to open dropdown (simulate click)
|
|
38
130
|
function openDropdown() {
|
|
39
131
|
if (isOpen) return;
|
|
40
132
|
|
|
41
|
-
// Kill any existing timeline
|
|
42
|
-
if (currentTimeline) {
|
|
43
|
-
currentTimeline.kill();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
133
|
// Close all other dropdowns first
|
|
47
134
|
closeAllDropdowns(wrapper);
|
|
48
135
|
|
|
@@ -50,115 +137,203 @@ export const init = () => {
|
|
|
50
137
|
|
|
51
138
|
// Update ARIA states
|
|
52
139
|
toggle.setAttribute('aria-expanded', 'true');
|
|
53
|
-
|
|
140
|
+
dropdownList.inert = false;
|
|
54
141
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}, 0)
|
|
63
|
-
.to(arrow, {
|
|
64
|
-
rotation: 90,
|
|
65
|
-
scale: 1.2,
|
|
66
|
-
x: 4,
|
|
67
|
-
color: 'var(--swatch--brand)',
|
|
68
|
-
duration: animationDuration,
|
|
69
|
-
ease: 'ease'
|
|
70
|
-
}, 0)
|
|
71
|
-
.to(text, {
|
|
72
|
-
scale: 1.1,
|
|
73
|
-
color: 'var(--swatch--brand)',
|
|
74
|
-
duration: animationDuration,
|
|
75
|
-
ease: 'ease'
|
|
76
|
-
}, 0);
|
|
142
|
+
// Simulate click on wrapper to trigger Webflow interactions
|
|
143
|
+
const clickEvent = new MouseEvent('click', {
|
|
144
|
+
bubbles: true,
|
|
145
|
+
cancelable: true,
|
|
146
|
+
view: window
|
|
147
|
+
});
|
|
148
|
+
wrapper.dispatchEvent(clickEvent);
|
|
77
149
|
}
|
|
78
150
|
|
|
79
|
-
//
|
|
151
|
+
// Function to close dropdown (simulate second click)
|
|
80
152
|
function closeDropdown() {
|
|
81
153
|
if (!isOpen) return;
|
|
82
154
|
|
|
83
|
-
// Kill any existing timeline
|
|
84
|
-
if (currentTimeline) {
|
|
85
|
-
currentTimeline.kill();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
155
|
// Check if focus should be restored to toggle
|
|
89
|
-
const shouldRestoreFocus =
|
|
156
|
+
const shouldRestoreFocus = dropdownList.contains(document.activeElement);
|
|
90
157
|
|
|
91
158
|
isOpen = false;
|
|
92
159
|
currentMenuItemIndex = -1;
|
|
93
160
|
|
|
94
|
-
//
|
|
95
|
-
toggle.setAttribute('aria-expanded', 'false');
|
|
96
|
-
list.setAttribute('aria-hidden', 'true');
|
|
97
|
-
|
|
98
|
-
// Temporarily remove role="menu" to help screen readers understand menu is closed
|
|
99
|
-
const originalRole = list.getAttribute('role');
|
|
100
|
-
list.removeAttribute('role');
|
|
101
|
-
|
|
102
|
-
// GSAP animation
|
|
103
|
-
currentTimeline = gsap.timeline();
|
|
104
|
-
currentTimeline.to(contain, {
|
|
105
|
-
yPercent: -110,
|
|
106
|
-
duration: animationDuration,
|
|
107
|
-
ease: 'ease'
|
|
108
|
-
}, 0)
|
|
109
|
-
.to(arrow, {
|
|
110
|
-
rotation: 0,
|
|
111
|
-
scale: 1,
|
|
112
|
-
x: 0,
|
|
113
|
-
color: '', // back to default color
|
|
114
|
-
duration: animationDuration,
|
|
115
|
-
ease: 'ease'
|
|
116
|
-
}, 0)
|
|
117
|
-
.to(text, {
|
|
118
|
-
scale: 1,
|
|
119
|
-
color: '', // back to default color
|
|
120
|
-
duration: animationDuration,
|
|
121
|
-
ease: 'ease'
|
|
122
|
-
}, 0)
|
|
123
|
-
.set(list, { display: 'none' })
|
|
124
|
-
.call(() => {
|
|
125
|
-
// Restore role after animation completes
|
|
126
|
-
list.setAttribute('role', originalRole || 'menu');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Restore focus to toggle only if focus was inside dropdown
|
|
161
|
+
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
|
130
162
|
if (shouldRestoreFocus) {
|
|
131
|
-
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
toggle.focus();
|
|
134
|
-
}, 50);
|
|
163
|
+
toggle.focus();
|
|
135
164
|
}
|
|
165
|
+
|
|
166
|
+
// Update ARIA states after focus is moved
|
|
167
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
168
|
+
dropdownList.inert = true;
|
|
169
|
+
|
|
170
|
+
// Simulate second click on wrapper to trigger Webflow interactions
|
|
171
|
+
const clickEvent = new MouseEvent('click', {
|
|
172
|
+
bubbles: true,
|
|
173
|
+
cancelable: true,
|
|
174
|
+
view: window
|
|
175
|
+
});
|
|
176
|
+
wrapper.dispatchEvent(clickEvent);
|
|
136
177
|
}
|
|
137
178
|
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
// Hover events (native) - trigger click events for Webflow
|
|
180
|
+
wrapper.addEventListener('mouseenter', () => {
|
|
181
|
+
if (!isOpen) {
|
|
182
|
+
// Trigger click to open
|
|
183
|
+
const clickEvent = new MouseEvent('click', {
|
|
184
|
+
bubbles: true,
|
|
185
|
+
cancelable: true,
|
|
186
|
+
view: window
|
|
187
|
+
});
|
|
188
|
+
wrapper.dispatchEvent(clickEvent);
|
|
189
|
+
|
|
190
|
+
// Update our state
|
|
191
|
+
closeAllDropdowns(wrapper);
|
|
192
|
+
isOpen = true;
|
|
193
|
+
toggle.setAttribute('aria-expanded', 'true');
|
|
194
|
+
dropdownList.inert = false;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
141
197
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
198
|
+
wrapper.addEventListener('mouseleave', () => {
|
|
199
|
+
if (isOpen) {
|
|
200
|
+
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
|
201
|
+
if (dropdownList.contains(document.activeElement)) {
|
|
202
|
+
toggle.focus();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Trigger click to close
|
|
206
|
+
const clickEvent = new MouseEvent('click', {
|
|
207
|
+
bubbles: true,
|
|
208
|
+
cancelable: true,
|
|
209
|
+
view: window
|
|
210
|
+
});
|
|
211
|
+
wrapper.dispatchEvent(clickEvent);
|
|
212
|
+
|
|
213
|
+
// Update our state after focus is moved
|
|
214
|
+
isOpen = false;
|
|
215
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
216
|
+
dropdownList.inert = true;
|
|
217
|
+
currentMenuItemIndex = -1;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
145
220
|
|
|
146
|
-
//
|
|
147
|
-
|
|
221
|
+
// Keyboard navigation within dropdown - attach to document to catch all events
|
|
222
|
+
document.addEventListener('keydown', function(e) {
|
|
148
223
|
if (!isOpen) return;
|
|
149
224
|
|
|
225
|
+
// Only handle if focus is on toggle or within wrapper
|
|
226
|
+
if (!wrapper.contains(document.activeElement)) return;
|
|
227
|
+
|
|
150
228
|
if (e.key === 'ArrowDown') {
|
|
151
229
|
e.preventDefault();
|
|
152
|
-
|
|
153
|
-
|
|
230
|
+
// If currently on toggle, start at first item
|
|
231
|
+
if (document.activeElement === toggle) {
|
|
232
|
+
currentMenuItemIndex = 0;
|
|
233
|
+
menuItems[currentMenuItemIndex].focus();
|
|
234
|
+
} else {
|
|
235
|
+
// If at last item in dropdown, move to next sibling element
|
|
236
|
+
if (currentMenuItemIndex === menuItems.length - 1) {
|
|
237
|
+
// Find next focusable element after this dropdown
|
|
238
|
+
const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
|
|
239
|
+
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
|
240
|
+
if (nextElement) {
|
|
241
|
+
closeDropdown();
|
|
242
|
+
nextElement.focus();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
|
|
247
|
+
menuItems[currentMenuItemIndex].focus();
|
|
248
|
+
}
|
|
154
249
|
} else if (e.key === 'ArrowUp') {
|
|
155
250
|
e.preventDefault();
|
|
156
|
-
|
|
157
|
-
|
|
251
|
+
// If currently on toggle, start at last item
|
|
252
|
+
if (document.activeElement === toggle) {
|
|
253
|
+
currentMenuItemIndex = menuItems.length - 1;
|
|
254
|
+
menuItems[currentMenuItemIndex].focus();
|
|
255
|
+
} else {
|
|
256
|
+
// If at first item in dropdown, move to previous sibling element
|
|
257
|
+
if (currentMenuItemIndex === 0) {
|
|
258
|
+
// Find previous focusable element before this dropdown
|
|
259
|
+
const prevElement = wrapper.previousElementSibling?.querySelector('a, button');
|
|
260
|
+
if (prevElement) {
|
|
261
|
+
closeDropdown();
|
|
262
|
+
prevElement.focus();
|
|
263
|
+
return;
|
|
264
|
+
} else {
|
|
265
|
+
// If no previous sibling, go back to toggle
|
|
266
|
+
closeDropdown();
|
|
267
|
+
toggle.focus();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
currentMenuItemIndex = currentMenuItemIndex <= 0 ? menuItems.length - 1 : currentMenuItemIndex - 1;
|
|
272
|
+
menuItems[currentMenuItemIndex].focus();
|
|
273
|
+
}
|
|
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
|
+
} else if (e.key === 'Tab') {
|
|
295
|
+
// Handle Tab navigation within dropdown
|
|
296
|
+
if (e.shiftKey) {
|
|
297
|
+
// Shift+Tab going backwards
|
|
298
|
+
if (document.activeElement === menuItems[0]) {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
closeDropdown();
|
|
301
|
+
toggle.focus();
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
// Tab going forwards
|
|
305
|
+
if (document.activeElement === menuItems[menuItems.length - 1]) {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
// Find next element BEFORE closing dropdown to avoid focus issues
|
|
308
|
+
const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
|
|
309
|
+
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
|
310
|
+
|
|
311
|
+
// Close dropdown first
|
|
312
|
+
closeDropdown();
|
|
313
|
+
|
|
314
|
+
// Then move focus to next element
|
|
315
|
+
if (nextElement) {
|
|
316
|
+
setTimeout(() => {
|
|
317
|
+
nextElement.focus();
|
|
318
|
+
}, 10);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
158
322
|
} else if (e.key === 'Escape') {
|
|
159
323
|
e.preventDefault();
|
|
160
324
|
closeDropdown();
|
|
161
325
|
toggle.focus();
|
|
326
|
+
} else if (e.key === 'Home') {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
currentMenuItemIndex = 0;
|
|
329
|
+
menuItems[0].focus();
|
|
330
|
+
} else if (e.key === 'End') {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
currentMenuItemIndex = menuItems.length - 1;
|
|
333
|
+
menuItems[menuItems.length - 1].focus();
|
|
334
|
+
} else if (e.key === ' ') {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
// Just prevent space from scrolling page - Enter activates links
|
|
162
337
|
}
|
|
163
338
|
});
|
|
164
339
|
|
|
@@ -170,20 +345,34 @@ export const init = () => {
|
|
|
170
345
|
// Focus first menu item after opening
|
|
171
346
|
if (menuItems.length > 0) {
|
|
172
347
|
currentMenuItemIndex = 0;
|
|
173
|
-
setTimeout(() => menuItems[0].focus(),
|
|
348
|
+
setTimeout(() => menuItems[0].focus(), 100);
|
|
174
349
|
}
|
|
175
350
|
} else if (e.key === ' ') {
|
|
176
351
|
e.preventDefault();
|
|
177
|
-
// Simple toggle: if closed open, if open close
|
|
178
352
|
if (isOpen) {
|
|
179
353
|
closeDropdown();
|
|
180
354
|
} else {
|
|
181
355
|
openDropdown();
|
|
356
|
+
// Focus first item when opening with spacebar
|
|
357
|
+
if (menuItems.length > 0) {
|
|
358
|
+
currentMenuItemIndex = 0;
|
|
359
|
+
setTimeout(() => menuItems[0].focus(), 100);
|
|
360
|
+
}
|
|
182
361
|
}
|
|
183
|
-
} else if (e.key === 'ArrowUp'
|
|
362
|
+
} else if (e.key === 'ArrowUp') {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
if (isOpen) {
|
|
365
|
+
// Focus last menu item
|
|
366
|
+
currentMenuItemIndex = menuItems.length - 1;
|
|
367
|
+
menuItems[currentMenuItemIndex].focus();
|
|
368
|
+
} else {
|
|
369
|
+
closeDropdown();
|
|
370
|
+
}
|
|
371
|
+
} else if (e.key === 'Escape') {
|
|
184
372
|
e.preventDefault();
|
|
185
373
|
closeDropdown();
|
|
186
374
|
}
|
|
375
|
+
// Note: Enter key naturally follows the link - no preventDefault needed
|
|
187
376
|
});
|
|
188
377
|
|
|
189
378
|
// Close dropdown when clicking outside
|
|
@@ -197,7 +386,9 @@ export const init = () => {
|
|
|
197
386
|
allDropdowns.push({
|
|
198
387
|
wrapper,
|
|
199
388
|
isOpen: () => isOpen,
|
|
200
|
-
closeDropdown
|
|
389
|
+
closeDropdown,
|
|
390
|
+
toggle,
|
|
391
|
+
dropdownList
|
|
201
392
|
});
|
|
202
393
|
});
|
|
203
394
|
|
|
@@ -210,5 +401,302 @@ export const init = () => {
|
|
|
210
401
|
});
|
|
211
402
|
});
|
|
212
403
|
|
|
213
|
-
|
|
214
|
-
|
|
404
|
+
// Global arrow key navigation for navbar
|
|
405
|
+
document.addEventListener('keydown', function(e) {
|
|
406
|
+
// Only handle arrow keys
|
|
407
|
+
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
|
|
408
|
+
|
|
409
|
+
// Only handle if we're in the navbar
|
|
410
|
+
const navbar = document.querySelector('.navbar_component, [data-hs-nav]');
|
|
411
|
+
if (!navbar || !navbar.contains(document.activeElement)) return;
|
|
412
|
+
|
|
413
|
+
// Allow left/right navigation from dropdown items, but handle it specially
|
|
414
|
+
const activeDropdown = allDropdowns.find(d => d.wrapper.contains(document.activeElement) && d.isOpen());
|
|
415
|
+
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
|
|
418
|
+
if (activeDropdown) {
|
|
419
|
+
// If we're in a dropdown, navigate to sibling dropdowns/elements
|
|
420
|
+
if (e.key === 'ArrowRight') {
|
|
421
|
+
// Find next focusable element after this dropdown
|
|
422
|
+
const nextElement = activeDropdown.wrapper.nextElementSibling?.querySelector('a, button') ||
|
|
423
|
+
document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
|
|
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);
|
|
444
|
+
|
|
445
|
+
const currentIndex = focusableArray.indexOf(document.activeElement);
|
|
446
|
+
if (currentIndex === -1) return;
|
|
447
|
+
|
|
448
|
+
let nextIndex;
|
|
449
|
+
if (e.key === 'ArrowRight') {
|
|
450
|
+
nextIndex = (currentIndex + 1) % focusableArray.length;
|
|
451
|
+
} else { // ArrowLeft
|
|
452
|
+
nextIndex = currentIndex === 0 ? focusableArray.length - 1 : currentIndex - 1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
focusableArray[nextIndex].focus();
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Dynamic mobile menu button system for Webflow interactions
|
|
461
|
+
function setupMobileMenuButton() {
|
|
462
|
+
// Find mobile menu button and menu
|
|
463
|
+
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
|
464
|
+
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
|
465
|
+
|
|
466
|
+
if (!menuButton || !mobileMenu) return;
|
|
467
|
+
|
|
468
|
+
// Generate unique ID for the menu
|
|
469
|
+
const menuId = `mobile-menu-${Date.now()}`;
|
|
470
|
+
|
|
471
|
+
// Set up ARIA attributes for button
|
|
472
|
+
menuButton.setAttribute('aria-expanded', 'false');
|
|
473
|
+
menuButton.setAttribute('aria-controls', menuId);
|
|
474
|
+
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
|
475
|
+
|
|
476
|
+
// Set up ARIA attributes for menu
|
|
477
|
+
mobileMenu.id = menuId;
|
|
478
|
+
mobileMenu.setAttribute('role', 'dialog');
|
|
479
|
+
mobileMenu.setAttribute('aria-modal', 'true');
|
|
480
|
+
mobileMenu.inert = true;
|
|
481
|
+
|
|
482
|
+
// Track menu state
|
|
483
|
+
let isMenuOpen = false;
|
|
484
|
+
|
|
485
|
+
// Function to open menu (simulate click)
|
|
486
|
+
function openMenu() {
|
|
487
|
+
if (isMenuOpen) return;
|
|
488
|
+
|
|
489
|
+
isMenuOpen = true;
|
|
490
|
+
|
|
491
|
+
// Update ARIA states
|
|
492
|
+
menuButton.setAttribute('aria-expanded', 'true');
|
|
493
|
+
menuButton.setAttribute('aria-label', 'Close navigation menu');
|
|
494
|
+
mobileMenu.inert = false;
|
|
495
|
+
|
|
496
|
+
// Simulate click on button to trigger Webflow interactions
|
|
497
|
+
const clickEvent = new MouseEvent('click', {
|
|
498
|
+
bubbles: true,
|
|
499
|
+
cancelable: true,
|
|
500
|
+
view: window
|
|
501
|
+
});
|
|
502
|
+
menuButton.dispatchEvent(clickEvent);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Function to close menu (simulate second click)
|
|
506
|
+
function closeMenu() {
|
|
507
|
+
if (!isMenuOpen) return;
|
|
508
|
+
|
|
509
|
+
isMenuOpen = false;
|
|
510
|
+
|
|
511
|
+
// Move focus away BEFORE setting aria-hidden to avoid ARIA violation
|
|
512
|
+
if (mobileMenu.contains(document.activeElement)) {
|
|
513
|
+
menuButton.focus();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update ARIA states after focus is moved
|
|
517
|
+
menuButton.setAttribute('aria-expanded', 'false');
|
|
518
|
+
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
|
519
|
+
mobileMenu.inert = true;
|
|
520
|
+
|
|
521
|
+
// Simulate second click on button to trigger Webflow interactions
|
|
522
|
+
const clickEvent = new MouseEvent('click', {
|
|
523
|
+
bubbles: true,
|
|
524
|
+
cancelable: true,
|
|
525
|
+
view: window
|
|
526
|
+
});
|
|
527
|
+
menuButton.dispatchEvent(clickEvent);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Function to toggle menu state
|
|
531
|
+
function toggleMenu() {
|
|
532
|
+
if (isMenuOpen) {
|
|
533
|
+
closeMenu();
|
|
534
|
+
} else {
|
|
535
|
+
openMenu();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Keyboard events for button
|
|
540
|
+
menuButton.addEventListener('keydown', function(e) {
|
|
541
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
toggleMenu();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Listen for actual clicks to update state (for cases where Webflow triggers clicks)
|
|
548
|
+
menuButton.addEventListener('click', function(e) {
|
|
549
|
+
// Only update state if this wasn't triggered by our own events
|
|
550
|
+
if (!e.isTrusted) return;
|
|
551
|
+
|
|
552
|
+
// If closing and focus is inside menu, move it out first
|
|
553
|
+
if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
|
|
554
|
+
menuButton.focus();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Toggle our internal state
|
|
558
|
+
isMenuOpen = !isMenuOpen;
|
|
559
|
+
|
|
560
|
+
// Update ARIA attributes after focus is handled
|
|
561
|
+
menuButton.setAttribute('aria-expanded', isMenuOpen);
|
|
562
|
+
menuButton.setAttribute('aria-label',
|
|
563
|
+
isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
|
564
|
+
);
|
|
565
|
+
mobileMenu.inert = !isMenuOpen;
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Helper function to sanitize text for HTML IDs
|
|
570
|
+
function sanitizeForID(text) {
|
|
571
|
+
return text
|
|
572
|
+
.toLowerCase()
|
|
573
|
+
.replace(/[^a-z0-9\s]/g, '') // Remove special characters
|
|
574
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
575
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
576
|
+
.substring(0, 50); // Limit length
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Setup mobile menu ARIA automation
|
|
580
|
+
function setupMobileMenuARIA() {
|
|
581
|
+
const menuContainer = document.querySelector('[data-hs-nav="menu"]');
|
|
582
|
+
if (!menuContainer) return;
|
|
583
|
+
|
|
584
|
+
// Find all buttons and links inside the menu container
|
|
585
|
+
const buttons = menuContainer.querySelectorAll('button');
|
|
586
|
+
const links = menuContainer.querySelectorAll('a');
|
|
587
|
+
|
|
588
|
+
// Process each button (dropdown toggles)
|
|
589
|
+
buttons.forEach(button => {
|
|
590
|
+
const buttonText = button.textContent?.trim();
|
|
591
|
+
if (!buttonText) return;
|
|
592
|
+
|
|
593
|
+
const sanitizedText = sanitizeForID(buttonText);
|
|
594
|
+
const buttonId = `navbar-mobile-${sanitizedText}-toggle`;
|
|
595
|
+
const listId = `navbar-mobile-${sanitizedText}-list`;
|
|
596
|
+
|
|
597
|
+
// Set button ID and ARIA attributes
|
|
598
|
+
button.id = buttonId;
|
|
599
|
+
button.setAttribute('aria-expanded', 'false');
|
|
600
|
+
button.setAttribute('aria-controls', listId);
|
|
601
|
+
|
|
602
|
+
// Find associated dropdown list (look for next sibling or nearby element with links)
|
|
603
|
+
let dropdownList = button.nextElementSibling;
|
|
604
|
+
|
|
605
|
+
// If next sibling doesn't contain links, look for parent's next sibling
|
|
606
|
+
if (!dropdownList || !dropdownList.querySelector('a')) {
|
|
607
|
+
dropdownList = button.parentElement?.nextElementSibling;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// If still not found, look for a nearby element that contains multiple links
|
|
611
|
+
if (!dropdownList || !dropdownList.querySelector('a')) {
|
|
612
|
+
const parent = button.closest('[data-hs-nav="menu"]');
|
|
613
|
+
const allListElements = parent?.querySelectorAll('div, ul, nav');
|
|
614
|
+
dropdownList = Array.from(allListElements || []).find(el =>
|
|
615
|
+
el.querySelectorAll('a').length > 1 &&
|
|
616
|
+
!el.contains(button)
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Set dropdown list ID and initial hidden state if found
|
|
621
|
+
if (dropdownList && dropdownList.querySelector('a')) {
|
|
622
|
+
dropdownList.id = listId;
|
|
623
|
+
dropdownList.inert = true;
|
|
624
|
+
|
|
625
|
+
// Add click listener to button to manage dropdown state
|
|
626
|
+
button.addEventListener('click', function() {
|
|
627
|
+
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
|
628
|
+
const newState = !isExpanded;
|
|
629
|
+
|
|
630
|
+
// Update button state
|
|
631
|
+
button.setAttribute('aria-expanded', newState);
|
|
632
|
+
|
|
633
|
+
// Update dropdown state
|
|
634
|
+
dropdownList.inert = !newState;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Process each link (navigation items) - but don't override dropdown links
|
|
640
|
+
links.forEach(link => {
|
|
641
|
+
const linkText = link.textContent?.trim();
|
|
642
|
+
if (!linkText) return;
|
|
643
|
+
|
|
644
|
+
const sanitizedText = sanitizeForID(linkText);
|
|
645
|
+
const linkId = `navbar-mobile-${sanitizedText}-link`;
|
|
646
|
+
|
|
647
|
+
// Set link ID
|
|
648
|
+
link.id = linkId;
|
|
649
|
+
|
|
650
|
+
// Links inside inert containers are automatically non-focusable
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Add arrow key navigation for mobile menu
|
|
654
|
+
setupMobileMenuArrowNavigation(menuContainer);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Arrow key navigation for mobile menu
|
|
658
|
+
function setupMobileMenuArrowNavigation(menuContainer) {
|
|
659
|
+
// Get all focusable elements in the mobile menu
|
|
660
|
+
function getFocusableElements() {
|
|
661
|
+
return menuContainer.querySelectorAll('button:not([tabindex="-1"]), a:not([tabindex="-1"])');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let currentFocusIndex = -1;
|
|
665
|
+
|
|
666
|
+
menuContainer.addEventListener('keydown', function(e) {
|
|
667
|
+
const focusableElements = Array.from(getFocusableElements());
|
|
668
|
+
if (focusableElements.length === 0) return;
|
|
669
|
+
|
|
670
|
+
// Find current focus index
|
|
671
|
+
const activeElement = document.activeElement;
|
|
672
|
+
currentFocusIndex = focusableElements.indexOf(activeElement);
|
|
673
|
+
|
|
674
|
+
if (e.key === 'ArrowDown') {
|
|
675
|
+
e.preventDefault();
|
|
676
|
+
currentFocusIndex = (currentFocusIndex + 1) % focusableElements.length;
|
|
677
|
+
focusableElements[currentFocusIndex].focus();
|
|
678
|
+
} else if (e.key === 'ArrowUp') {
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
currentFocusIndex = currentFocusIndex <= 0 ? focusableElements.length - 1 : currentFocusIndex - 1;
|
|
681
|
+
focusableElements[currentFocusIndex].focus();
|
|
682
|
+
} else if (e.key === 'Home') {
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
currentFocusIndex = 0;
|
|
685
|
+
focusableElements[0].focus();
|
|
686
|
+
} else if (e.key === 'End') {
|
|
687
|
+
e.preventDefault();
|
|
688
|
+
currentFocusIndex = focusableElements.length - 1;
|
|
689
|
+
focusableElements[focusableElements.length - 1].focus();
|
|
690
|
+
} else if (e.key === ' ' && activeElement.tagName === 'A') {
|
|
691
|
+
e.preventDefault();
|
|
692
|
+
// Just prevent space from scrolling page - Enter activates links
|
|
693
|
+
} else if (e.key === 'Escape') {
|
|
694
|
+
// Close mobile menu
|
|
695
|
+
const mobileMenuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
|
696
|
+
if (mobileMenuButton) {
|
|
697
|
+
mobileMenuButton.click();
|
|
698
|
+
mobileMenuButton.focus();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
package/configure-example.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
function configure() {
|
|
2
|
-
// Check if hsmain is loaded AND the specific animation modules are available
|
|
3
|
-
if (!window.hsmain?.loaded ||
|
|
4
|
-
!window.hsmain?.textAnimations?.updateConfig ||
|
|
5
|
-
!window.hsmain?.heroAnimations?.updateConfig) {
|
|
6
|
-
setTimeout(configure, 10);
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const api = window.hsmain;
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
api.textAnimations.updateConfig({
|
|
14
|
-
// your text animation config
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
api.heroAnimations.updateConfig({
|
|
18
|
-
headingSplit: {
|
|
19
|
-
// your hero animation config
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
api.textAnimations.restart();
|
|
24
|
-
api.heroAnimations.restart();
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.warn('Animation configuration failed:', error);
|
|
27
|
-
// Retry after a short delay if needed
|
|
28
|
-
setTimeout(configure, 50);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
configure();
|