@discourser/design-system 0.3.1 → 0.4.0
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/guidelines/Guidelines.md +120 -13
- package/guidelines/components/accordion.md +639 -0
- package/guidelines/components/avatar.md +945 -0
- package/guidelines/components/badge.md +667 -0
- package/guidelines/components/checkbox.md +583 -0
- package/guidelines/components/drawer.md +961 -0
- package/guidelines/components/heading.md +505 -0
- package/guidelines/components/popover.md +1200 -0
- package/guidelines/components/progress.md +773 -0
- package/guidelines/components/radio-group.md +757 -0
- package/guidelines/components/select.md +1155 -0
- package/guidelines/components/skeleton.md +726 -0
- package/guidelines/components/tabs.md +834 -0
- package/guidelines/components/textarea.md +425 -0
- package/guidelines/components/toast.md +707 -0
- package/guidelines/components/tooltip.md +832 -0
- package/guidelines/overview-components.md +56 -8
- package/package.json +1 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# Accordion
|
|
2
|
+
|
|
3
|
+
**Purpose:** Collapsible content panels for organizing and revealing information progressively, following Material Design 3 principles.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { Accordion } from '@discourser/design-system';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Component Structure
|
|
12
|
+
|
|
13
|
+
The Accordion is a **compound component** that follows the composition pattern. All parts must be used together:
|
|
14
|
+
|
|
15
|
+
| Component | Purpose | Required |
|
|
16
|
+
| ------------------------- | ------------------------------------------- | ----------- |
|
|
17
|
+
| `Accordion.Root` | Container that manages accordion state | Yes |
|
|
18
|
+
| `Accordion.Item` | Individual collapsible section | Yes |
|
|
19
|
+
| `Accordion.ItemTrigger` | Clickable header to toggle item | Yes |
|
|
20
|
+
| `Accordion.ItemContent` | Collapsible content area | Yes |
|
|
21
|
+
| `Accordion.ItemIndicator` | Visual indicator (chevron icon) | Recommended |
|
|
22
|
+
| `Accordion.ItemBody` | Wrapper for content with consistent spacing | Optional |
|
|
23
|
+
| `Accordion.Context` | Access accordion state in custom components | Advanced |
|
|
24
|
+
| `Accordion.RootProvider` | Provide external accordion state | Advanced |
|
|
25
|
+
|
|
26
|
+
**Important:** Never use accordion parts in isolation. They must be nested within `Accordion.Root`.
|
|
27
|
+
|
|
28
|
+
## Variants
|
|
29
|
+
|
|
30
|
+
The Accordion component supports 2 visual variants:
|
|
31
|
+
|
|
32
|
+
| Variant | Visual Style | Usage | When to Use |
|
|
33
|
+
| --------- | --------------------------- | ---------------- | --------------------------------------- |
|
|
34
|
+
| `outline` | Border between items | Default style | FAQs, settings panels, content sections |
|
|
35
|
+
| `plain` | No borders, minimal styling | Clean appearance | Minimalist designs, nested accordions |
|
|
36
|
+
|
|
37
|
+
### Visual Characteristics
|
|
38
|
+
|
|
39
|
+
- **outline**: 1px border-bottom between items, clear visual separation
|
|
40
|
+
- **plain**: No borders, relying on spacing and typography for hierarchy
|
|
41
|
+
|
|
42
|
+
## Sizes
|
|
43
|
+
|
|
44
|
+
| Size | Trigger Height | Padding | Font Size | Usage |
|
|
45
|
+
| ---- | -------------- | ------------------- | ------------- | ---------------------- |
|
|
46
|
+
| `md` | auto | 12px (y) / 16px (x) | textStyle: md | Default, all use cases |
|
|
47
|
+
|
|
48
|
+
**Note:** Currently only `md` size is defined. Additional sizes can be added to the recipe as needed.
|
|
49
|
+
|
|
50
|
+
## Props
|
|
51
|
+
|
|
52
|
+
### Root Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
| --------------- | ---------------------------------------- | ----------- | --------------------------------------------------------- |
|
|
56
|
+
| `defaultValue` | `string \| string[]` | - | Initially expanded item(s) |
|
|
57
|
+
| `value` | `string \| string[]` | - | Controlled expanded item(s) |
|
|
58
|
+
| `onValueChange` | `(details: { value: string[] }) => void` | - | Callback when expansion changes |
|
|
59
|
+
| `multiple` | `boolean` | `false` | Allow multiple items to be expanded simultaneously |
|
|
60
|
+
| `collapsible` | `boolean` | `true` | Allow all items to be collapsed (when `multiple={false}`) |
|
|
61
|
+
| `disabled` | `boolean` | `false` | Disable all accordion items |
|
|
62
|
+
| `variant` | `'outline' \| 'plain'` | `'outline'` | Visual style variant |
|
|
63
|
+
| `size` | `'md'` | `'md'` | Accordion size |
|
|
64
|
+
|
|
65
|
+
### Item Props
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| ---------- | --------- | -------- | ------------------------------ |
|
|
69
|
+
| `value` | `string` | Required | Unique identifier for the item |
|
|
70
|
+
| `disabled` | `boolean` | `false` | Disable this specific item |
|
|
71
|
+
|
|
72
|
+
### ItemTrigger Props
|
|
73
|
+
|
|
74
|
+
| Prop | Type | Default | Description |
|
|
75
|
+
| ---------- | ----------- | -------- | ------------------------------------ |
|
|
76
|
+
| `children` | `ReactNode` | Required | Trigger content (usually title text) |
|
|
77
|
+
|
|
78
|
+
### ItemContent Props
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
| ---------- | ----------- | -------- | ------------------- |
|
|
82
|
+
| `children` | `ReactNode` | Required | Collapsible content |
|
|
83
|
+
|
|
84
|
+
### ItemIndicator Props
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Default | Description |
|
|
87
|
+
| ---------- | ----------- | --------------------- | ------------------------------------------- |
|
|
88
|
+
| `children` | `ReactNode` | `<ChevronDownIcon />` | Custom indicator icon (defaults to chevron) |
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
### Basic Usage
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Single item expanded at a time (default)
|
|
96
|
+
<Accordion.Root defaultValue="item-1">
|
|
97
|
+
<Accordion.Item value="item-1">
|
|
98
|
+
<Accordion.ItemTrigger>
|
|
99
|
+
What is React?
|
|
100
|
+
<Accordion.ItemIndicator />
|
|
101
|
+
</Accordion.ItemTrigger>
|
|
102
|
+
<Accordion.ItemContent>
|
|
103
|
+
<Accordion.ItemBody>
|
|
104
|
+
React is a JavaScript library for building user interfaces.
|
|
105
|
+
</Accordion.ItemBody>
|
|
106
|
+
</Accordion.ItemContent>
|
|
107
|
+
</Accordion.Item>
|
|
108
|
+
|
|
109
|
+
<Accordion.Item value="item-2">
|
|
110
|
+
<Accordion.ItemTrigger>
|
|
111
|
+
What is TypeScript?
|
|
112
|
+
<Accordion.ItemIndicator />
|
|
113
|
+
</Accordion.ItemTrigger>
|
|
114
|
+
<Accordion.ItemContent>
|
|
115
|
+
<Accordion.ItemBody>
|
|
116
|
+
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
|
|
117
|
+
</Accordion.ItemBody>
|
|
118
|
+
</Accordion.ItemContent>
|
|
119
|
+
</Accordion.Item>
|
|
120
|
+
</Accordion.Root>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Multiple Items Expanded
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// Allow multiple items to be open simultaneously
|
|
127
|
+
<Accordion.Root multiple defaultValue={['item-1', 'item-2']}>
|
|
128
|
+
<Accordion.Item value="item-1">
|
|
129
|
+
<Accordion.ItemTrigger>
|
|
130
|
+
Section 1
|
|
131
|
+
<Accordion.ItemIndicator />
|
|
132
|
+
</Accordion.ItemTrigger>
|
|
133
|
+
<Accordion.ItemContent>
|
|
134
|
+
<Accordion.ItemBody>
|
|
135
|
+
First section content
|
|
136
|
+
</Accordion.ItemBody>
|
|
137
|
+
</Accordion.ItemContent>
|
|
138
|
+
</Accordion.Item>
|
|
139
|
+
|
|
140
|
+
<Accordion.Item value="item-2">
|
|
141
|
+
<Accordion.ItemTrigger>
|
|
142
|
+
Section 2
|
|
143
|
+
<Accordion.ItemIndicator />
|
|
144
|
+
</Accordion.ItemTrigger>
|
|
145
|
+
<Accordion.ItemContent>
|
|
146
|
+
<Accordion.ItemBody>
|
|
147
|
+
Second section content
|
|
148
|
+
</Accordion.ItemBody>
|
|
149
|
+
</Accordion.ItemContent>
|
|
150
|
+
</Accordion.Item>
|
|
151
|
+
</Accordion.Root>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Controlled State
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const [value, setValue] = useState<string[]>(['item-1']);
|
|
158
|
+
|
|
159
|
+
<Accordion.Root
|
|
160
|
+
multiple
|
|
161
|
+
value={value}
|
|
162
|
+
onValueChange={(details) => setValue(details.value)}
|
|
163
|
+
>
|
|
164
|
+
<Accordion.Item value="item-1">
|
|
165
|
+
<Accordion.ItemTrigger>
|
|
166
|
+
Controlled Item 1
|
|
167
|
+
<Accordion.ItemIndicator />
|
|
168
|
+
</Accordion.ItemTrigger>
|
|
169
|
+
<Accordion.ItemContent>
|
|
170
|
+
<Accordion.ItemBody>
|
|
171
|
+
This accordion is controlled by React state
|
|
172
|
+
</Accordion.ItemBody>
|
|
173
|
+
</Accordion.ItemContent>
|
|
174
|
+
</Accordion.Item>
|
|
175
|
+
</Accordion.Root>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Non-Collapsible Mode
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// At least one item must always be open
|
|
182
|
+
<Accordion.Root multiple={false} collapsible={false} defaultValue="item-1">
|
|
183
|
+
<Accordion.Item value="item-1">
|
|
184
|
+
<Accordion.ItemTrigger>
|
|
185
|
+
Always One Open
|
|
186
|
+
<Accordion.ItemIndicator />
|
|
187
|
+
</Accordion.ItemTrigger>
|
|
188
|
+
<Accordion.ItemContent>
|
|
189
|
+
<Accordion.ItemBody>
|
|
190
|
+
You cannot collapse all items in this mode
|
|
191
|
+
</Accordion.ItemBody>
|
|
192
|
+
</Accordion.ItemContent>
|
|
193
|
+
</Accordion.Item>
|
|
194
|
+
</Accordion.Root>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Disabled Items
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
<Accordion.Root>
|
|
201
|
+
<Accordion.Item value="item-1">
|
|
202
|
+
<Accordion.ItemTrigger>
|
|
203
|
+
Active Item
|
|
204
|
+
<Accordion.ItemIndicator />
|
|
205
|
+
</Accordion.ItemTrigger>
|
|
206
|
+
<Accordion.ItemContent>
|
|
207
|
+
<Accordion.ItemBody>
|
|
208
|
+
This item can be toggled
|
|
209
|
+
</Accordion.ItemBody>
|
|
210
|
+
</Accordion.ItemContent>
|
|
211
|
+
</Accordion.Item>
|
|
212
|
+
|
|
213
|
+
<Accordion.Item value="item-2" disabled>
|
|
214
|
+
<Accordion.ItemTrigger>
|
|
215
|
+
Disabled Item
|
|
216
|
+
<Accordion.ItemIndicator />
|
|
217
|
+
</Accordion.ItemTrigger>
|
|
218
|
+
<Accordion.ItemContent>
|
|
219
|
+
<Accordion.ItemBody>
|
|
220
|
+
This content cannot be accessed
|
|
221
|
+
</Accordion.ItemBody>
|
|
222
|
+
</Accordion.ItemContent>
|
|
223
|
+
</Accordion.Item>
|
|
224
|
+
</Accordion.Root>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Plain Variant
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Minimal styling without borders
|
|
231
|
+
<Accordion.Root variant="plain">
|
|
232
|
+
<Accordion.Item value="item-1">
|
|
233
|
+
<Accordion.ItemTrigger>
|
|
234
|
+
Clean Design
|
|
235
|
+
<Accordion.ItemIndicator />
|
|
236
|
+
</Accordion.ItemTrigger>
|
|
237
|
+
<Accordion.ItemContent>
|
|
238
|
+
<Accordion.ItemBody>
|
|
239
|
+
No borders for a minimal aesthetic
|
|
240
|
+
</Accordion.ItemBody>
|
|
241
|
+
</Accordion.ItemContent>
|
|
242
|
+
</Accordion.Item>
|
|
243
|
+
</Accordion.Root>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Dynamic Content
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const faqs = [
|
|
250
|
+
{ id: 'faq-1', question: 'How do I reset my password?', answer: 'Click on "Forgot Password" on the login page.' },
|
|
251
|
+
{ id: 'faq-2', question: 'How do I contact support?', answer: 'Email us at support@example.com' },
|
|
252
|
+
{ id: 'faq-3', question: 'What payment methods do you accept?', answer: 'We accept all major credit cards and PayPal.' },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
<Accordion.Root defaultValue="faq-1">
|
|
256
|
+
{faqs.map((faq) => (
|
|
257
|
+
<Accordion.Item key={faq.id} value={faq.id}>
|
|
258
|
+
<Accordion.ItemTrigger>
|
|
259
|
+
{faq.question}
|
|
260
|
+
<Accordion.ItemIndicator />
|
|
261
|
+
</Accordion.ItemTrigger>
|
|
262
|
+
<Accordion.ItemContent>
|
|
263
|
+
<Accordion.ItemBody>
|
|
264
|
+
{faq.answer}
|
|
265
|
+
</Accordion.ItemBody>
|
|
266
|
+
</Accordion.ItemContent>
|
|
267
|
+
</Accordion.Item>
|
|
268
|
+
))}
|
|
269
|
+
</Accordion.Root>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Common Patterns
|
|
273
|
+
|
|
274
|
+
### FAQ Section
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
<section>
|
|
278
|
+
<h2>Frequently Asked Questions</h2>
|
|
279
|
+
<Accordion.Root variant="outline">
|
|
280
|
+
<Accordion.Item value="shipping">
|
|
281
|
+
<Accordion.ItemTrigger>
|
|
282
|
+
What are your shipping options?
|
|
283
|
+
<Accordion.ItemIndicator />
|
|
284
|
+
</Accordion.ItemTrigger>
|
|
285
|
+
<Accordion.ItemContent>
|
|
286
|
+
<Accordion.ItemBody>
|
|
287
|
+
We offer standard (5-7 days) and express (2-3 days) shipping options.
|
|
288
|
+
Free shipping on orders over $50.
|
|
289
|
+
</Accordion.ItemBody>
|
|
290
|
+
</Accordion.ItemContent>
|
|
291
|
+
</Accordion.Item>
|
|
292
|
+
|
|
293
|
+
<Accordion.Item value="returns">
|
|
294
|
+
<Accordion.ItemTrigger>
|
|
295
|
+
What is your return policy?
|
|
296
|
+
<Accordion.ItemIndicator />
|
|
297
|
+
</Accordion.ItemTrigger>
|
|
298
|
+
<Accordion.ItemContent>
|
|
299
|
+
<Accordion.ItemBody>
|
|
300
|
+
We accept returns within 30 days of purchase. Items must be unused
|
|
301
|
+
and in original packaging.
|
|
302
|
+
</Accordion.ItemBody>
|
|
303
|
+
</Accordion.ItemContent>
|
|
304
|
+
</Accordion.Item>
|
|
305
|
+
</Accordion.Root>
|
|
306
|
+
</section>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Settings Panel
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
<Accordion.Root multiple defaultValue={['account', 'privacy']}>
|
|
313
|
+
<Accordion.Item value="account">
|
|
314
|
+
<Accordion.ItemTrigger>
|
|
315
|
+
Account Settings
|
|
316
|
+
<Accordion.ItemIndicator />
|
|
317
|
+
</Accordion.ItemTrigger>
|
|
318
|
+
<Accordion.ItemContent>
|
|
319
|
+
<Accordion.ItemBody>
|
|
320
|
+
<Input label="Email" defaultValue="user@example.com" />
|
|
321
|
+
<Input label="Username" defaultValue="johndoe" />
|
|
322
|
+
<Button>Save Changes</Button>
|
|
323
|
+
</Accordion.ItemBody>
|
|
324
|
+
</Accordion.ItemContent>
|
|
325
|
+
</Accordion.Item>
|
|
326
|
+
|
|
327
|
+
<Accordion.Item value="privacy">
|
|
328
|
+
<Accordion.ItemTrigger>
|
|
329
|
+
Privacy Settings
|
|
330
|
+
<Accordion.ItemIndicator />
|
|
331
|
+
</Accordion.ItemTrigger>
|
|
332
|
+
<Accordion.ItemContent>
|
|
333
|
+
<Accordion.ItemBody>
|
|
334
|
+
<Switch label="Allow marketing emails" />
|
|
335
|
+
<Switch label="Public profile" defaultChecked />
|
|
336
|
+
</Accordion.ItemBody>
|
|
337
|
+
</Accordion.ItemContent>
|
|
338
|
+
</Accordion.Item>
|
|
339
|
+
</Accordion.Root>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Rich Content
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
<Accordion.Root>
|
|
346
|
+
<Accordion.Item value="features">
|
|
347
|
+
<Accordion.ItemTrigger>
|
|
348
|
+
Key Features
|
|
349
|
+
<Accordion.ItemIndicator />
|
|
350
|
+
</Accordion.ItemTrigger>
|
|
351
|
+
<Accordion.ItemContent>
|
|
352
|
+
<Accordion.ItemBody>
|
|
353
|
+
<ul>
|
|
354
|
+
<li>Real-time collaboration</li>
|
|
355
|
+
<li>Cloud storage integration</li>
|
|
356
|
+
<li>Advanced security features</li>
|
|
357
|
+
</ul>
|
|
358
|
+
<Button variant="text" rightIcon={<ArrowIcon />}>
|
|
359
|
+
Learn More
|
|
360
|
+
</Button>
|
|
361
|
+
</Accordion.ItemBody>
|
|
362
|
+
</Accordion.ItemContent>
|
|
363
|
+
</Accordion.Item>
|
|
364
|
+
</Accordion.Root>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## DO NOT
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// ❌ Don't use accordion parts without Root
|
|
371
|
+
<Accordion.Item value="item-1">
|
|
372
|
+
<Accordion.ItemTrigger>Won't work</Accordion.ItemTrigger>
|
|
373
|
+
</Accordion.Item>
|
|
374
|
+
|
|
375
|
+
// ❌ Don't forget unique values for each item
|
|
376
|
+
<Accordion.Root>
|
|
377
|
+
<Accordion.Item value="same">...</Accordion.Item>
|
|
378
|
+
<Accordion.Item value="same">...</Accordion.Item> // Collision!
|
|
379
|
+
</Accordion.Root>
|
|
380
|
+
|
|
381
|
+
// ❌ Don't use multiple={false} with array defaultValue
|
|
382
|
+
<Accordion.Root multiple={false} defaultValue={['item-1', 'item-2']}>
|
|
383
|
+
// Only works with single string value when multiple={false}
|
|
384
|
+
</Accordion.Root>
|
|
385
|
+
|
|
386
|
+
// ❌ Don't nest interactive elements in ItemTrigger
|
|
387
|
+
<Accordion.ItemTrigger>
|
|
388
|
+
<button>Nested button</button> // Breaks accessibility
|
|
389
|
+
<Accordion.ItemIndicator />
|
|
390
|
+
</Accordion.ItemTrigger>
|
|
391
|
+
|
|
392
|
+
// ❌ Don't omit ItemIndicator (poor UX)
|
|
393
|
+
<Accordion.ItemTrigger>
|
|
394
|
+
No visual cue for expansion // Users won't know it's expandable
|
|
395
|
+
</Accordion.ItemTrigger>
|
|
396
|
+
|
|
397
|
+
// ❌ Don't override styles with inline styles
|
|
398
|
+
<Accordion.Root style={{ backgroundColor: 'red' }}> // Use variants instead
|
|
399
|
+
</Accordion.Root>
|
|
400
|
+
|
|
401
|
+
// ✅ Use compound components properly
|
|
402
|
+
<Accordion.Root>
|
|
403
|
+
<Accordion.Item value="item-1">
|
|
404
|
+
<Accordion.ItemTrigger>
|
|
405
|
+
Proper Structure
|
|
406
|
+
<Accordion.ItemIndicator />
|
|
407
|
+
</Accordion.ItemTrigger>
|
|
408
|
+
<Accordion.ItemContent>
|
|
409
|
+
<Accordion.ItemBody>Content here</Accordion.ItemBody>
|
|
410
|
+
</Accordion.ItemContent>
|
|
411
|
+
</Accordion.Item>
|
|
412
|
+
</Accordion.Root>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Accessibility
|
|
416
|
+
|
|
417
|
+
The Accordion component follows WCAG 2.1 Level AA standards and implements WAI-ARIA Accordion Pattern:
|
|
418
|
+
|
|
419
|
+
- **Keyboard Navigation**:
|
|
420
|
+
- `Tab` / `Shift+Tab`: Navigate between triggers
|
|
421
|
+
- `Enter` / `Space`: Toggle item expansion
|
|
422
|
+
- `ArrowDown`: Focus next trigger
|
|
423
|
+
- `ArrowUp`: Focus previous trigger
|
|
424
|
+
- `Home`: Focus first trigger
|
|
425
|
+
- `End`: Focus last trigger
|
|
426
|
+
|
|
427
|
+
- **ARIA Attributes**: Automatically managed
|
|
428
|
+
- `role="region"` on content areas
|
|
429
|
+
- `aria-expanded` on triggers (true/false)
|
|
430
|
+
- `aria-controls` links trigger to content
|
|
431
|
+
- `aria-labelledby` links content to trigger
|
|
432
|
+
- `aria-disabled` on disabled items
|
|
433
|
+
|
|
434
|
+
- **Focus Management**: Clear focus indicators on keyboard navigation
|
|
435
|
+
- **Screen Readers**: Announce expansion state and content structure
|
|
436
|
+
|
|
437
|
+
### Accessibility Best Practices
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// ✅ Use descriptive trigger text
|
|
441
|
+
<Accordion.ItemTrigger>
|
|
442
|
+
How do I reset my password?
|
|
443
|
+
<Accordion.ItemIndicator />
|
|
444
|
+
</Accordion.ItemTrigger>
|
|
445
|
+
|
|
446
|
+
// ✅ Provide meaningful content
|
|
447
|
+
<Accordion.ItemContent>
|
|
448
|
+
<Accordion.ItemBody>
|
|
449
|
+
Step-by-step instructions with clear language
|
|
450
|
+
</Accordion.ItemBody>
|
|
451
|
+
</Accordion.ItemContent>
|
|
452
|
+
|
|
453
|
+
// ✅ Use semantic HTML in content
|
|
454
|
+
<Accordion.ItemContent>
|
|
455
|
+
<Accordion.ItemBody>
|
|
456
|
+
<h3>Subsection Title</h3>
|
|
457
|
+
<p>Well-structured content improves screen reader navigation</p>
|
|
458
|
+
</Accordion.ItemBody>
|
|
459
|
+
</Accordion.ItemContent>
|
|
460
|
+
|
|
461
|
+
// ✅ Indicate disabled state clearly
|
|
462
|
+
<Accordion.Item value="locked" disabled>
|
|
463
|
+
<Accordion.ItemTrigger>
|
|
464
|
+
Premium Feature (Upgrade Required)
|
|
465
|
+
<Accordion.ItemIndicator />
|
|
466
|
+
</Accordion.ItemTrigger>
|
|
467
|
+
</Accordion.Item>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Variant Selection Guide
|
|
471
|
+
|
|
472
|
+
| Scenario | Recommended Variant | Reasoning |
|
|
473
|
+
| ------------------ | ------------------- | ------------------------------------------ |
|
|
474
|
+
| FAQ section | `outline` | Clear visual separation between questions |
|
|
475
|
+
| Settings panel | `outline` | Organized, scannable interface |
|
|
476
|
+
| Nested content | `plain` | Avoid visual clutter with too many borders |
|
|
477
|
+
| Minimal design | `plain` | Clean, modern aesthetic |
|
|
478
|
+
| Form sections | `outline` | Clear boundaries between form groups |
|
|
479
|
+
| Sidebar navigation | `plain` | Streamlined appearance |
|
|
480
|
+
|
|
481
|
+
## State Behaviors
|
|
482
|
+
|
|
483
|
+
| State | Visual Change | Behavior |
|
|
484
|
+
| ------------- | --------------------------------------------------- | ------------------------------------------- |
|
|
485
|
+
| **Collapsed** | Content hidden, chevron points down | ItemContent has `display: none` |
|
|
486
|
+
| **Expanded** | Content visible, chevron points up (rotated 180deg) | Smooth height animation via `expand-height` |
|
|
487
|
+
| **Hover** | Trigger shows hover state | Visual feedback on interactive element |
|
|
488
|
+
| **Focus** | Focus ring on trigger | Keyboard navigation indicator |
|
|
489
|
+
| **Disabled** | 38% opacity, cursor not-allowed | Item cannot be toggled |
|
|
490
|
+
|
|
491
|
+
## Animation Details
|
|
492
|
+
|
|
493
|
+
The Accordion uses smooth expand/collapse animations:
|
|
494
|
+
|
|
495
|
+
- **Expand**: `expand-height` + `fade-in` over `normal` duration
|
|
496
|
+
- **Collapse**: `collapse-height` + `fade-out` over `normal` duration
|
|
497
|
+
- **Indicator**: 0.2s rotation transition
|
|
498
|
+
|
|
499
|
+
## Responsive Considerations
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
// Mobile: Full width is default
|
|
503
|
+
<Accordion.Root>
|
|
504
|
+
{/* Automatically responsive */}
|
|
505
|
+
</Accordion.Root>
|
|
506
|
+
|
|
507
|
+
// Desktop: May want to constrain width
|
|
508
|
+
<div className={css({ maxWidth: '800px', mx: 'auto' })}>
|
|
509
|
+
<Accordion.Root>
|
|
510
|
+
{/* Content with reasonable line length */}
|
|
511
|
+
</Accordion.Root>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
// Responsive padding in content
|
|
515
|
+
<Accordion.ItemContent>
|
|
516
|
+
<Accordion.ItemBody>
|
|
517
|
+
<div className={css({
|
|
518
|
+
px: { base: '4', md: '6' },
|
|
519
|
+
py: { base: '3', md: '4' }
|
|
520
|
+
})}>
|
|
521
|
+
Responsive content spacing
|
|
522
|
+
</div>
|
|
523
|
+
</Accordion.ItemBody>
|
|
524
|
+
</Accordion.ItemContent>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Testing
|
|
528
|
+
|
|
529
|
+
When testing Accordion components:
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { render, screen } from '@testing-library/react';
|
|
533
|
+
import userEvent from '@testing-library/user-event';
|
|
534
|
+
|
|
535
|
+
test('accordion expands on trigger click', async () => {
|
|
536
|
+
render(
|
|
537
|
+
<Accordion.Root>
|
|
538
|
+
<Accordion.Item value="test">
|
|
539
|
+
<Accordion.ItemTrigger>
|
|
540
|
+
Question
|
|
541
|
+
<Accordion.ItemIndicator />
|
|
542
|
+
</Accordion.ItemTrigger>
|
|
543
|
+
<Accordion.ItemContent>
|
|
544
|
+
<Accordion.ItemBody>Answer</Accordion.ItemBody>
|
|
545
|
+
</Accordion.ItemContent>
|
|
546
|
+
</Accordion.Item>
|
|
547
|
+
</Accordion.Root>
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
const trigger = screen.getByText('Question');
|
|
551
|
+
expect(screen.queryByText('Answer')).not.toBeVisible();
|
|
552
|
+
|
|
553
|
+
await userEvent.click(trigger);
|
|
554
|
+
expect(screen.getByText('Answer')).toBeVisible();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test('disabled item cannot be toggled', async () => {
|
|
558
|
+
render(
|
|
559
|
+
<Accordion.Root>
|
|
560
|
+
<Accordion.Item value="test" disabled>
|
|
561
|
+
<Accordion.ItemTrigger>
|
|
562
|
+
Disabled
|
|
563
|
+
<Accordion.ItemIndicator />
|
|
564
|
+
</Accordion.ItemTrigger>
|
|
565
|
+
<Accordion.ItemContent>
|
|
566
|
+
<Accordion.ItemBody>Content</Accordion.ItemBody>
|
|
567
|
+
</Accordion.ItemContent>
|
|
568
|
+
</Accordion.Item>
|
|
569
|
+
</Accordion.Root>
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const trigger = screen.getByText('Disabled');
|
|
573
|
+
await userEvent.click(trigger);
|
|
574
|
+
|
|
575
|
+
expect(screen.queryByText('Content')).not.toBeVisible();
|
|
576
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test('multiple mode allows multiple items open', async () => {
|
|
580
|
+
render(
|
|
581
|
+
<Accordion.Root multiple>
|
|
582
|
+
<Accordion.Item value="item-1">
|
|
583
|
+
<Accordion.ItemTrigger>
|
|
584
|
+
First
|
|
585
|
+
<Accordion.ItemIndicator />
|
|
586
|
+
</Accordion.ItemTrigger>
|
|
587
|
+
<Accordion.ItemContent>
|
|
588
|
+
<Accordion.ItemBody>First content</Accordion.ItemBody>
|
|
589
|
+
</Accordion.ItemContent>
|
|
590
|
+
</Accordion.Item>
|
|
591
|
+
<Accordion.Item value="item-2">
|
|
592
|
+
<Accordion.ItemTrigger>
|
|
593
|
+
Second
|
|
594
|
+
<Accordion.ItemIndicator />
|
|
595
|
+
</Accordion.ItemTrigger>
|
|
596
|
+
<Accordion.ItemContent>
|
|
597
|
+
<Accordion.ItemBody>Second content</Accordion.ItemBody>
|
|
598
|
+
</Accordion.ItemContent>
|
|
599
|
+
</Accordion.Item>
|
|
600
|
+
</Accordion.Root>
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
await userEvent.click(screen.getByText('First'));
|
|
604
|
+
await userEvent.click(screen.getByText('Second'));
|
|
605
|
+
|
|
606
|
+
expect(screen.getByText('First content')).toBeVisible();
|
|
607
|
+
expect(screen.getByText('Second content')).toBeVisible();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('controlled accordion updates on value change', async () => {
|
|
611
|
+
const TestComponent = () => {
|
|
612
|
+
const [value, setValue] = useState<string[]>([]);
|
|
613
|
+
return (
|
|
614
|
+
<>
|
|
615
|
+
<button onClick={() => setValue(['test'])}>Expand</button>
|
|
616
|
+
<Accordion.Root value={value} onValueChange={(d) => setValue(d.value)}>
|
|
617
|
+
<Accordion.Item value="test">
|
|
618
|
+
<Accordion.ItemTrigger>Trigger</Accordion.ItemTrigger>
|
|
619
|
+
<Accordion.ItemContent>
|
|
620
|
+
<Accordion.ItemBody>Content</Accordion.ItemBody>
|
|
621
|
+
</Accordion.ItemContent>
|
|
622
|
+
</Accordion.Item>
|
|
623
|
+
</Accordion.Root>
|
|
624
|
+
</>
|
|
625
|
+
);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
render(<TestComponent />);
|
|
629
|
+
await userEvent.click(screen.getByText('Expand'));
|
|
630
|
+
expect(screen.getByText('Content')).toBeVisible();
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Related Components
|
|
635
|
+
|
|
636
|
+
- **Tabs**: For switching between different views (not progressive disclosure)
|
|
637
|
+
- **Dialog**: For modal overlays with focused content
|
|
638
|
+
- **Collapsible**: For single collapsible sections (simpler alternative)
|
|
639
|
+
- **Menu**: For navigation or action menus
|