Skip to content

Commit 16482e3

Browse files
committed
feat: refactor Accordion component
1 parent c4aad36 commit 16482e3

8 files changed

+209
-142
lines changed

src/components/Accordion.tsx

-64
This file was deleted.
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Children, cloneElement, FC, ReactElement, useMemo } from 'react';
2+
import { AccordionPanel, AccordionPanelProps } from './AccordionPanel';
3+
import { AccordionTitle } from './AccordionTitle';
4+
import { AccordionContent } from './AccordionContent';
5+
import classNames from 'classnames';
6+
7+
export type AccordionProps = {
8+
flush?: boolean;
9+
};
10+
11+
const AccordionComponent: FC<AccordionProps> = ({ children, flush }) => {
12+
const panels = useMemo(
13+
() => Children.map(children as ReactElement<AccordionPanelProps>[], (child) => cloneElement(child, { flush })),
14+
[children, flush],
15+
);
16+
17+
return (
18+
<div
19+
className={classNames('divide-y divide-gray-200 border-gray-200 dark:divide-gray-700 dark:border-gray-700', {
20+
'rounded-lg border': !flush,
21+
'border-b': flush,
22+
})}
23+
>
24+
{panels}
25+
</div>
26+
);
27+
};
28+
29+
AccordionComponent.displayName = 'Accordion';
30+
31+
export const Accordion = Object.assign(AccordionComponent, {
32+
Panel: AccordionPanel,
33+
Title: AccordionTitle,
34+
Content: AccordionContent,
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ComponentProps, FC } from 'react';
2+
import classNames from 'classnames';
3+
4+
import { useAccordionContext } from './AccordionPanelContext';
5+
6+
export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, className, ...props }) => {
7+
const { isOpen } = useAccordionContext();
8+
9+
return isOpen ? (
10+
<div {...props} className={classNames('py-5 dark:bg-gray-900', className)}>
11+
{children}
12+
</div>
13+
) : null;
14+
};
15+
16+
AccordionContent.displayName = 'Accordion.Content';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Children, cloneElement, ComponentProps, FC, ReactElement, useMemo, useState } from 'react';
2+
import classNames from 'classnames';
3+
4+
import { AccordionPanelContext } from './AccordionPanelContext';
5+
6+
export type AccordionPanelProps = {
7+
open?: boolean;
8+
flush?: boolean;
9+
};
10+
11+
export const AccordionPanel: FC<AccordionPanelProps> = ({ children, open, flush }) => {
12+
const [isOpen, setIsOpen] = useState(open);
13+
const items = useMemo(
14+
() =>
15+
Children.map(children as ReactElement<ComponentProps<'div' | 'button'>>[], (child) =>
16+
cloneElement(child, {
17+
className: classNames(child.props.className, { 'px-5 first:rounded-t-lg last:rounded-b-lg': !flush }),
18+
}),
19+
),
20+
[children, flush],
21+
);
22+
23+
return <AccordionPanelContext.Provider value={{ flush, isOpen, setIsOpen }}>{items}</AccordionPanelContext.Provider>;
24+
};
25+
26+
AccordionPanel.displayName = 'Accordion.Panel';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createContext, useContext } from 'react';
2+
3+
type AccordionPanelContext = {
4+
flush?: boolean;
5+
isOpen?: boolean;
6+
setIsOpen: (isOpen: boolean) => void;
7+
};
8+
9+
export const AccordionPanelContext = createContext<AccordionPanelContext | undefined>(undefined);
10+
11+
export function useAccordionContext(): AccordionPanelContext {
12+
const context = useContext(AccordionPanelContext);
13+
14+
if (!context) {
15+
throw new Error('useAccordionContext should be used within the AccordionPanelContext provider!');
16+
}
17+
18+
return context;
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ComponentProps, FC } from 'react';
2+
import classNames from 'classnames';
3+
import { HiChevronDown } from 'react-icons/hi';
4+
5+
import { useAccordionContext } from './AccordionPanelContext';
6+
7+
export type AccordionTitleProps = ComponentProps<'button'> & {
8+
arrowIcon?: FC<ComponentProps<'svg'>>;
9+
};
10+
11+
export const AccordionTitle: FC<AccordionTitleProps> = ({
12+
children,
13+
className,
14+
arrowIcon: ArrowIcon = HiChevronDown,
15+
...props
16+
}) => {
17+
const { flush, isOpen, setIsOpen } = useAccordionContext();
18+
19+
const onClick = () => setIsOpen(!isOpen);
20+
21+
return (
22+
<button
23+
{...props}
24+
type="button"
25+
className={classNames(
26+
'flex w-full items-center justify-between py-5 text-left font-medium text-gray-500 dark:text-gray-400',
27+
{
28+
'hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-800 dark:focus:ring-gray-800': !flush,
29+
'text-gray-900 dark:text-white': isOpen,
30+
'bg-gray-100 dark:bg-gray-800': isOpen && !flush,
31+
},
32+
className,
33+
)}
34+
onClick={onClick}
35+
>
36+
<h2>{children}</h2>
37+
<ArrowIcon className={classNames('h-6 w-6 shrink-0', { 'rotate-180': isOpen })} />
38+
</button>
39+
);
40+
};
41+
42+
AccordionTitle.displayName = 'Accordion.Title';

src/components/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './Alert';
2-
export * from './Accordion';
2+
export * from './accordion/Accordion';
33
export * from './Badge';
44
export * from './Breadcrumb';
55
export * from './Button';

src/pages/AccordionPage.tsx

+70-77
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,98 @@
1-
import { FC } from 'react';
1+
import { ComponentProps, FC } from 'react';
22
import { HiOutlineArrowCircleDown } from 'react-icons/hi';
33

4-
import { Accordion, AccordionItem } from '../components';
4+
import { Accordion } from '../components';
55
import { CodeExample, DemoPage } from './DemoPage';
66

77
const AccordionPage: FC = () => {
8-
const items: AccordionItem[] = [
9-
{
10-
open: true,
11-
title: 'What is Flowbite?',
12-
body: (
13-
<div>
14-
<p className="mb-2 text-gray-500 dark:text-gray-400">
15-
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
16-
dropdowns, modals, navbars, and more.
17-
</p>
18-
<p className="text-gray-500 dark:text-gray-400">
19-
Check out this guide to learn how to{' '}
8+
const panels = (icon?: FC<ComponentProps<'svg'>>) => [
9+
<Accordion.Panel key={0} open>
10+
<Accordion.Title arrowIcon={icon}>What is Flowbite?</Accordion.Title>
11+
<Accordion.Content>
12+
<p className="mb-2 text-gray-500 dark:text-gray-400">
13+
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
14+
dropdowns, modals, navbars, and more.
15+
</p>
16+
<p className="text-gray-500 dark:text-gray-400">
17+
Check out this guide to learn how to{' '}
18+
<a
19+
href="https://flowbite.com/docs/getting-started/introduction/"
20+
className="text-blue-600 hover:underline dark:text-blue-500"
21+
>
22+
get started
23+
</a>{' '}
24+
and start developing websites even faster with components on top of Tailwind CSS.
25+
</p>
26+
</Accordion.Content>
27+
</Accordion.Panel>,
28+
<Accordion.Panel key={1}>
29+
<Accordion.Title arrowIcon={icon}>Is there a Figma file available?</Accordion.Title>
30+
<Accordion.Content>
31+
<p className="mb-2 text-gray-500 dark:text-gray-400">
32+
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
33+
has a design equivalent in our Figma file.
34+
</p>
35+
<p className="text-gray-500 dark:text-gray-400">
36+
Check out the{' '}
37+
<a href="https://flowbite.com/figma/" className="text-blue-600 hover:underline dark:text-blue-500">
38+
Figma design system
39+
</a>{' '}
40+
based on the utility classes from Tailwind CSS and components from Flowbite.
41+
</p>
42+
</Accordion.Content>
43+
</Accordion.Panel>,
44+
<Accordion.Panel key={2}>
45+
<Accordion.Title arrowIcon={icon}>What are the differences between Flowbite and Tailwind UI?</Accordion.Title>
46+
<Accordion.Content>
47+
<p className="mb-2 text-gray-500 dark:text-gray-400">
48+
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
49+
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
50+
components, whereas Tailwind UI offers sections of pages.
51+
</p>
52+
<p className="mb-2 text-gray-500 dark:text-gray-400">
53+
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
54+
technical reason stopping you from using the best of two worlds.
55+
</p>
56+
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
57+
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
58+
<li>
59+
<a href="https://flowbite.com/pro/" className="text-blue-600 hover:underline dark:text-blue-500">
60+
Flowbite Pro
61+
</a>
62+
</li>
63+
<li>
2064
<a
21-
href="https://flowbite.com/docs/getting-started/introduction/"
65+
href="https://tailwindui.com/"
66+
rel="nofollow"
2267
className="text-blue-600 hover:underline dark:text-blue-500"
2368
>
24-
get started
25-
</a>{' '}
26-
and start developing websites even faster with components on top of Tailwind CSS.
27-
</p>
28-
</div>
29-
),
30-
},
31-
{
32-
title: 'Is there a Figma file available?',
33-
body: (
34-
<div>
35-
<p className="mb-2 text-gray-500 dark:text-gray-400">
36-
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
37-
has a design equivalent in our Figma file.
38-
</p>
39-
<p className="text-gray-500 dark:text-gray-400">
40-
Check out the{' '}
41-
<a href="https://flowbite.com/figma/" className="text-blue-600 hover:underline dark:text-blue-500">
42-
Figma design system
43-
</a>{' '}
44-
based on the utility classes from Tailwind CSS and components from Flowbite.
45-
</p>
46-
</div>
47-
),
48-
},
49-
{
50-
title: 'What are the differences between Flowbite and Tailwind UI?',
51-
body: (
52-
<div>
53-
<p className="mb-2 text-gray-500 dark:text-gray-400">
54-
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
55-
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
56-
components, whereas Tailwind UI offers sections of pages.
57-
</p>
58-
<p className="mb-2 text-gray-500 dark:text-gray-400">
59-
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
60-
technical reason stopping you from using the best of two worlds.
61-
</p>
62-
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
63-
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
64-
<li>
65-
<a href="https://flowbite.com/pro/" className="text-blue-600 hover:underline dark:text-blue-500">
66-
Flowbite Pro
67-
</a>
68-
</li>
69-
<li>
70-
<a
71-
href="https://tailwindui.com/"
72-
rel="nofollow"
73-
className="text-blue-600 hover:underline dark:text-blue-500"
74-
>
75-
Tailwind UI
76-
</a>
77-
</li>
78-
</ul>
79-
</div>
80-
),
81-
},
69+
Tailwind UI
70+
</a>
71+
</li>
72+
</ul>
73+
</Accordion.Content>
74+
</Accordion.Panel>,
8275
];
8376

8477
const examples: CodeExample[] = [
8578
{
8679
title: 'Default accordion',
87-
code: <Accordion items={items} />,
80+
code: <Accordion>{panels()}</Accordion>,
8881
codeClassName: 'dark:!bg-gray-900',
8982
},
9083
{
9184
title: 'Always open',
92-
code: <Accordion items={items} />,
85+
code: <Accordion>{panels()}</Accordion>,
9386
codeClassName: 'dark:!bg-gray-900',
9487
},
9588
{
9689
title: 'Flush accordion',
97-
code: <Accordion items={items} flush />,
90+
code: <Accordion flush>{panels()}</Accordion>,
9891
codeClassName: 'dark:!bg-gray-900',
9992
},
10093
{
10194
title: 'Arrow style',
102-
code: <Accordion items={items} arrowIcon={HiOutlineArrowCircleDown} />,
95+
code: <Accordion>{panels(HiOutlineArrowCircleDown)}</Accordion>,
10396
codeClassName: 'dark:!bg-gray-900',
10497
},
10598
];

0 commit comments

Comments
 (0)