Skip to content

Commit

Permalink
[Feature] Tab 컴포넌트 구현 (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
SeieunYoo authored Oct 12, 2024
1 parent 185475a commit 7b19f05
Show file tree
Hide file tree
Showing 16 changed files with 713 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-pumas-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wowds-ui": patch
---

Tab 컴포넌트를 구현합니다.
20 changes: 20 additions & 0 deletions apps/wow-docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import RadioButton from "wowds-ui/RadioButton";
import RadioGroup from "wowds-ui/RadioGroup";
import SearchBar from "wowds-ui/SearchBar";
import Switch from "wowds-ui/Switch";
import Tabs from "wowds-ui/Tabs";
import TabsContent from "wowds-ui/TabsContent";
import TabsItem from "wowds-ui/TabsItem";
import TabsList from "wowds-ui/TabsList";

const Home = () => {
return (
Expand Down Expand Up @@ -43,6 +47,22 @@ const Home = () => {
<Switch label="switch4" value="switch4" />
</MultiGroup>
<SearchBar />
<Tabs>
<TabsList>
<TabsItem value="1">첫번째첫번째첫번째첫번째</TabsItem>
<TabsItem value="2">두 번째</TabsItem>
<TabsItem value="3">세 번쨰</TabsItem>
</TabsList>
<TabsContent value="1">
<span>첫번째 탭</span>
</TabsContent>
<TabsContent value="2">
<span>두번째 탭</span>
</TabsContent>
<TabsContent value="3">
<span>세번째 탭</span>
</TabsContent>
</Tabs>
</>
);
};
Expand Down
20 changes: 20 additions & 0 deletions packages/wow-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@
"require": "./dist/Tag.cjs",
"import": "./dist/Tag.js"
},
"./Tabs": {
"types": "./dist/components/Tabs/index.d.ts",
"require": "./dist/Tabs.cjs",
"import": "./dist/Tabs.js"
},
"./TabsContent": {
"types": "./dist/components/Tabs/TabsContent.d.ts",
"require": "./dist/TabsContent.cjs",
"import": "./dist/TabsContent.js"
},
"./TabsItem": {
"types": "./dist/components/Tabs/TabsItem.d.ts",
"require": "./dist/TabsItem.cjs",
"import": "./dist/TabsItem.js"
},
"./TabsList": {
"types": "./dist/components/Tabs/TabsList.d.ts",
"require": "./dist/TabsList.cjs",
"import": "./dist/TabsList.js"
},
"./Switch": {
"types": "./dist/components/Switch/index.d.ts",
"require": "./dist/Switch.cjs",
Expand Down
4 changes: 4 additions & 0 deletions packages/wow-ui/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default {
TextField: "./src/components/TextField",
TextButton: "./src/components/TextButton",
Tag: "./src/components/Tag",
Tabs: "./src/components/Tabs",
TabsContent: "./src/components/Tabs/TabsContent",
TabsItem: "./src/components/Tabs/TabsItem",
TabsList: "./src/components/Tabs/TabsList",
Switch: "./src/components/Switch",
Stepper: "./src/components/Stepper",
BlueSpinner: "./src/components/Spinner/BlueSpinner",
Expand Down
2 changes: 1 addition & 1 deletion packages/wow-ui/src/components/Checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
})}
{...inputProps}
value={value}
onClick={() => handleClick(value)}
onChange={() => handleClick(value)}
/>
{checked && (
<styled.span
Expand Down
2 changes: 1 addition & 1 deletion packages/wow-ui/src/components/Switch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const Switch = forwardRef<HTMLInputElement, SwitchProps>(
ref={ref}
type="checkbox"
value={value}
onClick={() => handleClick(value)}
onChange={() => handleClick(value)}
{...inputProps}
/>
<SwitchIcon checked={checked} disabled={disabled} pressed={pressed} />
Expand Down
196 changes: 196 additions & 0 deletions packages/wow-ui/src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";

import type { TabsProps } from "@/components/Tabs";
import Tabs from "@/components/Tabs";
import TabsContent from "@/components/Tabs/TabsContent";
import TabsItem from "@/components/Tabs/TabsItem";
import TabsList from "@/components/Tabs/TabsList";

const meta: Meta<TabsProps> = {
title: "UI/Tabs",
component: Tabs,
tags: ["autodocs"],
parameters: {
componentSubtitle: "탭을 통해 콘텐츠를 선택할 수 있는 컴포넌트입니다.",
docs: {
description: {
component:
"TabsList 로 TabsItem을 감싸서 탭 트리거를 관리하고 TabsContent 로 탭 콘텐츠를 관리합니다.",
},
},
a11y: {
config: {
rules: [{ id: "color-contrast", enabled: false }],
},
},
},
argTypes: {
children: {
description: "TabsList,TabsItem,TabsContent 를 children 으로 받습니다.",
table: {
type: { summary: "ReactNode" },
},
control: false,
},
value: {
description: "현재 선택된 탭의 값을 나타냅니다.",
table: {
type: { summary: "string" },
},
control: "text",
},
defaultValue: {
description: "초기 선택된 탭 값을 나타냅니다.",
table: {
type: { summary: "string" },
},
control: "text",
},
onChange: {
description: "탭 값이 변경될 때 호출되는 함수입니다.",
table: {
type: { summary: "(value: string) => void" },
},
action: "changed",
},
label: {
description: "각 탭을 구분할 수 있는 레이블입니다.",
table: {
type: { summary: "string" },
},
control: "text",
},
style: {
description: "탭의 커스텀 스타일을 설정합니다.",
table: {
type: { summary: "CSSProperties" },
defaultValue: { summary: "{}" },
},
control: false,
},
className: {
description: "탭에 전달하는 커스텀 클래스를 설정합니다.",
table: {
type: { summary: "string" },
},
control: {
type: "text",
},
},
},
} satisfies Meta<typeof Tabs>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {
children: (
<>
<TabsList>
<TabsItem value="tab1">Tab 1</TabsItem>
<TabsItem value="tab2">Tab 2</TabsItem>
</TabsList>
<TabsContent value="tab1">Tab 1 Content</TabsContent>
<TabsContent value="tab2">Tab 2 Content</TabsContent>
</>
),
defaultValue: "tab1",
},
parameters: {
docs: {
description: {
story: "기본적인 탭 컴포넌트입니다. 탭 1과 탭 2가 제공됩니다.",
},
},
},
};

export const WithDefaultValue: Story = {
args: {
children: (
<>
<TabsList>
<TabsItem value="tab1">Tab 1</TabsItem>
<TabsItem value="tab2">Tab 2</TabsItem>
<TabsItem value="tab3">Tab 3</TabsItem>
</TabsList>
<TabsContent value="tab1">Tab 1 Content</TabsContent>
<TabsContent value="tab2">Tab 2 Content</TabsContent>
<TabsContent value="tab3">Tab 3 Content</TabsContent>
</>
),
defaultValue: "tab2",
},
parameters: {
docs: {
description: {
story:
"초기 값으로 두 번째 탭이 선택된 상태로 시작하는 컴포넌트입니다.",
},
},
},
};

const ControlledTabsComponent = () => {
const [selectedTab, setSelectedTab] = useState("tab1");

const handleChange = (value: string) => {
setSelectedTab(value);
};

return (
<Tabs value={selectedTab} onChange={handleChange}>
<TabsList>
<TabsItem value="tab1">Tab 1</TabsItem>
<TabsItem value="tab2">Tab 2</TabsItem>
<TabsItem value="tab3">Tab 3</TabsItem>
</TabsList>
<TabsContent value="tab1">Tab 1 Content</TabsContent>
<TabsContent value="tab2">Tab 2 Content</TabsContent>
<TabsContent value="tab3">Tab 3 Content</TabsContent>
</Tabs>
);
};

export const ControlledValue: Story = {
render: () => <ControlledTabsComponent />,
parameters: {
docs: {
description: {
story: "외부 상태에 따라 제어되는 탭 컴포넌트입니다.",
},
},
},
};

export const ManyTabs: Story = {
args: {
children: (
<>
<TabsList>
{Array.from({ length: 10 }, (_, index) => (
<TabsItem key={index} value={`tab${index + 1}`}>
Tab {index + 1}
</TabsItem>
))}
</TabsList>
{Array.from({ length: 10 }, (_, index) => (
<TabsContent key={index} value={`tab${index + 1}`}>
Tab {index + 1} Content
</TabsContent>
))}
</>
),
defaultValue: "tab1",
},
parameters: {
docs: {
description: {
story: "여러 개의 탭을 가진 탭 컴포넌트입니다.",
},
},
},
};
87 changes: 87 additions & 0 deletions packages/wow-ui/src/components/Tabs/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { RenderResult } from "@testing-library/react";
import { render, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";

import type { TabsProps } from "@/components/Tabs";
import Tabs from "@/components/Tabs";
import TabsContent from "@/components/Tabs/TabsContent";
import TabsItem from "@/components/Tabs/TabsItem";
import TabsList from "@/components/Tabs/TabsList";

describe("Tabs component", () => {
const uncontrolledTabs = (props: Partial<TabsProps> = {}): RenderResult => {
return render(
<Tabs defaultValue="tab1" {...props}>
<TabsList>
<TabsItem value="tab1">Tab 1</TabsItem>
<TabsItem value="tab2">Tab 2</TabsItem>
</TabsList>
<TabsContent value="tab1">Tab 1 Content</TabsContent>
<TabsContent value="tab2">Tab 2 Content</TabsContent>
</Tabs>
);
};

const controlledTabs = (props: Partial<TabsProps> = {}): RenderResult => {
return render(
<Tabs {...props}>
<TabsList>
<TabsItem value="tab1">Tab 1</TabsItem>
<TabsItem value="tab2">Tab 2</TabsItem>
</TabsList>
<TabsContent value="tab1">Tab 1 Content</TabsContent>
<TabsContent value="tab2">Tab 2 Content</TabsContent>
</Tabs>
);
};
test("renders correctly with default value", async () => {
const { getByText } = uncontrolledTabs();
expect(getByText("Tab 1 Content")).toBeVisible();
});

test("switches content when clicking on tab triggers", async () => {
const { getByText } = uncontrolledTabs();
await userEvent.click(getByText("Tab 2"));
await waitFor(() => {
expect(getByText("Tab 2 Content")).toBeVisible();
});
});

test("calls onChange when the tab is changed", async () => {
const handleChange = jest.fn();
const { getByText } = controlledTabs({
value: "tab1",
onChange: handleChange,
});
await userEvent.click(getByText("Tab 2"));
expect(handleChange).toHaveBeenCalledWith("tab2");
});

test("can navigate between tabs using keyboard (ArrowRight)", async () => {
const { getByText } = uncontrolledTabs();
const tab1 = getByText("Tab 1");
const tab2 = getByText("Tab 2");

tab1.focus();
await userEvent.keyboard("{ArrowRight}");
expect(tab2).toHaveFocus();

await waitFor(() => {
expect(getByText("Tab 2 Content")).toBeVisible();
});
});

test("can navigate between tabs using keyboard (ArrowLeft)", async () => {
const { getByText } = uncontrolledTabs();
const tab1 = getByText("Tab 1");
const tab2 = getByText("Tab 2");

tab1.focus();
await userEvent.keyboard("{ArrowLeft}");
expect(tab2).toHaveFocus();

await waitFor(() => {
expect(getByText("Tab 2 Content")).toBeVisible();
});
});
});
Loading

0 comments on commit 7b19f05

Please sign in to comment.