wing-ops/frontend/src/common/components/ui/ComboBox.tsx

100 lines
3.1 KiB
TypeScript
Executable File

import { useState, useRef, useEffect } from 'react';
interface ComboBoxOption {
value: string;
label: string;
}
interface ComboBoxProps {
value: string | number;
onChange: (value: string) => void;
options: ComboBoxOption[];
placeholder?: string;
className?: string;
}
export function ComboBox({ value, onChange, options, placeholder, className }: ComboBoxProps) {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const selectedOption = options.find((opt) => opt.value === String(value));
const displayText = selectedOption?.label || placeholder || '선택';
return (
<div ref={containerRef} className="relative">
<div
className={`cursor-pointer flex items-center justify-between pr-2 ${className ?? ''}`}
onClick={() => setIsOpen(!isOpen)}
>
<span>{displayText}</span>
<span
className="text-caption text-fg-disabled"
style={{
transition: 'transform 0.2s',
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
}}
>
</span>
</div>
{isOpen && (
<div
className="absolute left-0 right-0 bg-bg-base border border-stroke overflow-y-auto z-[1000]"
style={{
top: 'calc(100% + 2px)',
borderRadius: 'var(--radius-sm)',
maxHeight: '200px',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
animation: 'fadeSlideDown 0.15s ease-out',
}}
>
{options.map((option) => (
<div
key={option.value}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
className="text-label-2 cursor-pointer"
style={{
padding: '8px 10px',
color: option.value === String(value) ? 'var(--color-accent)' : 'var(--fg-sub)',
background: option.value === String(value) ? 'rgba(6,182,212,0.1)' : 'transparent',
transition: '0.1s',
borderLeft:
option.value === String(value)
? '2px solid var(--color-accent)'
: '2px solid transparent',
}}
onMouseEnter={(e) => {
if (option.value !== String(value)) {
e.currentTarget.style.background = 'rgba(255,255,255,0.03)';
}
}}
onMouseLeave={(e) => {
if (option.value !== String(value)) {
e.currentTarget.style.background = 'transparent';
}
}}
>
{option.label}
</div>
))}
</div>
)}
</div>
);
}