Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/accordion component #783

Merged
merged 8 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/lib/components/accordion/Accordion.component.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box style={{ width: '100%', height: 'auto' }}>
<h3 style={{ margin: 0 }}>
<AccordionHeader
id={`Accordion-header-${id}`}
onClick={handleToggleContent}
aria-controls={id}
aria-expanded={isOpen}
onKeyDown={(e) =>
(e.key === 'Enter' || e.key === ' ') && handleToggleContent
}
>
<Stack direction="horizontal" gap="r8">
<Icon
name="Chevron-up"
size="lg"
style={{
transform: isOpen ? 'rotate(0deg)' : 'rotate(180deg)',
transition: 'transform 0.3s ease-in',
}}
/>
<Text isEmphazed>{title}</Text>
</Stack>
</AccordionHeader>
</h3>

<AccordionContainer
ref={(element) => {
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"
>
<Wrapper style={style}>{children}</Wrapper>
</AccordionContainer>
</Box>
);
};
52 changes: 52 additions & 0 deletions src/lib/components/accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<QueryClientProvider client={queryClient}>
<Accordion title="Advanced Testings" id="test-accordion">
<div>Test content</div>
</Accordion>
</QueryClientProvider>,
);
};
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();
});
});
1 change: 1 addition & 0 deletions src/lib/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
26 changes: 26 additions & 0 deletions stories/Accordion/accordion.guideline.mdx
Original file line number Diff line number Diff line change
@@ -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';

<Meta of={Stories} name="Guideline" />

# 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

<Canvas of={Stories.Playground} layout="fullscreen" />

<Controls of={Stories.Playground} />
65 changes: 65 additions & 0 deletions stories/Accordion/accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<AccordionProps>;

const meta: Meta<AccordionProps> = {
title: 'Components/Accordion',
component: Accordion,
args: {
title: 'Accordion title',
children: (
<Stack direction="vertical" gap="r8">
<div>This is the content of the accordion.</div>
<Button label={'Check'} onClick={() => console.log('click')}></Button>
</Stack>
),
},
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) => (
<Stack direction="vertical" gap="r8">
<Accordion {...args} />
<Accordion {...args} />
<Accordion {...args} style={{ backgroundColor: 'grey' }} />
</Stack>
),
};
Loading