Skip to content

Commit

Permalink
feat: refactor Accordion component
Browse files Browse the repository at this point in the history
  • Loading branch information
bacali95 committed Mar 14, 2022
1 parent c4aad36 commit 16482e3
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 142 deletions.
64 changes: 0 additions & 64 deletions src/components/Accordion.tsx

This file was deleted.

35 changes: 35 additions & 0 deletions src/components/accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Children, cloneElement, FC, ReactElement, useMemo } from 'react';
import { AccordionPanel, AccordionPanelProps } from './AccordionPanel';
import { AccordionTitle } from './AccordionTitle';
import { AccordionContent } from './AccordionContent';
import classNames from 'classnames';

export type AccordionProps = {
flush?: boolean;
};

const AccordionComponent: FC<AccordionProps> = ({ children, flush }) => {
const panels = useMemo(
() => Children.map(children as ReactElement<AccordionPanelProps>[], (child) => cloneElement(child, { flush })),
[children, flush],
);

return (
<div
className={classNames('divide-y divide-gray-200 border-gray-200 dark:divide-gray-700 dark:border-gray-700', {
'rounded-lg border': !flush,
'border-b': flush,
})}
>
{panels}
</div>
);
};

AccordionComponent.displayName = 'Accordion';

export const Accordion = Object.assign(AccordionComponent, {
Panel: AccordionPanel,
Title: AccordionTitle,
Content: AccordionContent,
});
16 changes: 16 additions & 0 deletions src/components/accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ComponentProps, FC } from 'react';
import classNames from 'classnames';

import { useAccordionContext } from './AccordionPanelContext';

export const AccordionContent: FC<ComponentProps<'div'>> = ({ children, className, ...props }) => {
const { isOpen } = useAccordionContext();

return isOpen ? (
<div {...props} className={classNames('py-5 dark:bg-gray-900', className)}>
{children}
</div>
) : null;
};

AccordionContent.displayName = 'Accordion.Content';
26 changes: 26 additions & 0 deletions src/components/accordion/AccordionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Children, cloneElement, ComponentProps, FC, ReactElement, useMemo, useState } from 'react';
import classNames from 'classnames';

import { AccordionPanelContext } from './AccordionPanelContext';

export type AccordionPanelProps = {
open?: boolean;
flush?: boolean;
};

export const AccordionPanel: FC<AccordionPanelProps> = ({ children, open, flush }) => {
const [isOpen, setIsOpen] = useState(open);
const items = useMemo(
() =>
Children.map(children as ReactElement<ComponentProps<'div' | 'button'>>[], (child) =>
cloneElement(child, {
className: classNames(child.props.className, { 'px-5 first:rounded-t-lg last:rounded-b-lg': !flush }),
}),
),
[children, flush],
);

return <AccordionPanelContext.Provider value={{ flush, isOpen, setIsOpen }}>{items}</AccordionPanelContext.Provider>;
};

AccordionPanel.displayName = 'Accordion.Panel';
19 changes: 19 additions & 0 deletions src/components/accordion/AccordionPanelContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContext, useContext } from 'react';

type AccordionPanelContext = {
flush?: boolean;
isOpen?: boolean;
setIsOpen: (isOpen: boolean) => void;
};

export const AccordionPanelContext = createContext<AccordionPanelContext | undefined>(undefined);

export function useAccordionContext(): AccordionPanelContext {
const context = useContext(AccordionPanelContext);

if (!context) {
throw new Error('useAccordionContext should be used within the AccordionPanelContext provider!');
}

return context;
}
42 changes: 42 additions & 0 deletions src/components/accordion/AccordionTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ComponentProps, FC } from 'react';
import classNames from 'classnames';
import { HiChevronDown } from 'react-icons/hi';

import { useAccordionContext } from './AccordionPanelContext';

export type AccordionTitleProps = ComponentProps<'button'> & {
arrowIcon?: FC<ComponentProps<'svg'>>;
};

export const AccordionTitle: FC<AccordionTitleProps> = ({
children,
className,
arrowIcon: ArrowIcon = HiChevronDown,
...props
}) => {
const { flush, isOpen, setIsOpen } = useAccordionContext();

const onClick = () => setIsOpen(!isOpen);

return (
<button
{...props}
type="button"
className={classNames(
'flex w-full items-center justify-between py-5 text-left font-medium text-gray-500 dark:text-gray-400',
{
'hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-800 dark:focus:ring-gray-800': !flush,
'text-gray-900 dark:text-white': isOpen,
'bg-gray-100 dark:bg-gray-800': isOpen && !flush,
},
className,
)}
onClick={onClick}
>
<h2>{children}</h2>
<ArrowIcon className={classNames('h-6 w-6 shrink-0', { 'rotate-180': isOpen })} />
</button>
);
};

AccordionTitle.displayName = 'Accordion.Title';
2 changes: 1 addition & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './Alert';
export * from './Accordion';
export * from './accordion/Accordion';
export * from './Badge';
export * from './Breadcrumb';
export * from './Button';
Expand Down
147 changes: 70 additions & 77 deletions src/pages/AccordionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,98 @@
import { FC } from 'react';
import { ComponentProps, FC } from 'react';
import { HiOutlineArrowCircleDown } from 'react-icons/hi';

import { Accordion, AccordionItem } from '../components';
import { Accordion } from '../components';
import { CodeExample, DemoPage } from './DemoPage';

const AccordionPage: FC = () => {
const items: AccordionItem[] = [
{
open: true,
title: 'What is Flowbite?',
body: (
<div>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
dropdowns, modals, navbars, and more.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out this guide to learn how to{' '}
const panels = (icon?: FC<ComponentProps<'svg'>>) => [
<Accordion.Panel key={0} open>
<Accordion.Title arrowIcon={icon}>What is Flowbite?</Accordion.Title>
<Accordion.Content>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
dropdowns, modals, navbars, and more.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out this guide to learn how to{' '}
<a
href="https://flowbite.com/docs/getting-started/introduction/"
className="text-blue-600 hover:underline dark:text-blue-500"
>
get started
</a>{' '}
and start developing websites even faster with components on top of Tailwind CSS.
</p>
</Accordion.Content>
</Accordion.Panel>,
<Accordion.Panel key={1}>
<Accordion.Title arrowIcon={icon}>Is there a Figma file available?</Accordion.Title>
<Accordion.Content>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
has a design equivalent in our Figma file.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out the{' '}
<a href="https://flowbite.com/figma/" className="text-blue-600 hover:underline dark:text-blue-500">
Figma design system
</a>{' '}
based on the utility classes from Tailwind CSS and components from Flowbite.
</p>
</Accordion.Content>
</Accordion.Panel>,
<Accordion.Panel key={2}>
<Accordion.Title arrowIcon={icon}>What are the differences between Flowbite and Tailwind UI?</Accordion.Title>
<Accordion.Content>
<p className="mb-2 text-gray-500 dark:text-gray-400">
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
components, whereas Tailwind UI offers sections of pages.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
technical reason stopping you from using the best of two worlds.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
<li>
<a href="https://flowbite.com/pro/" className="text-blue-600 hover:underline dark:text-blue-500">
Flowbite Pro
</a>
</li>
<li>
<a
href="https://flowbite.com/docs/getting-started/introduction/"
href="https://tailwindui.com/"
rel="nofollow"
className="text-blue-600 hover:underline dark:text-blue-500"
>
get started
</a>{' '}
and start developing websites even faster with components on top of Tailwind CSS.
</p>
</div>
),
},
{
title: 'Is there a Figma file available?',
body: (
<div>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
has a design equivalent in our Figma file.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out the{' '}
<a href="https://flowbite.com/figma/" className="text-blue-600 hover:underline dark:text-blue-500">
Figma design system
</a>{' '}
based on the utility classes from Tailwind CSS and components from Flowbite.
</p>
</div>
),
},
{
title: 'What are the differences between Flowbite and Tailwind UI?',
body: (
<div>
<p className="mb-2 text-gray-500 dark:text-gray-400">
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
components, whereas Tailwind UI offers sections of pages.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
technical reason stopping you from using the best of two worlds.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
<li>
<a href="https://flowbite.com/pro/" className="text-blue-600 hover:underline dark:text-blue-500">
Flowbite Pro
</a>
</li>
<li>
<a
href="https://tailwindui.com/"
rel="nofollow"
className="text-blue-600 hover:underline dark:text-blue-500"
>
Tailwind UI
</a>
</li>
</ul>
</div>
),
},
Tailwind UI
</a>
</li>
</ul>
</Accordion.Content>
</Accordion.Panel>,
];

const examples: CodeExample[] = [
{
title: 'Default accordion',
code: <Accordion items={items} />,
code: <Accordion>{panels()}</Accordion>,
codeClassName: 'dark:!bg-gray-900',
},
{
title: 'Always open',
code: <Accordion items={items} />,
code: <Accordion>{panels()}</Accordion>,
codeClassName: 'dark:!bg-gray-900',
},
{
title: 'Flush accordion',
code: <Accordion items={items} flush />,
code: <Accordion flush>{panels()}</Accordion>,
codeClassName: 'dark:!bg-gray-900',
},
{
title: 'Arrow style',
code: <Accordion items={items} arrowIcon={HiOutlineArrowCircleDown} />,
code: <Accordion>{panels(HiOutlineArrowCircleDown)}</Accordion>,
codeClassName: 'dark:!bg-gray-900',
},
];
Expand Down

0 comments on commit 16482e3

Please sign in to comment.