@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
@@ -0,0 +1,451 @@
1
+ # **Accordion Accessibility**
2
+
3
+ ## **Overview**
4
+
5
+ Universal accordion system for FAQ, summary/read-more, and general toggle components. Automatically manages ARIA attributes, visual state, and optional text swapping.
6
+
7
+ **This function handles all accordion/toggle patterns with a single unified system.**
8
+
9
+ ---
10
+
11
+ ## **Required Elements**
12
+
13
+ **Accordion Wrapper** _(container and state manager)_
14
+
15
+ - `data-hs-accordion="wrapper"`
16
+ - Gets `is-active` class when open
17
+ - Optional: `data-hs-accordion-default="open"` - Start in open state
18
+ - Optional: `data-hs-accordion-open="Close"` - Text for open state
19
+ - Optional: `data-hs-accordion-closed="Open"` - Text for closed state
20
+
21
+ **Accordion Toggle Button** _(clickable trigger)_
22
+
23
+ - `data-hs-accordion="toggle"`
24
+ - Must be a `<button>` element
25
+ - Receives click handler and ARIA attributes
26
+
27
+ **Accordion Content** _(expandable area)_
28
+
29
+ - `data-hs-accordion="content"`
30
+ - Contains the content that expands/collapses
31
+
32
+ **State Text Elements** _(optional - for text swapping)_
33
+
34
+ - `data-hs-accordion-text="state"`
35
+ - Text content swaps between open/closed values
36
+ - Requires open/closed attributes on wrapper
37
+
38
+ ---
39
+
40
+ ## **How It Works**
41
+
42
+ ### **State Management:**
43
+
44
+ 1. Button click toggles `is-active` class on wrapper
45
+ 2. MutationObserver watches wrapper for class changes
46
+ 3. When `is-active` changes, ARIA attributes update
47
+ 4. Text elements with `[data-hs-accordion-text="state"]` update if configured
48
+ 5. Content height animates to auto (open) or 0 (closed) via GSAP
49
+
50
+ ### **ARIA Updates:**
51
+
52
+ - `aria-expanded` on button (true/false)
53
+ - `aria-hidden` on content (true/false)
54
+ - `aria-controls` links button to content
55
+ - `role="region"` on content
56
+ - No `aria-label` - button text is used directly
57
+
58
+ ### **Animation:**
59
+
60
+ - Uses GSAP (loaded globally via Webflow)
61
+ - Animates content `height` from 0 to auto (0.3s, power2.inOut)
62
+ - Initial state set without animation
63
+ - All animations killed on destroy (SPA safe)
64
+ - Gracefully degrades if GSAP not available
65
+
66
+ ### **Focus Management:**
67
+
68
+ - When closing, if focus is inside content, focus returns to button first
69
+ - Then ARIA updates apply
70
+ - Prevents keyboard users from losing focus
71
+
72
+ ---
73
+
74
+ ## **Usage Examples**
75
+
76
+ ### **Standard FAQ (No Text Swap)**
77
+
78
+ ```html
79
+ <div data-hs-accordion="wrapper">
80
+ <h3>
81
+ <button data-hs-accordion="toggle">
82
+ What is this product?
83
+ <span aria-hidden="true">▼</span>
84
+ </button>
85
+ </h3>
86
+ <div data-hs-accordion="content">
87
+ <p>This is a detailed answer about the product...</p>
88
+ </div>
89
+ </div>
90
+ ```
91
+
92
+ **Result:**
93
+
94
+ - Only `aria-expanded` changes on button ✅
95
+ - Button text stays static ✅
96
+ - Arrow provides visual indicator ✅
97
+ - Screen reader: "What is this product?, button, collapsed/expanded"
98
+
99
+ ---
100
+
101
+ ### **FAQ with Default Open**
102
+
103
+ ```html
104
+ <div data-hs-accordion="wrapper" data-hs-accordion-default="open">
105
+ <h3>
106
+ <button data-hs-accordion="toggle">What is this product?</button>
107
+ </h3>
108
+ <div data-hs-accordion="content">
109
+ <p>This is a detailed answer...</p>
110
+ </div>
111
+ </div>
112
+ ```
113
+
114
+ **Result:**
115
+
116
+ - Accordion starts in open state ✅
117
+ - `is-active` class applied on page load ✅
118
+
119
+ ---
120
+
121
+ ### **Show/Hide Toggle (With Text Swap)**
122
+
123
+ ```html
124
+ <div
125
+ data-hs-accordion="wrapper"
126
+ data-hs-accordion-open="Hide Details"
127
+ data-hs-accordion-closed="Show Details"
128
+ >
129
+ <div>
130
+ <button data-hs-accordion="toggle">
131
+ <span data-hs-accordion-text="state">Show Details</span>
132
+ <span aria-hidden="true">▼</span>
133
+ </button>
134
+ </div>
135
+ <div data-hs-accordion="content">
136
+ <p>Product specifications and details...</p>
137
+ </div>
138
+ </div>
139
+ ```
140
+
141
+ **Result:**
142
+
143
+ - Span text swaps: "Show Details" ↔ "Hide Details" ✅
144
+ - `aria-expanded` still provides semantic state ✅
145
+ - Visual text matches current action ✅
146
+
147
+ ---
148
+
149
+ ### **Complex Accordion with State Indicator**
150
+
151
+ ```html
152
+ <div data-hs-accordion="wrapper" data-hs-accordion-open="Close" data-hs-accordion-closed="Open">
153
+ <h3>
154
+ <button data-hs-accordion="toggle">
155
+ <span>Frequently Asked Question</span>
156
+ <span aria-hidden="true">
157
+ <span data-hs-accordion-text="state">Open</span>
158
+ <span class="arrow">→</span>
159
+ </span>
160
+ </button>
161
+ </h3>
162
+ <div data-hs-accordion="content">
163
+ <p>Answer content here...</p>
164
+ </div>
165
+ </div>
166
+ ```
167
+
168
+ **Result:**
169
+
170
+ - Question text stays visible ✅
171
+ - State indicator swaps: "Open" ↔ "Close" ✅
172
+ - State container has `aria-hidden="true"` to prevent duplication ✅
173
+ - Screen reader only announces question + expanded state ✅
174
+
175
+ ---
176
+
177
+ ### **Nested Accordions**
178
+
179
+ ```html
180
+ <div data-hs-accordion="wrapper">
181
+ <h2>
182
+ <button data-hs-accordion="toggle">Product Details</button>
183
+ </h2>
184
+ <div data-hs-accordion="content">
185
+ <p>Product information...</p>
186
+
187
+ <!-- Nested accordion -->
188
+ <div data-hs-accordion="wrapper">
189
+ <h3>
190
+ <button data-hs-accordion="toggle">Shipping Questions</button>
191
+ </h3>
192
+ <div data-hs-accordion="content">
193
+ <p>Shipping details...</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ ```
199
+
200
+ **Result:**
201
+
202
+ - Each accordion manages its own state independently ✅
203
+ - Focus returns to parent when parent closes ✅
204
+ - Full keyboard navigation support ✅
205
+
206
+ ---
207
+
208
+ ## **Key Attributes**
209
+
210
+ | Attribute | Element | Purpose | Required |
211
+ | ---------------------------------- | ----------- | ------------------------------- | -------- |
212
+ | `data-hs-accordion="wrapper"` | Container | Holds state (`is-active` class) | Yes |
213
+ | `data-hs-accordion="toggle"` | `<button>` | Clickable trigger | Yes |
214
+ | `data-hs-accordion="content"` | Container | Expandable content area | Yes |
215
+ | `data-hs-accordion-default="open"` | Wrapper | Start in open state | No |
216
+ | `data-hs-accordion-open` | Wrapper | Text for open state | No\* |
217
+ | `data-hs-accordion-closed` | Wrapper | Text for closed state | No\* |
218
+ | `data-hs-accordion-text="state"` | Any element | Text content will swap | No\* |
219
+
220
+ **Note:** `open`, `closed`, and `state` attributes work together - all three required for text swapping.
221
+
222
+ ---
223
+
224
+ ## **Text Swapping System**
225
+
226
+ ### **When to Use:**
227
+
228
+ - Show/Hide toggles
229
+ - Read More/Read Less buttons
230
+ - Any toggle where button text should change
231
+
232
+ ### **When NOT to Use:**
233
+
234
+ - FAQ items (question should stay visible)
235
+ - Accordions with static headings
236
+ - When arrow/icon is sufficient visual indicator
237
+
238
+ ### **Requirements:**
239
+
240
+ 1. `data-hs-accordion-open` on wrapper (text for open state)
241
+ 2. `data-hs-accordion-closed` on wrapper (text for closed state)
242
+ 3. `data-hs-accordion-text="state"` on elements that should swap
243
+
244
+ ### **How It Works:**
245
+
246
+ - Finds all `[data-hs-accordion-text="state"]` inside button
247
+ - Updates `textContent` of those elements based on state
248
+ - Multiple elements can have `state` attribute
249
+ - Text swaps instantly when state changes
250
+
251
+ ---
252
+
253
+ ## **Accessibility Features**
254
+
255
+ ### **ARIA Attributes:**
256
+
257
+ - ✅ `aria-expanded` on button (announces open/closed state)
258
+ - ✅ `aria-controls` links button to content
259
+ - ✅ `aria-hidden` on content (hides from screen readers when closed)
260
+ - ✅ `role="region"` on content (landmark for navigation)
261
+ - ✅ `aria-labelledby` on content (associates with button)
262
+ - ✅ Unique IDs generated automatically
263
+
264
+ ### **Keyboard Support:**
265
+
266
+ - ✅ Button is natively focusable
267
+ - ✅ Enter/Space toggles accordion
268
+ - ✅ Focus returns to button when closing if focus was inside content
269
+ - ✅ Tab navigation works correctly
270
+
271
+ ### **Screen Reader Announcements:**
272
+
273
+ - Closed: "Question text, button, collapsed"
274
+ - Open: "Question text, button, expanded"
275
+ - No redundant announcements from visual state indicators
276
+
277
+ ---
278
+
279
+ ## **Best Practices**
280
+
281
+ ### **Semantic HTML:**
282
+
283
+ ```html
284
+ <!-- Good: Heading wraps button -->
285
+ <h3>
286
+ <button data-hs-accordion="toggle">Question</button>
287
+ </h3>
288
+
289
+ <!-- Bad: Button inside div -->
290
+ <div>
291
+ <button data-hs-accordion="toggle">Question</button>
292
+ </div>
293
+ ```
294
+
295
+ ### **Visual State Indicators:**
296
+
297
+ ```html
298
+ <!-- Good: Hidden from screen readers -->
299
+ <button data-hs-accordion="toggle">
300
+ <span>Question</span>
301
+ <span aria-hidden="true">▼</span>
302
+ </button>
303
+
304
+ <!-- Bad: Not hidden -->
305
+ <button data-hs-accordion="toggle">
306
+ <span>Question</span>
307
+ <span>▼</span>
308
+ </button>
309
+ ```
310
+
311
+ ### **State Text:**
312
+
313
+ ```html
314
+ <!-- Good: State container hidden -->
315
+ <button data-hs-accordion="toggle">
316
+ <span>Question</span>
317
+ <span aria-hidden="true">
318
+ <span data-hs-accordion-text="state">Open</span>
319
+ </span>
320
+ </button>
321
+
322
+ <!-- Bad: State text not hidden, causes redundancy -->
323
+ <button data-hs-accordion="toggle">
324
+ <span>Question</span>
325
+ <span data-hs-accordion-text="state">Open</span>
326
+ </button>
327
+ ```
328
+
329
+ ---
330
+
331
+ ## **Barba.js / SPA Compatibility**
332
+
333
+ The accordion system is fully compatible with Barba.js and other SPA frameworks:
334
+
335
+ **On Initialize:**
336
+
337
+ - Adds click handlers to all buttons
338
+ - Sets up MutationObservers on all wrappers
339
+ - Applies default states
340
+ - Syncs initial ARIA
341
+
342
+ **On Destroy:**
343
+
344
+ - Removes all click event listeners
345
+ - Disconnects all MutationObservers
346
+ - Cleans up tracking arrays
347
+ - Safe for page transitions
348
+
349
+ **No memory leaks or orphaned listeners!** ✅
350
+
351
+ ---
352
+
353
+ ## **Common Patterns**
354
+
355
+ ### **FAQ List:**
356
+
357
+ ```html
358
+ <!-- No text swap, just arrow indicators -->
359
+ <div data-hs-accordion="wrapper">
360
+ <h3>
361
+ <button data-hs-accordion="toggle">
362
+ Question text?
363
+ <span aria-hidden="true">▼</span>
364
+ </button>
365
+ </h3>
366
+ <div data-hs-accordion="content">
367
+ <p>Answer text...</p>
368
+ </div>
369
+ </div>
370
+ ```
371
+
372
+ ### **Read More Toggle:**
373
+
374
+ ```html
375
+ <!-- Text swaps for "Read More" / "Read Less" -->
376
+ <div
377
+ data-hs-accordion="wrapper"
378
+ data-hs-accordion-open="Read Less"
379
+ data-hs-accordion-closed="Read More"
380
+ >
381
+ <button data-hs-accordion="toggle">
382
+ <span data-hs-accordion-text="state">Read More</span>
383
+ </button>
384
+ <div data-hs-accordion="content">
385
+ <p>Full content here...</p>
386
+ </div>
387
+ </div>
388
+ ```
389
+
390
+ ### **Show/Hide Details:**
391
+
392
+ ```html
393
+ <!-- Text and visual indicator both swap -->
394
+ <div
395
+ data-hs-accordion="wrapper"
396
+ data-hs-accordion-open="Hide Details"
397
+ data-hs-accordion-closed="Show Details"
398
+ >
399
+ <button data-hs-accordion="toggle">
400
+ <span data-hs-accordion-text="state">Show Details</span>
401
+ <span aria-hidden="true"> <span data-hs-accordion-text="state">Show</span> → </span>
402
+ </button>
403
+ <div data-hs-accordion="content">
404
+ <p>Details content...</p>
405
+ </div>
406
+ </div>
407
+ ```
408
+
409
+ ---
410
+
411
+ ## **Troubleshooting**
412
+
413
+ **Accordion not opening:**
414
+
415
+ - Ensure `is-active` class is defined in CSS
416
+ - Check that wrapper has `data-hs-accordion="wrapper"`
417
+ - Verify button has `data-hs-accordion="toggle"`
418
+
419
+ **Text not swapping:**
420
+
421
+ - Ensure both `data-hs-accordion-open` and `data-hs-accordion-closed` are present
422
+ - Verify elements have `data-hs-accordion-text="state"`
423
+ - Check that state elements are inside the button
424
+
425
+ **Focus issues:**
426
+
427
+ - Focus return only works when closing
428
+ - Only triggers if focus is inside content area
429
+ - Button must be focusable (not disabled)
430
+
431
+ **ARIA not updating:**
432
+
433
+ - Check browser console for warnings
434
+ - Ensure toggle is a `<button>` element
435
+ - Verify content element exists
436
+
437
+ ---
438
+
439
+ ## **Version**
440
+
441
+ **Current version:** 2.0.0
442
+
443
+ **Breaking changes from v1.x:**
444
+
445
+ - Removed `data-site-clickable` wrapper pattern
446
+ - Removed `data-hs-accordion-text-swap` attribute
447
+ - Added `data-hs-accordion-open` and `data-hs-accordion-closed`
448
+ - Added `data-hs-accordion-text="state"` for selective text swapping
449
+ - `is-active` now on wrapper instead of toggle
450
+ - Toggle must be `<button>` element directly
451
+ - No longer updates `aria-label` (uses button text directly)
@@ -0,0 +1,189 @@
1
+ import {
2
+ querySelectorAll,
3
+ querySelector,
4
+ getSelector,
5
+ globalConfig,
6
+ animateHeight,
7
+ setHeight,
8
+ } from '@utils';
9
+
10
+ export function init(config) {
11
+ const cleanup = {
12
+ observers: [],
13
+ handlers: [],
14
+ liveRegions: [],
15
+ };
16
+
17
+ const addObserver = (observer) => cleanup.observers.push(observer);
18
+ const addHandler = (element, event, handler, options) => {
19
+ element.addEventListener(event, handler, options);
20
+ cleanup.handlers.push({ element, event, handler, options });
21
+ };
22
+
23
+ function setupAccordionAccessibility(addObserver, addHandler) {
24
+ const accordionWrappers = querySelectorAll(config, 'wrapper');
25
+
26
+ if (accordionWrappers.length === 0) {
27
+ return 0;
28
+ }
29
+
30
+ let initializedCount = 0;
31
+
32
+ accordionWrappers.forEach((wrapper, index) => {
33
+ const button = querySelector(config, 'toggle', wrapper);
34
+ const content = querySelector(config, 'content', wrapper);
35
+
36
+ if (!button || !content) {
37
+ return;
38
+ }
39
+
40
+ initializedCount++;
41
+
42
+ // Check for visual height wrapper (contains data-hs-height="element")
43
+ // If found, animate that instead of the content (content is for ARIA only)
44
+ const heightWrapper = wrapper.querySelector('[data-hs-height="element"]');
45
+ const animationTarget = heightWrapper ? heightWrapper.parentElement : content;
46
+
47
+ // Generate unique IDs
48
+ const buttonId = `hs-accordion-btn-${index}`;
49
+ const contentId = `hs-accordion-content-${index}`;
50
+
51
+ // Create live region for announcements
52
+ const liveRegion = document.createElement('div');
53
+ liveRegion.className = 'sr-only';
54
+ liveRegion.setAttribute('aria-live', 'polite');
55
+ liveRegion.setAttribute('aria-atomic', 'true');
56
+ liveRegion.style.cssText =
57
+ 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
58
+ wrapper.appendChild(liveRegion);
59
+ cleanup.liveRegions.push(liveRegion);
60
+
61
+ // Get text state attributes (optional)
62
+ const openText = wrapper.getAttribute(config.attributes.properties.open);
63
+ const closedText = wrapper.getAttribute(config.attributes.properties.closed);
64
+ const hasTextSwap = openText && closedText;
65
+
66
+ // Find all state text elements (if text swap enabled)
67
+ let stateElements = [];
68
+ if (hasTextSwap) {
69
+ const stateSelector = getSelector(config, 'state');
70
+ stateElements = Array.from(button.querySelectorAll(stateSelector));
71
+ }
72
+
73
+ // Function to update state text elements
74
+ function updateStateText(text) {
75
+ if (!hasTextSwap) return;
76
+ stateElements.forEach((el) => {
77
+ el.textContent = text;
78
+ });
79
+ }
80
+
81
+ // Set initial IDs and ARIA attributes
82
+ button.setAttribute('id', buttonId);
83
+ content.setAttribute('id', contentId);
84
+ content.setAttribute('role', 'region');
85
+ content.setAttribute('aria-labelledby', buttonId);
86
+ button.setAttribute('aria-controls', contentId);
87
+
88
+ // Function to check if accordion is open
89
+ function isAccordionOpen() {
90
+ return wrapper.classList.contains(globalConfig.classes.active);
91
+ }
92
+
93
+ // Announce state change to screen readers
94
+ function announceStateChange(isOpen: boolean) {
95
+ const buttonText = button.textContent?.trim() || 'Section';
96
+ liveRegion.textContent = `${buttonText} ${isOpen ? 'expanded' : 'collapsed'}`;
97
+ setTimeout(() => (liveRegion.textContent = ''), 1000);
98
+ }
99
+
100
+ // Update ARIA states based on current visual state
101
+ function updateARIAStates() {
102
+ const isOpen = isAccordionOpen();
103
+ const wasOpen = button.getAttribute('aria-expanded') === 'true';
104
+
105
+ // If closing and focus is inside content, return focus first
106
+ if (wasOpen && !isOpen && content.contains(document.activeElement)) {
107
+ const buttonEl = button as HTMLElement;
108
+ buttonEl.focus();
109
+ }
110
+
111
+ // Update ARIA attributes
112
+ button.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
113
+ const contentEl = content as HTMLElement;
114
+ contentEl.inert = !isOpen; // Use inert instead of aria-hidden
115
+
116
+ // Announce state change if it actually changed
117
+ if (isOpen !== wasOpen) {
118
+ announceStateChange(isOpen);
119
+ }
120
+
121
+ // Update state text if text swap enabled
122
+ if (hasTextSwap && stateElements.length > 0) {
123
+ updateStateText(isOpen ? openText : closedText);
124
+ }
125
+
126
+ // Animate height wrapper (visual container, not ARIA content)
127
+ animateHeight(animationTarget, isOpen, { duration: 300, ease: 'power2.inOut' });
128
+ }
129
+
130
+ // Handle default open state
131
+ const defaultState = wrapper.getAttribute(config.attributes.properties.default);
132
+ if (defaultState && defaultState.toLowerCase() === 'open') {
133
+ wrapper.classList.add(globalConfig.classes.active);
134
+ }
135
+
136
+ // Set initial height without animation
137
+ setHeight(animationTarget, isAccordionOpen());
138
+
139
+ // Set initial state based on existing is-active class
140
+ updateARIAStates();
141
+
142
+ // Add click handler to button
143
+ const clickHandler = (e: Event) => {
144
+ e.preventDefault();
145
+ wrapper.classList.toggle(globalConfig.classes.active);
146
+ };
147
+ addHandler(button, 'click', clickHandler);
148
+
149
+ // Monitor for class changes on wrapper
150
+ const observer = new MutationObserver(() => {
151
+ updateARIAStates();
152
+ });
153
+
154
+ observer.observe(wrapper, {
155
+ attributes: true,
156
+ attributeFilter: ['class'],
157
+ });
158
+
159
+ addObserver(observer);
160
+ });
161
+
162
+ return initializedCount;
163
+ }
164
+
165
+ setupAccordionAccessibility(addObserver, addHandler);
166
+
167
+ return {
168
+ result: 'accordion initialized',
169
+ destroy: () => {
170
+ // Remove all live regions
171
+ cleanup.liveRegions.forEach((liveRegion) => {
172
+ if (liveRegion.parentNode) {
173
+ liveRegion.parentNode.removeChild(liveRegion);
174
+ }
175
+ });
176
+ cleanup.liveRegions.length = 0;
177
+
178
+ // Disconnect all observers
179
+ cleanup.observers.forEach((obs) => obs.disconnect());
180
+ cleanup.observers.length = 0;
181
+
182
+ // Remove all event listeners
183
+ cleanup.handlers.forEach(({ element, event, handler, options }) => {
184
+ element.removeEventListener(event, handler, options);
185
+ });
186
+ cleanup.handlers.length = 0;
187
+ },
188
+ };
189
+ }