Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 5 additions & 1 deletion packages/tailwind-config/theme.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
@import "./tokens/colors.css";
@import "./tokens/typography.css";
@import "./tokens/radius.css";
@import "./scrollbar.css";
@import "./scrollbar.css";

@theme {
--shadow-timo: 0px 0px 3px 0px rgba(0, 0, 0, 0.12);
}
Comment thread
ehye1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useState } from "react";

import { Dropdown } from "./Dropdown";
Comment thread
ehye1 marked this conversation as resolved.
Outdated

import type { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "Components/Dropdown",
component: Dropdown,
parameters: { layout: "centered" },
} satisfies Meta<typeof Dropdown>;

export default meta;
type Story = StoryObj<typeof meta>;

const DefaultStory = () => {
const [value, setValue] = useState("");
return <Dropdown items={["기본", "7일"]} value={value} onChange={setValue} />;
};

export const Default: Story = {
args: { items: [], value: "", onChange: () => {} },
render: () => <DefaultStory />,
};
122 changes: 122 additions & 0 deletions packages/timo-design-system/src/components/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { cn } from "@lib";
import { useEffect, useRef, useState } from "react";

export interface DropdownProps {
items: string[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}

export const Dropdown = ({
Comment thread
ehye1 marked this conversation as resolved.
Outdated
items,
value,
onChange,
placeholder = "기본",
className,
}: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);

const handleSelect = (item: string) => {
onChange(item);
setIsOpen(false);
};
Comment thread
ehye1 marked this conversation as resolved.
Outdated

const closeWithFocus = () => {
setIsOpen(false);
triggerRef.current?.focus();
};

useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") closeWithFocus();
};
const handleClickOutside = (e: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
closeWithFocus();
}
};
Comment thread
ehye1 marked this conversation as resolved.
Outdated
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);

return (
<div
ref={containerRef}
className={cn("inline-flex flex-col", isOpen && "shadow-timo", className)}
>
<button
ref={triggerRef}
type="button"
onClick={() => setIsOpen((prev) => !prev)}
aria-haspopup="listbox"
aria-expanded={isOpen}
className={cn(
"flex h-8 items-center gap-2.5 bg-white px-2",
"border-timo-gray-500 border",
isOpen ? "rounded-t-[4px]" : "rounded-[4px]",
)}
>
<span className="typo-headline-m-14 text-timo-gray-900 whitespace-nowrap">
{value || placeholder}
</span>
<div className="flex size-6 shrink-0 items-center justify-center">
<svg
Comment thread
ehye1 marked this conversation as resolved.
Outdated
width="16"
height="8"
viewBox="0 0 16 8"
fill="none"
aria-hidden="true"
className="text-timo-gray-900"
>
<path
d={isOpen ? "M1 7L8 1L15 7" : "M1 1L8 7L15 1"}
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</button>

{isOpen && (
<ul
role="listbox"
className="border-timo-gray-500 flex flex-col gap-1.5 rounded-b-[4px] border-x border-b bg-white px-2 py-1"
>
{items.map((item, i) => (
<li key={item} className="flex flex-col gap-1">
Comment thread
ehye1 marked this conversation as resolved.
Outdated
<span
role="option"
aria-selected={item === value}
tabIndex={0}
onClick={() => handleSelect(item)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") handleSelect(item);
}}
className="typo-body-m-12 text-timo-gray-900 cursor-pointer"
>
{item}
</span>
{i < items.length - 1 && (
<div className="bg-timo-gray-500 h-px w-full" />
)}
</li>
))}
</ul>
)}
</div>
);
};
1 change: 1 addition & 0 deletions packages/timo-design-system/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Checkbox } from "@components/checkbox/Checkbox";
export { Dropdown } from "@components/dropdown/Dropdown";
export { Color } from "@components/color/Color";
export { Typography } from "@components/typography/Typography";
export { Tag } from "@components/tag/Tag";
Expand Down
Loading