Skip to content

Commit

Permalink
[Feature/BAR-53] Tabs 컴포넌트 구현 (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmswl98 authored Jan 1, 2024
1 parent 12dfa2a commit 4138a56
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 4 deletions.
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/react';
import '@/src/styles/global.css';

const preview: Preview = {
parameters: {
Expand Down
14 changes: 10 additions & 4 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import type { AppProps } from 'next/app';

import '@/src/styles/global.css';

import Layout from '@/src/components/Layout/Layout';
import TanstackQueryProvider from '@/src/components/Providers/TanstackQueryProvider';
import Toast from '@/src/components/Toast/Toast';
import { pretendard } from '@/src/styles/font';

const App = ({ Component, pageProps }: AppProps) => {
return (
<TanstackQueryProvider dehydratedState={pageProps.dehydratedState}>
<Component {...pageProps} />
<Toast />
</TanstackQueryProvider>
<main className={pretendard.className}>
<TanstackQueryProvider dehydratedState={pageProps.dehydratedState}>
<Layout>
<Component {...pageProps} />
<Toast />
</Layout>
</TanstackQueryProvider>
</main>
);
};

Expand Down
Binary file added public/fonts/PretendardVariable.woff2
Binary file not shown.
7 changes: 7 additions & 0 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PropsWithChildren } from 'react';

const Layout = ({ children }: PropsWithChildren) => {
return <>{children}</>;
};

export default Layout;
51 changes: 51 additions & 0 deletions src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { StoryObj } from '@storybook/react';
import { type Meta } from '@storybook/react';

import Tabs from './Tabs';

const COMPONENT_DESCRIPTION = `
- \`<Tabs />\`: 모든 컴포넌트에 대한 컨텍스트와 상태를 제공합니다.
- \`<Tabs.List />\`: \`<Tabs.Trigger />\` 컴포넌트들을 위한 Wrapper 컴포넌트입니다.
- \`<Tabs.Trigger />\`: 선택된 탭에 해당되는 컨텐츠를 표시하기 위한 버튼입니다.
- \`<Tabs.Content />\`: 선택된 탭에 해당되는 컨텐츠를 보여줍니다.
`;

const meta: Meta<typeof Tabs> = {
title: 'Components/Tabs',
component: Tabs,
tags: ['autodocs'],
argTypes: {
defaultValue: {
description: 'Tabs 컴포넌트의 초기에 활성화될 탭의 기본값',
},
},
parameters: {
componentSubtitle: '여러 내용을 한 화면에서 전환하며 볼 수 있는 컴포넌트',
docs: {
description: {
component: COMPONENT_DESCRIPTION,
},
},
},
};

export default meta;

type Story = StoryObj<typeof Tabs>;

export const Basic: Story = {
render: () => (
<Tabs 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>
),
};
41 changes: 41 additions & 0 deletions src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createContext, type PropsWithChildren, useState } from 'react';

import TabsContent from './TabsContent';
import TabsList from './TabsList';
import TabsTrigger from './TabsTrigger';

interface TabsContextProps {
selectedTab: string;
onSelectTab: (selectedTab: string) => void;
}

export interface TabsRootProps {
defaultValue: string;
}

export const TabsContext = createContext<TabsContextProps | null>(null);

const TabsRoot = ({
children,
defaultValue,
}: PropsWithChildren<TabsRootProps>) => {
const [selectedTab, setSelectedTab] = useState(defaultValue);

const handleTabSelect = (selectedTab: string) => {
setSelectedTab(selectedTab);
};

return (
<TabsContext.Provider value={{ selectedTab, onSelectTab: handleTabSelect }}>
{children}
</TabsContext.Provider>
);
};

const Tabs = Object.assign(TabsRoot, {
List: TabsList,
Trigger: TabsTrigger,
Content: TabsContent,
});

export default Tabs;
18 changes: 18 additions & 0 deletions src/components/Tabs/TabsContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { PropsWithChildren } from 'react';

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

interface TabsContentProps {
value: string;
}

const TabsContent = ({
children,
value,
}: PropsWithChildren<TabsContentProps>) => {
const { selectedTab } = useTabsContext();

return <>{selectedTab === value ? children : null}</>;
};

export default TabsContent;
9 changes: 9 additions & 0 deletions src/components/Tabs/TabsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PropsWithChildren } from 'react';

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

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

export default TabsList;
28 changes: 28 additions & 0 deletions src/components/Tabs/TabsTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { PropsWithChildren } from 'react';

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

interface TabsTriggerProps {
value: string;
}

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

return (
<li>
<button
className={styles.tabsTrigger({ isActive: selectedTab === value })}
onClick={() => onSelectTab(value)}
>
{children}
</button>
</li>
);
};

export default TabsTrigger;
38 changes: 38 additions & 0 deletions src/components/Tabs/style.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';

import { COLORS } from '@/src/styles/tokens';
import * as utils from '@/src/styles/utils.css';

export const tabsList = style([
utils.flexCenter,
{
padding: '16px 0 12px',
gap: '40px',
backgroundColor: COLORS['Grey/White'],
},
]);

export const tabsTrigger = recipe({
base: {
fontSize: '15px',
fontWeight: 500,
lineHeight: '18px',
letterSpacing: '0px',
paddingBottom: '4px',
borderBottom: '2px solid transparent',
transition: 'all 150ms ease-in-out',

':hover': {
fontWeight: 700,
},
},
variants: {
isActive: {
true: {
fontWeight: 700,
borderBottomColor: '#121212',
},
},
},
});
13 changes: 13 additions & 0 deletions src/hooks/useTabsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useContext } from 'react';

import { TabsContext } from '../components/Tabs/Tabs';

export const useTabsContext = () => {
const ctx = useContext(TabsContext);

if (!ctx) {
throw new Error('useTabsContext hook must be used within a Tabs component');
}

return ctx;
};
24 changes: 24 additions & 0 deletions src/styles/font.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import localFont from 'next/font/local';

export const pretendard = localFont({
src: '../../public/fonts/PretendardVariable.woff2',
display: 'swap',
preload: true,
fallback: [
'Pretendard Variable',
'Pretendard',
'-apple-system',
'BlinkMacSystemFont',
'system-ui',
'Roboto',
'Helvetica Neue',
'Segoe UI',
'Apple SD Gothic Neo',
'Noto Sans KR',
'Malgun Gothic',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'sans-serif',
],
});

0 comments on commit 4138a56

Please sign in to comment.