Skip to content

Commit

Permalink
[Feature/�BAR-161] Tab 컴포넌트 Switcher 디자인 구현 (#42)
Browse files Browse the repository at this point in the history
* chore(package): framer-motion 추가

* chore(assets): 아이콘 추가

* feat(Tab): filter, switcher 타입 추가

* feat(Tab): tab 타입별 스토리 코드 추가

* fix: 옵셔널 타입으로 수정

* feat: Tabs.Trigger에 아이콘을 추가하는 경우, 아닌 경우의 스토리 코드 추가

* fix: 삼항 연산자에서 && 연산자로 수정

* refactor: 조건부 아이콘 렌더링 조건 수정
  • Loading branch information
dmswl98 authored Jan 20, 2024
1 parent 0c36dad commit 5359425
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 36 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@vanilla-extract/sprinkles": "^1.6.1",
"axios": "^1.6.2",
"clsx": "^2.0.0",
"framer-motion": "^10.18.0",
"next": "14.0.3",
"react": "^18",
"react-dom": "^18",
Expand Down
35 changes: 35 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/assets/icons/pencil-active.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/assets/icons/pencil-default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/icons/template-active.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/icons/template-default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
145 changes: 140 additions & 5 deletions src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { type ReactNode } from 'react';

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

import Tabs from '.';
import Tabs, { type TabsRootProps } from '.';
import { type TabsTriggerProps } from './components/TabsTrigger';

interface TabsMeta extends TabsRootProps, TabsTriggerProps {
tabListChildrenProp: ReactNode;
tabContentChildrenProp: ReactNode;
tabContentValueProp: string;
}

const CATEGORY = {
TABS: 'Tabs',
TABS_LIST: 'Tabs.List',
TABS_TRIGGER: 'Tabs.Trigger',
TABS_CONTENT: 'Tabs.Content',
};

const COMPONENT_DESCRIPTION = `
- \`<Tabs />\`: 모든 컴포넌트에 대한 컨텍스트와 상태를 제공합니다.
Expand All @@ -10,13 +26,61 @@ const COMPONENT_DESCRIPTION = `
- \`<Tabs.Content />\`: 선택된 탭에 해당되는 컨텐츠를 보여줍니다.
`;

const meta: Meta<typeof Tabs> = {
const meta: Meta<TabsMeta> = {
title: 'Components/Tabs',
component: Tabs,
tags: ['autodocs'],
argTypes: {
type: {
description: 'Tabs 컴포넌트의 종류',
table: {
category: CATEGORY.TABS,
},
},
defaultValue: {
description: 'Tabs 컴포넌트의 초기에 활성화될 탭의 기본값',
table: {
category: CATEGORY.TABS,
},
},
tabListChildrenProp: {
name: 'children',
description: '여러 개의 Tabs.Trigger 컴포넌트를 감싸줍니다.',
table: {
type: { summary: 'ReactNode' },
category: CATEGORY.TABS_LIST,
},
},
value: {
description:
'Tabs.Trigger 컴포넌트와 Tabs.Content 컴포넌트를 연결하는 고유한 값',
table: {
type: { summary: 'string' },
category: CATEGORY.TABS_TRIGGER,
},
},
icon: {
description: 'Tabs.Trigger 컴포넌트 내 아이콘',
table: {
type: { summary: 'Icons' },
category: CATEGORY.TABS_TRIGGER,
},
},
tabContentChildrenProp: {
name: 'children',
description: '활성화되었을 때 보여줄 컨텐츠를 감싸줍니다.',
table: {
type: { summary: 'ReactNode' },
category: CATEGORY.TABS_CONTENT,
},
},
tabContentValueProp: {
name: 'value',
description:
'Tabs.Trigger 컴포넌트와 Tabs.Content 컴포넌트를 연결하는 고유한 값',
table: {
type: { summary: 'string' },
category: CATEGORY.TABS_CONTENT,
},
},
},
parameters: {
Expand All @@ -33,9 +97,53 @@ export default meta;

type Story = StoryObj<typeof Tabs>;

export const Basic: Story = {
export const Filter: Story = {
render: () => (
<Tabs type="filter" defaultValue="끄적이는">
<Tabs.List>
<Tabs.Trigger value="끄적이는">끄적이는</Tabs.Trigger>
<Tabs.Trigger value="참고하는">참고하는</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="끄적이는">
<div>끄적이는 내용</div>
</Tabs.Content>
<Tabs.Content value="참고하는">
<div>참고하는 내용</div>
</Tabs.Content>
</Tabs>
),
};

export const FilterWithIcon: Story = {
render: () => (
<Tabs defaultValue="끄적이는">
<Tabs type="filter" defaultValue="끄적이는">
<Tabs.List>
<Tabs.Trigger
value="끄적이는"
icon={{ default: 'pencilDefault', active: 'pencilActive' }}
>
끄적이는
</Tabs.Trigger>
<Tabs.Trigger
value="참고하는"
icon={{ default: 'templateDefault', active: 'templateActive' }}
>
참고하는
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="끄적이는">
<div>끄적이는 내용</div>
</Tabs.Content>
<Tabs.Content value="참고하는">
<div>참고하는 내용</div>
</Tabs.Content>
</Tabs>
),
};

export const Switcher: Story = {
render: () => (
<Tabs type="switcher" defaultValue="끄적이는">
<Tabs.List>
<Tabs.Trigger value="끄적이는">끄적이는</Tabs.Trigger>
<Tabs.Trigger value="참고하는">참고하는</Tabs.Trigger>
Expand All @@ -49,3 +157,30 @@ export const Basic: Story = {
</Tabs>
),
};

export const SwitcherWithIcon: Story = {
render: () => (
<Tabs type="switcher" defaultValue="끄적이는">
<Tabs.List>
<Tabs.Trigger
value="끄적이는"
icon={{ default: 'pencilDefault', active: 'pencilActive' }}
>
끄적이는
</Tabs.Trigger>
<Tabs.Trigger
value="참고하는"
icon={{ default: 'templateDefault', active: 'templateActive' }}
>
참고하는
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="끄적이는">
<div>끄적이는 내용</div>
</Tabs.Content>
<Tabs.Content value="참고하는">
<div>참고하는 내용</div>
</Tabs.Content>
</Tabs>
),
};
2 changes: 1 addition & 1 deletion src/components/Tabs/components/TabsContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { PropsWithChildren } from 'react';

import { useTabsContext } from '../../../hooks/useTabsContext';

interface TabsContentProps {
export interface TabsContentProps {
value: string;
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/Tabs/components/TabsList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { PropsWithChildren } from 'react';

import { useTabsContext } from '@hooks/useTabsContext';

import * as styles from '../style.css';

const TabsList = ({ children }: PropsWithChildren) => {
return <ul className={styles.tabsList}>{children}</ul>;
const { type } = useTabsContext();

return <ul className={styles.tabsList({ type })}>{children}</ul>;
};

export default TabsList;
37 changes: 32 additions & 5 deletions src/components/Tabs/components/TabsTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
import type { PropsWithChildren } from 'react';
import { type PropsWithChildren } from 'react';
import clsx from 'clsx';
import { motion } from 'framer-motion';

import Icon from '@components/Icon';
import { type Icons } from '@constants/icon';

import { useTabsContext } from '../../../hooks/useTabsContext';
import * as styles from '../style.css';

interface TabsTriggerProps {
export interface TabsTriggerProps {
value: string;
icon?: { default: Icons; active: Icons };
}

const TabsTrigger = ({
children,
value,
icon,
}: PropsWithChildren<TabsTriggerProps>) => {
const { selectedTab, onSelectTab } = useTabsContext();
const { type, selectedTab, onSelectTab } = useTabsContext();

const isActive = selectedTab === value;
const isActiveFilterTab = isActive && type === 'filter';
const isActiveSwitcherTab = isActive && type === 'switcher';

return (
<li>
<li className={styles.tabItem}>
<button
className={styles.tabsTrigger({ isActive: selectedTab === value })}
className={clsx(
styles.tabsTrigger({ type }),
isActiveFilterTab && styles.isActiveFilter,
isActiveSwitcherTab && styles.isActiveSwitcher,
)}
onClick={() => onSelectTab(value)}
>
{icon &&
(!isActive ? (
<Icon icon={icon?.default} />
) : (
<Icon icon={icon?.active} />
))}
{children}
</button>
{isActiveFilterTab && (
<motion.div className={styles.underline} layoutId="underline" />
)}
{isActiveSwitcherTab && (
<motion.div className={styles.handle} layoutId="handle" />
)}
</li>
);
};
Expand Down
Loading

0 comments on commit 5359425

Please sign in to comment.