@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.
- package/.prettierrc +8 -0
- package/README.md +146 -0
- package/eslint.config.js +32 -0
- package/index.ts +275 -0
- package/package.json +19 -2
- package/public/bootstrap.js +16 -0
- package/src/animations/animations.ts +93 -0
- package/src/animations/functions/counter/counter.ts +137 -0
- package/src/config.json +570 -0
- package/src/config.ts +105 -0
- package/src/modules/default/README.md +167 -0
- package/src/modules/default/default.ts +71 -0
- package/{autoInit → src/modules/default/functions}/accessibility/README.md +44 -12
- package/src/modules/default/functions/accessibility/accessibility.ts +54 -0
- package/src/modules/default/functions/accordion/README.md +451 -0
- package/src/modules/default/functions/accordion/accordion.ts +189 -0
- package/src/modules/default/functions/comparison/comparison.ts +424 -0
- package/src/modules/default/functions/marquee/marquee.ts +206 -0
- package/src/modules/default/functions/navbar/README.md +393 -0
- package/src/modules/default/functions/navbar/functions/arrow-navigation/arrow-navigation.ts +183 -0
- package/src/modules/default/functions/navbar/functions/dropdown/dropdown.ts +313 -0
- package/src/modules/default/functions/navbar/functions/menu/menu.ts +315 -0
- package/src/modules/default/functions/navbar/navbar.ts +51 -0
- package/{autoInit → src/modules/default/functions}/smooth-scroll/README.md +45 -14
- package/{autoInit/smooth-scroll/smooth-scroll.js → src/modules/default/functions/smooth-scroll/smooth-scroll.ts} +33 -38
- package/{autoInit → src/modules/default/functions}/transition/README.md +59 -32
- package/src/modules/default/functions/transition/transition.ts +290 -0
- package/src/modules/normalize/README.md +172 -0
- package/src/modules/normalize/functions/clickable/README.md +84 -0
- package/src/modules/normalize/functions/clickable/clickable.ts +43 -0
- package/src/modules/normalize/functions/clickable/functions/normalize/README.md +213 -0
- package/src/modules/normalize/functions/clickable/functions/normalize/normalize.ts +68 -0
- package/src/modules/normalize/functions/dupe/README.md +405 -0
- package/src/modules/normalize/functions/dupe/dupe.ts +197 -0
- package/src/modules/normalize/functions/sync/sync.ts +378 -0
- package/src/modules/normalize/normalize.ts +58 -0
- package/src/modules/structure/README.md +190 -0
- package/src/modules/structure/functions/form/README.md +94 -0
- package/src/modules/structure/functions/form/form.ts +54 -0
- package/src/modules/structure/functions/form/functions/honeypot/README.md +77 -0
- package/src/modules/structure/functions/form/functions/honeypot/honeypot.ts +37 -0
- package/src/modules/structure/functions/form/functions/range/README.md +410 -0
- package/src/modules/structure/functions/form/functions/range/range.ts +92 -0
- package/src/modules/structure/functions/form/functions/select/README.md +393 -0
- package/src/modules/structure/functions/form/functions/select/functions/custom-select/custom-select.ts +637 -0
- package/src/modules/structure/functions/form/functions/select/functions/states/states.ts +118 -0
- package/src/modules/structure/functions/form/functions/select/select.ts +48 -0
- package/src/modules/structure/functions/form/functions/test/test.ts +132 -0
- package/src/modules/structure/functions/pagination/README.md +527 -0
- package/src/modules/structure/functions/pagination/pagination.ts +493 -0
- package/src/modules/structure/functions/site-settings/README.md +395 -0
- package/src/modules/structure/functions/site-settings/site-settings.ts +158 -0
- package/{autoInit/accessibility → src/modules/structure}/functions/toc/README.md +18 -15
- package/{autoInit/accessibility/functions/toc/toc.js → src/modules/structure/functions/toc/functions/heading-links/heading-links.ts} +43 -63
- package/src/modules/structure/functions/toc/functions/progress-bar/progress-bar.ts +101 -0
- package/src/modules/structure/functions/toc/toc.ts +35 -0
- package/{autoInit/accessibility → src/modules/structure}/functions/year-replacement/README.md +7 -6
- package/src/modules/structure/functions/year-replacement/year-replacement.ts +59 -0
- package/src/modules/structure/structure.ts +59 -0
- package/src/utils/attributeSelector.ts +78 -0
- package/src/utils/cssVariables.ts +24 -0
- package/src/utils/gsap.ts +198 -0
- package/src/utils/heightAnimator.ts +130 -0
- package/src/utils/modalManager.ts +150 -0
- package/src/utils.ts +54 -0
- package/tsconfig.json +24 -0
- package/vite.config.js +45 -0
- package/.claude/settings.local.json +0 -70
- package/archive/hero.js +0 -794
- package/archive/modal.js +0 -80
- package/archive/text.js +0 -628
- package/autoInit/accessibility/accessibility.js +0 -53
- package/autoInit/accessibility/functions/blog-remover/README.md +0 -61
- package/autoInit/accessibility/functions/blog-remover/blog-remover.js +0 -31
- package/autoInit/accessibility/functions/click-forwarding/README.md +0 -60
- package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +0 -82
- package/autoInit/accessibility/functions/dropdown/README.md +0 -212
- package/autoInit/accessibility/functions/dropdown/dropdown.js +0 -167
- package/autoInit/accessibility/functions/list-accessibility/README.md +0 -56
- package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +0 -23
- package/autoInit/accessibility/functions/pagination/README.md +0 -428
- package/autoInit/accessibility/functions/pagination/pagination.js +0 -359
- package/autoInit/accessibility/functions/text-synchronization/README.md +0 -62
- package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
- package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
- package/autoInit/button/README.md +0 -122
- package/autoInit/button/button.js +0 -51
- package/autoInit/counter/README.md +0 -274
- package/autoInit/counter/counter.js +0 -185
- package/autoInit/form/README.md +0 -338
- package/autoInit/form/form.js +0 -374
- package/autoInit/navbar/README.md +0 -366
- package/autoInit/navbar/navbar.js +0 -786
- package/autoInit/site-settings/README.md +0 -218
- package/autoInit/site-settings/site-settings.js +0 -134
- package/autoInit/transition/transition.js +0 -116
- package/index.js +0 -305
- package/utils/before-after/README.md +0 -520
- package/utils/before-after/before-after.js +0 -653
- package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
- package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
- package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
- package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
- package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
- package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
- package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
- package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
- package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
- package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
- package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
- package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
- package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
- package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
- package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
- package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
- package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
- package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
- package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
- package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
- package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
- package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
- package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
- package/utils/css-animations/cards/card-clickable.html +0 -11
- 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
|
+
}
|