Skip to content

Commit c91602f

Browse files
feat: Add Accordion component (#534)
* wip: accordion component * feat: accordion component * feat: add accordion tests * feat: strucuture accordion into separate files * feat: add accordion docs * fix: accordion style resolve
1 parent b44b2d9 commit c91602f

File tree

12 files changed

+810
-0
lines changed

12 files changed

+810
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
'use client';
2+
3+
import { getPropsString } from '@/lib/utils';
4+
5+
export const getCode = (props: Record<string, unknown>) => {
6+
return `<Accordion${getPropsString(props)}>
7+
<Accordion.Item value="item-1">
8+
<Accordion.Trigger>Is it accessible?</Accordion.Trigger>
9+
<Accordion.Content>
10+
Yes. It adheres to the WAI-ARIA design pattern.
11+
</Accordion.Content>
12+
</Accordion.Item>
13+
<Accordion.Item value="item-2">
14+
<Accordion.Trigger>Is it styled?</Accordion.Trigger>
15+
<Accordion.Content>
16+
Yes. It comes with default styles that matches the other components.
17+
</Accordion.Content>
18+
</Accordion.Item>
19+
<Accordion.Item value="item-3">
20+
<Accordion.Trigger>Is it animated?</Accordion.Trigger>
21+
<Accordion.Content>
22+
Yes. It's animated by default, but you can disable it if you prefer.
23+
</Accordion.Content>
24+
</Accordion.Item>
25+
</Accordion>`;
26+
};
27+
28+
export const playground = {
29+
type: 'playground',
30+
controls: {
31+
type: {
32+
type: 'select',
33+
options: ['single', 'multiple'],
34+
defaultValue: 'single'
35+
}
36+
},
37+
getCode
38+
};
39+
40+
export const typeDemo = {
41+
type: 'code',
42+
tabs: [
43+
{
44+
name: 'Single',
45+
code: `
46+
<Accordion type="single" collapsible>
47+
<Accordion.Item value="item-1">
48+
<Accordion.Trigger>What is Apsara?</Accordion.Trigger>
49+
<Accordion.Content>
50+
Apsara is a modern design system and component library built with React and TypeScript.
51+
</Accordion.Content>
52+
</Accordion.Item>
53+
<Accordion.Item value="item-2">
54+
<Accordion.Trigger>How do I get started?</Accordion.Trigger>
55+
<Accordion.Content>
56+
You can install Apsara using your preferred package manager and start building your application.
57+
</Accordion.Content>
58+
</Accordion.Item>
59+
<Accordion.Item value="item-3">
60+
<Accordion.Trigger>Is it customizable?</Accordion.Trigger>
61+
<Accordion.Content>
62+
Yes, Apsara provides extensive customization options through CSS variables and component props.
63+
</Accordion.Content>
64+
</Accordion.Item>
65+
</Accordion>`
66+
},
67+
{
68+
name: 'Multiple',
69+
code: `
70+
<Accordion type="multiple">
71+
<Accordion.Item value="item-1">
72+
<Accordion.Trigger>What is Apsara?</Accordion.Trigger>
73+
<Accordion.Content>
74+
Apsara is a modern design system and component library built with React and TypeScript.
75+
</Accordion.Content>
76+
</Accordion.Item>
77+
<Accordion.Item value="item-2">
78+
<Accordion.Trigger>How do I get started?</Accordion.Trigger>
79+
<Accordion.Content>
80+
You can install Apsara using your preferred package manager and start building your application.
81+
</Accordion.Content>
82+
</Accordion.Item>
83+
<Accordion.Item value="item-3">
84+
<Accordion.Trigger>Is it customizable?</Accordion.Trigger>
85+
<Accordion.Content>
86+
Yes, Apsara provides extensive customization options through CSS variables and component props.
87+
</Accordion.Content>
88+
</Accordion.Item>
89+
</Accordion>`
90+
}
91+
]
92+
};
93+
export const controlledDemo = {
94+
type: 'code',
95+
code: `
96+
function ControlledAccordion() {
97+
const [value, setValue] = React.useState('item-1');
98+
99+
return (
100+
<Accordion value={value} onValueChange={setValue}>
101+
<Accordion.Item value="item-1">
102+
<Accordion.Trigger>Item 1</Accordion.Trigger>
103+
<Accordion.Content>Content for item 1</Accordion.Content>
104+
</Accordion.Item>
105+
<Accordion.Item value="item-2">
106+
<Accordion.Trigger>Item 2</Accordion.Trigger>
107+
<Accordion.Content>Content for item 2</Accordion.Content>
108+
</Accordion.Item>
109+
</Accordion>
110+
);
111+
}`
112+
};
113+
114+
export const disabledDemo = {
115+
type: 'code',
116+
code: `
117+
<Accordion>
118+
<Accordion.Item value="item-1">
119+
<Accordion.Trigger>Enabled Item</Accordion.Trigger>
120+
<Accordion.Content>This item is enabled and can be toggled.</Accordion.Content>
121+
</Accordion.Item>
122+
<Accordion.Item value="item-2" disabled>
123+
<Accordion.Trigger>Disabled Item</Accordion.Trigger>
124+
<Accordion.Content>This item is disabled and cannot be toggled.</Accordion.Content>
125+
</Accordion.Item>
126+
<Accordion.Item value="item-3">
127+
<Accordion.Trigger>Another Enabled Item</Accordion.Trigger>
128+
<Accordion.Content>This item is also enabled.</Accordion.Content>
129+
</Accordion.Item>
130+
</Accordion>`
131+
};
132+
133+
export const customContentDemo = {
134+
type: 'code',
135+
code: `
136+
<Accordion>
137+
<Accordion.Item value="item-1">
138+
<Accordion.Trigger>Product Features</Accordion.Trigger>
139+
<Accordion.Content>
140+
<div style={{ padding: '16px' }}>
141+
<h4>Key Features</h4>
142+
<ul>
143+
<li>Responsive design</li>
144+
<li>Accessible components</li>
145+
<li>TypeScript support</li>
146+
<li>Customizable themes</li>
147+
</ul>
148+
</div>
149+
</Accordion.Content>
150+
</Accordion.Item>
151+
<Accordion.Item value="item-2">
152+
<Accordion.Trigger>Documentation</Accordion.Trigger>
153+
<Accordion.Content>
154+
<div style={{ padding: '16px' }}>
155+
<p>Comprehensive documentation with examples and API references.</p>
156+
<a href="/docs">View Documentation</a>
157+
</div>
158+
</Accordion.Content>
159+
</Accordion.Item>
160+
</Accordion>`
161+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: Accordion
3+
description: A vertically stacked set of interactive headings that each reveal a section of content.
4+
tag: new
5+
---
6+
7+
import {
8+
playground,
9+
typeDemo,
10+
controlledDemo,
11+
disabledDemo,
12+
customContentDemo,
13+
} from "./demo.ts";
14+
15+
<Demo data={playground} />
16+
17+
## Usage
18+
19+
```tsx
20+
import { Accordion } from '@raystack/apsara'
21+
```
22+
23+
## Accordion Props
24+
25+
<auto-type-table path="./props.ts" name="AccordionRootProps" />
26+
27+
### Accordion.Item Props
28+
29+
<auto-type-table path="./props.ts" name="AccordionItemProps" />
30+
31+
### Accordion.Trigger Props
32+
33+
<auto-type-table path="./props.ts" name="AccordionTriggerProps" />
34+
35+
### Accordion.Content Props
36+
37+
<auto-type-table path="./props.ts" name="AccordionContentProps" />
38+
39+
## Examples
40+
41+
### Single vs Multiple
42+
43+
The Accordion component supports two types of behavior:
44+
45+
- **Single**: Only one item can be open at a time
46+
- **Multiple**: Multiple items can be open simultaneously
47+
48+
<Demo data={typeDemo} />
49+
50+
### Controlled
51+
52+
You can control the accordion's state by providing `value` and `onValueChange` props.
53+
54+
<Demo data={controlledDemo} />
55+
56+
### Disabled Items
57+
58+
Individual accordion items can be disabled using the `disabled` prop.
59+
60+
<Demo data={disabledDemo} />
61+
62+
### Custom Content
63+
64+
The accordion content can contain any React elements, allowing for rich layouts and complex content.
65+
66+
<Demo data={customContentDemo} />
67+
68+
## Accessibility
69+
70+
The Accordion component is built on top of [Radix UI's Accordion primitive](https://www.radix-ui.com/primitives/docs/components/accordion) and follows the WAI-ARIA design pattern for accordions. It includes:
71+
72+
- Proper ARIA attributes for screen readers
73+
- Keyboard navigation support
74+
- Focus management
75+
- Semantic HTML structure
76+
77+
## Keyboard Navigation
78+
79+
- **Space** or **Enter**: Toggle the focused accordion item
80+
- **Arrow Down**: Move focus to the next accordion item
81+
- **Arrow Up**: Move focus to the previous accordion item
82+
- **Home**: Move focus to the first accordion item
83+
- **End**: Move focus to the last accordion item
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export interface AccordionRootProps {
2+
/**
3+
* Controls how many accordion items can be open at once.
4+
* - "single": Only one item can be open at a time
5+
* - "multiple": Multiple items can be open simultaneously
6+
* @defaultValue "single"
7+
*/
8+
type?: 'single' | 'multiple';
9+
10+
/**
11+
* The controlled value of the accordion
12+
*/
13+
value?: string | string[];
14+
15+
/**
16+
* The default value of the accordion
17+
*/
18+
defaultValue?: string | string[];
19+
20+
/**
21+
* Event handler called when the value changes
22+
*/
23+
onValueChange?: (value: string | string[]) => void;
24+
25+
/**
26+
* Whether the accordion is collapsible when type is single
27+
* @defaultValue true
28+
*/
29+
collapsible?: boolean;
30+
31+
/**
32+
* Whether the accordion is disabled
33+
* @defaultValue false
34+
*/
35+
disabled?: boolean;
36+
37+
/**
38+
* The orientation of the accordion
39+
* @defaultValue "vertical"
40+
*/
41+
orientation?: 'horizontal' | 'vertical';
42+
43+
/**
44+
* The direction of the accordion
45+
* @defaultValue "ltr"
46+
*/
47+
dir?: 'ltr' | 'rtl';
48+
49+
/** Custom CSS class names */
50+
className?: string;
51+
}
52+
53+
export interface AccordionItemProps {
54+
/**
55+
* A unique value for the item
56+
*/
57+
value: string;
58+
59+
/**
60+
* Whether the item is disabled
61+
* @defaultValue false
62+
*/
63+
disabled?: boolean;
64+
65+
/** Custom CSS class names */
66+
className?: string;
67+
}
68+
69+
export interface AccordionTriggerProps {
70+
/** Custom CSS class names */
71+
className?: string;
72+
}
73+
74+
export interface AccordionContentProps {
75+
/**
76+
* Whether the content is force mounted
77+
* @defaultValue false
78+
*/
79+
forceMount?: boolean;
80+
81+
/** Custom CSS class names */
82+
className?: string;
83+
}

0 commit comments

Comments
 (0)