diff --git a/src/icons/DropdownIcons.tsx b/src/icons/DropdownIcons.tsx new file mode 100644 index 0000000..07b0745 --- /dev/null +++ b/src/icons/DropdownIcons.tsx @@ -0,0 +1,43 @@ +/** + * DropBox 열림(Up) 아이콘 SVG 컴포넌트 + */ +export const DropdownUpArrowIcon = ({ className }: { className?: string }) => ( + + + +); + +/** + * DropBox 닫힘(Down) 아이콘 SVG 컴포넌트 + */ +export const DropdownDownArrowIcon = ({ className }: { className?: string }) => ( + + + +); diff --git a/src/index.ts b/src/index.ts index c796dbe..19a75f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,4 @@ export { default as Divider } from "./libs/divider/Divider"; export { default as Checkbox } from "./libs/checkbox/Checkbox"; export { default as Button } from "./libs/button/Button"; export { default as Switch } from "./libs/switch/Switch"; +export { default as Dropdown } from "./libs/dropdown/Dropdown"; diff --git a/src/libs/dropdown/Dropdown.mdx b/src/libs/dropdown/Dropdown.mdx new file mode 100644 index 0000000..4dbb943 --- /dev/null +++ b/src/libs/dropdown/Dropdown.mdx @@ -0,0 +1,65 @@ +import { Canvas, Meta, Controls } from "@storybook/blocks"; +import * as DropdownStories from "./Dropdown.stories"; +import Dropdown from "./Dropdown"; +import React from "react"; + + + +# Dropdown + +Dropdown은 여러 선택지 중 하나를 선택할 수 있도록 하는 **선택형 UI 컴포넌트**입니다. +정보를 수집하는 양식에서 사용하거나 정보 필터링 및 정렬에 사용합니다. + + + + +
+
+ +## 사용법 + +Dropdown은 반드시 `options`를 전달해야 합니다. +`placeholder`는 선택 전 안내 문구이며, `width`를 통해 너비를 조절할 수 있습니다. + +```tsx +import { Dropdown } from "@woori-design"; +``` + +
+
+ +### props + +- 선택 : `placeholder` (`string`) : 선택 전 안내 문구를 보여줍니다. +- 선택 : `width` (`string`) : 컴포넌트의 너비를 직접 설정할 수 있습니다. +- 필수 : `options` (`string[]`) : Dropdown에 표시할 선택 항목 리스트입니다. + +
+
+ +### 세부 사용법 + +- Options는 배열 형태로 원하는 옵션 값을 추가할 수 있습니다. +- Trigger(헤더)와 옵션 리스트는 구분선을 통해 시각적으로 분리됩니다. +- 컬러는 디자인 시스템 파운데이션 변수(var(--color-xxx))만 사용합니다. +- 선택된 값은 Trigger(헤더) 영역에 표시되며, 다시 Trigger를 클릭해 다른 값을 선택할 수 있습니다. +- 선택 전에는 placeholder가 회색으로 표시되며, 값을 선택하면 텍스트가 더 굵고 진한 색으로 표시되어 구분됩니다. + +
+
+ +## 예시 + +### 1. 기본 사용 예시 (Option 5개 이하) + + + +
+
+ +### 2. 다중 옵션 예시 (Option 5개 초과) + + + +
+
\ No newline at end of file diff --git a/src/libs/dropdown/Dropdown.stories.tsx b/src/libs/dropdown/Dropdown.stories.tsx new file mode 100644 index 0000000..f206e0a --- /dev/null +++ b/src/libs/dropdown/Dropdown.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Dropdown from "./Dropdown"; + +const meta: Meta = { + title: "libs/Dropdown", + component: Dropdown, + parameters: { + layout: "centered", + docs: { + description: { + component: + "선택 목록을 표시하는 Dropdown 컴포넌트입니다. 옵션 선택 시 Trigger에 반영됩니다.", + }, + }, + }, + tags: ["autodocs"], + argTypes: { + placeholder: { + description: "Dropdown 안내 문구", + control: { type: "text" }, + }, + options: { + description: "Dropdown 내부 선택 가능 옵션 리스트", + control: { type: "object" }, + }, + width: { + description: "Dropdown 너비 (px, %, rem)", + control: { type: "text" }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: "Dropdown", + options: ["Option A", "Option B", "Option C"], + width: "300px", + }, +}; + +export const BasicOptions: Story = { + args: { + placeholder: "Select number...", + options: ["1", "2", "3", "4", "5"], + width: "300px", + }, +}; + +export const ManyOptions: Story = { + args: { + placeholder: "Select number...", + options: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], + width: "300px", + }, +}; diff --git a/src/libs/dropdown/Dropdown.tsx b/src/libs/dropdown/Dropdown.tsx new file mode 100644 index 0000000..a13aa58 --- /dev/null +++ b/src/libs/dropdown/Dropdown.tsx @@ -0,0 +1,128 @@ +import React, { useState } from "react"; +import { DropdownProps } from "./Dropdown.type"; +import { + DropdownUpArrowIcon, + DropdownDownArrowIcon, +} from "../../icons/DropdownIcons"; +import { typography } from "../../styles/foundation/typography/typography"; + +const Dropdown: React.FC = ({ placeholder, options, width }) => { + const [selected, setSelected] = useState(null); + const [isOpen, setIsOpen] = useState(false); + const [isTriggerHovered, setIsTriggerHovered] = useState(false); + + const handleSelect = (option: string) => { + setSelected(option); + setIsOpen(false); + }; + + return ( +
+
+
setIsOpen(!isOpen)} + onMouseEnter={() => setIsTriggerHovered(true)} + onMouseLeave={() => setIsTriggerHovered(false)} + style={{ + flex: 1, + textAlign: "left", + ...typography.Rg_16, + fontWeight: selected ? 700 : isTriggerHovered ? 400 : 300, + color: selected + ? "var(--color-bw-black)" + : "var(--color-gray-medium)", + transition: "font-weight 0.2s ease", + }} + > + {selected || placeholder} +
+ +
setIsOpen(!isOpen)} + style={{ + marginLeft: "8px", + cursor: "pointer", + }} + > + {isOpen ? : } +
+
+ {isOpen && ( +
+ )} +
5 + ? `${5 * 50}px` + : `${options.length * 50}px` + : "0", + overflowY: options.length > 5 ? "auto" : "hidden", + overflowX: "hidden", + transition: "max-height 0.2s ease", + }} + > + {options.map((option, index) => ( + + {index !== 0 && ( +
+ )} +
handleSelect(option)} + onMouseEnter={(e) => { + e.currentTarget.style.fontWeight = "500"; + e.currentTarget.style.color = "var(--color-gray-strong)"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.fontWeight = "400"; + e.currentTarget.style.color = "var(--color-bw-black)"; + }} + style={{ + ...typography.Rg_16, + color: "var(--color-bw-black)", + padding: "12px 21px", + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + transition: "font-weight 0.2s ease", + }} + > + {option} +
+ + ))} +
+
+ ); +}; + +export default Dropdown; diff --git a/src/libs/dropdown/Dropdown.type.ts b/src/libs/dropdown/Dropdown.type.ts new file mode 100644 index 0000000..f270563 --- /dev/null +++ b/src/libs/dropdown/Dropdown.type.ts @@ -0,0 +1,5 @@ +export interface DropdownProps { + placeholder?: string; + options: string[]; + width?: string; +}