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

Refactor Accordion [Composition pattern, optimisations, contributing standards] #793

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3733a81
feat: [WIP] Accordion refactor
timoheddes Aug 16, 2023
26c668b
feat: accordion refactor
timoheddes Aug 17, 2023
51b21a0
chore: update accordion implementation [tools app]
timoheddes Aug 17, 2023
57e767a
chore: explicit type imports everywhere
timoheddes Aug 17, 2023
0736216
refactor: remove unneccesary accordion wrapper
timoheddes Aug 21, 2023
a5991dc
chore: remove unused file
timoheddes Aug 21, 2023
b3ad892
chore: container -> root & tsx -> ts
timoheddes Aug 21, 2023
8ecc832
refactor: accordion section title -> string
timoheddes Aug 21, 2023
7130337
refactor: remove redundant div
timoheddes Aug 21, 2023
01ac389
chore: update accordion implementation
timoheddes Aug 21, 2023
3654108
refactor: logic and styles
timoheddes Aug 28, 2023
d864ba9
Merge branch 'main' into feat/composition-component-structure
timoheddes Aug 28, 2023
c02b9d9
fix: merge
timoheddes Aug 28, 2023
8a762dd
fix: merge
timoheddes Aug 28, 2023
6ab1891
chore: PR suggestions
timoheddes Aug 28, 2023
18a809f
chore: PR suggestions
timoheddes Aug 28, 2023
3e7ec43
chore: PR suggestions
timoheddes Aug 28, 2023
fc5819e
chore: semantics
timoheddes Aug 28, 2023
206f3b8
chore: semantics
timoheddes Aug 28, 2023
204ddc5
feat: separation of concerns
timoheddes Aug 28, 2023
bcdc717
chore: cleanup
timoheddes Aug 28, 2023
54d9d0b
refactor: section -> article [suggestion]
timoheddes Aug 29, 2023
ad26a19
refactor: rotate icon instead of button on toggle
timoheddes Aug 29, 2023
1f5a0de
refactor: structure and style
timoheddes Aug 29, 2023
3f55a85
feat: story description etc
timoheddes Aug 29, 2023
ae2549b
feat: story description etc
timoheddes Aug 29, 2023
3f301f9
chore: remove unused import
timoheddes Aug 29, 2023
94d1738
feat: expand story
timoheddes Aug 29, 2023
9259250
chore: remove unused css
timoheddes Aug 29, 2023
07b3866
feat: support dark/light mode
timoheddes Aug 29, 2023
e16927c
chore: semantics
timoheddes Aug 29, 2023
28fa8f6
Merge branch 'main' into feat/composition-component-structure
timoheddes Aug 30, 2023
c83e266
chore: merge mistake
timoheddes Aug 30, 2023
a8bee2f
chore: changeset
timoheddes Aug 31, 2023
6c3df79
Merge branch 'main' into feat/composition-component-structure
timoheddes Aug 31, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ export const Menu: FC = () => {
title={activeMenu.title}
/>
</div>
<Accordion
sections={
activeMenu.items?.map((item) => ({
title: '', // @todo: fix Type error: Property 'title' does not exist on type 'ISidebarSubMenuItem'.
children: <Tree isOpen={true} items={item.items} />,
})) ?? []
}
/>
<Accordion.Root>
{activeMenu.items?.map((item, index) => (
<Accordion.Section title="" key={index}>
<Tree isOpen={true} items={item.items} />
</Accordion.Section>
))}
</Accordion.Root>
</div>
);
};
28 changes: 20 additions & 8 deletions packages/libs/react-ui/src/components/Accordion/Accordion.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { style, styleVariants } from '@vanilla-extract/css';

export const accordionSectionClass = style([
sprinkles({
display: 'block',
flexGrow: 1,
marginBottom: '$6',
}),
{
Expand All @@ -13,30 +15,39 @@ export const accordionSectionClass = style([
},
},
]);
export const accordionTitleClass = style([

export const accordionSectionHeadingClass = style([
sprinkles({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
fontSize: '$base',
fontWeight: '$medium',
paddingBottom: '$2',
display: 'flex',
}),
{
transition: 'color 0.2s ease 0s',
borderBottom: '1px solid',
},
]);

export const accordionTitleClass = style([
sprinkles({
alignItems: 'center',
display: 'flex',
fontSize: '$base',
fontWeight: '$medium',
justifyContent: 'space-between',
paddingBottom: '$2',
width: '100%',
}),
]);

export const accordionTitleVariants = styleVariants({
closed: [sprinkles({ color: '$negativeContrast' })],
opened: [sprinkles({ color: '$foreground' })],
});

export const toggleButtonClass = style([
sprinkles({
border: 'none',
background: 'none',
border: 'none',
color: 'inherit',
}),
{
Expand All @@ -49,6 +60,7 @@ export const toggleButtonClass = style([
},
},
]);

export const accordionContentWrapperClass = style([
sprinkles({
paddingTop: '$2',
Expand Down
101 changes: 61 additions & 40 deletions packages/libs/react-ui/src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,80 @@
import { Accordion, IAccordionProps } from './Accordion';
import type { IAccordionProps, IAccordionSectionProps } from './';
import { Accordion } from './';

import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

const meta: Meta<{} & IAccordionProps> = {
const generateSection = (i: number): IAccordionSectionProps => ({
title: `Section ${i}`,
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 + 1));

const sampleSections: IAccordionSectionProps[] = generateSections(5);

type StoryProps = { sectionCount: number; linked: boolean } & IAccordionProps;

const meta: Meta<StoryProps> = {
title: 'Components/Accordion',
parameters: {
controls: {
hideNoControlsWarning: true,
sort: 'requiredFirst',
},
docs: {
description: {
component:
'Note: optional property of `initialOpenSection` may be set to the Root component to set the initially opened section. This defaults to `undefined` and has only been explcitly set to -1 in the story code for demonstration purposes.',
},
},
},
argTypes: {
linked: {
type: 'boolean',
defaultValue: true,
control: { type: 'boolean' },
description:
'Each section will close the other sections if they are linked',
control: {
type: 'boolean',
},
'When linked, only one section can be open at a time. If a section is opened, the previously opened section will be closed.',
},
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<StoryProps>;

export const Dynamic: Story = {
export const Dynamic: IStory = {
name: 'Accordion',
args: {
linked: true,
sections: [
{
title: <span>First Section</span>,
children: <p>This is the content for the first section</p>,
onOpen: () => console.log('open first item'),
onClose: () => console.log('close first item'),
},
{
title: <span>Second Section</span>,
children: <p>This is the content for the second section</p>,
onOpen: () => console.log('open second item'),
onClose: () => console.log('close second item'),
},
{
title: <span>Third Section</span>,
children: <p>This is the content for the third section</p>,
onOpen: () => console.log('open third item'),
onClose: () => console.log('close third item'),
},
],
linked: false,
sectionCount: 3,
},
render: ({ linked, sections }) => {
return <Accordion linked={Boolean(linked)} sections={sections} />;
render: ({ linked, sectionCount }) => {
return (
<Accordion.Root linked={linked} initialOpenSection={-1}>
{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>
);
},
};

export default meta;
98 changes: 0 additions & 98 deletions packages/libs/react-ui/src/components/Accordion/Accordion.test.tsx

This file was deleted.

74 changes: 42 additions & 32 deletions packages/libs/react-ui/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
'use client';
import { IAccordionSectionProps } from '.';

import { AccordionSection, IAccordionSectionProps } from './AccordionSection';
import type { FC, FunctionComponentElement } from 'react';
import React, { useEffect, useState } from 'react';

import React, { FC, useState } from 'react';

export interface IAccordionProps {
sections: Omit<IAccordionSectionProps, 'isOpen' | 'onToggle'>[];
export interface IAccordionRootProps {
children?: FunctionComponentElement<IAccordionSectionProps>[];
linked?: boolean;
initialOpenSection?: number;
}

export const Accordion: FC<IAccordionProps> = ({ sections, linked = true }) => {
const [expandedSections, setExpandedSections] = useState<number[]>([]);
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);
export const AccordionRoot: FC<IAccordionRootProps> = ({
children,
linked = false,
initialOpenSection = undefined,
}) => {
const [openSections, setOpenSections] = useState([initialOpenSection]);

const handleToggle = (index: number): void => {
if (isOpen(index)) handleClose(index);
else handleOpen(index);
};
useEffect(() => {
if (linked && openSections.length > 1) {
const lastOpen = openSections.pop() || undefined;
setOpenSections([lastOpen]);
}
}, [linked]);

return (
<div data-testid="kda-accordion-wrapper">
{sections.map((section, index) => (
<AccordionSection
{...section}
isOpen={isOpen(index)}
onToggle={() => handleToggle(index)}
key={String(section.title)}
>
{section.children}
</AccordionSection>
))}
<div data-testid="kda-accordion-sections">
{React.Children.map(children, (section, sectionIndex) =>
React.cloneElement(
section as React.ReactElement<
HTMLElement | IAccordionSectionProps,
| string
| React.JSXElementConstructor<JSX.Element & IAccordionSectionProps>
>,
{
index: sectionIndex,
isOpen: openSections.includes(sectionIndex),
onOpen: () => section?.props.onOpen?.(),
onClose: () => section?.props.onClose?.(),
timoheddes marked this conversation as resolved.
Show resolved Hide resolved
onClick: () =>
openSections.includes(sectionIndex)
? setOpenSections(
openSections.filter((i) => i !== sectionIndex),
)
: setOpenSections(
linked ? [sectionIndex] : [...openSections, sectionIndex],
),
},
),
)}
</div>
);
};
Loading