-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(design-system): Dropdown 구현 #61
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
Changes from 4 commits
c63c34a
9b68f19
37c8e7f
b5ccfca
dfd731b
e7f34d8
913bf09
54e1850
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,127 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import { useState } from 'react'; | ||
| import Dropdown from './Dropdown'; | ||
|
|
||
| const meta: Meta<typeof Dropdown> = { | ||
| title: 'Components/Dropdown', | ||
| component: Dropdown, | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| docs: { | ||
| description: { | ||
| component: | ||
| '**선택형 입력 컴포넌트입니다.** `selectedValue`로 선택 상태를 제어하고 `onChange`로 변경 이벤트를 처리합니다. ' + | ||
| '`onAddItem`을 지정하면 목록 하단에 “추가” 버튼을 노출하며, `limit`으로 최대 개수를 제어합니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| options: { | ||
| control: 'object', | ||
| description: '드롭다운 항목 문자열 배열입니다.', | ||
| }, | ||
| selectedValue: { | ||
| control: 'text', | ||
| description: '현재 선택된 값(제어 컴포넌트에서 사용).', | ||
| }, | ||
| placeholder: { | ||
| control: 'text', | ||
| description: '선택 전 표시되는 안내 문구.', | ||
| }, | ||
| addItemLabel: { | ||
| control: 'text', | ||
| description: '추가 버튼 라벨 (`onAddItem`과 함께 사용).', | ||
| }, | ||
| onChange: { | ||
| action: 'changed', | ||
| description: '선택이 변경될 때 호출됩니다.', | ||
| }, | ||
| limit: { | ||
| control: 'number', | ||
| description: '옵션 개수 상한. 도달 시 “추가” 버튼 숨김.', | ||
| }, | ||
| onAddItem: { | ||
| action: 'add item clicked', | ||
| description: '“추가” 버튼 클릭 시 호출됩니다.', | ||
| }, | ||
| className: { table: { disable: true } }, | ||
| }, | ||
| args: { | ||
| options: ['사과', '바나나', '체리'], | ||
| selectedValue: null, | ||
| placeholder: '항목을 선택하세요', | ||
| addItemLabel: '항목 추가', | ||
| limit: 5, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Dropdown>; | ||
|
|
||
| function ControlledRender(args: any) { | ||
| const [value, setValue] = useState<string | null>(args.selectedValue ?? null); | ||
|
|
||
| return ( | ||
| <div className="h-[20rem]"> | ||
| <Dropdown | ||
| {...args} | ||
| selectedValue={value} | ||
| onChange={(next: string | null) => { | ||
| setValue(next); | ||
| args.onChange?.(next); | ||
| }} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export const Default: Story = { | ||
| name: '기본', | ||
| render: ControlledRender, | ||
| }; | ||
|
|
||
| export const WithPreselected: Story = { | ||
| name: '선택값 초기화', | ||
| args: { selectedValue: '바나나' }, | ||
| render: ControlledRender, | ||
| }; | ||
|
|
||
| export const WithAddItemAlert: Story = { | ||
| name: '추가 버튼 클릭 시 (test Alert)', | ||
| args: { | ||
| options: ['사과', '바나나'], | ||
| limit: 5, | ||
| addItemLabel: '새 항목 추가', | ||
| onAddItem: () => { | ||
| alert('추가 버튼이 클릭되었습니다.'); | ||
| }, | ||
| }, | ||
| render: ControlledRender, | ||
| }; | ||
|
|
||
| export const LimitReached: Story = { | ||
| name: '추가 제한 도달 (limit)', | ||
| args: { | ||
| options: ['사과', '바나나', '체리'], | ||
| limit: 3, | ||
| }, | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| '`options.length`가 `limit`에 도달하여 “추가” 버튼이 표시되지 않습니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| render: ControlledRender, | ||
| }; | ||
|
|
||
| export const ManyOptions: Story = { | ||
| name: '옵션이 많은 경우', | ||
| args: { | ||
| options: Array.from({ length: 20 }, (_, i) => `옵션 ${i + 1}`), | ||
| }, | ||
| render: ControlledRender, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { Icon } from '@pinback/design-system/icons'; | ||
| import { useState } from 'react'; | ||
|
|
||
| interface DropdownProps { | ||
| options: string[]; | ||
| selectedValue: string | null; | ||
| onChange: (selected: string | null) => void; | ||
| placeholder: string; | ||
| onAddItem?: () => void; | ||
| addItemLabel?: string; | ||
| limit?: number; | ||
| className?: string; | ||
| } | ||
|
|
||
| const Dropdown = ({ | ||
| options, | ||
| selectedValue, | ||
| onChange, | ||
| placeholder, | ||
| onAddItem, | ||
| addItemLabel, | ||
| limit, | ||
| className = '', | ||
| }: DropdownProps) => { | ||
|
Comment on lines
+15
to
+24
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. 빈 라벨/플레이스홀더 노출 위험 수정: 기본 placeholder·addItemLabel 기본값을 부여하세요. - placeholder,
+ placeholder = '선택하세요',
onAddItem,
- addItemLabel,
+ addItemLabel = '추가',
@@
- <span className={selectedValue ? 'text-black' : 'text-font-gray-3'}>
- {selectedValue || placeholder}
+ <span className={selectedValue !== null ? 'text-black' : 'text-font-gray-3'}>
+ {selectedValue ?? placeholder}
</span>
@@
- {addItemLabel}
+ {addItemLabel}Also applies to: 42-44, 69-81 🤖 Prompt for AI Agents |
||
| const [isOpen, setIsOpen] = useState(false); | ||
|
|
||
| const handleSelect = (option: string) => { | ||
| onChange(option); | ||
| setIsOpen(false); | ||
| }; | ||
|
|
||
| const showAddItemButton = | ||
| onAddItem && (limit === undefined || options.length < limit); | ||
|
|
||
| return ( | ||
| <div className={`relative w-full ${className}`}> | ||
| <button | ||
| type="button" | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className={`body4-r flex h-[4.4rem] w-[24.8rem] items-center justify-between rounded-[4px] border px-[0.8rem] py-[1.2rem] transition-colors duration-200 ${isOpen ? 'border-main500' : 'border-gray200'}`} | ||
| > | ||
| <span className={selectedValue ? 'text-black' : 'text-font-gray-3'}> | ||
| {selectedValue || placeholder} | ||
| </span> | ||
| <Icon | ||
| name="ic_arrow_down_disable" | ||
| width={16} | ||
| height={16} | ||
| // TODO: Icon 컴포넌트 내부에서 animation 관련 처리 고민하기 | ||
| // rotate={isOpen ? 180 : undefined} | ||
| className={`transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} | ||
|
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. 이부분도 고민해보았는데요!
Icon 내에서
Member
Author
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. 오 이렇게 처리해도 가능하겠네요! 이렇게 Icon파일에서 한 번에 적용하면 오히려 모든 icon의 rotate animation이 통일되니까 디자인 시스템의 의미도 갖을 것 같아요!
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. (회전이나 애니메이션 들이 아이콘 단위로 많이 쓰이지는 않지만) |
||
| /> | ||
| </button> | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| {isOpen && ( | ||
| <div className="common-shadow absolute z-10 mt-[1.5rem] w-[24.8rem] rounded-[0.4rem] bg-white"> | ||
| <ul className="flex flex-col gap-[0.2rem]"> | ||
| {options.map((option) => ( | ||
| <li | ||
| key={option} | ||
| onClick={() => handleSelect(option)} | ||
| className={`body4-r cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`} | ||
| > | ||
| {option} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
|
|
||
| {showAddItemButton && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => { | ||
| onAddItem?.(); | ||
| setIsOpen(false); | ||
| }} | ||
| className="text-main500 body4-r flex w-full cursor-pointer items-center gap-[0.8rem] p-[0.8rem]" | ||
| > | ||
| <Icon name="ic_plus" width={16} height={16} /> | ||
| {addItemLabel} | ||
| </button> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Dropdown; | ||
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.
리밋 속성을 통해서, 카테고리 추가 노출여부를 제어할 수 있어서! 확장성 측면에서 좋은 것 같네요!
++ 대신 디자인적으로 궁금한 점이, 카테고리 리스트개수가 적은 경우에도 그 박스의 height가 고정값일까요?
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.
네네 일단 그렇게 구현이 되어있어요. 리스트가 많아지는 경우에 스크롤을 보여주려면 box에
height값을 줄 수 밖에 없어서..!개수에 따라
height를 다르게 분기처리 할 수 있기는 하지만 현재도 괜찮다고 일단 생각이 드는데 어떻게 생각하시나요??애매하다면 QA때 디자인 분들께 질문 드려도 좋을 것 같아요 👍
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.
아하, 그러네요 스크롤 적용하려면 Height 지정을 해두긴 하네요,, 그렇다고 개수에 따라 height 분기하는 건 오히려 너무 불필요한 거 같아서! 저는 좋아요! QA때 디자이너 분들 피드백에 따라 추후에 수정해보는 걸로 해용!