-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] TogglePanel 컴포넌트 추가 #76
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
b303c83
0df552c
1b385b9
4aa0522
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,114 @@ | ||
| import { | ||
| TogglePanel, | ||
| TogglePanelValue, | ||
| } from "@components/toggle-panel/TogglePanel"; | ||
| import { useEffect, useState } from "react"; | ||
|
|
||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
|
|
||
| const meta = { | ||
| title: "Components/TogglePanel", | ||
| component: TogglePanel, | ||
| parameters: { | ||
| layout: "centered", | ||
| }, | ||
| args: { | ||
| value: "timebox", | ||
| onChange: () => {}, | ||
| }, | ||
| } satisfies Meta<typeof TogglePanel>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| const PlaygroundTogglePanel = ({ | ||
| width, | ||
| ...args | ||
| }: React.ComponentProps<typeof TogglePanel> & { width: string }) => { | ||
| const [value, setValue] = useState<TogglePanelValue>(args.value); | ||
|
|
||
| useEffect(() => { | ||
| setValue(args.value); | ||
| }, [args.value]); | ||
|
|
||
| return ( | ||
| <div className={width}> | ||
| <TogglePanel {...args} value={value} onChange={setValue} /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const Default: Story = { | ||
| render: (args) => <PlaygroundTogglePanel {...args} width="w-67" />, | ||
| }; | ||
|
|
||
| export const Big: Story = { | ||
| args: { | ||
| value: "timer", | ||
| }, | ||
| render: (args) => <PlaygroundTogglePanel {...args} width="w-101" />, | ||
| }; | ||
|
|
||
| const TogglePanelWithPanel = ( | ||
| args: React.ComponentProps<typeof TogglePanel>, | ||
| ) => { | ||
| const [value, setValue] = useState<TogglePanelValue>(args.value); | ||
|
|
||
| useEffect(() => { | ||
| setValue(args.value); | ||
| }, [args.value]); | ||
|
|
||
| return ( | ||
| <div className="w-67"> | ||
| <TogglePanel | ||
| {...args} | ||
| id="toggle-panel-demo" | ||
| value={value} | ||
| onChange={setValue} | ||
| timeboxControls="toggle-panel-demo-timebox-panel" | ||
| timerControls="toggle-panel-demo-timer-panel" | ||
| /> | ||
| <div | ||
| id="toggle-panel-demo-timebox-panel" | ||
| role="tabpanel" | ||
| aria-labelledby="toggle-panel-demo-timebox-tab" | ||
| hidden={value !== "timebox"} | ||
| className="bg-timo-blue-100 mt-4 rounded-lg p-4 text-sm" | ||
| > | ||
| Timebox 관련 콘텐츠 영역 | ||
| </div> | ||
| <div | ||
| id="toggle-panel-demo-timer-panel" | ||
| role="tabpanel" | ||
| aria-labelledby="toggle-panel-demo-timer-tab" | ||
| hidden={value !== "timer"} | ||
| className="border-timo-gray-500 mt-4 rounded-lg border p-4 text-sm" | ||
| > | ||
| Timer 관련 콘텐츠 영역 | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const WithPanel: Story = { | ||
| render: (args) => <TogglePanelWithPanel {...args} />, | ||
| }; | ||
|
|
||
| export const AllSizes: Story = { | ||
| render: () => ( | ||
| <div className="flex flex-col items-start gap-6"> | ||
| <div className="flex flex-col items-start gap-2"> | ||
| <p className="text-xs">Default (268px)</p> | ||
| <div className="w-67"> | ||
| <TogglePanel value="timebox" onChange={() => {}} /> | ||
| </div> | ||
| </div> | ||
| <div className="flex flex-col items-start gap-2"> | ||
| <p className="text-xs">Big (404px)</p> | ||
| <div className="w-101"> | ||
| <TogglePanel value="timer" onChange={() => {}} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ), | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,85 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { cn } from "@lib"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useId, useRef } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import type { KeyboardEvent, RefObject } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export type TogglePanelValue = "timebox" | "timer"; | ||||||||||||||||||||||||||||||||||||||||||||
|
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 type alias 네이밍 컨벤션 확인해주세요.
As per path instructions, " ♻️ 제안-export type TogglePanelValue = "timebox" | "timer";
+export type TogglePanelValueTypes = "timebox" | "timer";📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export interface TogglePanelProps { | ||||||||||||||||||||||||||||||||||||||||||||
| id?: string; | ||||||||||||||||||||||||||||||||||||||||||||
| value: TogglePanelValue; | ||||||||||||||||||||||||||||||||||||||||||||
| onChange: (value: TogglePanelValue) => void; | ||||||||||||||||||||||||||||||||||||||||||||
| timeboxControls?: string; | ||||||||||||||||||||||||||||||||||||||||||||
| timerControls?: string; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| interface TogglePanelOption { | ||||||||||||||||||||||||||||||||||||||||||||
| value: TogglePanelValue; | ||||||||||||||||||||||||||||||||||||||||||||
| label: string; | ||||||||||||||||||||||||||||||||||||||||||||
| ref: RefObject<HTMLButtonElement | null>; | ||||||||||||||||||||||||||||||||||||||||||||
| controls?: string; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| export const TogglePanel = ({ | ||||||||||||||||||||||||||||||||||||||||||||
| id: idProp, | ||||||||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||||||||
| onChange, | ||||||||||||||||||||||||||||||||||||||||||||
| timeboxControls, | ||||||||||||||||||||||||||||||||||||||||||||
| timerControls, | ||||||||||||||||||||||||||||||||||||||||||||
| }: TogglePanelProps) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const generatedId = useId(); | ||||||||||||||||||||||||||||||||||||||||||||
| const id = idProp ?? generatedId; | ||||||||||||||||||||||||||||||||||||||||||||
| const timeboxRef = useRef<HTMLButtonElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||
| const timerRef = useRef<HTMLButtonElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const options: TogglePanelOption[] = [ | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| value: "timebox", | ||||||||||||||||||||||||||||||||||||||||||||
| label: "Timebox", | ||||||||||||||||||||||||||||||||||||||||||||
| ref: timeboxRef, | ||||||||||||||||||||||||||||||||||||||||||||
| controls: timeboxControls, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| { value: "timer", label: "Timer", ref: timerRef, controls: timerControls }, | ||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||
| const next = options.find((option) => option.value !== value); | ||||||||||||||||||||||||||||||||||||||||||||
| if (!next) return; | ||||||||||||||||||||||||||||||||||||||||||||
| onChange(next.value); | ||||||||||||||||||||||||||||||||||||||||||||
| next.ref.current?.focus(); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+53
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. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win 방향 구분 없이 항상 "다른 옵션"으로 이동합니다.
인덱스 기반으로 방향에 맞게 다음/이전 옵션을 계산하도록 방어적으로 구현해두면 향후 확장에도 안전할 것 같습니다. 🛡️ 제안 const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
- const next = options.find((option) => option.value !== value);
- if (!next) return;
+ const currentIndex = options.findIndex((option) => option.value === value);
+ if (currentIndex === -1) return;
+ const delta = event.key === "ArrowRight" ? 1 : -1;
+ const nextIndex = (currentIndex + delta + options.length) % options.length;
+ const next = options[nextIndex];
onChange(next.value);
next.ref.current?.focus();
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||
| role="tablist" | ||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={-1} | ||||||||||||||||||||||||||||||||||||||||||||
| onKeyDown={handleKeyDown} | ||||||||||||||||||||||||||||||||||||||||||||
| className="flex h-7.5 w-full overflow-hidden rounded-[4px]" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+61
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. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win tablist 컨테이너에 접근성 이름이 없어요.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| {options.map((option) => ( | ||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||
| key={option.value} | ||||||||||||||||||||||||||||||||||||||||||||
| ref={option.ref} | ||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||
| id={`${id}-${option.value}-tab`} | ||||||||||||||||||||||||||||||||||||||||||||
| role="tab" | ||||||||||||||||||||||||||||||||||||||||||||
| aria-selected={value === option.value} | ||||||||||||||||||||||||||||||||||||||||||||
| aria-controls={option.controls} | ||||||||||||||||||||||||||||||||||||||||||||
| tabIndex={value === option.value ? 0 : -1} | ||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => onChange(option.value)} | ||||||||||||||||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||||||||||||||||
| "typo-body-sb-12 flex flex-1 items-center justify-center px-4 whitespace-nowrap", | ||||||||||||||||||||||||||||||||||||||||||||
| value === option.value | ||||||||||||||||||||||||||||||||||||||||||||
| ? "bg-timo-black text-white" | ||||||||||||||||||||||||||||||||||||||||||||
| : "bg-timo-gray-300 text-timo-black", | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| {option.label} | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.