@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.
@@ -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