From 74b1db7abeeea5e5fb42faea397aeec870951287 Mon Sep 17 00:00:00 2001 From: evdmatvey Date: Mon, 15 Jul 2024 20:53:12 +0700 Subject: [PATCH] Add accordion ui component --- .storybook/index.css | 2 +- src/shared/ui/Accordion/index.ts | 1 + .../ui/Accordion/model/accordion.context.ts | 9 + .../ui/Accordion/model/useAccordionContext.ts | 9 + .../Accordion/stories/Accordion.stories.tsx | 158 ++++++++++++++++++ .../ui/Accordion/Accordion.module.css | 5 + .../ui/Accordion/ui/Accordion/Accordion.tsx | 32 ++++ .../ui/AccordionItem/AccordionItem.module.css | 123 ++++++++++++++ .../ui/AccordionItem/AccordionItem.tsx | 53 ++++++ .../ui/Typography/ui/Typography.module.css | 4 +- 10 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 src/shared/ui/Accordion/index.ts create mode 100644 src/shared/ui/Accordion/model/accordion.context.ts create mode 100644 src/shared/ui/Accordion/model/useAccordionContext.ts create mode 100644 src/shared/ui/Accordion/stories/Accordion.stories.tsx create mode 100644 src/shared/ui/Accordion/ui/Accordion/Accordion.module.css create mode 100644 src/shared/ui/Accordion/ui/Accordion/Accordion.tsx create mode 100644 src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.module.css create mode 100644 src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.tsx diff --git a/.storybook/index.css b/.storybook/index.css index 433c0de..79e99e1 100644 --- a/.storybook/index.css +++ b/.storybook/index.css @@ -1,4 +1,4 @@ -@import url(../src/app/styles/variables.css); +@import url(../src/app/styles/globals.css); * { box-sizing: border-box; diff --git a/src/shared/ui/Accordion/index.ts b/src/shared/ui/Accordion/index.ts new file mode 100644 index 0000000..a679fa7 --- /dev/null +++ b/src/shared/ui/Accordion/index.ts @@ -0,0 +1 @@ +export { default } from './ui/Accordion/Accordion'; diff --git a/src/shared/ui/Accordion/model/accordion.context.ts b/src/shared/ui/Accordion/model/accordion.context.ts new file mode 100644 index 0000000..80a6c2a --- /dev/null +++ b/src/shared/ui/Accordion/model/accordion.context.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react'; + +export interface AccordionContext { + size?: 'large' | 'standard'; + activeId?: number; + toggleAccordion?: (id: number) => void; +} + +export const AccordionContext = createContext({}); diff --git a/src/shared/ui/Accordion/model/useAccordionContext.ts b/src/shared/ui/Accordion/model/useAccordionContext.ts new file mode 100644 index 0000000..9007906 --- /dev/null +++ b/src/shared/ui/Accordion/model/useAccordionContext.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { AccordionContext } from './accordion.context'; + +export const useAccordionContext = () => { + const { activeId, toggleAccordion, size } = + useContext(AccordionContext); + + return { activeId, toggleAccordion, size }; +}; diff --git a/src/shared/ui/Accordion/stories/Accordion.stories.tsx b/src/shared/ui/Accordion/stories/Accordion.stories.tsx new file mode 100644 index 0000000..9a20d5e --- /dev/null +++ b/src/shared/ui/Accordion/stories/Accordion.stories.tsx @@ -0,0 +1,158 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import HomeIcon from '@/shared/ui/Icons/ui/HomeIcon'; +import Accordion from '../ui/Accordion/Accordion'; + +const meta: Meta = { + component: Accordion, + title: 'Components/Accordion', + tags: ['autodocs'], + parameters: { + docs: { + subtitle: + 'Accordion component that includes all cases in our design layout', + description: { + component: + 'This component is an implemented compound component pattern. `Accordion` is the wrapper component, `Accordion.Item` - the component of the element', + }, + }, + }, + argTypes: { + defaultActiveId: { control: 'number' }, + size: { control: 'radio', options: ['standard', 'large'] }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + name: 'Accordion', + args: { + defaultActiveId: 1, + size: 'standard', + }, + parameters: { + docs: { + description: { + story: + 'You should use the option that is specified in the design layout', + }, + }, + }, + render: (args) => ( +
+ + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Iste, + provident accusantium, deleniti ratione minima repellat, sapiente + soluta earum expedita recusandae sunt enim deserunt velit itaque. + + + Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorum + dolorem, similique hic molestiae, non tenetur possimus doloribus neque + quibusdam at officia dignissimos pariatur nostrum. Assumenda minima + placeat quia quas quam! + + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Pariatur, + maxime. + + +
+ ), +}; + +export const WithIcon: Story = { + name: 'Accordion with icon', + args: { + defaultActiveId: 1, + size: 'standard', + }, + parameters: { + docs: { + description: { + story: 'You can see what the Accordion component looks like with icon', + }, + }, + }, + render: (args) => ( +
+ + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Iste, + provident accusantium, deleniti ratione minima repellat, sapiente + soluta earum expedita recusandae sunt enim deserunt velit itaque. + + } + id={2} + title="Lorem ipsum dolor sit amet consectetur adipisicing elit." + > + Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorum + dolorem, similique hic molestiae, non tenetur possimus doloribus neque + quibusdam at officia dignissimos pariatur nostrum. Assumenda minima + placeat quia quas quam! + + } + id={3} + title="Lorem ipsum dolor sit amet." + > + Lorem ipsum dolor sit amet consectetur adipisicing elit. Pariatur, + maxime. + + +
+ ), +}; + +export const DarkMode: Story = { + name: 'Accordion on dark mode', + args: { + defaultActiveId: 1, + size: 'standard', + }, + parameters: { + docs: { + description: { + story: + 'You can see what the Accordion component looks like with dark mode', + }, + }, + backgrounds: { + default: 'dark', + }, + }, + render: (args) => ( +
+ + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Iste, + provident accusantium, deleniti ratione minima repellat, sapiente + soluta earum expedita recusandae sunt enim deserunt velit itaque. + + } + id={2} + title="Lorem ipsum dolor sit amet consectetur adipisicing elit." + > + Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorum + dolorem, similique hic molestiae, non tenetur possimus doloribus neque + quibusdam at officia dignissimos pariatur nostrum. Assumenda minima + placeat quia quas quam! + + } + id={3} + title="Lorem ipsum dolor sit amet." + > + Lorem ipsum dolor sit amet consectetur adipisicing elit. Pariatur, + maxime. + + +
+ ), +}; diff --git a/src/shared/ui/Accordion/ui/Accordion/Accordion.module.css b/src/shared/ui/Accordion/ui/Accordion/Accordion.module.css new file mode 100644 index 0000000..ef524ce --- /dev/null +++ b/src/shared/ui/Accordion/ui/Accordion/Accordion.module.css @@ -0,0 +1,5 @@ +.root { + display: flex; + flex-direction: column; + gap: 20px; +} diff --git a/src/shared/ui/Accordion/ui/Accordion/Accordion.tsx b/src/shared/ui/Accordion/ui/Accordion/Accordion.tsx new file mode 100644 index 0000000..bf260c0 --- /dev/null +++ b/src/shared/ui/Accordion/ui/Accordion/Accordion.tsx @@ -0,0 +1,32 @@ +import { type FC, type ReactNode, useState } from 'react'; +import { AccordionContext } from '../../model/accordion.context'; +import AccordionItem, { + AccordionItemProps, +} from '../AccordionItem/AccordionItem'; +import styles from './Accordion.module.css'; + +interface AccordionProps { + children: ReactNode; + defaultActiveId: number; + size: 'standard' | 'large'; +} + +const Accordion: FC & { Item: FC } = ({ + size = 'standard', + defaultActiveId = 1, + children, +}) => { + const [activeId, setActiveId] = useState(defaultActiveId); + + const toggleAccordion = (id: number) => setActiveId(id); + + return ( + +
{children}
+
+ ); +}; + +Accordion.Item = AccordionItem; + +export default Accordion; diff --git a/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.module.css b/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.module.css new file mode 100644 index 0000000..4e5b417 --- /dev/null +++ b/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.module.css @@ -0,0 +1,123 @@ +.root { + padding-bottom: 20px; + display: flex; + flex-direction: column; + gap: 16px; + border-bottom: 1px solid var(--gray-200); +} + +[data-theme='dark'] .root { + border-bottom-color: var(--gray-700); +} + +.header { + display: flex; + align-items: center; + gap: 16px; + color: var(--gray-900); + cursor: pointer; +} + +[data-theme='dark'] .header { + color: var(--white-100); +} + +.header svg path { + fill: var(--gray-900); +} + +[data-theme='dark'] .header svg path { + fill: var(--white-100); +} + +.header .title { + flex-grow: 1; +} + +.header h3 { + display: inline-block; + border-bottom: 2px solid transparent; + transition: all 0.15s ease-in; +} + +.title { + display: flex; + align-items: center; + gap: 12px; +} + +.root.standard .title span { + width: 20px; + height: 20px; +} + +.root.large .title span { + width: 24px; + height: 24px; +} + +.header:hover h3 { + border-bottom-color: var(--gray-900); +} + +[data-theme='dark'] .header:hover h3 { + border-bottom-color: var(--white-100); +} + +.header span { + width: 16px; + height: 16px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.root.standard .header span { + width: 16px; + height: 16px; +} + +.root.large .header span { + width: 20px; + height: 20px; +} + +.content { + overflow: hidden; + animation: flipdown 0.5s ease-out forwards; + color: var(--gray-900); +} + +[data-theme='dark'] .content { + color: var(--gray-300); +} + +@keyframes flipdown { + 0% { + opacity: 0; + transform-origin: top center; + transform: rotateX(-90deg); + } + + 5% { + opacity: 1; + } + + 80% { + transform: rotateX(8deg); + } + + 83% { + transform: rotateX(6deg); + } + + 92% { + transform: rotateX(-3deg); + } + + 100% { + transform-origin: top center; + transform: rotateX(0deg); + } +} diff --git a/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.tsx b/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.tsx new file mode 100644 index 0000000..fcf3018 --- /dev/null +++ b/src/shared/ui/Accordion/ui/AccordionItem/AccordionItem.tsx @@ -0,0 +1,53 @@ +import clsx from 'clsx'; +import { type ReactNode } from 'react'; +import { MinusIcon, PlusIcon } from '@/shared/ui/Icons'; +import Typography from '@/shared/ui/Typography'; +import { useAccordionContext } from '../../model/useAccordionContext'; +import styles from './AccordionItem.module.css'; + +export interface AccordionItemProps { + title: string; + id: number; + children: ReactNode; + icon?: ReactNode; +} + +const AccordionItem = ({ children, id, title, icon }: AccordionItemProps) => { + const { activeId, toggleAccordion, size } = useAccordionContext(); + + const classes = clsx(styles.root, styles[size ?? 'standard']); + const isActive = activeId === id; + + const toggleAccordionHandler = () => { + if (toggleAccordion) toggleAccordion(id); + }; + + return ( +
+
+
+ {icon && {icon}} + + {title} + +
+ {isActive ? : } +
+ {isActive && ( +
+ + {children} + +
+ )} +
+ ); +}; + +export default AccordionItem; diff --git a/src/shared/ui/Typography/ui/Typography.module.css b/src/shared/ui/Typography/ui/Typography.module.css index 514521f..2b41e8c 100644 --- a/src/shared/ui/Typography/ui/Typography.module.css +++ b/src/shared/ui/Typography/ui/Typography.module.css @@ -124,8 +124,7 @@ .heading3, .heading4, .heading5, -.heading6, -.s { +.heading6 { font-weight: 600; } @@ -137,6 +136,7 @@ .l, .xl, +.s, .regular { font-weight: 400; }