@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.
- 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/{autoInit/accessibility → src/modules/structure}/functions/pagination/README.md +147 -72
- package/{autoInit/accessibility/functions/pagination/pagination.js → src/modules/structure/functions/pagination/pagination.ts} +98 -50
- package/{autoInit → src/modules/structure/functions}/site-settings/README.md +57 -27
- package/{autoInit/site-settings/site-settings.js → src/modules/structure/functions/site-settings/site-settings.ts} +36 -32
- 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/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/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,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
|
+
}
|