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

[Feature/BAR-161] Tab 컴포넌트 Switcher 디자인 구현 #42

Merged
merged 9 commits into from
Jan 20, 2024
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' }}
>
Comment on lines +119 to +124
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dongkyun-dev @wonjin-dev
개인적으로 궁금한 부분인데
아이콘 상태에 따라 구분짓고 아이콘과 관련된 값을 그룹핑하기 위해 객체 prop으로 정의해두었는데 이렇게도 많이 사용하나요? 👀

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmswl98
저라면 굳이 객체로 그룹핑하지 않았을 것 같긴 합니다.

끄적이는
</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;
31 changes: 27 additions & 4 deletions src/components/Tabs/components/TabsTrigger.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
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)}
>
{!isActive && icon && <Icon icon={icon?.default} />}
dmswl98 marked this conversation as resolved.
Show resolved Hide resolved
{isActive && icon && <Icon icon={icon?.active} />}
{children}
</button>
{isActiveFilterTab ? (
<motion.div className={styles.underline} layoutId="underline" />
) : null}
{isActiveSwitcherTab ? (
<motion.div className={styles.handle} layoutId="handle" />
) : null}
</li>
);
};
Expand Down
Loading