diff --git a/src/components/dropdown/Dropdown.module.css b/src/components/dropdown/Dropdown.module.css new file mode 100644 index 0000000..acae877 --- /dev/null +++ b/src/components/dropdown/Dropdown.module.css @@ -0,0 +1,117 @@ +.dropdownContainer { + position: relative; + width: max-content; +} + +.trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 36px; + cursor: pointer; + user-select: none; + font-weight: 500; + border: 1px solid; + border-radius: var(--form-border-radius); + transition: + border-color 0.15s ease, + background-color 0.15s ease; + box-shadow: var(--form-box-shadow); + + &.neutral { + color: var(--gray-9); + border-color: var(--gray-3); + background-color: var(--gray-0); + + &:hover { + @media (hover: hover) { + background-color: var(--gray-3); + } + } + } + + &.brand { + border-color: var(--brand-color); + color: var(--gray-9); + background-color: var(--gray-0); + + &:hover { + @media (hover: hover) { + background-color: var(--brand-color); + } + } + } + + &.small { + font-size: 12px; + padding: 6px; + } + + &.medium { + font-size: 14px; + padding: 7px; + } + + &.large { + font-size: 16px; + padding: 10px; + } +} + +.icon { + transition: transform 0.2s ease; + + &.open { + transform: rotate(180deg); + } +} + +.menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background-color: var(--gray-0); + border: 1px solid var(--brand-color); + border-radius: var(--form-border-radius); + box-shadow: var(--form-box-shadow); + z-index: 10; +} + +.option { + display: block; + width: 100%; + text-align: left; + border: none; + background: none; + cursor: pointer; + color: var(--gray-9); + transition: background-color 0.15s ease; + text-decoration: none; + + &:hover { + @media (hover: hover) { + background-color: var(--gray-3); + } + } + + &.selected { + background-color: var(--gray-2); + } + + &.small { + font-size: 12px; + padding: 4px 8px; + } + + &.medium { + font-size: 14px; + padding: 6px 12px; + } + + &.large { + font-size: 16px; + padding: 8px 16px; + } +} diff --git a/src/components/dropdown/index.tsx b/src/components/dropdown/index.tsx new file mode 100644 index 0000000..03ce118 --- /dev/null +++ b/src/components/dropdown/index.tsx @@ -0,0 +1,153 @@ +import { useState, useRef, useEffect } from "react"; + +import { ChevronDown } from "lucide-react"; +import classNames from "classnames"; + +import s from "./Dropdown.module.css"; +import Link from "next/link"; + +type DropdownSize = "small" | "medium" | "large"; +type DropdownTheme = "neutral" | "brand"; + +interface DropdownOptionBase { + label: string; +} + +interface DropdownOptionWithValue extends DropdownOptionBase { + value: string; + href?: never; +} + +interface DropdownOptionWithLink extends DropdownOptionBase { + href: string; + value?: never; +} + +type DropdownOption = DropdownOptionWithValue | DropdownOptionWithLink; + +// Helper type to check if array of options has value property +type IsValueOption = T extends DropdownOptionWithValue ? true : false; + +type DropdownProps = { + options: T[]; + placeholder?: string; + size?: DropdownSize; + theme?: DropdownTheme; + className?: string; + width?: string; +} & (IsValueOption extends true + ? { + value?: string; + onChange: (value: string) => void; + } + : { + value?: never; + onChange?: never; + }); + +function Dropdown({ + options, + value, + onChange, + placeholder = "Select option", + size = "medium", + theme = "brand", + className, + width, +}: DropdownProps) { + const [open, setOpen] = useState(false); + const toggleOpen = () => setOpen(!open); + + const dropdownRef = useRef(null); + + // Find selected option by either value or current path + const selectedOption = options.find((opt) => + "value" in opt ? opt.value === value : false, + ); + + const handleSelect = (option: DropdownOption) => { + if ("value" in option && option.value) { + onChange?.(option.value); + } + + setOpen(false); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + {open && ( +
+ {options.map((option) => { + const optionClasses = classNames(s.option, { + [s.selected]: "value" in option ? option.value === value : false, + [s.small]: size === "small", + [s.medium]: size === "medium", + [s.large]: size === "large", + }); + + if ("href" in option && option.href) { + return ( + handleSelect(option)} + > + {option.label} + + ); + } + + return ( + + ); + })} +
+ )} +
+ ); +} + +export default Dropdown;