@hortonstudio/main 1.9.6 → 1.9.7

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 (40) hide show
  1. package/autoInit/accessibility/README.md +126 -0
  2. package/autoInit/accessibility/accessibility.js +56 -0
  3. package/autoInit/accessibility/functions/blog-remover/README.md +61 -0
  4. package/autoInit/accessibility/functions/blog-remover/blog-remover.js +31 -0
  5. package/autoInit/accessibility/functions/click-forwarding/README.md +60 -0
  6. package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +82 -0
  7. package/autoInit/accessibility/functions/convert-to-span/README.md +59 -0
  8. package/autoInit/accessibility/functions/convert-to-span/convert-to-span.js +70 -0
  9. package/autoInit/accessibility/functions/custom-values-replacement/README.md +71 -0
  10. package/autoInit/accessibility/functions/custom-values-replacement/custom-values-replacement.js +102 -0
  11. package/autoInit/accessibility/functions/dropdown/README.md +212 -0
  12. package/autoInit/accessibility/functions/dropdown/dropdown.js +167 -0
  13. package/autoInit/accessibility/functions/list-accessibility/README.md +56 -0
  14. package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +23 -0
  15. package/autoInit/accessibility/functions/prevent-default/README.md +58 -0
  16. package/autoInit/accessibility/functions/prevent-default/prevent-default.js +58 -0
  17. package/autoInit/accessibility/functions/remove-list-accessibility/README.md +57 -0
  18. package/autoInit/accessibility/functions/remove-list-accessibility/remove-list-accessibility.js +68 -0
  19. package/autoInit/accessibility/functions/text-synchronization/README.md +62 -0
  20. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +101 -0
  21. package/autoInit/accessibility/functions/toc/README.md +79 -0
  22. package/autoInit/accessibility/functions/toc/toc.js +191 -0
  23. package/autoInit/accessibility/functions/year-replacement/README.md +54 -0
  24. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +43 -0
  25. package/autoInit/button/README.md +122 -0
  26. package/autoInit/counter/README.md +274 -0
  27. package/autoInit/{counter.js → counter/counter.js} +20 -5
  28. package/autoInit/form/README.md +338 -0
  29. package/autoInit/{form.js → form/form.js} +44 -29
  30. package/autoInit/navbar/README.md +366 -0
  31. package/autoInit/site-settings/README.md +218 -0
  32. package/autoInit/smooth-scroll/README.md +386 -0
  33. package/autoInit/transition/README.md +301 -0
  34. package/autoInit/{transition.js → transition/transition.js} +13 -2
  35. package/index.js +7 -7
  36. package/package.json +1 -1
  37. package/autoInit/accessibility.js +0 -786
  38. /package/autoInit/{button.js → button/button.js} +0 -0
  39. /package/autoInit/{site-settings.js → site-settings/site-settings.js} +0 -0
  40. /package/autoInit/{smooth-scroll.js → smooth-scroll/smooth-scroll.js} +0 -0
@@ -0,0 +1,338 @@
1
+ # **Form System Documentation**
2
+
3
+ ## **Overview**
4
+
5
+ The form system provides two main features:
6
+ 1. **Honeypot spam prevention** - Blocks bot submissions using a hidden field
7
+ 2. **Custom select component** - Fully accessible, styled select dropdowns that sync with native `<select>` elements for form submission
8
+
9
+ ---
10
+
11
+ ## **Honeypot Spam Prevention**
12
+
13
+ ### **Required Element**
14
+
15
+ **Honeypot Field** *(hidden field in your form)*
16
+
17
+ * data-hs-form="form-handler"
18
+ * Should be hidden with CSS (bots will auto-fill it, humans won't see it)
19
+
20
+ ### **What It Does**
21
+
22
+ 1. **Listens for form submissions** - Captures all form submit events
23
+ 2. **Checks honeypot field** - If the hidden field has a value, it's likely a bot
24
+ 3. **Blocks submission** - Prevents form from submitting if honeypot is filled
25
+
26
+ ### **Example**
27
+
28
+ ```html
29
+ <form>
30
+ <input type="text" name="name" placeholder="Your Name">
31
+ <input type="email" name="email" placeholder="Your Email">
32
+
33
+ <!-- Honeypot field (hidden with CSS) -->
34
+ <input type="text" name="website" data-hs-form="form-handler" style="display: none;">
35
+
36
+ <button type="submit">Submit</button>
37
+ </form>
38
+ ```
39
+
40
+ **Result:** Legitimate users never see the honeypot field. Bots auto-fill all fields, triggering the spam prevention.
41
+
42
+ ---
43
+
44
+ ## **Custom Select Component**
45
+
46
+ ### **Required Elements**
47
+
48
+ **Select Wrapper** *(main container)*
49
+
50
+ * data-hs-form="select"
51
+
52
+ **Native Select** *(hidden select element for form submission)*
53
+
54
+ * Standard `<select>` element with `<option>` children
55
+ * Should have `name` attribute for form submission
56
+ * Will be hidden but remains functional
57
+
58
+ **Toggle Button** *(visual button to open/close dropdown)*
59
+
60
+ * `<button>` element or element with `role="button"`
61
+ * Should contain a `<span>` element for displaying selected text
62
+ * Webflow IX must add/remove `.is-active` class on this button
63
+
64
+ **Custom List** *(dropdown container)*
65
+
66
+ * data-hs-form="select-list"
67
+ * First child is used as template for generating options
68
+ * Webflow IX must show/hide this when button is clicked
69
+
70
+ **Template Element** *(first child of select-list)*
71
+
72
+ * Should contain a `<span>` element for option text
73
+ * Will be cloned for each `<option>` in the native select
74
+ * Original template is removed after cloning
75
+
76
+ **Label** *(optional but recommended)*
77
+
78
+ * Standard `<label>` element
79
+ * Can be inside wrapper or reference select by `for` attribute
80
+
81
+ **Typical element layout:**
82
+
83
+ 1. Select Wrapper (data-hs-form="select")
84
+ 1. Label (optional)
85
+ 1. Text: "Choose an option"
86
+ 2. Native Select (`<select>`)
87
+ 1. Option 1 (`<option value="value1">`)
88
+ 1. Text: "Option 1"
89
+ 2. Option 2 (`<option value="value2">`)
90
+ 1. Text: "Option 2"
91
+ 3. Toggle Button (`<button>`)
92
+ 1. Button Text (`<span>`)
93
+ 1. Text: "Choose an option"
94
+ 4. Custom List (data-hs-form="select-list")
95
+ 1. Template Element
96
+ 1. Text Span (`<span>`)
97
+ 1. Text: "Placeholder"
98
+
99
+ ### **What It Does**
100
+
101
+ 1. **Unwraps Webflow slots** - Removes any `<div>` wrappers inside `<select>` elements
102
+ 2. **Clones template** - Uses first child of select-list as template
103
+ 3. **Generates options** - Creates custom option elements from native `<option>` elements
104
+ 4. **Syncs state** - Keeps custom UI and native select in sync
105
+ 5. **ARIA management** - Automatically updates accessibility attributes
106
+ 6. **Keyboard navigation** - Full arrow key, Tab, Enter, Space, Escape support
107
+ 7. **Focus management** - Handles focus return, tabbing out, and keyboard navigation
108
+
109
+ ### **When It Runs**
110
+
111
+ * Runs once on page load (or DOMContentLoaded)
112
+ * Available for manual re-initialization via `window.initCustomSelects()`
113
+ * Runs again on Barba.js page transitions (via `reinitialize()`)
114
+
115
+ ### **Interaction Requirements (IX3)**
116
+
117
+ **CRITICAL:** Webflow interactions MUST add/remove `.is-active` class on the toggle button.
118
+
119
+ **Triggers:**
120
+
121
+ 1. **Click trigger** on `<button>` element
122
+ 1. `Each click: Toggle play/reverse`
123
+
124
+ **Timeline:**
125
+
126
+ 1. 0s: Toggle `.is-active` class on the button element
127
+ 2. .06s: Rest of animation (fade in, slide, etc.)
128
+
129
+ ---
130
+
131
+ ## **Accessibility**
132
+
133
+ ### **Automatic ARIA Setup**
134
+
135
+ For each select, the system automatically:
136
+
137
+ 1. **Button (combobox)**
138
+ - `role="combobox"`
139
+ - `aria-haspopup="listbox"`
140
+ - `aria-expanded="true|false"` (synced with `.is-active` class)
141
+ - `aria-controls="[listbox-id]"`
142
+ - `aria-labelledby="[label-id]"` (if label exists)
143
+ - `aria-activedescendant="[option-id]"` (when navigating with arrows)
144
+
145
+ 2. **Select List**
146
+ - `role="listbox"`
147
+ - `tabindex="-1"` (not directly tabbable)
148
+
149
+ 3. **Options**
150
+ - `role="option"`
151
+ - `aria-selected="true|false"`
152
+ - `tabindex="0"` when dropdown open, `tabindex="-1"` when closed
153
+ - Unique `id` for aria-activedescendant
154
+
155
+ 4. **Label**
156
+ - Connected to native select with `for` attribute
157
+ - Linked to button with `aria-labelledby`
158
+
159
+ ### **Focus Management**
160
+
161
+ 1. **Opening dropdown** - All options become tabbable
162
+ 2. **Closing dropdown** - Focus returns to button if inside list
163
+ 3. **Tabbing out** - Automatically closes dropdown
164
+ 4. **Arrow navigation** - Focuses options with visual `.focused` class
165
+
166
+ ---
167
+
168
+ ## **Usage Examples**
169
+
170
+ ### **Basic Select**
171
+
172
+ ```html
173
+ <div data-hs-form="select">
174
+ <label for="fruit-select">Choose a fruit</label>
175
+
176
+ <select name="fruit" id="fruit-select">
177
+ <option value="">Choose an option</option>
178
+ <option value="apple">Apple</option>
179
+ <option value="banana">Banana</option>
180
+ <option value="orange">Orange</option>
181
+ </select>
182
+
183
+ <button>
184
+ <span>Choose an option</span>
185
+ </button>
186
+
187
+ <div data-hs-form="select-list">
188
+ <div>
189
+ <span>Template</span>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ ```
194
+
195
+ **Result:** Creates 4 custom options (including empty "Choose an option"). Native select stays synced for form submission.
196
+
197
+ ### **Pre-selected Option**
198
+
199
+ ```html
200
+ <select name="size">
201
+ <option value="small">Small</option>
202
+ <option value="medium" selected>Medium</option>
203
+ <option value="large">Large</option>
204
+ </select>
205
+ ```
206
+
207
+ **Result:** Button text automatically shows "Medium" on page load. Corresponding custom option has `aria-selected="true"`.
208
+
209
+ ### **Options with Special Characters**
210
+
211
+ ```html
212
+ <option value='product"deluxe"'>Deluxe Edition</option>
213
+ <option value="user's choice">Custom Option</option>
214
+ ```
215
+
216
+ **Result:** Works correctly - uses safe value comparison instead of selector injection.
217
+
218
+ ---
219
+
220
+ ## **Keyboard Navigation**
221
+
222
+ ### **When Button is Focused**
223
+
224
+ 1. **Space** or **Enter** � Open/close dropdown
225
+ 2. **ArrowDown** � Open dropdown (or focus first option if already open)
226
+ 3. **ArrowUp** � Focus last option (if dropdown is open)
227
+ 4. **Escape** � Close dropdown (if open)
228
+
229
+ ### **When Option is Focused**
230
+
231
+ 1. **ArrowDown** � Focus next option
232
+ 2. **ArrowUp** � Focus previous option (or close and focus button if on first option)
233
+ 3. **Enter** or **Space** � Select option and close dropdown
234
+ 4. **Escape** � Close dropdown and return focus to button
235
+ 5. **Tab** � Navigate through options, tab out of last option closes dropdown
236
+
237
+ ---
238
+
239
+ ## **State Synchronization**
240
+
241
+ ### **Native Select � Custom UI**
242
+
243
+ When native select value changes (programmatically or via Webflow):
244
+ 1. Finds matching custom option by value
245
+ 2. Updates button text
246
+ 3. Updates `aria-selected` on all options
247
+
248
+ ### **Custom UI � Native Select**
249
+
250
+ When user selects custom option:
251
+ 1. Updates native select's `value`
252
+ 2. Dispatches `change` event on native select
253
+ 3. Updates button text
254
+ 4. Updates `aria-selected`
255
+ 5. Closes dropdown
256
+
257
+ **Both stay in sync at all times.**
258
+
259
+ ---
260
+
261
+ ## **Key Attributes Summary**
262
+
263
+ | Attribute | Purpose | Required On |
264
+ | ----- | ----- | ----- |
265
+ | `data-hs-form="select"` | Select wrapper | Wrapper div |
266
+ | `data-hs-form="select-list"` | Custom options container | List div |
267
+ | `data-hs-form="form-handler"` | Honeypot spam field | Hidden input (optional) |
268
+
269
+ ---
270
+
271
+ ## **API**
272
+
273
+ ### **Manual Re-initialization**
274
+
275
+ ```javascript
276
+ window.initCustomSelects();
277
+ ```
278
+
279
+ Re-scans page for new selects and initializes them. Useful for dynamically added content.
280
+
281
+ ---
282
+
283
+ ## **Notes**
284
+
285
+ 1. **Native select required** - Must have actual `<select>` element for form submission
286
+ 2. **Button must have span** - `<span>` child is required for text updates
287
+ 3. **Template is removed** - First child of select-list is cloned then deleted
288
+ 4. **Webflow IX required** - Must toggle `.is-active` class on button
289
+ 5. **Barba.js compatible** - Automatically cleans up on destroy/reinitialize
290
+ 6. **Silent failures** - Missing required elements cause function to return early (no errors)
291
+ 7. **Empty values supported** - Options with `value=""` work correctly
292
+ 8. **Special characters safe** - Handles quotes and special characters in option values
293
+
294
+ ---
295
+
296
+ ## **Common Issues**
297
+
298
+ **Dropdown not opening:**
299
+
300
+ 1. Verify Webflow IX adds `.is-active` class to button (not wrapper or list)
301
+ 2. Check that button exists and is `<button>` or has `role="button"`
302
+ 3. Ensure select-list exists with `data-hs-form="select-list"`
303
+
304
+ **Button text not updating:**
305
+
306
+ 1. Verify button has a `<span>` child element
307
+ 2. Check that template element has a `<span>` child
308
+ 3. Ensure option values match between native and custom
309
+
310
+ **Options not clickable:**
311
+
312
+ 1. Check that template element exists as first child of select-list
313
+ 2. Verify template has `<span>` for text
314
+ 3. Ensure options are being generated (inspect DOM)
315
+
316
+ **Can't tab to options:**
317
+
318
+ 1. Verify dropdown is open (`.is-active` class on button)
319
+ 2. Check Webflow IX animation shows the select-list
320
+ 3. Ensure `display: none` is removed when open
321
+
322
+ **Form not submitting selected value:**
323
+
324
+ 1. Native `<select>` must have `name` attribute
325
+ 2. Check that native select's value is updating (inspect DOM or use console)
326
+ 3. Verify native select is inside `<form>` element
327
+
328
+ **Dropdown doesn't close when tabbing out:**
329
+
330
+ 1. Ensure wrapper element properly contains button and list
331
+ 2. Check that `data-hs-form="select"` is on wrapper
332
+ 3. Verify Webflow IX removes `.is-active` when clicking button again
333
+
334
+ **Spam prevention not working:**
335
+
336
+ 1. Verify honeypot field has `data-hs-form="form-handler"`
337
+ 2. Ensure field is hidden with CSS (not removed from DOM)
338
+ 3. Check that field is inside the `<form>` element
@@ -105,6 +105,7 @@ export function init() {
105
105
  // Add ARIA attributes
106
106
  customList.setAttribute('role', 'listbox');
107
107
  customList.setAttribute('id', `${selectName}-listbox`);
108
+ customList.setAttribute('tabindex', '-1');
108
109
 
109
110
  button.setAttribute('role', 'combobox');
110
111
  button.setAttribute('aria-haspopup', 'listbox');
@@ -127,7 +128,6 @@ export function init() {
127
128
  }
128
129
 
129
130
  // Track state
130
- let currentIndex = -1;
131
131
  let isOpen = false;
132
132
 
133
133
  // Update expanded state
@@ -148,7 +148,6 @@ export function init() {
148
148
  });
149
149
 
150
150
  // Add new focus
151
- currentIndex = index;
152
151
  options[index].classList.add('focused');
153
152
  options[index].setAttribute('tabindex', '0');
154
153
  options[index].focus();
@@ -157,7 +156,7 @@ export function init() {
157
156
 
158
157
  // Select option
159
158
  function selectOption(optionElement) {
160
- const value = optionElement.getAttribute('data-value');
159
+ const value = optionElement.getAttribute('data-value') || '';
161
160
  const text = optionElement.querySelector('span')?.textContent || optionElement.textContent;
162
161
 
163
162
  // Update real select
@@ -176,7 +175,7 @@ export function init() {
176
175
  });
177
176
  optionElement.setAttribute('aria-selected', 'true');
178
177
 
179
- // Click the button to close
178
+ // Close dropdown by clicking button
180
179
  button.click();
181
180
  }
182
181
 
@@ -266,33 +265,46 @@ export function init() {
266
265
  };
267
266
  addHandler(customList, 'click', listClickHandler);
268
267
 
269
- // Track open/close state
270
- const observer = new MutationObserver((mutations) => {
271
- mutations.forEach((mutation) => {
272
- if (mutation.type === 'attributes') {
273
- // Check if dropdown is visible
274
- const isVisible = !customList.hidden &&
275
- customList.style.display !== 'none' &&
276
- !customList.classList.contains('hidden');
277
-
278
- updateExpandedState(isVisible);
279
-
280
- if (!isVisible) {
281
- currentIndex = -1;
282
- button.removeAttribute('aria-activedescendant');
283
- // Return focus to button if it was in the list
284
- if (document.activeElement?.closest('[data-hs-form="select-list"]') === customList) {
285
- button.focus();
286
- }
287
- }
268
+ // Handle tabbing out of select list
269
+ const listFocusoutHandler = (e) => {
270
+ // Check if focus is moving outside the wrapper (or to browser chrome)
271
+ if ((!e.relatedTarget || !wrapper.contains(e.relatedTarget)) && button.classList.contains('is-active')) {
272
+ button.click(); // Close the dropdown
273
+ }
274
+ };
275
+ addHandler(customList, 'focusout', listFocusoutHandler);
276
+
277
+ // Track open/close state via .is-active class on button
278
+ const observer = new MutationObserver(() => {
279
+ const isActive = button.classList.contains('is-active');
280
+
281
+ updateExpandedState(isActive);
282
+
283
+ if (isActive) {
284
+ // Make all options tabbable when opened
285
+ const options = customList.querySelectorAll('[role="option"]');
286
+ options.forEach(opt => {
287
+ opt.setAttribute('tabindex', '0');
288
+ });
289
+ } else {
290
+ // Return focus to button BEFORE aria changes if it was in the list
291
+ if (document.activeElement?.closest('[data-hs-form="select-list"]') === customList) {
292
+ button.focus();
288
293
  }
289
- });
294
+
295
+ // Reset on close
296
+ button.removeAttribute('aria-activedescendant');
297
+ const options = customList.querySelectorAll('[role="option"]');
298
+ options.forEach(opt => {
299
+ opt.setAttribute('tabindex', '-1');
300
+ });
301
+ }
290
302
  });
291
303
 
292
- // Observe the custom list for visibility changes
293
- observer.observe(customList, {
304
+ // Observe the button for .is-active class changes
305
+ observer.observe(button, {
294
306
  attributes: true,
295
- attributeFilter: ['hidden', 'style', 'class']
307
+ attributeFilter: ['class']
296
308
  });
297
309
  addObserver(observer);
298
310
 
@@ -300,7 +312,10 @@ export function init() {
300
312
  const selectChangeHandler = () => {
301
313
  const selectedOption = realSelect.options[realSelect.selectedIndex];
302
314
  if (selectedOption) {
303
- const customOption = customList.querySelector(`[data-value="${selectedOption.value}"]`);
315
+ const options = customList.querySelectorAll('[role="option"]');
316
+ const customOption = Array.from(options).find(opt =>
317
+ opt.getAttribute('data-value') === selectedOption.value
318
+ );
304
319
  if (customOption) {
305
320
  // Update button text
306
321
  const text = customOption.querySelector('span')?.textContent || customOption.textContent;
@@ -310,7 +325,7 @@ export function init() {
310
325
  }
311
326
 
312
327
  // Update aria-selected
313
- customList.querySelectorAll('[role="option"]').forEach(opt => {
328
+ options.forEach(opt => {
314
329
  opt.setAttribute('aria-selected', 'false');
315
330
  });
316
331
  customOption.setAttribute('aria-selected', 'true');