Skip to content

Commit 37715fa

Browse files
feat: create AccordionItem component (#95)
1 parent af37a31 commit 37715fa

4 files changed

Lines changed: 286 additions & 0 deletions

File tree

src/components/Accordion/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Accordion Component
2+
3+
The `Accordion` component is used to show and hide additional content inside a collapsible card.
4+
5+
## Usage
6+
7+
```jsx
8+
// Import component
9+
import { Accordion } from 'dept-central-lib-client'
10+
```
11+
12+
```jsx
13+
// Example usage
14+
const items = [
15+
{ title: 'Accordion Title 1', content: 'This is the content for item 1.' },
16+
{ title: 'Accordion Title 2', content: 'This is the content for item 2.' },
17+
]
18+
19+
<Accordion items={items} />
20+
```
21+
22+
User prop implements the following interface:
23+
24+
```tsx
25+
interface User {
26+
name: string
27+
email?: string
28+
image?: string
29+
}
30+
```
31+
32+
## Props
33+
34+
| Prop | Type | Description | Default Value |
35+
| --------- | ------------------------------------ | -------------------------------------------------- | ------------- |
36+
| items | { title: string; content: string }[] | Array of objects representing each accordion item. | - |
37+
| className | string | Custom CSS className. | "" |
38+
| width | string | Specifies the card width. | "700px" |
39+
40+
## Examples
41+
42+
```jsx
43+
// Simple accordion
44+
<Accordion
45+
items={[
46+
{ title: 'Item 1', content: 'Content for item 1' },
47+
{ title: 'Item 2', content: 'Content for item 2' },
48+
]}
49+
/>
50+
51+
// Accordion with long text
52+
<Accordion
53+
items={[
54+
{
55+
title: 'Details',
56+
content:
57+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet.',
58+
},
59+
]}
60+
/>
61+
62+
// Accordion with custom styles
63+
<Accordion
64+
className="border border-blue-500 rounded-xl p-4"
65+
items={[
66+
{ title: 'Styled Item', content: 'This item has custom container styles.' },
67+
]}
68+
/>
69+
70+
// Accordion with custom width
71+
<Accordion
72+
width="100%"
73+
items={[
74+
{ title: 'Wide Item', content: 'This accordion takes the full width.' },
75+
]}
76+
/>
77+
```
78+
79+
## Go main README
80+
81+
[Main README](../../../README.md#components)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { render, fireEvent } from '@testing-library/react'
2+
import { AccordionItem, Accordion } from '.'
3+
import '@testing-library/jest-dom/extend-expect'
4+
5+
describe('AccordionItem Component', () => {
6+
it('renders the AccordionItem with the correct title', () => {
7+
const { getByText } = render(
8+
<AccordionItem title="Test Title" content="Test Content" />,
9+
)
10+
expect(getByText('Test Title')).toBeInTheDocument()
11+
})
12+
13+
it('does not show content initially', () => {
14+
const { getByText } = render(
15+
<AccordionItem title="Title" content="Hidden Content" />,
16+
)
17+
const contentContainer = getByText('Hidden Content').parentElement
18+
expect(contentContainer).toHaveClass('max-h-0', 'opacity-0')
19+
})
20+
21+
it('toggles content visibility when button is clicked', () => {
22+
const { getByText, getByRole } = render(
23+
<AccordionItem title="Toggle Title" content="Toggle Content" />,
24+
)
25+
const button = getByRole('button')
26+
const contentContainer = getByText('Toggle Content').parentElement
27+
28+
// Content initially hidden
29+
expect(button).toHaveAttribute('aria-expanded', 'false')
30+
expect(contentContainer).toHaveClass('max-h-0', 'opacity-0')
31+
32+
// Click to expand
33+
fireEvent.click(button)
34+
expect(button).toHaveAttribute('aria-expanded', 'true')
35+
expect(contentContainer).toHaveClass('max-h-40', 'opacity-100')
36+
37+
// Click to collapse
38+
fireEvent.click(button)
39+
expect(button).toHaveAttribute('aria-expanded', 'false')
40+
expect(contentContainer).toHaveClass('max-h-0', 'opacity-0')
41+
})
42+
})
43+
44+
describe('Accordion Component', () => {
45+
it('renders multiple AccordionItem components', () => {
46+
const items = [
47+
{ title: 'Item 1', content: 'Content 1' },
48+
{ title: 'Item 2', content: 'Content 2' },
49+
]
50+
const { getByText } = render(<Accordion items={items} />)
51+
52+
expect(getByText('Item 1')).toBeInTheDocument()
53+
expect(getByText('Item 2')).toBeInTheDocument()
54+
expect(getByText('Content 1').parentElement).toHaveClass('max-h-0')
55+
expect(getByText('Content 2').parentElement).toHaveClass('max-h-0')
56+
})
57+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Meta, StoryObj } from '@storybook/react'
2+
import { Accordion } from '.'
3+
4+
export default {
5+
title: 'Components/Accordion',
6+
component: Accordion,
7+
parameters: {
8+
docs: {
9+
toc: {
10+
title: 'On this page',
11+
disable: false,
12+
},
13+
},
14+
layout: 'centered',
15+
},
16+
tags: ['autodocs'],
17+
argTypes: {
18+
items: {
19+
control: 'object',
20+
description: 'Array of accordion items with title and content',
21+
table: {
22+
type: { summary: 'AccordionProps[]' },
23+
},
24+
},
25+
className: {
26+
control: 'text',
27+
description: 'Additional classes for customization',
28+
table: {
29+
type: { summary: 'string' },
30+
},
31+
},
32+
},
33+
} as Meta<typeof Accordion>
34+
35+
type Story = StoryObj<typeof Accordion>
36+
37+
export const Default: Story = {
38+
parameters: {
39+
docs: {
40+
source: {
41+
code: `<Accordion items={[{ title: 'Item 1', content: 'Content 1' }, { title: 'Item 2', content: 'Content 2' }]} />`,
42+
},
43+
},
44+
},
45+
args: {
46+
items: [
47+
{ title: 'Accordion Item 1', content: 'This is the content of item 1.' },
48+
{ title: 'Accordion Item 2', content: 'This is the content of item 2.' },
49+
{ title: 'Accordion Item 3', content: 'This is the content of item 3.' },
50+
],
51+
},
52+
}
53+
54+
export const WithLongContent: Story = {
55+
args: {
56+
items: [
57+
{
58+
title: 'Accordion with long content',
59+
content:
60+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet.',
61+
},
62+
{
63+
title: 'Another Item',
64+
content: 'Short content here.',
65+
},
66+
],
67+
},
68+
parameters: {
69+
docs: {
70+
source: {
71+
code: `<Accordion items={[{ title: 'Accordion with long content', content: 'Lorem ipsum...' }, { title: 'Another Item', content: 'Short content' }]} />`,
72+
},
73+
},
74+
},
75+
}

src/components/Accordion/index.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
2+
import classNames from 'classnames'
3+
import React, { useState } from 'react'
4+
5+
type AccordionItemProps = {
6+
title: string
7+
content: string
8+
}
9+
10+
type AccordionProps = {
11+
items: AccordionItemProps[]
12+
className?: string
13+
width?: string
14+
}
15+
16+
export const AccordionItem: React.FC<AccordionItemProps> = ({
17+
title,
18+
content,
19+
}) => {
20+
const [isExpanded, setIsExpanded] = useState(false)
21+
const toggleExpand = () => setIsExpanded(!isExpanded)
22+
23+
return (
24+
<div className="max-w-full p-4 border-b border-gray-300 rounded-lg">
25+
<div className={classNames(`flex justify-between items-center w-full`)}>
26+
<span className="font-semibold text-slate-700">{title}</span>
27+
28+
<button
29+
onClick={toggleExpand}
30+
aria-expanded={isExpanded}
31+
className="ml-3"
32+
>
33+
{isExpanded ? (
34+
<ChevronUpIcon
35+
className="w-6 h-6 cursor-pointer"
36+
strokeWidth={2}
37+
stroke={'#704FFB'}
38+
/>
39+
) : (
40+
<ChevronDownIcon
41+
className="w-6 h-6 cursor-pointer"
42+
strokeWidth={2}
43+
stroke={'#704FFB'}
44+
/>
45+
)}
46+
</button>
47+
</div>
48+
49+
<div
50+
className={classNames(
51+
'overflow-hidden transition-all duration-300',
52+
isExpanded ? 'max-h-40 opacity-100 mt-3' : 'max-h-0 opacity-0',
53+
)}
54+
>
55+
<p className="text-slate-700 leading-6">{content}</p>
56+
</div>
57+
</div>
58+
)
59+
}
60+
61+
export const Accordion: React.FC<AccordionProps> = ({
62+
items,
63+
className = '',
64+
width = '700px',
65+
}) => {
66+
return (
67+
<div className={className} style={{ width }}>
68+
{items.map((item, index) => (
69+
<AccordionItem key={index} title={item.title} content={item.content} />
70+
))}
71+
</div>
72+
)
73+
}

0 commit comments

Comments
 (0)