Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/icons/DropdownIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* DropBox 열림(Up) 아이콘 SVG 컴포넌트
*/
export const DropdownUpArrowIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
className={className}
>
<path
d="M4.5 10.5L9 6L13.5 10.5"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

/**
* DropBox 닫힘(Down) 아이콘 SVG 컴포넌트
*/
export const DropdownDownArrowIcon = ({ className }: { className?: string }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="none"
viewBox="0 0 18 18"
className={className}
>
<path
d="M4.5 7.5L9 12L13.5 7.5"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
65 changes: 65 additions & 0 deletions src/libs/dropdown/Dropdown.mdx
Original file line number Diff line number Diff line change
@@ -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";

<Meta of={DropdownStories} />

# Dropdown

Dropdown은 여러 선택지 중 하나를 선택할 수 있도록 하는 **선택형 UI 컴포넌트**입니다.
정보를 수집하는 양식에서 사용하거나 정보 필터링 및 정렬에 사용합니다.

<Canvas of={DropdownStories.Default} />
<Controls />

<br />
<br />

## 사용법

Dropdown은 반드시 `options`를 전달해야 합니다.
`placeholder`는 선택 전 안내 문구이며, `width`를 통해 너비를 조절할 수 있습니다.

```tsx
import { Dropdown } from "@woori-design";
```

<br />
<br />

### props

- 선택 : `placeholder` (`string`) : 선택 전 안내 문구를 보여줍니다.
- 선택 : `width` (`string`) : 컴포넌트의 너비를 직접 설정할 수 있습니다.
- 필수 : `options` (`string[]`) : Dropdown에 표시할 선택 항목 리스트입니다.

<br />
<br />

### 세부 사용법

- Options는 배열 형태로 원하는 옵션 값을 추가할 수 있습니다.
- Trigger(헤더)와 옵션 리스트는 구분선을 통해 시각적으로 분리됩니다.
- 컬러는 디자인 시스템 파운데이션 변수(var(--color-xxx))만 사용합니다.
- 선택된 값은 Trigger(헤더) 영역에 표시되며, 다시 Trigger를 클릭해 다른 값을 선택할 수 있습니다.
- 선택 전에는 placeholder가 회색으로 표시되며, 값을 선택하면 텍스트가 더 굵고 진한 색으로 표시되어 구분됩니다.

<br />
<br />

## 예시

### 1. 기본 사용 예시 (Option 5개 이하)

<Canvas of={DropdownStories.BasicOptions} />

<br />
<br />

### 2. 다중 옵션 예시 (Option 5개 초과)

<Canvas of={DropdownStories.ManyOptions} />

<br />
<br />
59 changes: 59 additions & 0 deletions src/libs/dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import Dropdown from "./Dropdown";

const meta: Meta<typeof Dropdown> = {
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<typeof Dropdown>;

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",
},
};
128 changes: 128 additions & 0 deletions src/libs/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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<DropdownProps> = ({ placeholder, options, width }) => {
const [selected, setSelected] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isTriggerHovered, setIsTriggerHovered] = useState<boolean>(false);

const handleSelect = (option: string) => {
setSelected(option);
setIsOpen(false);
};

return (
<div
style={{
width,
border: `1px solid var(--color-gray-strong)`,
borderRadius: "12px",
backgroundColor: "var(--color-bw-white)",
overflow: "hidden",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 21px",
cursor: "pointer",
}}
>
<div
onClick={() => 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}
</div>

<div
onClick={() => setIsOpen(!isOpen)}
style={{
marginLeft: "8px",
cursor: "pointer",
}}
>
{isOpen ? <DropdownUpArrowIcon /> : <DropdownDownArrowIcon />}
</div>
</div>
{isOpen && (
<div
style={{
height: "2px",
backgroundColor: "var(--color-gray-light)",
margin: "0 21px",
}}
/>
)}
<div
style={{
maxHeight: isOpen
? options.length > 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) => (
<React.Fragment key={index}>
{index !== 0 && (
<div
style={{
height: "1px",
backgroundColor: "var(--color-gray-light)",
margin: "0 21px",
}}
/>
)}
<div
onClick={() => 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}
</div>
</React.Fragment>
))}
</div>
</div>
);
};

export default Dropdown;
5 changes: 5 additions & 0 deletions src/libs/dropdown/Dropdown.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface DropdownProps {
placeholder?: string;
options: string[];
width?: string;
}