@hortonstudio/main 1.9.11 → 1.9.20

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.
Files changed (120) hide show
  1. package/.prettierrc +8 -0
  2. package/README.md +146 -0
  3. package/eslint.config.js +32 -0
  4. package/index.ts +275 -0
  5. package/package.json +19 -2
  6. package/public/bootstrap.js +16 -0
  7. package/src/animations/animations.ts +93 -0
  8. package/src/animations/functions/counter/counter.ts +137 -0
  9. package/src/config.json +570 -0
  10. package/src/config.ts +105 -0
  11. package/src/modules/default/README.md +167 -0
  12. package/src/modules/default/default.ts +71 -0
  13. package/{autoInit → src/modules/default/functions}/accessibility/README.md +44 -12
  14. package/src/modules/default/functions/accessibility/accessibility.ts +54 -0
  15. package/src/modules/default/functions/accordion/README.md +451 -0
  16. package/src/modules/default/functions/accordion/accordion.ts +189 -0
  17. package/src/modules/default/functions/comparison/comparison.ts +424 -0
  18. package/src/modules/default/functions/marquee/marquee.ts +206 -0
  19. package/src/modules/default/functions/navbar/README.md +393 -0
  20. package/src/modules/default/functions/navbar/functions/arrow-navigation/arrow-navigation.ts +183 -0
  21. package/src/modules/default/functions/navbar/functions/dropdown/dropdown.ts +313 -0
  22. package/src/modules/default/functions/navbar/functions/menu/menu.ts +315 -0
  23. package/src/modules/default/functions/navbar/navbar.ts +51 -0
  24. package/{autoInit → src/modules/default/functions}/smooth-scroll/README.md +45 -14
  25. package/{autoInit/smooth-scroll/smooth-scroll.js → src/modules/default/functions/smooth-scroll/smooth-scroll.ts} +33 -38
  26. package/{autoInit → src/modules/default/functions}/transition/README.md +59 -32
  27. package/src/modules/default/functions/transition/transition.ts +290 -0
  28. package/src/modules/normalize/README.md +172 -0
  29. package/src/modules/normalize/functions/clickable/README.md +84 -0
  30. package/src/modules/normalize/functions/clickable/clickable.ts +43 -0
  31. package/src/modules/normalize/functions/clickable/functions/normalize/README.md +213 -0
  32. package/src/modules/normalize/functions/clickable/functions/normalize/normalize.ts +68 -0
  33. package/src/modules/normalize/functions/dupe/README.md +405 -0
  34. package/src/modules/normalize/functions/dupe/dupe.ts +197 -0
  35. package/src/modules/normalize/functions/sync/sync.ts +378 -0
  36. package/src/modules/normalize/normalize.ts +58 -0
  37. package/src/modules/structure/README.md +190 -0
  38. package/src/modules/structure/functions/form/README.md +94 -0
  39. package/src/modules/structure/functions/form/form.ts +54 -0
  40. package/src/modules/structure/functions/form/functions/honeypot/README.md +77 -0
  41. package/src/modules/structure/functions/form/functions/honeypot/honeypot.ts +37 -0
  42. package/src/modules/structure/functions/form/functions/range/README.md +410 -0
  43. package/src/modules/structure/functions/form/functions/range/range.ts +92 -0
  44. package/src/modules/structure/functions/form/functions/select/README.md +393 -0
  45. package/src/modules/structure/functions/form/functions/select/functions/custom-select/custom-select.ts +637 -0
  46. package/src/modules/structure/functions/form/functions/select/functions/states/states.ts +118 -0
  47. package/src/modules/structure/functions/form/functions/select/select.ts +48 -0
  48. package/src/modules/structure/functions/form/functions/test/test.ts +132 -0
  49. package/{autoInit/accessibility → src/modules/structure}/functions/pagination/README.md +147 -72
  50. package/{autoInit/accessibility/functions/pagination/pagination.js → src/modules/structure/functions/pagination/pagination.ts} +98 -50
  51. package/{autoInit → src/modules/structure/functions}/site-settings/README.md +57 -27
  52. package/{autoInit/site-settings/site-settings.js → src/modules/structure/functions/site-settings/site-settings.ts} +36 -32
  53. package/{autoInit/accessibility → src/modules/structure}/functions/toc/README.md +18 -15
  54. package/{autoInit/accessibility/functions/toc/toc.js → src/modules/structure/functions/toc/functions/heading-links/heading-links.ts} +43 -63
  55. package/src/modules/structure/functions/toc/functions/progress-bar/progress-bar.ts +101 -0
  56. package/src/modules/structure/functions/toc/toc.ts +35 -0
  57. package/{autoInit/accessibility → src/modules/structure}/functions/year-replacement/README.md +7 -6
  58. package/src/modules/structure/functions/year-replacement/year-replacement.ts +59 -0
  59. package/src/modules/structure/structure.ts +59 -0
  60. package/src/utils/attributeSelector.ts +78 -0
  61. package/src/utils/cssVariables.ts +24 -0
  62. package/src/utils/gsap.ts +198 -0
  63. package/src/utils/heightAnimator.ts +130 -0
  64. package/src/utils/modalManager.ts +150 -0
  65. package/src/utils.ts +54 -0
  66. package/tsconfig.json +24 -0
  67. package/vite.config.js +45 -0
  68. package/.claude/settings.local.json +0 -70
  69. package/archive/hero.js +0 -794
  70. package/archive/modal.js +0 -80
  71. package/archive/text.js +0 -628
  72. package/autoInit/accessibility/accessibility.js +0 -53
  73. package/autoInit/accessibility/functions/blog-remover/README.md +0 -61
  74. package/autoInit/accessibility/functions/blog-remover/blog-remover.js +0 -31
  75. package/autoInit/accessibility/functions/click-forwarding/README.md +0 -60
  76. package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +0 -82
  77. package/autoInit/accessibility/functions/dropdown/README.md +0 -212
  78. package/autoInit/accessibility/functions/dropdown/dropdown.js +0 -167
  79. package/autoInit/accessibility/functions/list-accessibility/README.md +0 -56
  80. package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +0 -23
  81. package/autoInit/accessibility/functions/text-synchronization/README.md +0 -62
  82. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
  83. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
  84. package/autoInit/button/README.md +0 -122
  85. package/autoInit/button/button.js +0 -51
  86. package/autoInit/counter/README.md +0 -274
  87. package/autoInit/counter/counter.js +0 -185
  88. package/autoInit/form/README.md +0 -338
  89. package/autoInit/form/form.js +0 -374
  90. package/autoInit/navbar/README.md +0 -366
  91. package/autoInit/navbar/navbar.js +0 -786
  92. package/autoInit/transition/transition.js +0 -116
  93. package/index.js +0 -305
  94. package/utils/before-after/README.md +0 -520
  95. package/utils/before-after/before-after.js +0 -653
  96. package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
  97. package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
  98. package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
  99. package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
  100. package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
  101. package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
  102. package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
  103. package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
  104. package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
  105. package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
  106. package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
  107. package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
  108. package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
  109. package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
  110. package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
  111. package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
  112. package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
  113. package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
  114. package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
  115. package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
  116. package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
  117. package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
  118. package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
  119. package/utils/css-animations/cards/card-clickable.html +0 -11
  120. package/utils/css-animations/defaults.html +0 -69
@@ -1,786 +0,0 @@
1
- export const init = () => {
2
- // Centralized cleanup tracking
3
- const cleanup = {
4
- observers: [],
5
- handlers: [],
6
- state: {
7
- focusTrapHandler: null,
8
- dropdownKeydownHandler: null,
9
- menuKeydownHandler: null
10
- }
11
- };
12
-
13
- const addObserver = (observer) => cleanup.observers.push(observer);
14
- const addHandler = (element, event, handler, options) => {
15
- element.addEventListener(event, handler, options);
16
- cleanup.handlers.push({ element, event, handler, options });
17
- };
18
-
19
- setupDynamicDropdowns(addObserver, addHandler);
20
- setupMenuButton(addHandler, addObserver);
21
- setupMenuARIA(addHandler, addObserver);
22
- setupMenuDisplayObserver(addObserver);
23
-
24
- return {
25
- result: "navbar initialized",
26
- destroy: () => {
27
- // Disconnect all observers
28
- cleanup.observers.forEach(obs => obs.disconnect());
29
- cleanup.observers.length = 0;
30
-
31
- // Remove all event listeners
32
- cleanup.handlers.forEach(({ element, event, handler, options }) => {
33
- element.removeEventListener(event, handler, options);
34
- });
35
- cleanup.handlers.length = 0;
36
-
37
- // Clean up focus trap
38
- if (cleanup.state.focusTrapHandler) {
39
- document.removeEventListener('keydown', cleanup.state.focusTrapHandler);
40
- cleanup.state.focusTrapHandler = null;
41
- }
42
-
43
- // Clean up global document listeners
44
- if (cleanup.state.dropdownKeydownHandler) {
45
- document.removeEventListener('keydown', cleanup.state.dropdownKeydownHandler);
46
- cleanup.state.dropdownKeydownHandler = null;
47
- }
48
-
49
- if (cleanup.state.menuKeydownHandler) {
50
- document.removeEventListener('keydown', cleanup.state.menuKeydownHandler);
51
- cleanup.state.menuKeydownHandler = null;
52
- }
53
-
54
- // Remove body overflow class if present
55
- document.body.classList.remove("u-overflow-hidden");
56
- }
57
- };
58
- };
59
-
60
-
61
- // Desktop dropdown system
62
- function setupDynamicDropdowns(addObserver, addHandler) {
63
- const dropdownWrappers = document.querySelectorAll(
64
- '[data-hs-nav="dropdown"]',
65
- );
66
- const allDropdowns = [];
67
-
68
- const closeAllDropdowns = (exceptWrapper = null) => {
69
- allDropdowns.forEach((dropdown) => {
70
- if (dropdown.wrapper !== exceptWrapper && dropdown.isOpen) {
71
- dropdown.closeDropdown();
72
- }
73
- });
74
- };
75
-
76
- dropdownWrappers.forEach((wrapper) => {
77
- const clickableElement = wrapper.querySelector('[data-site-clickable="element"]');
78
- const toggle = clickableElement ? clickableElement.children[0] : null;
79
- const dropdownList = wrapper.querySelector('[data-hs-nav="dropdown-list"]');
80
-
81
- if (!toggle || !dropdownList) {
82
- console.warn("Dropdown wrapper missing required elements:", wrapper);
83
- return;
84
- }
85
-
86
- const toggleText = toggle.textContent?.trim() || "dropdown";
87
- const sanitizedText = sanitizeForID(toggleText);
88
- const toggleId = `navbar-dropdown-${sanitizedText}-toggle`;
89
- const listId = `navbar-dropdown-${sanitizedText}-list`;
90
-
91
- toggle.id = toggleId;
92
- toggle.setAttribute("aria-haspopup", "menu");
93
- toggle.setAttribute("aria-expanded", "false");
94
- toggle.setAttribute("aria-controls", listId);
95
-
96
- dropdownList.id = listId;
97
- dropdownList.setAttribute("role", "menu");
98
- dropdownList.setAttribute("aria-hidden", "true");
99
-
100
- const clickableItems = dropdownList.querySelectorAll('[data-site-clickable="element"]');
101
- const menuItems = Array.from(clickableItems).map(el => el.children[0]).filter(Boolean);
102
- menuItems.forEach((item, index) => {
103
- item.setAttribute("role", "menuitem");
104
- item.setAttribute("tabindex", "-1");
105
-
106
- // Add context for first item to help screen readers understand dropdown content
107
- if (index === 0) {
108
- const toggleText = toggle.textContent?.trim() || "menu";
109
- const existingLabel = item.getAttribute("aria-label");
110
- if (!existingLabel) {
111
- item.setAttribute("aria-label", `${item.textContent?.trim()}, ${toggleText} submenu`);
112
- }
113
- }
114
- });
115
-
116
- let currentMenuItemIndex = -1;
117
-
118
- // Set initial ARIA states
119
- updateARIAStates();
120
-
121
- // Check if dropdown is open by looking for is-active class on wrapper
122
- function isDropdownOpen() {
123
- return wrapper.classList.contains('is-active');
124
- }
125
-
126
- // Update ARIA states based on current visual state
127
- function updateARIAStates() {
128
- const isOpen = isDropdownOpen();
129
- const wasOpen = toggle.getAttribute("aria-expanded") === "true";
130
-
131
- // If dropdown is closing (was open, now closed), focus the toggle first
132
- if (wasOpen && !isOpen && dropdownList.contains(document.activeElement)) {
133
- toggle.focus();
134
- }
135
-
136
- toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
137
- dropdownList.setAttribute("aria-hidden", isOpen ? "false" : "true");
138
- menuItems.forEach((item) => {
139
- item.setAttribute("tabindex", isOpen ? "0" : "-1");
140
- });
141
-
142
- if (!isOpen) {
143
- currentMenuItemIndex = -1;
144
- }
145
- }
146
-
147
- // Monitor for class changes and update ARIA states
148
- const observer = new MutationObserver(() => {
149
- updateARIAStates();
150
- });
151
-
152
- observer.observe(wrapper, {
153
- attributes: true,
154
- attributeFilter: ['class']
155
- });
156
-
157
- addObserver(observer);
158
-
159
- // Hover interactions now handled entirely by native Webflow
160
-
161
- // Add keyboard interactions that trigger programmatic mouse events
162
- const toggleKeydownHandler = function (e) {
163
- if (e.key === "ArrowDown") {
164
- e.preventDefault();
165
-
166
- if (!isDropdownOpen()) {
167
- // Trigger programmatic mouseenter to open dropdown
168
- const mouseEnterEvent = new MouseEvent("mouseenter", {
169
- bubbles: false,
170
- cancelable: false,
171
- view: window,
172
- relatedTarget: null
173
- });
174
- wrapper.dispatchEvent(mouseEnterEvent);
175
-
176
- // Focus first menu item after a brief delay
177
- if (menuItems.length > 0) {
178
- setTimeout(() => {
179
- currentMenuItemIndex = 0;
180
- menuItems[0].focus();
181
- }, 100);
182
- }
183
- }
184
- } else if (e.key === " ") {
185
- e.preventDefault();
186
-
187
- if (!isDropdownOpen()) {
188
- // Trigger programmatic mouseenter to open dropdown
189
- const mouseEnterEvent = new MouseEvent("mouseenter", {
190
- bubbles: false,
191
- cancelable: false,
192
- view: window,
193
- relatedTarget: null
194
- });
195
- wrapper.dispatchEvent(mouseEnterEvent);
196
-
197
- // Focus first menu item after a brief delay
198
- if (menuItems.length > 0) {
199
- setTimeout(() => {
200
- currentMenuItemIndex = 0;
201
- menuItems[0].focus();
202
- }, 100);
203
- }
204
- } else {
205
- // Trigger mouse leave to close dropdown
206
- const mouseOutEvent = new MouseEvent("mouseout", {
207
- bubbles: true,
208
- cancelable: false,
209
- view: window,
210
- relatedTarget: document.body
211
- });
212
- wrapper.dispatchEvent(mouseOutEvent);
213
-
214
- const mouseLeaveEvent = new MouseEvent("mouseleave", {
215
- bubbles: false,
216
- cancelable: false,
217
- view: window,
218
- relatedTarget: document.body
219
- });
220
- wrapper.dispatchEvent(mouseLeaveEvent);
221
- }
222
- } else if (e.key === "ArrowUp") {
223
- e.preventDefault();
224
-
225
- if (!isDropdownOpen()) {
226
- // Trigger programmatic mouseenter to open dropdown
227
- const mouseEnterEvent = new MouseEvent("mouseenter", {
228
- bubbles: false,
229
- cancelable: false,
230
- view: window,
231
- relatedTarget: null
232
- });
233
- wrapper.dispatchEvent(mouseEnterEvent);
234
-
235
- // Focus last menu item after a brief delay
236
- if (menuItems.length > 0) {
237
- setTimeout(() => {
238
- currentMenuItemIndex = menuItems.length - 1;
239
- menuItems[currentMenuItemIndex].focus();
240
- }, 100);
241
- }
242
- }
243
- } else if (e.key === "Escape") {
244
- e.preventDefault();
245
-
246
- if (isDropdownOpen()) {
247
- // Trigger mouse leave to close dropdown
248
- const mouseOutEvent = new MouseEvent("mouseout", {
249
- bubbles: true,
250
- cancelable: false,
251
- view: window,
252
- relatedTarget: document.body
253
- });
254
- wrapper.dispatchEvent(mouseOutEvent);
255
-
256
- const mouseLeaveEvent = new MouseEvent("mouseleave", {
257
- bubbles: false,
258
- cancelable: false,
259
- view: window,
260
- relatedTarget: document.body
261
- });
262
- wrapper.dispatchEvent(mouseLeaveEvent);
263
- }
264
- }
265
- };
266
-
267
- addHandler(toggle, "keydown", toggleKeydownHandler);
268
-
269
- // Handle navigation within open dropdown
270
- const dropdownKeydownHandler = function (e) {
271
- if (!isDropdownOpen()) return;
272
- if (!wrapper.contains(document.activeElement)) return;
273
-
274
- if (e.key === "ArrowDown") {
275
- e.preventDefault();
276
- if (currentMenuItemIndex < menuItems.length - 1) {
277
- currentMenuItemIndex++;
278
- menuItems[currentMenuItemIndex].focus();
279
- }
280
- } else if (e.key === "ArrowUp") {
281
- e.preventDefault();
282
- if (currentMenuItemIndex === 0) {
283
- // On first item, trigger mouse leave to close dropdown
284
- const mouseOutEvent = new MouseEvent("mouseout", {
285
- bubbles: true,
286
- cancelable: false,
287
- view: window,
288
- relatedTarget: document.body
289
- });
290
- wrapper.dispatchEvent(mouseOutEvent);
291
-
292
- const mouseLeaveEvent = new MouseEvent("mouseleave", {
293
- bubbles: false,
294
- cancelable: false,
295
- view: window,
296
- relatedTarget: document.body
297
- });
298
- wrapper.dispatchEvent(mouseLeaveEvent);
299
-
300
- // Focus back to toggle
301
- toggle.focus();
302
- currentMenuItemIndex = -1;
303
- } else if (currentMenuItemIndex > 0) {
304
- currentMenuItemIndex--;
305
- menuItems[currentMenuItemIndex].focus();
306
- } else {
307
- // Go back to toggle
308
- toggle.focus();
309
- currentMenuItemIndex = -1;
310
- }
311
- } else if (e.key === "Escape") {
312
- e.preventDefault();
313
-
314
- // Trigger mouse leave to close dropdown
315
- const mouseOutEvent = new MouseEvent("mouseout", {
316
- bubbles: true,
317
- cancelable: false,
318
- view: window,
319
- relatedTarget: document.body
320
- });
321
- wrapper.dispatchEvent(mouseOutEvent);
322
-
323
- const mouseLeaveEvent = new MouseEvent("mouseleave", {
324
- bubbles: false,
325
- cancelable: false,
326
- view: window,
327
- relatedTarget: document.body
328
- });
329
- wrapper.dispatchEvent(mouseLeaveEvent);
330
- }
331
- };
332
-
333
- addHandler(document, "keydown", dropdownKeydownHandler);
334
-
335
- allDropdowns.push({
336
- wrapper,
337
- isOpen: isDropdownOpen,
338
- closeDropdown: () => {
339
- // closeDropdown now handled by native Webflow interactions
340
- // This is kept for API compatibility but does nothing
341
- },
342
- toggle,
343
- dropdownList,
344
- });
345
- });
346
-
347
- const focusinHandler = function (e) {
348
- allDropdowns.forEach((dropdown) => {
349
- if (dropdown.isOpen() && !dropdown.wrapper.contains(e.target)) {
350
- dropdown.closeDropdown();
351
- }
352
- });
353
- };
354
-
355
- addHandler(document, "focusin", focusinHandler);
356
-
357
- addDesktopArrowNavigation(addHandler);
358
- }
359
-
360
- // Desktop left/right arrow navigation
361
- function addDesktopArrowNavigation(addHandler) {
362
- const keydownHandler = function (e) {
363
- if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
364
-
365
- const menu = document.querySelector('[data-hs-nav="menu"]');
366
- if (menu && menu.contains(document.activeElement)) return;
367
-
368
- const navbar = document.querySelector('[data-hs-nav="wrapper"]');
369
-
370
- if (!navbar || !navbar.contains(document.activeElement)) return;
371
-
372
- const openDropdownList = navbar.querySelector(
373
- '[aria-hidden="false"][role="menu"]',
374
- );
375
- if (openDropdownList && openDropdownList.contains(document.activeElement))
376
- return;
377
-
378
- e.preventDefault();
379
-
380
- const clickableElements = navbar.querySelectorAll('[data-site-clickable="element"]');
381
- const allNavbarElements = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
382
- const focusableElements = Array.from(allNavbarElements).filter((el) => {
383
- if (el.getAttribute("tabindex") === "-1") return false;
384
-
385
- const isInDropdownList = el.closest('[role="menu"]');
386
- if (isInDropdownList) return false;
387
-
388
- const isInMenu = el.closest('[data-hs-nav="menu"]');
389
- if (isInMenu) return false;
390
-
391
- const isInSkipLink = el.closest('[data-hs-nav="skip-link"]');
392
- if (isInSkipLink) return false;
393
-
394
- const computedStyle = window.getComputedStyle(el);
395
- const isHidden =
396
- computedStyle.display === "none" ||
397
- computedStyle.visibility === "hidden" ||
398
- computedStyle.opacity === "0" ||
399
- el.offsetWidth === 0 ||
400
- el.offsetHeight === 0;
401
- if (isHidden) return false;
402
-
403
- let parent = el.parentElement;
404
- while (parent && parent !== navbar) {
405
- const parentStyle = window.getComputedStyle(parent);
406
- const parentHidden =
407
- parentStyle.display === "none" ||
408
- parentStyle.visibility === "hidden" ||
409
- parent.offsetWidth === 0 ||
410
- parent.offsetHeight === 0;
411
- if (parentHidden) return false;
412
- parent = parent.parentElement;
413
- }
414
-
415
- return true;
416
- });
417
-
418
- const currentIndex = focusableElements.indexOf(document.activeElement);
419
- if (currentIndex === -1) return;
420
-
421
- if (e.key === "ArrowRight") {
422
- if (currentIndex < focusableElements.length - 1) {
423
- const nextIndex = currentIndex + 1;
424
- focusableElements[nextIndex].focus();
425
- }
426
- } else {
427
- if (currentIndex > 0) {
428
- const nextIndex = currentIndex - 1;
429
- focusableElements[nextIndex].focus();
430
- }
431
- }
432
- };
433
-
434
- addHandler(document, "keydown", keydownHandler);
435
- }
436
-
437
- // Menu button system with modal-like functionality
438
- function setupMenuButton(addHandler, addObserver) {
439
- const menuButtons = document.querySelectorAll('[data-hs-nav="menu-button"]');
440
- const menu = document.querySelector('[data-hs-nav="menu"]');
441
-
442
- if (!menuButtons.length || !menu) return;
443
-
444
- const menuId = `menu-${Date.now()}`;
445
-
446
- menu.id = menuId;
447
- menu.setAttribute("role", "dialog");
448
- menu.setAttribute("aria-modal", "true");
449
-
450
- function shouldPreventMenu() {
451
- const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
452
- if (!menuHideElement) return false;
453
-
454
- const computedStyle = window.getComputedStyle(menuHideElement);
455
- return computedStyle.display === "none";
456
- }
457
-
458
- function createFocusTrap(menuButton) {
459
- const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]');
460
-
461
- if (!navbarWrapper) return;
462
-
463
- const focusTrapHandler = (e) => {
464
- if (e.key === 'Tab') {
465
- const clickableElements = navbarWrapper.querySelectorAll('[data-site-clickable="element"]');
466
- const clickableItems = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
467
- const formElements = navbarWrapper.querySelectorAll('input, select, textarea, [tabindex]:not([tabindex="-1"])');
468
- const focusableArray = [...clickableItems, ...Array.from(formElements)];
469
- const firstElement = focusableArray[0];
470
- const lastElement = focusableArray[focusableArray.length - 1];
471
-
472
- if (e.shiftKey) {
473
- // Shift+Tab: moving backwards
474
- if (document.activeElement === firstElement) {
475
- e.preventDefault();
476
- lastElement.focus();
477
- }
478
- } else {
479
- // Tab: moving forwards
480
- if (document.activeElement === lastElement) {
481
- e.preventDefault();
482
- firstElement.focus();
483
- }
484
- }
485
- }
486
- };
487
-
488
- document.addEventListener('keydown', focusTrapHandler);
489
- return focusTrapHandler;
490
- }
491
-
492
- function removeFocusTrap(focusTrapHandler) {
493
- if (focusTrapHandler) {
494
- document.removeEventListener('keydown', focusTrapHandler);
495
- }
496
- }
497
-
498
- menuButtons.forEach(menuButton => {
499
- menuButton.setAttribute("aria-expanded", "false");
500
- menuButton.setAttribute("aria-controls", menuId);
501
- menuButton.setAttribute("aria-label", "Open navigation menu");
502
-
503
- let focusTrapHandler = null;
504
-
505
- // Check if menu is open by looking for is-active class on button
506
- function isMenuOpen() {
507
- return menuButton.classList.contains('is-active');
508
- }
509
-
510
- // Update ARIA states and menu behavior based on current visual state
511
- function updateMenuState() {
512
- const isOpen = isMenuOpen();
513
- const wasOpen = menuButton.getAttribute("aria-expanded") === "true";
514
-
515
- if (isOpen && !wasOpen) {
516
- // Opening
517
- document.body.classList.add("u-overflow-hidden");
518
- window.lenis?.stop();
519
-
520
- menuButtons.forEach(btn => {
521
- btn.setAttribute("aria-expanded", "true");
522
- btn.setAttribute("aria-label", "Close navigation menu");
523
- });
524
-
525
- // Create focus trap for navbar
526
- focusTrapHandler = createFocusTrap(menuButton);
527
-
528
- // Focus first menu item after menu opens
529
- setTimeout(() => {
530
- const firstClickable = menu.querySelector('[data-site-clickable="element"]');
531
- const firstElement = firstClickable?.children[0];
532
- if (firstElement) {
533
- firstElement.focus();
534
- }
535
- }, 100);
536
- } else if (!isOpen && wasOpen) {
537
- // Closing
538
- document.body.classList.remove("u-overflow-hidden");
539
- window.lenis?.start();
540
-
541
- // Return focus to button if focus was inside menu
542
- if (menu.contains(document.activeElement)) {
543
- menuButton.focus();
544
- }
545
-
546
- menuButtons.forEach(btn => {
547
- btn.setAttribute("aria-expanded", "false");
548
- btn.setAttribute("aria-label", "Open navigation menu");
549
- });
550
-
551
- // Remove focus trap
552
- removeFocusTrap(focusTrapHandler);
553
- focusTrapHandler = null;
554
- }
555
- }
556
-
557
- // Set initial ARIA states
558
- updateMenuState();
559
-
560
- // Monitor for class changes and update menu state
561
- const observer = new MutationObserver(() => {
562
- updateMenuState();
563
- });
564
-
565
- observer.observe(menuButton, {
566
- attributes: true,
567
- attributeFilter: ['class']
568
- });
569
-
570
- addObserver(observer);
571
-
572
- const clickHandler = function () {
573
- // Webflow interaction handles the visual state (is-active class)
574
- // MutationObserver will sync ARIA and behavior
575
- };
576
-
577
- addHandler(menuButton, "click", clickHandler);
578
- });
579
- }
580
-
581
-
582
- function setupMenuDisplayObserver(addObserver) {
583
- function handleDisplayChange() {
584
- const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
585
- if (!menuHideElement) return;
586
-
587
- const computedStyle = window.getComputedStyle(menuHideElement);
588
- const isMenuVisible = computedStyle.display !== "none";
589
-
590
- // Get menu button to check if menu is open
591
- const menuButton = document.querySelector('[data-hs-nav="menu-button"]');
592
- const isMenuOpen = menuButton && menuButton.getAttribute("aria-expanded") === "true";
593
-
594
- const shouldShowModal = isMenuVisible && isMenuOpen;
595
-
596
- // Toggle modal effects only when menu is visible AND menu is open
597
- document.body.classList.toggle("u-overflow-hidden", shouldShowModal);
598
- }
599
-
600
- const displayObserver = new ResizeObserver(handleDisplayChange);
601
- const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
602
- if (menuHideElement) {
603
- displayObserver.observe(menuHideElement);
604
- addObserver(displayObserver);
605
- // Initial check
606
- handleDisplayChange();
607
- }
608
- }
609
-
610
- function sanitizeForID(text) {
611
- return text
612
- .toLowerCase()
613
- .replace(/[^a-z0-9\s]/g, "")
614
- .replace(/\s+/g, "-")
615
- .replace(/^-+|-+$/g, "")
616
- .substring(0, 50);
617
- }
618
-
619
- // Menu ARIA setup
620
- function setupMenuARIA(addHandler, addObserver) {
621
- const menuContainer = document.querySelector('[data-hs-nav="menu"]');
622
- if (!menuContainer) return;
623
-
624
- const dropdownWrappers = menuContainer.querySelectorAll('[data-hs-nav="menu-dropdown"]');
625
- const clickableLinks = menuContainer.querySelectorAll('[data-site-clickable="element"]');
626
- const links = Array.from(clickableLinks).map(el => el.children[0]).filter(Boolean);
627
-
628
- dropdownWrappers.forEach((wrapper) => {
629
- const clickableElement = wrapper.querySelector('[data-site-clickable="element"]');
630
- const button = clickableElement ? clickableElement.children[0] : null;
631
- const dropdownList = wrapper.querySelector('[data-hs-nav="menu-dropdown-list"]');
632
-
633
- if (button && dropdownList) {
634
- const buttonText = button.textContent?.trim();
635
- const sanitizedText = sanitizeForID(buttonText);
636
- const buttonId = `navbar-menu-${sanitizedText}-toggle`;
637
- const listId = `navbar-menu-${sanitizedText}-list`;
638
-
639
- button.id = buttonId;
640
- button.setAttribute("aria-expanded", "false");
641
- button.setAttribute("aria-controls", listId);
642
-
643
- dropdownList.id = listId;
644
- dropdownList.setAttribute("aria-hidden", "true");
645
-
646
- // Check if dropdown is open by looking for is-active class on wrapper
647
- function isDropdownOpen() {
648
- return wrapper.classList.contains('is-active');
649
- }
650
-
651
- // Update ARIA states based on current visual state
652
- function updateARIAStates() {
653
- const isOpen = isDropdownOpen();
654
- const wasOpen = button.getAttribute("aria-expanded") === "true";
655
-
656
- // If dropdown is closing (was open, now closed), focus the button if focus is inside dropdown
657
- if (wasOpen && !isOpen && dropdownList.contains(document.activeElement)) {
658
- button.focus();
659
- }
660
-
661
- button.setAttribute("aria-expanded", isOpen ? "true" : "false");
662
- dropdownList.setAttribute("aria-hidden", isOpen ? "false" : "true");
663
- }
664
-
665
- // Set initial ARIA states
666
- updateARIAStates();
667
-
668
- // Monitor for class changes and update ARIA states
669
- const observer = new MutationObserver(() => {
670
- updateARIAStates();
671
- });
672
-
673
- observer.observe(wrapper, {
674
- attributes: true,
675
- attributeFilter: ['class']
676
- });
677
-
678
- addObserver(observer);
679
-
680
- const clickHandler = function () {
681
- // Webflow interaction handles the visual state (is-active class)
682
- // MutationObserver will sync ARIA attributes
683
- };
684
-
685
- addHandler(button, "click", clickHandler);
686
- }
687
- });
688
-
689
- links.forEach((link) => {
690
- const linkText = link.textContent?.trim();
691
- const sanitizedText = sanitizeForID(linkText);
692
- const linkId = `navbar-menu-${sanitizedText}-link`;
693
- link.id = linkId;
694
- });
695
-
696
- setupMenuArrowNavigation(menuContainer, addHandler);
697
- }
698
-
699
- // Menu arrow navigation
700
- function setupMenuArrowNavigation(menuContainer, addHandler) {
701
- function getFocusableElements() {
702
- const clickableElements = menuContainer.querySelectorAll('[data-site-clickable="element"]');
703
- const allElements = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
704
- return Array.from(allElements).filter((el) => {
705
- // Check if element or any ancestor has aria-hidden="true"
706
- let current = el;
707
- while (current && current !== menuContainer) {
708
- if (current.getAttribute("aria-hidden") === "true") {
709
- return false;
710
- }
711
- current = current.parentElement;
712
- }
713
- return true;
714
- });
715
- }
716
-
717
- let currentFocusIndex = -1;
718
-
719
- const keydownHandler = function (e) {
720
- const focusableElements = getFocusableElements();
721
- if (focusableElements.length === 0) return;
722
-
723
- const activeElement = document.activeElement;
724
- currentFocusIndex = focusableElements.indexOf(activeElement);
725
-
726
- if (e.key === "ArrowDown") {
727
- e.preventDefault();
728
- if (currentFocusIndex < focusableElements.length - 1) {
729
- currentFocusIndex = currentFocusIndex + 1;
730
- focusableElements[currentFocusIndex].focus();
731
- }
732
- } else if (e.key === "ArrowUp") {
733
- e.preventDefault();
734
- if (currentFocusIndex > 0) {
735
- currentFocusIndex = currentFocusIndex - 1;
736
- focusableElements[currentFocusIndex].focus();
737
- }
738
- } else if (e.key === "ArrowRight") {
739
- e.preventDefault();
740
- if (
741
- activeElement.tagName === "BUTTON" &&
742
- activeElement.hasAttribute("aria-controls")
743
- ) {
744
- const isExpanded =
745
- activeElement.getAttribute("aria-expanded") === "true";
746
- if (!isExpanded) {
747
- activeElement.click();
748
- }
749
- return;
750
- }
751
- } else if (e.key === "ArrowLeft") {
752
- e.preventDefault();
753
- if (
754
- activeElement.tagName === "BUTTON" &&
755
- activeElement.hasAttribute("aria-controls")
756
- ) {
757
- const isExpanded =
758
- activeElement.getAttribute("aria-expanded") === "true";
759
- if (isExpanded) {
760
- activeElement.click();
761
- }
762
- return;
763
- }
764
- } else if (e.key === "Home") {
765
- e.preventDefault();
766
- currentFocusIndex = 0;
767
- focusableElements[0].focus();
768
- } else if (e.key === "End") {
769
- e.preventDefault();
770
- currentFocusIndex = focusableElements.length - 1;
771
- focusableElements[focusableElements.length - 1].focus();
772
- } else if (e.key === " " && activeElement.tagName === "A") {
773
- e.preventDefault();
774
- } else if (e.key === "Escape") {
775
- const menuButton = document.querySelector(
776
- '[data-hs-nav="menu-button"]',
777
- );
778
- if (menuButton) {
779
- menuButton.click();
780
- menuButton.focus();
781
- }
782
- }
783
- };
784
-
785
- addHandler(menuContainer, "keydown", keydownHandler);
786
- }