From f5b041dacd9b71f55b41fa38ea8bdb0a4338c3d2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Tue, 8 Oct 2024 15:23:47 +0200 Subject: [PATCH 1/8] Accordion component v0 --- .../accordion/accordion.component.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/lib/components/accordion/accordion.component.tsx diff --git a/src/lib/components/accordion/accordion.component.tsx b/src/lib/components/accordion/accordion.component.tsx new file mode 100644 index 0000000000..934dc1b878 --- /dev/null +++ b/src/lib/components/accordion/accordion.component.tsx @@ -0,0 +1,92 @@ +import React, { useRef, useState } from 'react'; + +import { Box } from '../box/Box'; +import { Icon } from '../icon/Icon.component'; +import { spacing, Stack } from '../../spacing'; + +import styled from 'styled-components'; + +import { Text } from '../text/Text.component'; + +export type AccordionProps = { + title: string; + id: string; + children: React.ReactNode; +}; + +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.r8}; + width: 100%; +`; +const AccordionContent = styled.div<{ height: string }>` + overflow: hidden; + background-color: ${(props) => props.theme.backgroundLevel1}; + height: ${(props) => props.height}; + transition: height 0.3s ease-in; +`; +const Wrapper = styled.div` + padding: ${spacing.r16}; + padding-left: ${spacing.r24}; +`; + +const Accordion = ({ title, id, children }: AccordionProps) => { + const [isOpen, setIsOpen] = useState(false); + const contentRef = useRef(null); + const handleToggleContent = () => { + setIsOpen((prev) => !prev); + }; + const calcContentHeight = () => { + if (contentRef.current && isOpen) { + const height = contentRef.current.scrollHeight; + return height + 'px'; + } else return '0px'; + }; + + return ( + +

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

+ + + {children} + +
+ ); +}; + +export default Accordion; From fd757e55aeb85e43f3740a40c96f1a86db5cb1c5 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Tue, 8 Oct 2024 15:24:56 +0200 Subject: [PATCH 2/8] init stories and guideline for accordion --- stories/Accordion/accordion.guideline.mdx | 20 ++++++++++++++++ stories/Accordion/accordion.stories.tsx | 28 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 stories/Accordion/accordion.guideline.mdx create mode 100644 stories/Accordion/accordion.stories.tsx diff --git a/stories/Accordion/accordion.guideline.mdx b/stories/Accordion/accordion.guideline.mdx new file mode 100644 index 0000000000..2ec0522513 --- /dev/null +++ b/stories/Accordion/accordion.guideline.mdx @@ -0,0 +1,20 @@ +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. diff --git a/stories/Accordion/accordion.stories.tsx b/stories/Accordion/accordion.stories.tsx new file mode 100644 index 0000000000..966511ce69 --- /dev/null +++ b/stories/Accordion/accordion.stories.tsx @@ -0,0 +1,28 @@ +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 the subtitle explaining the content of the accordion.
+
This is the content of the accordion.
+
+ ), + }, +}; + +export default meta; + +export const Playground: AccordionStory = {}; From 26c0ec6b98aed738a4906d2fb1cb30a32e6e754c Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Wed, 9 Oct 2024 17:02:21 +0200 Subject: [PATCH 3/8] Accordion V0.1 --- .../accordion/accordion.component.tsx | 42 ++++++++------- .../components/accordion/accordion.test.tsx | 52 +++++++++++++++++++ stories/Accordion/accordion.guideline.mdx | 8 ++- stories/Accordion/accordion.stories.tsx | 41 ++++++++++++++- 4 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 src/lib/components/accordion/accordion.test.tsx diff --git a/src/lib/components/accordion/accordion.component.tsx b/src/lib/components/accordion/accordion.component.tsx index 934dc1b878..4f024365a7 100644 --- a/src/lib/components/accordion/accordion.component.tsx +++ b/src/lib/components/accordion/accordion.component.tsx @@ -12,6 +12,7 @@ export type AccordionProps = { title: string; id: string; children: React.ReactNode; + style?: React.CSSProperties; }; const AccordionHeader = styled.button` @@ -24,29 +25,35 @@ const AccordionHeader = styled.button` cursor: pointer; background-color: transparent; color: ${(props) => props.theme.textPrimary}; - padding: ${spacing.r8}; + padding: ${spacing.r4}; width: 100%; `; -const AccordionContent = styled.div<{ height: string }>` +const AccordionContainer = styled.div<{ + isOpen: boolean; + computedHeight: string; +}>` overflow: hidden; - background-color: ${(props) => props.theme.backgroundLevel1}; - height: ${(props) => props.height}; - transition: height 0.3s ease-in; + opacity: ${(props) => (props.isOpen ? 1 : 0)}; + height: ${(props) => props.computedHeight}; + 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: ${spacing.r16}; + background-color: ${(props) => props.theme.backgroundLevel2}; padding-left: ${spacing.r24}; + border-radius: ${spacing.r4}; `; -const Accordion = ({ title, id, children }: AccordionProps) => { +export const Accordion = ({ title, id, style, children }: AccordionProps) => { const [isOpen, setIsOpen] = useState(false); - const contentRef = useRef(null); + const containerRef = useRef(null); const handleToggleContent = () => { setIsOpen((prev) => !prev); }; const calcContentHeight = () => { - if (contentRef.current && isOpen) { - const height = contentRef.current.scrollHeight; + if (containerRef.current && isOpen) { + const height = containerRef.current.scrollHeight; return height + 'px'; } else return '0px'; }; @@ -55,7 +62,7 @@ const Accordion = ({ title, id, children }: AccordionProps) => {

{

- - {children} - + {children} +
); }; - -export default Accordion; diff --git a/src/lib/components/accordion/accordion.test.tsx b/src/lib/components/accordion/accordion.test.tsx new file mode 100644 index 0000000000..c10b50e345 --- /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/stories/Accordion/accordion.guideline.mdx b/stories/Accordion/accordion.guideline.mdx index 2ec0522513..878dcf239c 100644 --- a/stories/Accordion/accordion.guideline.mdx +++ b/stories/Accordion/accordion.guideline.mdx @@ -16,5 +16,11 @@ import * as Stories from './accordion.stories'; # Accordion -Accordions are used to toggle the visibility of content. +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 index 966511ce69..0e1e31b182 100644 --- a/stories/Accordion/accordion.stories.tsx +++ b/stories/Accordion/accordion.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; -import Accordion, { +import { + Accordion, AccordionProps, } from '../../src/lib/components/accordion/accordion.component'; import { Stack } from '../../src/lib/spacing'; @@ -16,13 +17,49 @@ const meta: Meta = { title: 'Accordion title', children: ( -
This the subtitle explaining the content of the accordion.
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) => ( + + + + + + ), +}; From 289bd85fd873a777b8bda484dbeb61d422fb662b Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 10 Oct 2024 16:23:44 +0200 Subject: [PATCH 4/8] V1 --- .../components/accordion/accordion.component.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib/components/accordion/accordion.component.tsx b/src/lib/components/accordion/accordion.component.tsx index 4f024365a7..a4c9a86012 100644 --- a/src/lib/components/accordion/accordion.component.tsx +++ b/src/lib/components/accordion/accordion.component.tsx @@ -39,10 +39,7 @@ const AccordionContainer = styled.div<{ visibility: ${(props) => (props.isOpen ? 'visible' : 'hidden')}; `; const Wrapper = styled.div` - padding: ${spacing.r16}; - background-color: ${(props) => props.theme.backgroundLevel2}; - padding-left: ${spacing.r24}; - border-radius: ${spacing.r4}; + padding-block: ${spacing.r8}; `; export const Accordion = ({ title, id, style, children }: AccordionProps) => { @@ -70,15 +67,16 @@ export const Accordion = ({ title, id, style, children }: AccordionProps) => { (e.key === 'Enter' || e.key === ' ') && handleToggleContent } > - + - {title} + {title} From a57de663fa1d8d9f1967ed3d099b265057a8b32f Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 10 Oct 2024 11:22:30 +0200 Subject: [PATCH 5/8] export component --- src/lib/next.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/next.ts b/src/lib/next.ts index 9c3ba85b17..e5dcb646dc 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'; From fa2b5dbba0a2ce47c0bc1c1333086cdab32a5f93 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 10 Oct 2024 18:18:27 +0200 Subject: [PATCH 6/8] rename and simplify code by removing ref and calc height function --- ....component.tsx => Accordion.component.tsx} | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) rename src/lib/components/accordion/{accordion.component.tsx => Accordion.component.tsx} (78%) diff --git a/src/lib/components/accordion/accordion.component.tsx b/src/lib/components/accordion/Accordion.component.tsx similarity index 78% rename from src/lib/components/accordion/accordion.component.tsx rename to src/lib/components/accordion/Accordion.component.tsx index a4c9a86012..f3df820844 100644 --- a/src/lib/components/accordion/accordion.component.tsx +++ b/src/lib/components/accordion/Accordion.component.tsx @@ -1,8 +1,8 @@ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; +import { spacing, Stack } from '../../spacing'; import { Box } from '../box/Box'; import { Icon } from '../icon/Icon.component'; -import { spacing, Stack } from '../../spacing'; import styled from 'styled-components'; @@ -30,11 +30,9 @@ const AccordionHeader = styled.button` `; const AccordionContainer = styled.div<{ isOpen: boolean; - computedHeight: string; }>` overflow: hidden; opacity: ${(props) => (props.isOpen ? 1 : 0)}; - height: ${(props) => props.computedHeight}; transition: height 0.3s ease-in, opacity 0.3s ease-in, visibility 0.3s; visibility: ${(props) => (props.isOpen ? 'visible' : 'hidden')}; `; @@ -44,22 +42,16 @@ const Wrapper = styled.div` export const Accordion = ({ title, id, style, children }: AccordionProps) => { const [isOpen, setIsOpen] = useState(false); - const containerRef = useRef(null); + const handleToggleContent = () => { setIsOpen((prev) => !prev); }; - const calcContentHeight = () => { - if (containerRef.current && isOpen) { - const height = containerRef.current.scrollHeight; - return height + 'px'; - } else return '0px'; - }; return ( - +

{

{ + if (isOpen) { + element?.style.setProperty('height', element.scrollHeight + 'px'); + } else { + element?.style.setProperty('height', '0px'); + } + }} isOpen={isOpen} id={id} - aria-labelledby={`Accordion header ${id}`} + aria-labelledby={`Accordion-header-${id}`} role="region" > {children} From 5cdf7293dbc6e2ecaedd68cc3cef54b2826e7398 Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 10 Oct 2024 18:19:38 +0200 Subject: [PATCH 7/8] rename test file --- .../accordion/{accordion.test.tsx => Accordion.test.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/lib/components/accordion/{accordion.test.tsx => Accordion.test.tsx} (97%) diff --git a/src/lib/components/accordion/accordion.test.tsx b/src/lib/components/accordion/Accordion.test.tsx similarity index 97% rename from src/lib/components/accordion/accordion.test.tsx rename to src/lib/components/accordion/Accordion.test.tsx index c10b50e345..2cff5aad17 100644 --- a/src/lib/components/accordion/accordion.test.tsx +++ b/src/lib/components/accordion/Accordion.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { Accordion } from './accordion.component'; +import { Accordion } from './Accordion.component'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from 'react-query'; From cf89cce75e48fe34a0e381ad1bdf615329c543eb Mon Sep 17 00:00:00 2001 From: Jean-Marc Millet Date: Thu, 10 Oct 2024 18:19:59 +0200 Subject: [PATCH 8/8] change import after rename --- src/lib/next.ts | 2 +- stories/Accordion/accordion.guideline.mdx | 2 +- stories/Accordion/accordion.stories.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/next.ts b/src/lib/next.ts index e5dcb646dc..75a4d3adbe 100644 --- a/src/lib/next.ts +++ b/src/lib/next.ts @@ -15,4 +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'; +export { Accordion } from './components/accordion/Accordion.component'; diff --git a/stories/Accordion/accordion.guideline.mdx b/stories/Accordion/accordion.guideline.mdx index 878dcf239c..f19be488b6 100644 --- a/stories/Accordion/accordion.guideline.mdx +++ b/stories/Accordion/accordion.guideline.mdx @@ -8,7 +8,7 @@ import { Source, Title, } from '@storybook/blocks'; -import { Accordion } from '../../src/lib/components/accordion/accordion.component'; +import { Accordion } from '../../src/lib/components/accordion/Accordion.component'; import * as Stories from './accordion.stories'; diff --git a/stories/Accordion/accordion.stories.tsx b/stories/Accordion/accordion.stories.tsx index 0e1e31b182..9a1612b624 100644 --- a/stories/Accordion/accordion.stories.tsx +++ b/stories/Accordion/accordion.stories.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Accordion, AccordionProps, -} from '../../src/lib/components/accordion/accordion.component'; +} from '../../src/lib/components/accordion/Accordion.component'; import { Stack } from '../../src/lib/spacing'; import { Button } from '../../src/lib/components/buttonv2/Buttonv2.component';