Skip to content

Commit

Permalink
feat: accordion refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
timoheddes committed Aug 17, 2023
1 parent 57b487d commit 7610ea5
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -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: <span>Section {index + 1}</span>,
children: <p>This is the content for section {index + 1}</p>,
onOpen: () => console.log(`open section ${index + 1}`),
onClose: () => console.log(`close section ${index + 1}`),
const generateSection = (i: number): IAccordionSectionProps => ({
title: <span>Section {i}</span>,
children: <p>This is the content for section {i}</p>,
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<StoryProps> = {
title: 'Components/Accordion',
Expand All @@ -34,7 +34,7 @@ const meta: Meta<StoryProps> = {
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 },
Expand All @@ -53,24 +53,26 @@ export const Dynamic: IStory = {
},
render: ({ linked, sectionCount }) => {
return (
<Accordion.Root linked={linked}>
{sampleSections
.slice(0, sectionCount)
.map(
(
{ title, children, onOpen, onClose }: IAccordionSectionProps,
index,
) => (
<Accordion.Section
onOpen={onOpen}
onClose={onClose}
title={title}
key={index}
>
{children}
</Accordion.Section>
),
)}
<Accordion.Root>
<Accordion.Sections linked={linked}>
{sampleSections
.slice(0, sectionCount)
.map(
(
{ title, children, onOpen, onClose }: IAccordionSectionProps,
index,
) => (
<Accordion.Section
onOpen={onOpen}
onClose={onClose}
title={title}
key={index}
>
{children}
</Accordion.Section>
),
)}
</Accordion.Sections>
</Accordion.Root>
);
},
Expand Down
15 changes: 2 additions & 13 deletions packages/libs/react-ui/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -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<IAccordionSectionsProps>;
}

export const AccordionRoot: FC<IAccordionRootProps> = ({
children,
linked,
openSection,
}) => {
if (linked) {
const { setUsingLinked } = useLinked(openSection);
setUsingLinked(true);
}
export const AccordionRoot: FC<IAccordionRootProps> = ({ children }) => {
return <div data-testid="kda-accordion-wrapper">{children}</div>;
};
Original file line number Diff line number Diff line change
@@ -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<IAccordionSectionProps> = ({
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 (
<div className={accordionSectionClass} data-testid="kda-accordion-section">
<div
data-testid="kda-accordion-section-title"
onClick={() => {
handleClick();
onToggle();
}}
className={classNames(
accordionTitleClass,
accordionTitleVariants[isOpen ? 'opened' : 'closed'],
)}
>
<span>{title}</span>
<div>
<span>{title}</span>

<button
role="button"
className={classNames(toggleButtonClass, {
isOpen,
})}
>
<SystemIcon.Close size="sm" />
</button>
</div>

{isOpen && <div className={accordionContentWrapperClass}>{children}</div>}
<button
role="button"
className={classNames(toggleButtonClass, {
isOpen,
})}
>
<SystemIcon.Close size="sm" />
</button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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<IAccordionSectionProps>[];
children?: React.ReactNode;
children?: FunctionComponentElement<IAccordionSectionProps>[];
linked?: boolean;
openSection?: number;
}

export const AccordionSections: FC<IAccordionSectionsProps> = ({
children,
linked,
openSection = 0,
linked = false,
openSection,
}) => {
return <div data-testid="kda-accordion-sections">{children}</div>;
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<IAccordionSectionProps, 'onOpen' | 'onClose'>,
): 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 (
<div data-testid="kda-accordion-sections">
{React.Children.map(children, (section, sectionIndex) => (
<section
className={accordionSectionClass}
data-testid="kda-accordion-section"
>
<div
data-testid="kda-accordion-section-title"
onClick={() =>
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),
},
)}
</div>
{openSections.includes(sectionIndex) && section && (
<div className={accordionContentWrapperClass}>
{section.props.children}
</div>
)}
</section>
))}
</div>
);
};
10 changes: 5 additions & 5 deletions packages/libs/react-ui/src/components/Accordion/useLinked.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useState } from 'react';

interface IUseLinkedReturn {
activeSection: number;
setActiveSection: React.Dispatch<React.SetStateAction<number>>;
openSections: number[];
setOpenSections: React.Dispatch<React.SetStateAction<number[]>>;
usingLinked: boolean;
setUsingLinked: React.Dispatch<React.SetStateAction<boolean>>;
}

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;

0 comments on commit 7610ea5

Please sign in to comment.