diff --git a/src/components/Accordion/Accordion.stories.ts b/src/components/Accordion/Accordion.stories.ts new file mode 100644 index 0000000..2f0a140 --- /dev/null +++ b/src/components/Accordion/Accordion.stories.ts @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Accordion } from './Accordion'; + +const meta: Meta = { + title: 'Components/Accordion', + component: Accordion, +}; + +export default meta; +type Story = StoryObj; + +const defaultProps = { + items: [ + { title: 'Section 1', content: 'Content for section 1' }, + { title: 'Section 2', content: 'Content for section 2' }, + ], +}; + +export const Default: Story = { + args: { + ...defaultProps, + }, +}; diff --git a/src/components/Accordion/Accordion.style.ts b/src/components/Accordion/Accordion.style.ts new file mode 100644 index 0000000..7f7b26f --- /dev/null +++ b/src/components/Accordion/Accordion.style.ts @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const AccordionContainer = styled.div` + width: 100%; +`; + +export const AccordionItem = styled.div` + border: 1px solid ${({ theme }) => theme.colors.border || '#ccc'}; + margin-bottom: ${({ theme }) => theme.spacing.xs || '5px'}; +`; + +export const AccordionTitle = styled.div` + background-color: ${({ theme }) => theme.colors.backgroundAlt || '#f1f1f1'}; + padding: ${({ theme }) => theme.spacing.sm || '10px'}; + cursor: pointer; +`; + +export const AccordionContent = styled.div` + padding: ${({ theme }) => theme.spacing.sm || '10px'}; + display: none; + + &.open { + display: block; + } +`; diff --git a/src/components/Accordion/Accordion.test.tsx b/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 0000000..738ff74 --- /dev/null +++ b/src/components/Accordion/Accordion.test.tsx @@ -0,0 +1,36 @@ +import { screen, fireEvent } from '@testing-library/react'; +import { renderWithDeps } from '../../../jest.utils'; +import { Accordion } from './Accordion'; +import { axe } from 'jest-axe'; + +describe('', () => { + it('renders', () => { + renderWithDeps(); + + const titleElement = screen.getByText('Title'); + + expect(titleElement).toBeInTheDocument(); + }); + + it('toggles content on click', () => { + renderWithDeps(); + + const titleElement = screen.getByText('Title'); + fireEvent.click(titleElement); + + const contentElement = screen.getByText('Content'); + expect(contentElement).toBeVisible(); + + fireEvent.click(titleElement); + expect(contentElement).not.toBeVisible(); + }); + + it('has no accessibility violations', async () => { + const { container } = renderWithDeps(); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); +}); + + diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000..5a111a3 --- /dev/null +++ b/src/components/Accordion/Accordion.tsx @@ -0,0 +1,34 @@ +import React, { useState, ReactNode } from 'react'; +import { + AccordionContainer, + AccordionItem, + AccordionTitle, + AccordionContent, +} from './Accordion.style'; + +type AccordionProps = { + items: { title: string; content: ReactNode }[]; // Change content to ReactNode +}; + +export const Accordion: React.FC = ({ items }) => { + const [openIndex, setOpenIndex] = useState(null); + + const handleToggle = (index: number) => { + setOpenIndex(index === openIndex ? null : index); + }; + + return ( + + {items.map((item, index) => ( + + handleToggle(index)}> + {item.title} + + + {item.content} + + + ))} + + ); +};