-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] Tab 컴포넌트 구현 #79
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import { | ||
| ChartHoverIcon, | ||
| ChartOffIcon, | ||
| ChartOnIcon, | ||
| HomeHoverIcon, | ||
| HomeOffIcon, | ||
| HomeOnIcon, | ||
| SettingHoverIcon, | ||
| SettingOffIcon, | ||
| SettingOnIcon, | ||
| TimerHoverIcon, | ||
| TimerOffIcon, | ||
| TimerOnIcon, | ||
| TodayHoverIcon, | ||
| TodayOffIcon, | ||
| TodayOnIcon, | ||
| } from "@icons"; | ||
| import { useState } from "react"; | ||
|
|
||
| import { Tab } from "./Tab"; | ||
|
|
||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import type { ComponentProps } from "react"; | ||
|
|
||
| const meta = { | ||
| title: "Components/Tab", | ||
| component: Tab, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| argTypes: { | ||
| label: { | ||
| control: "text", | ||
| }, | ||
| isSelected: { | ||
| control: "boolean", | ||
| }, | ||
| onClick: { | ||
| action: "clicked", | ||
| }, | ||
| }, | ||
| args: { | ||
| label: "홈", | ||
| icon: <HomeOffIcon width={24} height={24} />, | ||
| hoverIcon: <HomeHoverIcon width={24} height={24} />, | ||
| isSelected: false, | ||
| }, | ||
| } satisfies Meta<typeof Tab>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| export const AllStates: Story = { | ||
| parameters: { | ||
| controls: { disable: true }, | ||
| }, | ||
| render: () => ( | ||
| <div className="grid grid-cols-3 gap-6"> | ||
| <div className="flex flex-col gap-3"> | ||
| <Tab | ||
| label="홈" | ||
| icon={<HomeOnIcon width={24} height={24} />} | ||
| isSelected | ||
| /> | ||
| <Tab | ||
| label="홈" | ||
| icon={<HomeOffIcon width={24} height={24} />} | ||
| hoverIcon={<HomeHoverIcon width={24} height={24} />} | ||
| /> | ||
| <Tab label="홈" icon={<HomeOffIcon width={24} height={24} />} /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-3"> | ||
| <Tab | ||
| label="오늘" | ||
| icon={<TodayOnIcon width={24} height={24} />} | ||
| isSelected | ||
| /> | ||
| <Tab | ||
| label="오늘" | ||
| icon={<TodayOffIcon width={24} height={24} />} | ||
| hoverIcon={<TodayHoverIcon width={24} height={24} />} | ||
| /> | ||
| <Tab label="오늘" icon={<TodayOffIcon width={24} height={24} />} /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-3"> | ||
| <Tab | ||
| label="집중 모드" | ||
| icon={<TimerOnIcon width={24} height={24} />} | ||
| isSelected | ||
| /> | ||
| <Tab | ||
| label="집중 모드" | ||
| icon={<TimerOffIcon width={24} height={24} />} | ||
| hoverIcon={<TimerHoverIcon width={24} height={24} />} | ||
| /> | ||
| <Tab label="집중 모드" icon={<TimerOffIcon width={24} height={24} />} /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-3"> | ||
| <Tab | ||
| label="통계" | ||
| icon={<ChartOnIcon width={24} height={24} />} | ||
| isSelected | ||
| /> | ||
| <Tab | ||
| label="통계" | ||
| icon={<ChartOffIcon width={24} height={24} />} | ||
| hoverIcon={<ChartHoverIcon width={24} height={24} />} | ||
| /> | ||
| <Tab label="통계" icon={<ChartOffIcon width={24} height={24} />} /> | ||
| </div> | ||
|
|
||
| <div className="flex flex-col gap-3"> | ||
| <Tab | ||
| label="설정" | ||
| icon={<SettingOnIcon width={24} height={24} />} | ||
| isSelected | ||
| /> | ||
| <Tab | ||
| label="설정" | ||
| icon={<SettingOffIcon width={24} height={24} />} | ||
| hoverIcon={<SettingHoverIcon width={24} height={24} />} | ||
| /> | ||
| <Tab label="설정" icon={<SettingOffIcon width={24} height={24} />} /> | ||
| </div> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| const InteractiveHomeTab = (args: ComponentProps<typeof Tab>) => { | ||
| const [isSelected, setIsSelected] = useState(false); | ||
|
|
||
| return ( | ||
| <Tab | ||
| {...args} | ||
| label="홈" | ||
| icon={ | ||
| isSelected ? ( | ||
| <HomeOnIcon width={24} height={24} /> | ||
| ) : ( | ||
| <HomeOffIcon width={24} height={24} /> | ||
| ) | ||
| } | ||
| hoverIcon={<HomeHoverIcon width={24} height={24} />} | ||
| isSelected={isSelected} | ||
| onClick={() => { | ||
| args.onClick?.(); | ||
| setIsSelected((prevIsSelected) => !prevIsSelected); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export const Interactive: Story = { | ||
| parameters: { | ||
| controls: { disable: true }, | ||
| }, | ||
| render: (args) => <InteractiveHomeTab {...args} />, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "@lib"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ReactNode } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type TabVariant = "default" | "selected"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 타입 별칭 네이밍 컨벤션 위반: 리터럴 유니언 타입에는 접미사 ✏️ 제안 diff-type TabVariant = "default" | "selected";
+type TabVariantTypes = "default" | "selected";이후 As per path instructions, " 🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TAB_VARIANT_CLASS_NAME: Record<TabVariant, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: "text-timo-gray-800 hover:bg-timo-gray-500", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selected: "bg-timo-blue-65 text-timo-blue-300", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface TabProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon: ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hoverIcon?: ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSelected?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const Tab = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tab이라는 컴포넌트명보단, 지금 구현된 걸 보니까 버튼 컴포넌트인 것 같아요! 컴포넌트명 수정해서 button 내부에 구조화해 주면 좋을 것 같아요. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hoverIcon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isSelected = false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: TabProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const baseButtonClassName = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "group flex w-45 items-center gap-2 rounded-8 px-3 py-1"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rounded는 rem 영향을 받는 속성이 아니니 px 단위로 표기해도 괜찮을 것 같아요 🙂 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const variant: TabVariant = isSelected ? "selected" : "default"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasHoverIcon = hoverIcon && !isSelected; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onClick} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={cn(baseButtonClassName, TAB_VARIANT_CLASS_NAME[variant])} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {hasHoverIcon ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="group-hover:hidden">{icon}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="hidden group-hover:block">{hoverIcon}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| icon | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{label}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win 선택 상태를 스크린리더에도 알려주면 완벽할 것 같아요. 현재 선택 여부는 배경색으로만 표현되어 있어서, 보조기술 사용자는 어떤 탭이 선택됐는지 알기 어려워요. ♿ 제안 diff <button
type="button"
onClick={onClick}
+ aria-pressed={isSelected}
className={cn(baseButtonClassName, TAB_VARIANT_CLASS_NAME[variant])}
>관련 문서: WAI-ARIA aria-pressed 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요거 절대 경로로 수정해 줘용