@hortonstudio/main 1.2.27 → 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.
@@ -36,7 +36,8 @@
36
36
  "Bash(tree:*)",
37
37
  "Bash(grep:*)",
38
38
  "Bash(git reset:*)",
39
- "Bash(git checkout:*)"
39
+ "Bash(git checkout:*)",
40
+ "Bash(rg:*)"
40
41
  ],
41
42
  "deny": []
42
43
  }
@@ -45,11 +45,11 @@ const config = {
45
45
  },
46
46
  appear: {
47
47
  y: 50,
48
- duration: 1.5,
48
+ duration: 1,
49
49
  ease: "power3.out"
50
50
  },
51
51
  navStagger: {
52
- duration: 1.5,
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
- { opacity: 1, y: 0, duration: config.nav.duration, ease: config.nav.ease },
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
- { opacity: 1, duration: config.nav.duration, ease: config.nav.ease },
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
- // Restore page-wide tabbing and interactions after hero animation completes
543
- const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
544
- allFocusableElements.forEach(el => {
545
- el.style.pointerEvents = '';
546
- const originalTabindex = el.getAttribute('data-original-tabindex');
547
- if (originalTabindex === '0') {
548
- el.removeAttribute('tabindex');
549
- } else {
550
- el.setAttribute('tabindex', originalTabindex);
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, restore tabbing when timeline completes
611
+ // If no appear elements, check if interactions need restoring when timeline completes
564
612
  heroTimeline.call(() => {
565
- const allFocusableElements = document.querySelectorAll('[data-original-tabindex]');
566
- allFocusableElements.forEach(el => {
567
- el.style.pointerEvents = '';
568
- const originalTabindex = el.getAttribute('data-original-tabindex');
569
- if (originalTabindex === '0') {
570
- el.removeAttribute('tabindex');
571
- } else {
572
- el.setAttribute('tabindex', originalTabindex);
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
  }
@@ -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.27
1
+ // Version:1.2.29
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.2.27",
3
+ "version": "1.2.29",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/utils/navbar.js CHANGED
@@ -1,11 +1,15 @@
1
1
  export const init = () => {
2
- // Find all dropdown wrappers
3
- const dropdownWrappers = document.querySelectorAll('[data-hs-nav-dropdown="wrapper"]');
4
-
5
- // Global array to track all dropdown instances
2
+ setupDynamicDropdowns();
3
+ setupMobileMenuButton();
4
+ setupMobileMenuARIA();
5
+ return { result: 'navbar initialized' };
6
+ };
7
+
8
+ // Desktop dropdown system
9
+ function setupDynamicDropdowns() {
10
+ const dropdownWrappers = document.querySelectorAll('[data-hs-nav="dropdown"]');
6
11
  const allDropdowns = [];
7
12
 
8
- // Function to close all dropdowns except the specified one
9
13
  const closeAllDropdowns = (exceptWrapper = null) => {
10
14
  allDropdowns.forEach(dropdown => {
11
15
  if (dropdown.wrapper !== exceptWrapper && dropdown.isOpen) {
@@ -15,193 +19,250 @@ export const init = () => {
15
19
  };
16
20
 
17
21
  dropdownWrappers.forEach(wrapper => {
18
- const animationDuration = 0.3;
19
-
20
- // Find elements within this wrapper
21
- const toggle = wrapper.querySelector('a'); // the toggle link
22
- const list = wrapper.querySelector('[data-hs-nav-dropdown="list"]');
23
- const contain = list.querySelector('[data-hs-nav-dropdown="container"]');
24
- const arrow = toggle.querySelector('[data-hs-nav-dropdown="arrow"]');
25
- const text = toggle.querySelector('[data-hs-nav-dropdown="text"]'); // find the text element
26
-
27
- // Set initial states with GSAP
28
- gsap.set(contain, { yPercent: -110 });
29
- gsap.set(list, { display: 'none' });
30
- gsap.set(arrow, { rotation: 0, scale: 1, x: 0, color: '' });
31
- gsap.set(text, { scale: 1, color: '' }); // empty string = default color
32
-
33
- // Track if dropdown is open
22
+ const toggle = wrapper.querySelector('a');
23
+ if (!toggle) return;
24
+
25
+ const allElements = wrapper.querySelectorAll('*');
26
+ let dropdownList = null;
27
+
28
+ for (const element of allElements) {
29
+ const links = element.querySelectorAll('a');
30
+ if (links.length >= 2 && !element.contains(toggle)) {
31
+ dropdownList = element;
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (!dropdownList) return;
37
+
38
+ const toggleText = toggle.textContent?.trim() || 'dropdown';
39
+ const sanitizedText = sanitizeForID(toggleText);
40
+ const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
41
+ const listId = `navbar-dropdown-${sanitizedText}-list`;
42
+
43
+ toggle.id = toggleId;
44
+ toggle.setAttribute('aria-haspopup', 'menu');
45
+ toggle.setAttribute('aria-expanded', 'false');
46
+ toggle.setAttribute('aria-controls', listId);
47
+
48
+ dropdownList.id = listId;
49
+ dropdownList.setAttribute('role', 'menu');
50
+ dropdownList.setAttribute('aria-hidden', 'true');
51
+
52
+ const menuItems = dropdownList.querySelectorAll('a');
53
+ menuItems.forEach(item => {
54
+ item.setAttribute('role', 'menuitem');
55
+ item.setAttribute('tabindex', '-1');
56
+ });
57
+
34
58
  let isOpen = false;
35
- let currentTimeline = null;
59
+ let currentMenuItemIndex = -1;
36
60
 
37
- // Open animation
38
61
  function openDropdown() {
39
62
  if (isOpen) return;
40
-
41
- // Kill any existing timeline
42
- if (currentTimeline) {
43
- currentTimeline.kill();
44
- }
45
-
46
- // Close all other dropdowns first
47
63
  closeAllDropdowns(wrapper);
48
-
49
64
  isOpen = true;
50
-
51
- // Update ARIA states
52
65
  toggle.setAttribute('aria-expanded', 'true');
53
- list.setAttribute('aria-hidden', 'false');
54
-
55
- // GSAP animation
56
- currentTimeline = gsap.timeline();
57
- currentTimeline.set(list, { display: 'flex' })
58
- .to(contain, {
59
- yPercent: 0,
60
- duration: animationDuration,
61
- ease: 'ease'
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);
66
+ dropdownList.setAttribute('aria-hidden', 'false');
67
+ menuItems.forEach(item => {
68
+ item.setAttribute('tabindex', '0');
69
+ });
70
+ const clickEvent = new MouseEvent('click', {
71
+ bubbles: true,
72
+ cancelable: true,
73
+ view: window
74
+ });
75
+ wrapper.dispatchEvent(clickEvent);
77
76
  }
78
77
 
79
- // Close animation
80
78
  function closeDropdown() {
81
79
  if (!isOpen) return;
82
-
83
- // Kill any existing timeline
84
- if (currentTimeline) {
85
- currentTimeline.kill();
86
- }
87
-
88
- // Check if focus should be restored to toggle
89
- const shouldRestoreFocus = list.contains(document.activeElement);
90
-
80
+ const shouldRestoreFocus = dropdownList.contains(document.activeElement);
91
81
  isOpen = false;
92
82
  currentMenuItemIndex = -1;
93
-
94
- // Update ARIA states
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
130
83
  if (shouldRestoreFocus) {
131
- // Small delay to ensure screen reader announces the state change
132
- setTimeout(() => {
133
- toggle.focus();
134
- }, 50);
84
+ toggle.focus();
135
85
  }
86
+ toggle.setAttribute('aria-expanded', 'false');
87
+ dropdownList.setAttribute('aria-hidden', 'true');
88
+ menuItems.forEach(item => {
89
+ item.setAttribute('tabindex', '-1');
90
+ });
91
+ const clickEvent = new MouseEvent('click', {
92
+ bubbles: true,
93
+ cancelable: true,
94
+ view: window
95
+ });
96
+ wrapper.dispatchEvent(clickEvent);
136
97
  }
137
98
 
138
- // Get all menu items for navigation
139
- const menuItems = list.querySelectorAll('a, button, [role="menuitem"]');
140
- let currentMenuItemIndex = -1;
99
+ wrapper.addEventListener('mouseenter', () => {
100
+ if (!isOpen) {
101
+ const clickEvent = new MouseEvent('click', {
102
+ bubbles: true,
103
+ cancelable: true,
104
+ view: window
105
+ });
106
+ wrapper.dispatchEvent(clickEvent);
107
+ closeAllDropdowns(wrapper);
108
+ isOpen = true;
109
+ toggle.setAttribute('aria-expanded', 'true');
110
+ dropdownList.setAttribute('aria-hidden', 'false');
111
+ menuItems.forEach(item => {
112
+ item.setAttribute('tabindex', '0');
113
+ });
114
+ }
115
+ });
141
116
 
142
- // Hover events
143
- toggle.addEventListener('mouseenter', openDropdown);
144
- wrapper.addEventListener('mouseleave', closeDropdown);
117
+ wrapper.addEventListener('mouseleave', () => {
118
+ if (isOpen) {
119
+ if (dropdownList.contains(document.activeElement)) {
120
+ toggle.focus();
121
+ }
122
+ const clickEvent = new MouseEvent('click', {
123
+ bubbles: true,
124
+ cancelable: true,
125
+ view: window
126
+ });
127
+ wrapper.dispatchEvent(clickEvent);
128
+ isOpen = false;
129
+ toggle.setAttribute('aria-expanded', 'false');
130
+ dropdownList.setAttribute('aria-hidden', 'true');
131
+ menuItems.forEach(item => {
132
+ item.setAttribute('tabindex', '-1');
133
+ });
134
+ currentMenuItemIndex = -1;
135
+ }
136
+ });
145
137
 
146
- // Arrow key navigation within dropdown
147
- list.addEventListener('keydown', function(e) {
138
+ document.addEventListener('keydown', function(e) {
148
139
  if (!isOpen) return;
140
+ if (!wrapper.contains(document.activeElement)) return;
149
141
 
150
142
  if (e.key === 'ArrowDown') {
151
143
  e.preventDefault();
152
- currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
153
- menuItems[currentMenuItemIndex].focus();
144
+ if (document.activeElement === toggle) {
145
+ currentMenuItemIndex = 0;
146
+ menuItems[currentMenuItemIndex].focus();
147
+ } else {
148
+ if (currentMenuItemIndex === menuItems.length - 1) {
149
+ const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
150
+ document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
151
+ if (nextElement) {
152
+ closeDropdown();
153
+ nextElement.focus();
154
+ return;
155
+ }
156
+ }
157
+ currentMenuItemIndex = (currentMenuItemIndex + 1) % menuItems.length;
158
+ menuItems[currentMenuItemIndex].focus();
159
+ }
154
160
  } else if (e.key === 'ArrowUp') {
155
161
  e.preventDefault();
156
- currentMenuItemIndex = currentMenuItemIndex <= 0 ? menuItems.length - 1 : currentMenuItemIndex - 1;
157
- menuItems[currentMenuItemIndex].focus();
162
+ if (document.activeElement === toggle) {
163
+ currentMenuItemIndex = menuItems.length - 1;
164
+ menuItems[currentMenuItemIndex].focus();
165
+ } else {
166
+ if (currentMenuItemIndex === 0) {
167
+ const prevElement = wrapper.previousElementSibling?.querySelector('a, button');
168
+ if (prevElement) {
169
+ closeDropdown();
170
+ prevElement.focus();
171
+ return;
172
+ } else {
173
+ closeDropdown();
174
+ toggle.focus();
175
+ return;
176
+ }
177
+ }
178
+ currentMenuItemIndex = currentMenuItemIndex <= 0 ? menuItems.length - 1 : currentMenuItemIndex - 1;
179
+ menuItems[currentMenuItemIndex].focus();
180
+ }
181
+ } else if (e.key === 'Tab') {
182
+ if (e.shiftKey) {
183
+ if (document.activeElement === menuItems[0]) {
184
+ e.preventDefault();
185
+ closeDropdown();
186
+ toggle.focus();
187
+ }
188
+ } else {
189
+ if (document.activeElement === menuItems[menuItems.length - 1]) {
190
+ e.preventDefault();
191
+ const nextElement = wrapper.nextElementSibling?.querySelector('a, button') ||
192
+ document.querySelector('.navbar_cartsearch_wrap a, .navbar_cartsearch_wrap button');
193
+ closeDropdown();
194
+ if (nextElement) {
195
+ setTimeout(() => {
196
+ nextElement.focus();
197
+ }, 10);
198
+ }
199
+ }
200
+ }
158
201
  } else if (e.key === 'Escape') {
159
202
  e.preventDefault();
160
203
  closeDropdown();
161
204
  toggle.focus();
205
+ } else if (e.key === 'Home') {
206
+ e.preventDefault();
207
+ currentMenuItemIndex = 0;
208
+ menuItems[0].focus();
209
+ } else if (e.key === 'End') {
210
+ e.preventDefault();
211
+ currentMenuItemIndex = menuItems.length - 1;
212
+ menuItems[menuItems.length - 1].focus();
213
+ } else if (e.key === ' ') {
214
+ e.preventDefault();
162
215
  }
163
216
  });
164
217
 
165
- // Keyboard events for toggle
166
218
  toggle.addEventListener('keydown', function(e) {
167
219
  if (e.key === 'ArrowDown') {
168
220
  e.preventDefault();
169
221
  openDropdown();
170
- // Focus first menu item after opening
171
222
  if (menuItems.length > 0) {
172
223
  currentMenuItemIndex = 0;
173
- setTimeout(() => menuItems[0].focus(), 50);
224
+ setTimeout(() => menuItems[0].focus(), 100);
174
225
  }
175
226
  } else if (e.key === ' ') {
176
227
  e.preventDefault();
177
- // Simple toggle: if closed open, if open close
178
228
  if (isOpen) {
179
229
  closeDropdown();
180
230
  } else {
181
231
  openDropdown();
232
+ if (menuItems.length > 0) {
233
+ currentMenuItemIndex = 0;
234
+ setTimeout(() => menuItems[0].focus(), 100);
235
+ }
182
236
  }
183
- } else if (e.key === 'ArrowUp' || e.key === 'Escape') {
237
+ } else if (e.key === 'ArrowUp') {
238
+ e.preventDefault();
239
+ if (isOpen) {
240
+ currentMenuItemIndex = menuItems.length - 1;
241
+ menuItems[currentMenuItemIndex].focus();
242
+ } else {
243
+ closeDropdown();
244
+ }
245
+ } else if (e.key === 'Escape') {
184
246
  e.preventDefault();
185
247
  closeDropdown();
186
248
  }
187
249
  });
188
250
 
189
- // Close dropdown when clicking outside
190
251
  document.addEventListener('click', function(e) {
191
252
  if (!wrapper.contains(e.target) && isOpen) {
192
253
  closeDropdown();
193
254
  }
194
255
  });
195
256
 
196
- // Add this dropdown instance to the global array
197
257
  allDropdowns.push({
198
258
  wrapper,
199
259
  isOpen: () => isOpen,
200
- closeDropdown
260
+ closeDropdown,
261
+ toggle,
262
+ dropdownList
201
263
  });
202
264
  });
203
265
 
204
- // Global focus management - close dropdown when tab focus moves outside
205
266
  document.addEventListener('focusin', function(e) {
206
267
  allDropdowns.forEach(dropdown => {
207
268
  if (dropdown.isOpen() && !dropdown.wrapper.contains(e.target)) {
@@ -210,5 +271,371 @@ export const init = () => {
210
271
  });
211
272
  });
212
273
 
213
- return { result: 'navbar initialized' };
214
- };
274
+ addDesktopArrowNavigation();
275
+ }
276
+
277
+ // Desktop left/right arrow navigation
278
+ function addDesktopArrowNavigation() {
279
+ document.addEventListener('keydown', function(e) {
280
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
281
+
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
+
290
+ if (!navbar || !navbar.contains(document.activeElement)) return;
291
+
292
+ const openDropdownList = navbar.querySelector('[aria-hidden="false"][role="menu"]');
293
+ if (openDropdownList && openDropdownList.contains(document.activeElement)) return;
294
+
295
+ e.preventDefault();
296
+
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;
303
+
304
+ const isInMobileMenu = el.closest('[data-hs-nav="menu"]');
305
+ if (isInMobileMenu) return false;
306
+
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;
324
+ }
325
+
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;
337
+ }
338
+
339
+ focusableElements[nextIndex].focus();
340
+ });
341
+ }
342
+
343
+ // Mobile menu button system
344
+ function setupMobileMenuButton() {
345
+ const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
346
+ const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
347
+
348
+ if (!menuButton || !mobileMenu) return;
349
+
350
+ const menuId = `mobile-menu-${Date.now()}`;
351
+
352
+ menuButton.setAttribute('aria-expanded', 'false');
353
+ menuButton.setAttribute('aria-controls', menuId);
354
+ menuButton.setAttribute('aria-label', 'Open navigation menu');
355
+
356
+ mobileMenu.id = menuId;
357
+ mobileMenu.setAttribute('role', 'dialog');
358
+ mobileMenu.setAttribute('aria-modal', 'true');
359
+ mobileMenu.inert = true;
360
+
361
+ let isMenuOpen = false;
362
+
363
+ function openMenu() {
364
+ if (isMenuOpen) return;
365
+ isMenuOpen = true;
366
+ menuButton.setAttribute('aria-expanded', 'true');
367
+ menuButton.setAttribute('aria-label', 'Close navigation menu');
368
+ mobileMenu.inert = false;
369
+
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
+
384
+ const clickEvent = new MouseEvent('click', {
385
+ bubbles: true,
386
+ cancelable: true,
387
+ view: window
388
+ });
389
+ menuButton.dispatchEvent(clickEvent);
390
+ }
391
+
392
+ function closeMenu() {
393
+ if (!isMenuOpen) return;
394
+ isMenuOpen = false;
395
+ if (mobileMenu.contains(document.activeElement)) {
396
+ menuButton.focus();
397
+ }
398
+ menuButton.setAttribute('aria-expanded', 'false');
399
+ menuButton.setAttribute('aria-label', 'Open navigation menu');
400
+ mobileMenu.inert = true;
401
+
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
+
414
+ const clickEvent = new MouseEvent('click', {
415
+ bubbles: true,
416
+ cancelable: true,
417
+ view: window
418
+ });
419
+ menuButton.dispatchEvent(clickEvent);
420
+ }
421
+
422
+ function toggleMenu() {
423
+ if (isMenuOpen) {
424
+ closeMenu();
425
+ } else {
426
+ openMenu();
427
+ }
428
+ }
429
+
430
+ menuButton.addEventListener('keydown', function(e) {
431
+ if (e.key === 'Enter' || e.key === ' ') {
432
+ e.preventDefault();
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
+ }
448
+ }
449
+ });
450
+
451
+ menuButton.addEventListener('click', function(e) {
452
+ if (!e.isTrusted) return;
453
+ if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
454
+ menuButton.focus();
455
+ }
456
+ isMenuOpen = !isMenuOpen;
457
+ menuButton.setAttribute('aria-expanded', isMenuOpen);
458
+ menuButton.setAttribute('aria-label',
459
+ isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
460
+ );
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
+ }
489
+ });
490
+ }
491
+
492
+ function sanitizeForID(text) {
493
+ return text
494
+ .toLowerCase()
495
+ .replace(/[^a-z0-9\s]/g, '')
496
+ .replace(/\s+/g, '-')
497
+ .replace(/^-+|-+$/g, '')
498
+ .substring(0, 50);
499
+ }
500
+
501
+ // Mobile menu ARIA setup
502
+ function setupMobileMenuARIA() {
503
+ const menuContainer = document.querySelector('[data-hs-nav="menu"]');
504
+ if (!menuContainer) return;
505
+
506
+ const buttons = menuContainer.querySelectorAll('button');
507
+ const links = menuContainer.querySelectorAll('a');
508
+
509
+ buttons.forEach(button => {
510
+ const buttonText = button.textContent?.trim();
511
+ if (!buttonText) return;
512
+
513
+ const sanitizedText = sanitizeForID(buttonText);
514
+ const buttonId = `navbar-mobile-${sanitizedText}-toggle`;
515
+ const listId = `navbar-mobile-${sanitizedText}-list`;
516
+
517
+ button.id = buttonId;
518
+ button.setAttribute('aria-expanded', 'false');
519
+ button.setAttribute('aria-controls', listId);
520
+
521
+ let dropdownList = button.nextElementSibling;
522
+
523
+ if (!dropdownList || !dropdownList.querySelector('a')) {
524
+ dropdownList = button.parentElement?.nextElementSibling;
525
+ }
526
+
527
+ if (!dropdownList || !dropdownList.querySelector('a')) {
528
+ const parent = button.closest('[data-hs-nav="menu"]');
529
+ const allListElements = parent?.querySelectorAll('div, ul, nav');
530
+ dropdownList = Array.from(allListElements || []).find(el =>
531
+ el.querySelectorAll('a').length > 1 &&
532
+ !el.contains(button)
533
+ );
534
+ }
535
+
536
+ if (dropdownList && dropdownList.querySelector('a')) {
537
+ dropdownList.id = listId;
538
+ dropdownList.inert = true;
539
+
540
+ button.addEventListener('click', function() {
541
+ const isExpanded = button.getAttribute('aria-expanded') === 'true';
542
+ const newState = !isExpanded;
543
+ button.setAttribute('aria-expanded', newState);
544
+ dropdownList.inert = !newState;
545
+ });
546
+ }
547
+ });
548
+
549
+ links.forEach(link => {
550
+ const linkText = link.textContent?.trim();
551
+ if (!linkText) return;
552
+
553
+ const sanitizedText = sanitizeForID(linkText);
554
+ const linkId = `navbar-mobile-${sanitizedText}-link`;
555
+ link.id = linkId;
556
+ });
557
+
558
+ setupMobileMenuArrowNavigation(menuContainer);
559
+ }
560
+
561
+ // Mobile menu arrow navigation
562
+ function setupMobileMenuArrowNavigation(menuContainer) {
563
+ function getFocusableElements() {
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
+ });
575
+ }
576
+
577
+ let currentFocusIndex = -1;
578
+
579
+ menuContainer.addEventListener('keydown', function(e) {
580
+ const focusableElements = getFocusableElements();
581
+ if (focusableElements.length === 0) return;
582
+
583
+ const activeElement = document.activeElement;
584
+ currentFocusIndex = focusableElements.indexOf(activeElement);
585
+
586
+ if (e.key === 'ArrowDown') {
587
+ e.preventDefault();
588
+ if (currentFocusIndex >= focusableElements.length - 1) {
589
+ currentFocusIndex = 0;
590
+ } else {
591
+ currentFocusIndex = currentFocusIndex + 1;
592
+ }
593
+ focusableElements[currentFocusIndex].focus();
594
+ } else if (e.key === 'ArrowUp') {
595
+ e.preventDefault();
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;
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
+ }
623
+ } else if (e.key === 'Home') {
624
+ e.preventDefault();
625
+ currentFocusIndex = 0;
626
+ focusableElements[0].focus();
627
+ } else if (e.key === 'End') {
628
+ e.preventDefault();
629
+ currentFocusIndex = focusableElements.length - 1;
630
+ focusableElements[focusableElements.length - 1].focus();
631
+ } else if (e.key === ' ' && activeElement.tagName === 'A') {
632
+ e.preventDefault();
633
+ } else if (e.key === 'Escape') {
634
+ const mobileMenuButton = document.querySelector('[data-hs-nav="menubtn"]');
635
+ if (mobileMenuButton) {
636
+ mobileMenuButton.click();
637
+ mobileMenuButton.focus();
638
+ }
639
+ }
640
+ });
641
+ }