diff --git a/src/lib/components/accordion/Accordion.component.tsx b/src/lib/components/accordion/Accordion.component.tsx new file mode 100644 index 0000000000..f3df820844 --- /dev/null +++ b/src/lib/components/accordion/Accordion.component.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; + +import { spacing, Stack } from '../../spacing'; +import { Box } from '../box/Box'; +import { Icon } from '../icon/Icon.component'; + +import styled from 'styled-components'; + +import { Text } from '../text/Text.component'; + +export type AccordionProps = { + title: string; + id: string; + children: React.ReactNode; + style?: React.CSSProperties; +}; + +const AccordionHeader = styled.button` + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + gap: ${spacing.r8}; + width: 100%; + cursor: pointer; + background-color: transparent; + color: ${(props) => props.theme.textPrimary}; + padding: ${spacing.r4}; + width: 100%; +`; +const AccordionContainer = styled.div<{ + isOpen: boolean; +}>` + overflow: hidden; + opacity: ${(props) => (props.isOpen ? 1 : 0)}; + transition: height 0.3s ease-in, opacity 0.3s ease-in, visibility 0.3s; + visibility: ${(props) => (props.isOpen ? 'visible' : 'hidden')}; +`; +const Wrapper = styled.div` + padding-block: ${spacing.r8}; +`; + +export const Accordion = ({ title, id, style, children }: AccordionProps) => { + const [isOpen, setIsOpen] = useState(false); + + const handleToggleContent = () => { + setIsOpen((prev) => !prev); + }; + + return ( + +

+ + (e.key === 'Enter' || e.key === ' ') && handleToggleContent + } + > + + + {title} + + +

+ + { + if (isOpen) { + element?.style.setProperty('height', element.scrollHeight + 'px'); + } else { + element?.style.setProperty('height', '0px'); + } + }} + isOpen={isOpen} + id={id} + aria-labelledby={`Accordion-header-${id}`} + role="region" + > + {children} + +
+ ); +}; diff --git a/src/lib/components/accordion/Accordion.test.tsx b/src/lib/components/accordion/Accordion.test.tsx new file mode 100644 index 0000000000..2cff5aad17 --- /dev/null +++ b/src/lib/components/accordion/Accordion.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Accordion } from './Accordion.component'; +import userEvent from '@testing-library/user-event'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +describe('Accordion', () => { + const selectors = { + accordionToggle: () => screen.getByRole('button'), + accordionContainer: () => screen.getByRole('region'), + accordionContent: () => screen.queryByText(/Test content/i), + }; + const renderAccordion = () => { + const queryClient = new QueryClient(); + render( + + +
Test content
+
+
, + ); + }; + it('should render the Accordion component with title and content', () => { + renderAccordion(); + + const accordionToggle = selectors.accordionToggle(); + expect(accordionToggle).toBeInTheDocument(); + const accordionContent = selectors.accordionContent(); + expect(accordionContent).toBeInTheDocument(); + }); + + it('should toggle the content when clicking on the accordion header', () => { + renderAccordion(); + const accordionToggle = selectors.accordionToggle(); + const accordionContent = selectors.accordionContent(); + expect(accordionContent).not.toBeVisible(); + userEvent.click(accordionToggle); + expect(accordionContent).toBeVisible(); + }); + + it('should toggle the content when pressing the enter key or space key on the accordion header', () => { + renderAccordion(); + const accordionToggle = selectors.accordionToggle(); + const accordionContent = selectors.accordionContent(); + expect(accordionContent).not.toBeVisible(); + accordionToggle.focus(); + userEvent.keyboard('{enter}'); + expect(accordionContent).toBeVisible(); + userEvent.keyboard('{space}'); + expect(accordionContent).not.toBeVisible(); + }); +}); diff --git a/src/lib/next.ts b/src/lib/next.ts index 9c3ba85b17..75a4d3adbe 100644 --- a/src/lib/next.ts +++ b/src/lib/next.ts @@ -15,3 +15,4 @@ export { HealthSelector } from './components/healthselectorv2/HealthSelector.com export { CoreUiThemeProvider } from './components/coreuithemeprovider/CoreUiThemeProvider'; export { Box } from './components/box/Box'; export { Input } from './components/inputv2/inputv2'; +export { Accordion } from './components/accordion/Accordion.component'; diff --git a/stories/Accordion/accordion.guideline.mdx b/stories/Accordion/accordion.guideline.mdx new file mode 100644 index 0000000000..f19be488b6 --- /dev/null +++ b/stories/Accordion/accordion.guideline.mdx @@ -0,0 +1,26 @@ +import { + Meta, + Story, + Canvas, + Primary, + Controls, + Unstyled, + Source, + Title, +} from '@storybook/blocks'; +import { Accordion } from '../../src/lib/components/accordion/Accordion.component'; + +import * as Stories from './accordion.stories'; + + + +# Accordion + +Accordions are used to toggle the visibility of content. +It is used to hide non essential information or to reduce the amount of information displayed on the screen. + +## Playground + + + + diff --git a/stories/Accordion/accordion.stories.tsx b/stories/Accordion/accordion.stories.tsx new file mode 100644 index 0000000000..9a1612b624 --- /dev/null +++ b/stories/Accordion/accordion.stories.tsx @@ -0,0 +1,65 @@ +import { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { + Accordion, + AccordionProps, +} from '../../src/lib/components/accordion/Accordion.component'; +import { Stack } from '../../src/lib/spacing'; +import { Button } from '../../src/lib/components/buttonv2/Buttonv2.component'; + +type AccordionStory = StoryObj; + +const meta: Meta = { + title: 'Components/Accordion', + component: Accordion, + args: { + title: 'Accordion title', + children: ( + +
This is the content of the accordion.
+ +
+ ), + }, + argTypes: { + children: { + control: { disable: true }, + description: 'Content of the accordion', + table: { + type: { summary: 'React.ReactNode' }, + }, + }, + title: { + control: { type: 'text' }, + description: 'Title of the accordion', + table: { + type: { summary: 'string' }, + }, + }, + style: { + control: { disable: true }, + description: 'Use this to style the accordion content container', + table: { type: { summary: 'CSSProperties' } }, + }, + id: { + control: { disable: true }, + table: { type: { summary: 'string' } }, + description: 'Unique id for the accordion content container', + }, + }, +}; + +export default meta; + +export const Playground: AccordionStory = {}; + +export const Stacked: AccordionStory = { + render: (args) => ( + + + + + + ), +};