Skip to content

Commit

Permalink
(#0) 드롭다운 컴포넌트 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
baegofda committed Dec 1, 2023
1 parent b8aab52 commit ce35fc6
Show file tree
Hide file tree
Showing 24 changed files with 604 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/core/components/Drawer/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HTMLAttributes } from "react";

import { ModalBaseProps } from "../../Modal/ModalBase/types";
import { TypographyProps } from "../../Typography";
import { TypographyProps } from "../../Typography/types";

export interface DrawerProps extends Pick<ModalBaseProps, "target" | "isOpen">, HTMLAttributes<HTMLElement> {
title: TypographyProps<"strong">["text"];
Expand Down
69 changes: 69 additions & 0 deletions src/core/components/Dropdown/DropdownBase/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Meta } from "@storybook/react";

import { CaretDown } from "@phosphor-icons/react";
import { useState } from "react";
import DropdownBase from "./index";

const meta = {
title: "core/Dropdown/DropdownBase",
component: DropdownBase,
argTypes: {},
} satisfies Meta<typeof DropdownBase>;

export default meta;

export const Default = () => {
const [ currentValue, setCurrentValue ] = useState("");

const data = [ "A반", "B반", "C반", "D반", "E반" ];

const items = data.map(item => (
<DropdownBase.Item
onClick = {() => setCurrentValue(item)}
>
{item}
</DropdownBase.Item>
));

return (
<DropdownBase
trigger = {
<DropdownBase.Trigger>
{currentValue || "옵션을 선택해주세요"}
</DropdownBase.Trigger>
}
content = {<DropdownBase.Items items = {items} />}
/>
);
};

export const DropdownBaseWithIcon = () => {
const [ currentValue, setCurrentValue ] = useState("");

const data = [ "A반", "B반", "C반", "D반", "E반" ];

const items = data.map((item, idx) => (
<DropdownBase.Item
key = {idx}
onClick = {() => setCurrentValue(item)}
>
{item}
</DropdownBase.Item>
));

return (
<DropdownBase
trigger = {
<DropdownBase.Trigger>
{({ isToggle }) => (
<div className = "flex items-center">
{currentValue || "옵션을 선택해주세요"}
<CaretDown size = "16" className = {isToggle ? "rotate-180" : "rotate-0"}/>
</div>
)}
</DropdownBase.Trigger>
}
content = {<DropdownBase.Items items = {items} />}
/>
);
};
38 changes: 38 additions & 0 deletions src/core/components/Dropdown/DropdownBase/DropdownItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PropsWithChildren, forwardRef, useContext } from "react";

import clsx from "clsx";
import { DropdownContext } from ".";
import { DropdownContextValue, DropdownItemProps } from "./types";

const DropdownItem = forwardRef((
{
children,
onClick,
className,
...props
}: PropsWithChildren<DropdownItemProps>,
ref: React.Ref<HTMLLIElement>,
) => {
const { setIsToggle } = useContext(DropdownContext) as DropdownContextValue;

const onClickHandler = (e: React.MouseEvent<HTMLLIElement>) => {
setIsToggle(false);
onClick?.(e);
};

return (
<li
ref = {ref}
role = "option"
onClick = {onClickHandler}
className = {clsx("cursor-pointer", className)}
{...props}
>
{children}
</li>
);
});

export default DropdownItem;

DropdownItem.displayName = "DropdownItem";
29 changes: 29 additions & 0 deletions src/core/components/Dropdown/DropdownBase/DropdownItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import clsx from "clsx";
import { forwardRef } from "react";

import { DropdownItemsProps } from "./types";

const DropdownItems = forwardRef((
{
className,
items,
...props
}: DropdownItemsProps,
ref: React.Ref<HTMLUListElement>,
) => {

return (
<ul
ref = {ref}
className = {clsx("absolute z-10", className)}
role = "listbox"
{...props}
>
{items}
</ul>
);
});

export default DropdownItems;

DropdownItems.displayName = "DropdownItems";
40 changes: 40 additions & 0 deletions src/core/components/Dropdown/DropdownBase/DropdownTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { forwardRef, useContext } from "react";

import clsx from "clsx";
import { DropdownContext } from "./index";
import { DropdownContextValue, DropdownTriggerProps } from "./types";

const DropdownTrigger = forwardRef((
{
onClick,
className,
children,
...props
}: DropdownTriggerProps,
ref: React.Ref<HTMLButtonElement>,
) => {
const { isToggle, setIsToggle } = useContext(DropdownContext) as DropdownContextValue;
const content = typeof children === "function" ? children({ isToggle }) : children;

const onClickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
setIsToggle(v => !v);
onClick?.(e);
};

return (
<button
ref = {ref}
onClick = {onClickHandler}
className = {clsx("cursor-pointer", className)}
aria-haspopup = "listbox"
aria-expanded = {isToggle}
{...props}
>
{content}
</button>
);
});

export default DropdownTrigger;

DropdownTrigger.displayName = "DropdownTrigger";
32 changes: 32 additions & 0 deletions src/core/components/Dropdown/DropdownBase/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import useClickOutside from "@/hooks/useClickOutSide";
import clsx from "clsx";
import { createContext, useState } from "react";

import DropdownItem from "./DropdownItem";
import DropdownItems from "./DropdownItems";
import DropdownTrigger from "./DropdownTrigger";
import { DropdownContextValue, DropdownProps, ReturnType } from "./types";

export const DropdownContext = createContext<DropdownContextValue | undefined>(undefined);
DropdownContext.displayName = "DropdownContext";

const DropdownBase = ({ className, trigger, content }: DropdownProps) => {
const [ isToggle, setIsToggle ] = useState(false);
const { contentRef } = useClickOutside<HTMLDivElement>(() => setIsToggle(false));

return (
<DropdownContext.Provider value = {{ isToggle, setIsToggle }}>
<div ref = {contentRef} className = {clsx(className, "relative")}>
{trigger}
{isToggle && content}
</div>
</DropdownContext.Provider>
);
};

export default DropdownBase as unknown as ReturnType;

DropdownBase.displayName = "DropdownBase";
DropdownBase.Trigger = DropdownTrigger;
DropdownBase.Items = DropdownItems;
DropdownBase.Item = DropdownItem;
35 changes: 35 additions & 0 deletions src/core/components/Dropdown/DropdownBase/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Dispatch, HTMLAttributes, SetStateAction } from "react";
import DropdownItem from "../DropdownItem";
import DropdownItems from "../DropdownItems";
import DropdownTrigger from "../DropdownTrigger";

export interface DropdownProps {
className?: string,
trigger: React.ReactNode,
content: React.ReactNode,
}

export type DropdownContextValue = {
isToggle: boolean;
setIsToggle: Dispatch<SetStateAction<boolean>>;
};

export interface DropdownItemProps extends HTMLAttributes<HTMLLIElement> {}

export interface DropdownItemsProps extends HTMLAttributes<HTMLUListElement>{
items: React.ReactNode[];
}

export interface DropdownTriggerProps extends Omit<HTMLAttributes<HTMLButtonElement>, "children"> {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode | ((props: { isToggle: boolean }) => React.ReactNode);
}

type Dropdown = (props: DropdownProps) => React.ReactElement;

export type ReturnType = Dropdown & {
displayName: string;
Trigger: typeof DropdownTrigger;
Items: typeof DropdownItems;
Item: typeof DropdownItem;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Meta } from "@storybook/react";
import { useState } from "react";

import DropdownFilter from "../DropdownFilter";

const meta = {
title: "core/Dropdown/DropdownFilter",
component: DropdownFilter,
argTypes: {},
} satisfies Meta<typeof DropdownFilter>;

export default meta;

export const Default = () => {
const [ currentValue, setCurrentValue ] = useState("");
const data = [ "학부모", "교육기관", "둘다" ];

const items = data.map((item, idx) => (
<DropdownFilter.Item
key = {idx}
checked = {item === currentValue}
onClick = {() => setCurrentValue(item)}
>
{item}
</DropdownFilter.Item>
));

return (
<DropdownFilter
trigger = {<DropdownFilter.Trigger className = "w-[10rem]" currentValue = {currentValue || "선택해주세요"}/>}
content = {
<DropdownFilter.Items
className = "top-[1.31rem]"
items = {items}
/>
}
/>
);
};
28 changes: 28 additions & 0 deletions src/core/components/Dropdown/DropdownFilter/DropdownFilterItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import clsx from "clsx";
import { forwardRef } from "react";

import DropdownBase from "../DropdownBase";
import { DropdownFilterItemProps } from "./types";

const DropdownFilterItem = forwardRef((
{
className,
children,
checked,
...props
}: DropdownFilterItemProps,
ref: React.Ref<HTMLLIElement>,
) => {

return (
<DropdownBase.Item
ref = {ref}
className = {clsx("text-gray-08 text-body-01-regular hover:font-bold", checked && "font-bold", className)}
{...props}
>
{children}
</DropdownBase.Item>
);
});

export default DropdownFilterItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from "clsx";
import { forwardRef } from "react";

import DropdownBase from "../DropdownBase";
import { DropdownItemsProps } from "../DropdownBase/types";

const DropdownFilterItems = forwardRef((
{
className,
...props
}: DropdownItemsProps,
ref: React.Ref<HTMLUListElement>,
) => {

return (
<DropdownBase.Items
ref = {ref}
className = {clsx("flex-v-stack gap-y-6 px-3 py-4 border border-gray-03 overflow-hidden right-0 rounded-xl min-w-full", className)}
{...props}
/>
);
});

export default DropdownFilterItems;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { forwardRef } from "react";

import { CaretDown } from "@phosphor-icons/react";
import Typography from "../../Typography";
import DropdownBase from "../DropdownBase";
import { DropdownFilterTriggerProps } from "./types";

const DropdownFilterTrigger = forwardRef((
{
currentValue,
...props
}: DropdownFilterTriggerProps,
ref: React.Ref<HTMLButtonElement>,
) => {

return (
<DropdownBase.Trigger ref = {ref} {...props}>
{({ isToggle }) => (
<div className = "flex items-center justify-between gap-x-1">
<Typography theme = "body-02-regular" color = "gray-06" text = {currentValue}/>
<CaretDown size = "16" className = {isToggle ? "rotate-180 text-gray-06" : "rotate-0 text-gray-06"} weight = "fill"/>
</div>
)}
</DropdownBase.Trigger>
);
});

export default DropdownFilterTrigger;
Loading

0 comments on commit ce35fc6

Please sign in to comment.