@hortonstudio/main 1.9.10 → 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 (124) 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/src/modules/structure/functions/pagination/README.md +527 -0
  50. package/src/modules/structure/functions/pagination/pagination.ts +493 -0
  51. package/src/modules/structure/functions/site-settings/README.md +395 -0
  52. package/src/modules/structure/functions/site-settings/site-settings.ts +158 -0
  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/pagination/README.md +0 -428
  82. package/autoInit/accessibility/functions/pagination/pagination.js +0 -359
  83. package/autoInit/accessibility/functions/text-synchronization/README.md +0 -62
  84. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
  85. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
  86. package/autoInit/button/README.md +0 -122
  87. package/autoInit/button/button.js +0 -51
  88. package/autoInit/counter/README.md +0 -274
  89. package/autoInit/counter/counter.js +0 -185
  90. package/autoInit/form/README.md +0 -338
  91. package/autoInit/form/form.js +0 -374
  92. package/autoInit/navbar/README.md +0 -366
  93. package/autoInit/navbar/navbar.js +0 -786
  94. package/autoInit/site-settings/README.md +0 -218
  95. package/autoInit/site-settings/site-settings.js +0 -134
  96. package/autoInit/transition/transition.js +0 -116
  97. package/index.js +0 -305
  98. package/utils/before-after/README.md +0 -520
  99. package/utils/before-after/before-after.js +0 -653
  100. package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
  101. package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
  102. package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
  103. package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
  104. package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
  105. package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
  106. package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
  107. package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
  108. package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
  109. package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
  110. package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
  111. package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
  112. package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
  113. package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
  114. package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
  115. package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
  116. package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
  117. package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
  118. package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
  119. package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
  120. package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
  121. package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
  122. package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
  123. package/utils/css-animations/cards/card-clickable.html +0 -11
  124. package/utils/css-animations/defaults.html +0 -69
@@ -0,0 +1,393 @@
1
+ # **Navbar System Documentation**
2
+
3
+ ## **Webflow IX3 Notes**
4
+
5
+ When an element only has a click toggle/reverse trigger, it also works with enter/space. If it has click toggle/reverse AND hover triggers, it doesn't work with space but it does with enter.
6
+
7
+ ---
8
+
9
+ # **Navbar Setup Guide**
10
+
11
+ ## **Overview**
12
+
13
+ The navbar system uses Webflow IX3 interactions to manage visual states (`.is-active` class) while JavaScript automatically syncs ARIA attributes, keyboard navigation, and focus management.
14
+
15
+ ## **Required Structure**
16
+
17
+ ### **Navbar Setup**
18
+
19
+ Add **data-hs-nav="wrapper"** to the main wrapper of the navbar, e.g. the `<nav>` element.
20
+
21
+ **What it does:** Container for all navbar functionality. Used for arrow key navigation scope.
22
+
23
+ ---
24
+
25
+ ## **Navbar Dropdown List**
26
+
27
+ ### **Required Elements**
28
+
29
+ **Button Wrapper** _(wraps the button and dropdown list)_
30
+
31
+ - data-hs-nav="dropdown"
32
+
33
+ **Toggle Button** _(the wrapper of the actual button/link that opens/closes the dropdown list)_
34
+
35
+ - data-site-clickable="element"
36
+ - The Global / Clickable component already has this set
37
+
38
+ **Dropdown List** _(main wrapper of the list)_
39
+
40
+ - data-hs-nav="dropdown-list"
41
+
42
+ **Dropdown List Items**
43
+
44
+ - data-site-clickable="element"
45
+ - If using webflow framework, this is auto set with Global / Button and their respective Global / Clickable
46
+
47
+ **Typical element layout:**
48
+
49
+ 1. Button Wrapper (data-hs-nav="dropdown")
50
+ 1. Button Content
51
+ 1. Clickable Component (data-site-clickable="element")
52
+ 1. Toggle Button
53
+ 2. Dropdown List Component Slot
54
+ 1. Dropdown List
55
+ 1. Dropdown list items
56
+
57
+ ### **Interaction Requirements (IX3)**
58
+
59
+ **Triggers:**
60
+
61
+ 1. **Hover Trigger** on `[data-hs-nav="dropdown"]`
62
+ 1. `Mouse Enter -> Each Mouse Enter: Play`
63
+ 2. **Hover Trigger** on `[data-hs-nav="dropdown"]`
64
+ 1. `Mouse Leave -> Each Mouse Leave: Reverse`
65
+ 3. **Click Trigger** on `[data-hs-nav="dropdown"]`
66
+ 1. `Each click: Toggle play/reverse`
67
+ 4. **Add class** `.is-active` to wrapper on hover in
68
+ 5. **Remove class** `.is-active` on hover out
69
+
70
+ **Timeline requirements:**
71
+
72
+ 1. 0s: Toggle is-active class on the trigger element
73
+ 2. 0s: From: Display: none | To: Display: flex on the element you will be animating to open
74
+ 3. .06s: Rest of animation
75
+
76
+ ### **What It Does**
77
+
78
+ 1. **Keyboard navigation:** ArrowDown/ArrowUp/Space open dropdown, Escape closes
79
+ 2. **Focus management:** Auto-focuses first item on open, returns focus to toggle on close
80
+ 3. **ARIA sync:** Watches `.is-active` class, updates `aria-expanded` and `aria-hidden`
81
+ 4. **Arrow navigation:** Navigate items with ArrowDown/ArrowUp when open
82
+
83
+ ### **Notes**
84
+
85
+ 1. Using Global / Button is preferred for this system. For attribute adding, use this component props:
86
+ 1. Attribute 1 Name: set to “data-hs-nav”
87
+ 2. Attribute 1 Value: set to “dropdown”
88
+ 2. `[data-site-clickable="element"]` Is set up by default in the Global / Clickable component
89
+ 1. Toggle is the **first child** of `[data-site-clickable="element"]`
90
+ 2. Items must use same `[data-site-clickable="element"]` pattern
91
+ 3. Enter key navigates the toggle link (default behavior)
92
+
93
+ ---
94
+
95
+ ## **Mobile Menu**
96
+
97
+ ### **Required Elements**
98
+
99
+ **Menu Button** _(the wrapper of the actual button)_
100
+
101
+ - data-site-clickable="element"
102
+ - This one does not use the Global / Clickable component
103
+ - This is not required for the actual menu functionality, but for the arrow navigation throughout the Navbar
104
+
105
+ **Menu Button Element** _(the actual button)_
106
+
107
+ - data-hs-nav="menu-button"
108
+
109
+ **Menu** _(main wrapper of the menu)_
110
+
111
+ - data-hs-nav="menu"
112
+
113
+ **Menu Items**
114
+
115
+ - data-site-clickable="element"
116
+ - If using webflow framework, this is auto set with Global / Button and their respective Global / Clickable
117
+
118
+ **Breakpoint Indicator**
119
+
120
+ - This element wraps the Menu (main wrapper)
121
+ - Requires: data-hs-nav="menu-hide"
122
+
123
+ **Typical element layout:**
124
+
125
+ 1. Navbar Component (data-hs-nav=”wrapper”)
126
+ 1. Announce Bar
127
+ 1. Buttons
128
+ 2. Navbar Wrap
129
+ 1. Navbar Container
130
+ 1. Buttons
131
+ 2. Menu Button Wrapper (data-site-clickable="element")
132
+ 1. Menu Button (data-hs-nav="menu-button")
133
+ 2. Menu Hide (data-hs-nav=”menu-hide”)
134
+ 1. Menu (data-hs-nav="menu")
135
+ 1. Menu Items (elements with data-site-clickable="element")
136
+ 1. Typically Global / Button components, auto set attributes
137
+
138
+ ### **Interaction Requirements (IX3)**
139
+
140
+ #### Triggers:
141
+
142
+ 1. **Click trigger** on `[data-hs-nav="menu-button"]`
143
+ 1. `Toggle play/reverse`
144
+
145
+ #### Timeline:
146
+
147
+ 1. 0s: Toggle is-active class on the trigger element
148
+ 2. 0s: From: Display: none | To: Display: flex on the element you will be animating to open
149
+ 3. .06s: Rest of animation
150
+
151
+ ### **What It Does**
152
+
153
+ 1. **ARIA sync:** Watches `.is-active` on button, updates `aria-expanded`
154
+ 2. **Body overflow:** Adds `u-overflow-hidden` class to body when open
155
+ 1. If the site uses Lenis scrolling, this code triggers start and top so it works with lenis properly.
156
+ 3. **Focus trap:** Traps tab key within navbar when menu open
157
+ 4. **Auto-focus:** Focuses first menu item on open
158
+ 5. **Focus return:** Returns focus to button on close
159
+ 6. **Arrow navigation:** ArrowDown/ArrowUp navigate through all visible items
160
+
161
+ ### **Breakpoint Indicator (`menu-hide`)**
162
+
163
+ **CSS pattern (typically add data-responsive=”desktop/mobile” to the menu-hide element):**
164
+
165
+ /\* Mobile styles (default) \*/
166
+
167
+ \[data-responsive="desktop"\] { display: none\!important; }
168
+
169
+ \[data-responsive="mobile"\] { display: flex\!important; }
170
+
171
+ /\* Desktop styles \*/
172
+
173
+ @media (width \> 63em) {
174
+
175
+ \[data-responsive="desktop"\] { display: flex\!important; }
176
+
177
+ \[data-responsive="mobile"\] { display: none\!important; }
178
+
179
+ }
180
+
181
+ **What it does:** JavaScript checks if this element is visible to enable/disable menu button functionality at different breakpoints.
182
+
183
+ ---
184
+
185
+ ## **Mobile Menu Dropdown**
186
+
187
+ ### **Required Elements**
188
+
189
+ **Dropdown Wrapper**
190
+
191
+ - data-hs-nav="menu-dropdown"
192
+
193
+ **Dropdown Toggle Button** _(the wrapper of the actual button)_
194
+
195
+ - data-site-clickable="element"
196
+ - The Global / Clickable component already has this set
197
+
198
+ **Dropdown List** _(main wrapper of the list)_
199
+
200
+ - data-hs-nav="menu-dropdown-list"
201
+
202
+ **Dropdown List Items**
203
+
204
+ - data-site-clickable="element"
205
+ - If using webflow framework, this is auto set with Global / Button and their respective Global / Clickable
206
+
207
+ **Typical element layout:**
208
+
209
+ 1. Dropdown Wrapper (data-hs-nav="menu-dropdown")
210
+ 1. Button Wrapper (data-site-clickable="element")
211
+ 1. Toggle Button
212
+ 2. Dropdown List (data-hs-nav="menu-dropdown-list")
213
+ 1. List Items (data-site-clickable="element")
214
+ 1. Links or Buttons
215
+
216
+ ### **Interaction Requirements (IX3)**
217
+
218
+ #### Triggers:
219
+
220
+ 1. **Click trigger** on Dropdown Wrapper `[data-hs-nav="menu-dropdown"]`
221
+ 1. `Toggle play/reverse`
222
+
223
+ #### Timeline:
224
+
225
+ 1. 0s: Toggle is-active class on the trigger element
226
+ 2. 0s: From: Display: none | To: Display: flex on the element you will be animating to open
227
+ 3. .06s: Rest of animation
228
+
229
+ ### **What It Does**
230
+
231
+ 1. **ARIA sync:** Watches `.is-active` on wrapper, updates `aria-expanded` and `aria-hidden`
232
+ 2. **Focus return:** Returns focus to button when closing (if focus was inside list)
233
+ 3. **Arrow integration:** Items automatically included in menu arrow navigation when open (filtered when closed via `aria-hidden`)
234
+
235
+ ### **Notes**
236
+
237
+ 1. Using Global / Button is preferred for this system. For attribute adding, use this component props:
238
+ 1. Attribute 1 Name: set to “data-hs-nav”
239
+ 2. Attribute 1 Value: set to “menu-dropdown”
240
+ 2. Items inside closed dropdown lists are filtered from arrow navigation
241
+ 3. Uses same `[data-site-clickable="element"]` pattern as desktop
242
+ 4. Button is **first child** of clickable wrapper
243
+
244
+ ---
245
+
246
+ ## **Skip Link (Optional)**
247
+
248
+ **What it does:** When using a skip link, add these attributes so the system ignores it for arrow navigation.
249
+
250
+ ### **Required Elements**
251
+
252
+ **Skip Link Wrapper**
253
+
254
+ - data-hs-nav="skip-link"
255
+
256
+ ---
257
+
258
+ ## **Clickable Element Pattern**
259
+
260
+ **All interactive elements** (buttons, links) must follow this pattern:
261
+
262
+ **For Links:**
263
+
264
+ 1. Clickable Wrapper (data-site-clickable="element")
265
+ 1. Link Element
266
+
267
+ **For Buttons:**
268
+
269
+ 1. Clickable Wrapper (data-site-clickable="element")
270
+ 1. Button Element
271
+
272
+ **Why:** JavaScript queries `[data-site-clickable="element"]` then gets `.children[0]` to find the actual interactive element. This allows wrapping for styling while maintaining consistent selection.
273
+
274
+ This is for arrow navigation and proper accessibility labelling
275
+
276
+ ---
277
+
278
+ ## **Key Attributes Summary**
279
+
280
+ | Attribute | Purpose | Required On |
281
+ | ---------------------------------- | --------------------------- | ---------------------- |
282
+ | `data-hs-nav="wrapper"` | Navbar container | `<nav>` |
283
+ | `data-hs-nav="dropdown"` | Desktop dropdown wrapper | Wrapper div |
284
+ | `data-hs-nav="dropdown-list"` | Desktop dropdown content | Content div |
285
+ | `data-hs-nav="menu-button"` | Mobile menu toggle | `<button>` |
286
+ | `data-hs-nav="menu"` | Mobile menu container | Menu div |
287
+ | `data-hs-nav="menu-hide"` | Breakpoint indicator | Any element |
288
+ | `data-hs-nav="menu-dropdown"` | Mobile dropdown wrapper | Wrapper div |
289
+ | `data-hs-nav="menu-dropdown-list"` | Mobile dropdown content | Content div |
290
+ | `data-site-clickable="element"` | Interactive element wrapper | Wrapper div |
291
+ | `data-hs-nav="skip-link"` | Skip link container | Wrapper div (optional) |
292
+
293
+ ---
294
+
295
+ ## **Webflow Class Requirements**
296
+
297
+ ### **`.is-active` class**
298
+
299
+ Must be added/removed by Webflow interactions on:
300
+
301
+ 1. Desktop dropdown wrapper (`[data-hs-nav="dropdown"]`)
302
+ 2. Mobile menu button (`[data-hs-nav="menu-button"]`)
303
+ 3. Mobile menu dropdown wrapper (`[data-hs-nav="menu-dropdown"]`)
304
+
305
+ JavaScript watches for this class and syncs ARIA/behavior accordingly.
306
+
307
+ ### **`.u-overflow-hidden` class**
308
+
309
+ Applied to `<body>` by JavaScript when the menu is open. Style accordingly:
310
+
311
+ .u-overflow-hidden { overflow: hidden; }
312
+
313
+ ---
314
+
315
+ ## **Keyboard Navigation**
316
+
317
+ ### **Desktop Dropdown (when focused on toggle)**
318
+
319
+ 1. **ArrowDown** → Open dropdown, focus first item
320
+ 2. **ArrowUp** → Open dropdown, focus last item
321
+ 3. **Space** → Open/close dropdown
322
+ 4. **Escape** → Close dropdown
323
+ 5. **Enter** → Navigate to toggle link href
324
+
325
+ ### **Desktop Dropdown (when inside)**
326
+
327
+ 1. **ArrowDown** → Next item
328
+ 2. **ArrowUp** → Previous item (or close if on first item)
329
+ 3. **Escape** → Close dropdown, return focus to toggle
330
+
331
+ ### **Desktop Navbar**
332
+
333
+ 1. **ArrowLeft/ArrowRight** → Navigate between navbar elements
334
+
335
+ ### **Mobile Menu**
336
+
337
+ 1. **ArrowDown/ArrowUp** → Navigate through all visible items
338
+ 2. **ArrowLeft** → Collapse dropdown (when on dropdown button)
339
+ 3. **ArrowRight** → Expand dropdown (when on dropdown button)
340
+ 4. **Home/End** → First/last item
341
+ 5. **Escape** → Close menu
342
+
343
+ ---
344
+
345
+ ## **Barba.js / SPA Compatibility**
346
+
347
+ The navbar system is fully compatible with Barba.js and other SPA frameworks:
348
+
349
+ ### **v2.0.0 Improvements:**
350
+
351
+ - **setTimeout tracking and cleanup** - Properly clears all focus management timeouts on destroy (fixes edge case focus issues)
352
+ - **JSON config system** - Future-proof attribute management for easy rebranding
353
+ - **Enhanced SPA compatibility** - Perfect cleanup for Barba.js and other SPA frameworks
354
+
355
+ ### **On Destroy:**
356
+
357
+ 1. **Clears all pending timeouts** (4 focus management timeouts)
358
+ 2. Disconnects all MutationObservers and ResizeObservers
359
+ 3. Removes all event listeners (element-specific and document-level)
360
+ 4. Removes focus trap handler
361
+ 5. Clears body overflow class if present
362
+ 6. Resets all module state
363
+
364
+ ### **On Reinitialize:**
365
+
366
+ 1. Finds fresh navbar elements on new page
367
+ 2. Creates new observers and event handlers
368
+ 3. Applies all ARIA attributes to new elements
369
+ 4. Works like fresh page load on new DOM
370
+
371
+ ---
372
+
373
+ ## **Common Issues**
374
+
375
+ **Items not navigable with arrow keys:**
376
+
377
+ 1. Ensure items use `[data-site-clickable="element"]` wrapper
378
+ 2. Check that first child is the actual button/link
379
+
380
+ **Dropdown not syncing ARIA:**
381
+
382
+ 1. Verify Webflow interaction adds/removes `.is-active` class
383
+ 2. Check class is on correct element (wrapper for dropdowns, button for menu)
384
+
385
+ **Tab goes into hidden dropdowns:**
386
+
387
+ 1. Ensure Webflow interaction uses `display: none` or `visibility: hidden` when closed
388
+ 2. Or verify `.is-active` class is being removed properly
389
+
390
+ **Menu button doesn't work at desktop:**
391
+
392
+ 1. Check `[data-hs-nav="menu-hide"]` has correct responsive CSS
393
+ 2. Element should be `display: none` at desktop breakpoint
@@ -0,0 +1,183 @@
1
+ import { getSelector, globalConfig } from '@utils';
2
+
3
+ export function init(config) {
4
+ const cleanup = {
5
+ handlers: [],
6
+ };
7
+
8
+ const addHandler = (element, event, handler, options) => {
9
+ element.addEventListener(event, handler, options);
10
+ cleanup.handlers.push({ element, event, handler, options });
11
+ };
12
+
13
+ function setupDesktopArrowNavigation(addHandler) {
14
+ const keydownHandler = function (e) {
15
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
16
+
17
+ const menu = document.querySelector('[data-hs-nav-menu="wrapper"]');
18
+ if (menu && menu.contains(document.activeElement)) return;
19
+
20
+ const navbar = document.querySelector('[data-hs-nav="wrapper"]');
21
+
22
+ if (!navbar || !navbar.contains(document.activeElement)) return;
23
+
24
+ // Check if focus is inside an open dropdown (using .is-active class)
25
+ const openDropdown = navbar.querySelector(
26
+ `.${globalConfig.classes.active}[data-hs-nav-dropdown]`
27
+ );
28
+ if (openDropdown && openDropdown.contains(document.activeElement)) return;
29
+
30
+ e.preventDefault();
31
+
32
+ const clickableSelector = getSelector(globalConfig.clickable, 'button');
33
+ const allNavbarElements = Array.from(navbar.querySelectorAll(clickableSelector));
34
+ const focusableElements = Array.from(allNavbarElements).filter((el) => {
35
+ if (el.getAttribute('tabindex') === '-1') return false;
36
+
37
+ const isInDropdownList = el.closest('[role="menu"]');
38
+ if (isInDropdownList) return false;
39
+
40
+ const isInMenu = el.closest('[data-hs-nav-menu="wrapper"]');
41
+ if (isInMenu) return false;
42
+
43
+ const isInSkipLink = el.closest('[data-hs-nav="skip-link"]');
44
+ if (isInSkipLink) return false;
45
+
46
+ const computedStyle = window.getComputedStyle(el);
47
+ const isHidden =
48
+ computedStyle.display === 'none' ||
49
+ computedStyle.visibility === 'hidden' ||
50
+ computedStyle.opacity === '0' ||
51
+ el.offsetWidth === 0 ||
52
+ el.offsetHeight === 0;
53
+ if (isHidden) return false;
54
+
55
+ let parent = el.parentElement;
56
+ while (parent && parent !== navbar) {
57
+ const parentStyle = window.getComputedStyle(parent);
58
+ const parentHidden =
59
+ parentStyle.display === 'none' ||
60
+ parentStyle.visibility === 'hidden' ||
61
+ parent.offsetWidth === 0 ||
62
+ parent.offsetHeight === 0;
63
+ if (parentHidden) return false;
64
+ parent = parent.parentElement;
65
+ }
66
+
67
+ return true;
68
+ });
69
+
70
+ const currentIndex = focusableElements.indexOf(document.activeElement);
71
+ if (currentIndex === -1) return;
72
+
73
+ if (e.key === 'ArrowRight') {
74
+ if (currentIndex < focusableElements.length - 1) {
75
+ const nextIndex = currentIndex + 1;
76
+ focusableElements[nextIndex].focus();
77
+ }
78
+ } else {
79
+ if (currentIndex > 0) {
80
+ const nextIndex = currentIndex - 1;
81
+ focusableElements[nextIndex].focus();
82
+ }
83
+ }
84
+ };
85
+
86
+ addHandler(document, 'keydown', keydownHandler);
87
+ }
88
+
89
+ function setupMenuArrowNavigation(addHandler) {
90
+ const menuContainer = document.querySelector('[data-hs-nav-menu="wrapper"]');
91
+ if (!menuContainer) return;
92
+
93
+ function getFocusableElements() {
94
+ const menuClickableSelector = getSelector(globalConfig.clickable, 'button');
95
+ const allElements = Array.from(menuContainer.querySelectorAll(menuClickableSelector));
96
+ return Array.from(allElements).filter((el) => {
97
+ // Check if element or any ancestor is inert (inactive dropdown)
98
+ let current = el;
99
+ while (current && current !== menuContainer) {
100
+ if (current.inert) {
101
+ return false;
102
+ }
103
+ current = current.parentElement;
104
+ }
105
+ return true;
106
+ });
107
+ }
108
+
109
+ let currentFocusIndex = -1;
110
+
111
+ const keydownHandler = function (e) {
112
+ const focusableElements = getFocusableElements();
113
+ if (focusableElements.length === 0) return;
114
+
115
+ const activeElement = document.activeElement;
116
+ currentFocusIndex = focusableElements.indexOf(activeElement);
117
+
118
+ if (e.key === 'ArrowDown') {
119
+ e.preventDefault();
120
+ if (currentFocusIndex < focusableElements.length - 1) {
121
+ currentFocusIndex = currentFocusIndex + 1;
122
+ focusableElements[currentFocusIndex].focus();
123
+ }
124
+ } else if (e.key === 'ArrowUp') {
125
+ e.preventDefault();
126
+ if (currentFocusIndex > 0) {
127
+ currentFocusIndex = currentFocusIndex - 1;
128
+ focusableElements[currentFocusIndex].focus();
129
+ }
130
+ } else if (e.key === 'ArrowRight') {
131
+ e.preventDefault();
132
+ if (activeElement.tagName === 'BUTTON' && activeElement.hasAttribute('aria-controls')) {
133
+ const isExpanded = activeElement.getAttribute('aria-expanded') === 'true';
134
+ if (!isExpanded) {
135
+ activeElement.click();
136
+ }
137
+ return;
138
+ }
139
+ } else if (e.key === 'ArrowLeft') {
140
+ e.preventDefault();
141
+ if (activeElement.tagName === 'BUTTON' && activeElement.hasAttribute('aria-controls')) {
142
+ const isExpanded = activeElement.getAttribute('aria-expanded') === 'true';
143
+ if (isExpanded) {
144
+ activeElement.click();
145
+ }
146
+ return;
147
+ }
148
+ } else if (e.key === 'Home') {
149
+ e.preventDefault();
150
+ currentFocusIndex = 0;
151
+ focusableElements[0].focus();
152
+ } else if (e.key === 'End') {
153
+ e.preventDefault();
154
+ currentFocusIndex = focusableElements.length - 1;
155
+ focusableElements[focusableElements.length - 1].focus();
156
+ } else if (e.key === ' ' && activeElement.tagName === 'A') {
157
+ e.preventDefault();
158
+ } else if (e.key === 'Escape') {
159
+ const menuButton = document.querySelector('[data-hs-nav-menu="button"]');
160
+ if (menuButton) {
161
+ menuButton.click();
162
+ menuButton.focus();
163
+ }
164
+ }
165
+ };
166
+
167
+ addHandler(menuContainer, 'keydown', keydownHandler);
168
+ }
169
+
170
+ setupDesktopArrowNavigation(addHandler);
171
+ setupMenuArrowNavigation(addHandler);
172
+
173
+ return {
174
+ result: 'arrow-navigation initialized',
175
+ destroy: () => {
176
+ // Remove all event listeners
177
+ cleanup.handlers.forEach(({ element, event, handler, options }) => {
178
+ element.removeEventListener(event, handler, options);
179
+ });
180
+ cleanup.handlers.length = 0;
181
+ },
182
+ };
183
+ }