From 57b487d61b584ad97b2566d928180adb6f32cc89 Mon Sep 17 00:00:00 2001 From: Timo Heddes Date: Wed, 16 Aug 2023 18:15:25 +0200 Subject: [PATCH] feat: [WIP] Accordion refactor --- .../Accordion/Accordion.stories.tsx | 98 +++++++++++-------- .../components/Accordion/Accordion.test.tsx | 98 ------------------- .../src/components/Accordion/Accordion.tsx | 51 +++------- .../components/Accordion/AccordionSection.tsx | 33 +++---- .../Accordion/AccordionSections.tsx | 19 ++++ .../src/components/Accordion/index.ts | 25 ++++- .../src/components/Accordion/useLinked.ts | 16 +++ .../src/components/NavFooter/NavFooter.tsx | 2 +- .../src/components/NavFooter/index.ts | 4 +- .../src/components/NavHeader/NavHeader.tsx | 3 +- .../src/components/NavHeader/index.ts | 4 +- .../Notification/NotificationContainer.tsx | 4 +- 12 files changed, 155 insertions(+), 202 deletions(-) delete mode 100644 packages/libs/react-ui/src/components/Accordion/Accordion.test.tsx create mode 100644 packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx create mode 100644 packages/libs/react-ui/src/components/Accordion/useLinked.ts 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 ba132ab57b3..e955fd2e260 100644 --- a/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx +++ b/packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx @@ -1,59 +1,79 @@ -import { Accordion, IAccordionProps } from './Accordion'; +import type { IAccordionRootProps, IAccordionSectionProps } from './'; +import { Accordion } from './'; import type { Meta, StoryObj } from '@storybook/react'; import React from 'react'; -const meta: Meta<{} & IAccordionProps> = { +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 generateSections = (n: number): IAccordionSectionProps[] => + Array.from({ length: n }, (d, i) => generateSection(i)); + +const sampleSections: IAccordionSectionProps[] = generateSections(5); + +type StoryProps = { sectionCount: number } & IAccordionRootProps; + +const meta: Meta = { title: 'Components/Accordion', + parameters: { + controls: { + hideNoControlsWarning: true, + sort: 'requiredFirst', + }, + docs: { + description: { + component: '', + }, + }, + }, argTypes: { linked: { - type: 'boolean', - defaultValue: true, + control: { type: 'boolean' }, description: 'Each section will close the other sections if they are linked', - control: { - type: 'boolean', - }, }, - sections: { - defaultValue: [], - description: 'Accordion children', - control: { - type: 'array', - }, + sectionCount: { + control: { type: 'range', min: 1, max: sampleSections.length, step: 1 }, + description: 'Adjust sample section items count', }, }, }; -export default meta; -type Story = StoryObj<{} & IAccordionProps>; +type IStory = StoryObj; -export const Dynamic: Story = { +export const Dynamic: IStory = { name: 'Accordion', args: { - linked: true, - sections: [ - { - title: First Section, - children:

This is the content for the first section

, - onOpen: () => console.log('open first item'), - onClose: () => console.log('close first item'), - }, - { - title: Second Section, - children:

This is the content for the second section

, - onOpen: () => console.log('open second item'), - onClose: () => console.log('close second item'), - }, - { - title: Third Section, - children:

This is the content for the third section

, - onOpen: () => console.log('open third item'), - onClose: () => console.log('close third item'), - }, - ], + linked: false, + sectionCount: 3, }, - render: ({ linked, sections }) => { - return ; + render: ({ linked, sectionCount }) => { + return ( + + {sampleSections + .slice(0, sectionCount) + .map( + ( + { title, children, onOpen, onClose }: IAccordionSectionProps, + index, + ) => ( + + {children} + + ), + )} + + ); }, }; + +export default meta; diff --git a/packages/libs/react-ui/src/components/Accordion/Accordion.test.tsx b/packages/libs/react-ui/src/components/Accordion/Accordion.test.tsx deleted file mode 100644 index 5d68d71f3a9..00000000000 --- a/packages/libs/react-ui/src/components/Accordion/Accordion.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Accordion } from './Accordion'; - -import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; - -// eslint-disable-next-line @kadena-dev/typedef-var -const sections = [ - { - title: 'Section 1', - children: 'Section 1 content', - onOpen: jest.fn(), - onClose: jest.fn(), - }, - { - title: 'Section 2', - children: 'Section 2 content', - onOpen: jest.fn(), - onClose: jest.fn(), - }, - { - title: 'Section 3', - children: 'Section 3 content', - onOpen: jest.fn(), - onClose: jest.fn(), - }, -]; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('Accordion', () => { - test('renders the correct number of sections', () => { - const { getAllByTestId } = render(); - const sectionElements = getAllByTestId('kda-accordion-title'); - expect(sectionElements.length).toBe(sections.length); - }); - - test('opens a linked section when clicked and closes others', () => { - const { getAllByTestId } = render( - , - ); - - const sectionTitles = Array.from(getAllByTestId('kda-accordion-title')); - - expect(sections[0].onOpen).toHaveBeenCalledTimes(0); - fireEvent.click(sectionTitles[0]); // Open Section 1 - expect(sections[0].onOpen).toHaveBeenCalledTimes(1); - - expect(sections[0].onClose).toHaveBeenCalledTimes(0); - fireEvent.click(sectionTitles[1]); // Close section 1 and Open Section 2 - expect(sections[0].onClose).toHaveBeenCalledTimes(1); - expect(sections[1].onOpen).toHaveBeenCalledTimes(1); - - expect(sectionTitles[0].querySelector('[role="button"]')).not.toHaveClass( - 'isOpen', - ); - expect(sectionTitles[1].querySelector('[role="button"]')).toHaveClass( - 'isOpen', - ); - expect(sectionTitles[2].querySelector('[role="button"]')).not.toHaveClass( - 'isOpen', - ); - }); - - test('allows multiple unlinked sections to be open at the same time', () => { - const { getAllByTestId } = render( - , - ); - - const sectionTitles = Array.from(getAllByTestId('kda-accordion-title')); - - expect(sections[0].onOpen).toHaveBeenCalledTimes(0); - expect(sections[1].onOpen).toHaveBeenCalledTimes(0); - expect(sections[2].onOpen).toHaveBeenCalledTimes(0); - - fireEvent.click(sectionTitles[0]); // Open Section 1 - fireEvent.click(sectionTitles[2]); // Open Section 3 - - expect(sections[0].onOpen).toHaveBeenCalledTimes(1); - expect(sections[1].onOpen).toHaveBeenCalledTimes(0); - expect(sections[2].onOpen).toHaveBeenCalledTimes(1); - - expect(sections[0].onClose).toHaveBeenCalledTimes(0); - expect(sections[1].onClose).toHaveBeenCalledTimes(0); - expect(sections[2].onClose).toHaveBeenCalledTimes(0); - - expect(sectionTitles[0].querySelector('[role="button"]')).toHaveClass( - 'isOpen', - ); - expect(sectionTitles[1].querySelector('[role="button"]')).not.toHaveClass( - 'isOpen', - ); - expect(sectionTitles[2].querySelector('[role="button"]')).toHaveClass( - 'isOpen', - ); - }); -}); diff --git a/packages/libs/react-ui/src/components/Accordion/Accordion.tsx b/packages/libs/react-ui/src/components/Accordion/Accordion.tsx index b3b38abe7b3..1228d2013ea 100644 --- a/packages/libs/react-ui/src/components/Accordion/Accordion.tsx +++ b/packages/libs/react-ui/src/components/Accordion/Accordion.tsx @@ -1,41 +1,22 @@ -import { AccordionSection, IAccordionSectionProps } from './AccordionSection'; +import useLinked from './useLinked'; +import { IAccordionSectionsProps } from '.'; -import React, { FC, useState } from 'react'; +import React, { FC, FunctionComponentElement } from 'react'; -export interface IAccordionProps { - sections: Omit[]; +export interface IAccordionRootProps { + children?: React.ReactNode; linked?: boolean; + openSection?: number; } -export const Accordion: FC = ({ sections, linked = true }) => { - const [expandedSections, setExpandedSections] = useState([]); - const handleOpen = (index: number): void => { - if (linked) setExpandedSections([index]); - else setExpandedSections([...expandedSections, index]); - }; - const handleClose = (index: number): void => { - setExpandedSections(expandedSections.filter((item) => item !== index)); - }; - - const isOpen = (index: number): boolean => expandedSections.includes(index); - - const handleToggle = (index: number): void => { - if (isOpen(index)) handleClose(index); - else handleOpen(index); - }; - - return ( -
- {sections.map((section, index) => ( - handleToggle(index)} - key={String(section.title)} - > - {section.children} - - ))} -
- ); +export const AccordionRoot: FC = ({ + children, + linked, + openSection, +}) => { + if (linked) { + const { setUsingLinked } = useLinked(openSection); + setUsingLinked(true); + } + 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 e49b4062199..7a5bf5c1800 100644 --- a/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx +++ b/packages/libs/react-ui/src/components/Accordion/AccordionSection.tsx @@ -5,45 +5,42 @@ import { accordionTitleVariants, toggleButtonClass, } from './Accordion.css'; +import useLinked from './useLinked'; import { SystemIcon } from '@components/Icon'; import classNames from 'classnames'; -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC, useState } from 'react'; export interface IAccordionSectionProps { title: React.ReactNode; children: React.ReactNode; - isOpen: boolean; - onToggle: () => void; + onToggle?: () => void; onOpen?: () => void; onClose?: () => void; } export const AccordionSection: FC = ({ - isOpen = false, title, children, - onToggle, onOpen, onClose, }) => { - const didMountRef = useRef(false); + const { usingLinked, activeSection, setActiveSection } = useLinked(); + const [isOpen, setIsOpen] = useState(false); - useEffect(() => { - if (!didMountRef.current) { - didMountRef.current = true; - return; - } - - if (isOpen) onOpen?.(); - else onClose?.(); - }, [isOpen]); + const onToggle = (): void => (isOpen ? onClose?.() : onOpen?.()); + const handleClick = (): void => { + setIsOpen(!isOpen); + }; return ( -
+
{ + handleClick(); + onToggle(); + }} className={classNames( accordionTitleClass, accordionTitleVariants[isOpen ? 'opened' : 'closed'], diff --git a/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx b/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx new file mode 100644 index 00000000000..2d6a71e427e --- /dev/null +++ b/packages/libs/react-ui/src/components/Accordion/AccordionSections.tsx @@ -0,0 +1,19 @@ +import useLinked from './useLinked'; +import { IAccordionSectionProps } from '.'; + +import React, { FC, FunctionComponentElement } from 'react'; + +export interface IAccordionSectionsProps { + // children?: FunctionComponentElement[]; + children?: React.ReactNode; + linked?: boolean; + openSection?: number; +} + +export const AccordionSections: FC = ({ + children, + linked, + openSection = 0, +}) => { + return
{children}
; +}; diff --git a/packages/libs/react-ui/src/components/Accordion/index.ts b/packages/libs/react-ui/src/components/Accordion/index.ts index 397de139eee..d7e05c8d401 100644 --- a/packages/libs/react-ui/src/components/Accordion/index.ts +++ b/packages/libs/react-ui/src/components/Accordion/index.ts @@ -1,3 +1,22 @@ -export { Accordion } from './Accordion'; -export type { IAccordionProps } from './Accordion'; -export type { IAccordionSectionProps } from './AccordionSection'; +import type { IAccordionRootProps } from './Accordion'; +import { AccordionRoot } from './Accordion'; +import type { IAccordionSectionProps } from './AccordionSection'; +import { AccordionSection } from './AccordionSection'; +import type { IAccordionSectionsProps } from './AccordionSections'; +import { AccordionSections } from './AccordionSections'; + +import { FC } from 'react'; + +export { IAccordionRootProps, IAccordionSectionProps, IAccordionSectionsProps }; + +export interface IAccordionProps { + Root: FC; + Sections: FC; + Section: FC; +} + +export const Accordion: IAccordionProps = { + Root: AccordionRoot, + Sections: AccordionSections, + Section: AccordionSection, +}; diff --git a/packages/libs/react-ui/src/components/Accordion/useLinked.ts b/packages/libs/react-ui/src/components/Accordion/useLinked.ts new file mode 100644 index 00000000000..804c48e8e58 --- /dev/null +++ b/packages/libs/react-ui/src/components/Accordion/useLinked.ts @@ -0,0 +1,16 @@ +import { useState } from 'react'; + +interface IUseLinkedReturn { + activeSection: number; + setActiveSection: React.Dispatch>; + usingLinked: boolean; + setUsingLinked: React.Dispatch>; +} + +const useLinked = (openSection = 0): IUseLinkedReturn => { + const [usingLinked, setUsingLinked] = useState(false); + const [activeSection, setActiveSection] = useState(openSection); + return { activeSection, setActiveSection, usingLinked, setUsingLinked }; +}; + +export default useLinked; diff --git a/packages/libs/react-ui/src/components/NavFooter/NavFooter.tsx b/packages/libs/react-ui/src/components/NavFooter/NavFooter.tsx index f87f47922cb..5eec6d33eda 100644 --- a/packages/libs/react-ui/src/components/NavFooter/NavFooter.tsx +++ b/packages/libs/react-ui/src/components/NavFooter/NavFooter.tsx @@ -9,7 +9,7 @@ export interface INavFooterRootProps { darkMode?: boolean; } -export const NavFooterContainer: FC = ({ +export const NavFooterRoot: FC = ({ children, darkMode = false, }) => { diff --git a/packages/libs/react-ui/src/components/NavFooter/index.ts b/packages/libs/react-ui/src/components/NavFooter/index.ts index 21836b92980..235d0fdf553 100644 --- a/packages/libs/react-ui/src/components/NavFooter/index.ts +++ b/packages/libs/react-ui/src/components/NavFooter/index.ts @@ -1,5 +1,5 @@ import type { INavFooterRootProps } from './NavFooter'; -import { NavFooterContainer } from './NavFooter'; +import { NavFooterRoot } from './NavFooter'; import type { INavFooterIconButtonProps } from './NavFooterIconButton'; import { NavFooterIconButton } from './NavFooterIconButton'; import type { INavFooterLinkProps } from './NavFooterLink'; @@ -24,7 +24,7 @@ export interface INavFooterProps { } export const NavFooter: INavFooterProps = { - Root: NavFooterContainer, + Root: NavFooterRoot, Panel: NavFooterPanel, Link: NavFooterLink, IconButton: NavFooterIconButton, diff --git a/packages/libs/react-ui/src/components/NavHeader/NavHeader.tsx b/packages/libs/react-ui/src/components/NavHeader/NavHeader.tsx index ed82d625024..971ce2babca 100644 --- a/packages/libs/react-ui/src/components/NavHeader/NavHeader.tsx +++ b/packages/libs/react-ui/src/components/NavHeader/NavHeader.tsx @@ -20,10 +20,9 @@ export interface INavHeaderRootProps { children?: FunctionComponentElement< INavHeaderNavigationProps | INavHeaderContentProps >[]; - items?: INavItems; } -export const NavHeaderContainer: FC = ({ +export const NavHeaderRoot: FC = ({ brand = logoVariants[0], children, }) => { diff --git a/packages/libs/react-ui/src/components/NavHeader/index.ts b/packages/libs/react-ui/src/components/NavHeader/index.ts index d96393096d9..fc2a98c2186 100644 --- a/packages/libs/react-ui/src/components/NavHeader/index.ts +++ b/packages/libs/react-ui/src/components/NavHeader/index.ts @@ -1,5 +1,5 @@ import type { INavHeaderRootProps } from './NavHeader'; -import { NavHeaderContainer } from './NavHeader'; +import { NavHeaderRoot } from './NavHeader'; import type { INavHeaderContentProps } from './NavHeaderContent'; import { NavHeaderContent } from './NavHeaderContent'; import type { INavHeaderLinkProps } from './NavHeaderLink'; @@ -24,7 +24,7 @@ export interface INavHeaderProps { } export const NavHeader: INavHeaderProps = { - Root: NavHeaderContainer, + Root: NavHeaderRoot, Navigation: NavHeaderNavigation, Link: NavHeaderLink, Content: NavHeaderContent, diff --git a/packages/libs/react-ui/src/components/Notification/NotificationContainer.tsx b/packages/libs/react-ui/src/components/Notification/NotificationContainer.tsx index 308d1824033..62310a15de5 100644 --- a/packages/libs/react-ui/src/components/Notification/NotificationContainer.tsx +++ b/packages/libs/react-ui/src/components/Notification/NotificationContainer.tsx @@ -39,7 +39,7 @@ export const NotificationContainer: FC = ({ ); return ( -
+ ); };