From 63d7b784f7183b3c311cd730a3c49696b0e21092 Mon Sep 17 00:00:00 2001 From: Timo Heddes Date: Thu, 17 Aug 2023 08:56:50 +0200 Subject: [PATCH] feat: accordion refactor --- .../Accordion/Accordion.stories.tsx | 56 +++++------ .../src/components/Accordion/Accordion.tsx | 15 +-- .../components/Accordion/AccordionSection.tsx | 63 ++++--------- .../Accordion/AccordionSections.tsx | 94 +++++++++++++++++-- .../src/components/Accordion/useLinked.ts | 10 +- 5 files changed, 140 insertions(+), 98 deletions(-) diff --git a/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx b/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx index e955fd2e260..f086d19be86 100644 --- a/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx +++ b/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx @@ -1,21 +1,21 @@ -import type { IAccordionRootProps, IAccordionSectionProps } from './'; +import type { IAccordionProps, IAccordionSectionProps } from './'; import { Accordion } from './'; import type { Meta, StoryObj } from '@storybook/react'; import React from 'react'; -const generateSection = (index: number): IAccordionSectionProps => ({ - title: Section {index + 1}, - children:

This is the content for section {index + 1}

, - onOpen: () => console.log(`open section ${index + 1}`), - onClose: () => console.log(`close section ${index + 1}`), +const generateSection = (i: number): IAccordionSectionProps => ({ + title: Section {i}, + children:

This is the content for section {i}

, + onOpen: () => console.log(`open section ${i}`), + onClose: () => console.log(`close section ${i}`), }); const generateSections = (n: number): IAccordionSectionProps[] => - Array.from({ length: n }, (d, i) => generateSection(i)); + Array.from({ length: n }, (d, i) => generateSection(i + 1)); const sampleSections: IAccordionSectionProps[] = generateSections(5); -type StoryProps = { sectionCount: number } & IAccordionRootProps; +type StoryProps = { sectionCount: number; linked: boolean } & IAccordionProps; const meta: Meta = { title: 'Components/Accordion', @@ -34,7 +34,7 @@ const meta: Meta = { linked: { control: { type: 'boolean' }, description: - 'Each section will close the other sections if they are linked', + 'When linked, only one section can be open at a time. If a section is opened, the previously opened section will be closed.', }, sectionCount: { control: { type: 'range', min: 1, max: sampleSections.length, step: 1 }, @@ -53,24 +53,26 @@ export const Dynamic: IStory = { }, render: ({ linked, sectionCount }) => { return ( - - {sampleSections - .slice(0, sectionCount) - .map( - ( - { title, children, onOpen, onClose }: IAccordionSectionProps, - index, - ) => ( - - {children} - - ), - )} + + + {sampleSections + .slice(0, sectionCount) + .map( + ( + { title, children, onOpen, onClose }: IAccordionSectionProps, + index, + ) => ( + + {children} + + ), + )} + ); }, diff --git a/packages/libs/react-ui/src/components/Accordion/Accordion.tsx b/packages/libs/react-ui/src/components/Accordion/Accordion.tsx index 1228d2013ea..5e1f8794850 100644 --- a/packages/libs/react-ui/src/components/Accordion/Accordion.tsx +++ b/packages/libs/react-ui/src/components/Accordion/Accordion.tsx @@ -1,22 +1,11 @@ -import useLinked from './useLinked'; import { IAccordionSectionsProps } from '.'; import React, { FC, FunctionComponentElement } from 'react'; export interface IAccordionRootProps { - children?: React.ReactNode; - linked?: boolean; - openSection?: number; + children?: FunctionComponentElement; } -export const AccordionRoot: FC = ({ - children, - linked, - openSection, -}) => { - if (linked) { - const { setUsingLinked } = useLinked(openSection); - setUsingLinked(true); - } +export const AccordionRoot: FC = ({ children }) => { return
{children}
; }; diff --git a/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx b/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx index 7a5bf5c1800..e14db08d226 100644 --- a/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx +++ b/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx @@ -1,64 +1,33 @@ -import { - accordionContentWrapperClass, - accordionSectionClass, - accordionTitleClass, - accordionTitleVariants, - toggleButtonClass, -} from './Accordion.css'; -import useLinked from './useLinked'; +import { toggleButtonClass } from './Accordion.css'; import { SystemIcon } from '@components/Icon'; import classNames from 'classnames'; -import React, { FC, useState } from 'react'; +import React, { FC } from 'react'; export interface IAccordionSectionProps { - title: React.ReactNode; children: React.ReactNode; - onToggle?: () => void; - onOpen?: () => void; + isOpen?: boolean; onClose?: () => void; + onOpen?: () => void; + title: React.ReactNode; } export const AccordionSection: FC = ({ title, - children, - onOpen, - onClose, + isOpen, }) => { - const { usingLinked, activeSection, setActiveSection } = useLinked(); - const [isOpen, setIsOpen] = useState(false); - - const onToggle = (): void => (isOpen ? onClose?.() : onOpen?.()); - const handleClick = (): void => { - setIsOpen(!isOpen); - }; - return ( -
-
{ - handleClick(); - onToggle(); - }} - className={classNames( - accordionTitleClass, - accordionTitleVariants[isOpen ? 'opened' : 'closed'], - )} - > - {title} +
+ {title} - -
- - {isOpen &&
{children}
} +
); }; diff --git a/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx b/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx index 2d6a71e427e..7d61e68cc52 100644 --- a/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx +++ b/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx @@ -1,19 +1,101 @@ +import { + accordionContentWrapperClass, + accordionSectionClass, + accordionTitleClass, + accordionTitleVariants, +} from './Accordion.css'; import useLinked from './useLinked'; import { IAccordionSectionProps } from '.'; -import React, { FC, FunctionComponentElement } from 'react'; +import classNames from 'classnames'; +import React, { + FC, + FunctionComponentElement, + useCallback, + useEffect, +} from 'react'; export interface IAccordionSectionsProps { - // children?: FunctionComponentElement[]; - children?: React.ReactNode; + children?: FunctionComponentElement[]; linked?: boolean; openSection?: number; } export const AccordionSections: FC = ({ children, - linked, - openSection = 0, + linked = false, + openSection, }) => { - return
{children}
; + const { usingLinked, setUsingLinked, openSections, setOpenSections } = + useLinked(openSection); + + useEffect(() => { + setUsingLinked(linked); + if (linked && openSections.length > 1) { + const lastOpen = openSections.pop() || -1; + setOpenSections([lastOpen]); + } + }, [linked]); + + const handleToggleSection = useCallback( + ( + index: number, + { onOpen, onClose }: Pick, + ): void => { + const isOpen = openSections.includes(index); + if (isOpen) { + setOpenSections(openSections.filter((i) => i !== index)); + onClose?.(); + } else { + setOpenSections(usingLinked ? [index] : [...openSections, index]); + onOpen?.(); + } + }, + [openSections, usingLinked], + ); + + return ( +
+ {React.Children.map(children, (section, sectionIndex) => ( +
+
+ handleToggleSection(sectionIndex, { + onOpen: section?.props.onOpen, + onClose: section?.props.onClose, + }) + } + className={classNames( + accordionTitleClass, + accordionTitleVariants[ + openSections.includes(sectionIndex) ? 'opened' : 'closed' + ], + )} + > + {React.cloneElement( + section as React.ReactElement< + HTMLElement | IAccordionSectionProps, + | string + | React.JSXElementConstructor< + JSX.Element & IAccordionSectionProps + > + >, + { + isOpen: openSections.includes(sectionIndex), + }, + )} +
+ {openSections.includes(sectionIndex) && section && ( +
+ {section.props.children} +
+ )} +
+ ))} +
+ ); }; diff --git a/packages/libs/react-ui/src/components/Accordion/useLinked.ts b/packages/libs/react-ui/src/components/Accordion/useLinked.ts index 804c48e8e58..bae70c042b1 100644 --- a/packages/libs/react-ui/src/components/Accordion/useLinked.ts +++ b/packages/libs/react-ui/src/components/Accordion/useLinked.ts @@ -1,16 +1,16 @@ import { useState } from 'react'; interface IUseLinkedReturn { - activeSection: number; - setActiveSection: React.Dispatch>; + openSections: number[]; + setOpenSections: React.Dispatch>; usingLinked: boolean; setUsingLinked: React.Dispatch>; } -const useLinked = (openSection = 0): IUseLinkedReturn => { +const useLinked = (openSection = -1): IUseLinkedReturn => { const [usingLinked, setUsingLinked] = useState(false); - const [activeSection, setActiveSection] = useState(openSection); - return { activeSection, setActiveSection, usingLinked, setUsingLinked }; + const [openSections, setOpenSections] = useState([openSection]); + return { openSections, setOpenSections, usingLinked, setUsingLinked }; }; export default useLinked;