131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { useState, forwardRef, useRef, useEffect } from 'react';
|
|
|
|
interface SelectProps {
|
|
value?: string | string[];
|
|
onChange: (value: string | string[]) => void;
|
|
options: { label: string; value: string }[];
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
multiple?: boolean;
|
|
}
|
|
|
|
const Select = forwardRef<HTMLDivElement, SelectProps>(
|
|
({ value, onChange, options, placeholder, disabled = false, multiple = false }, ref) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const selectRef = useRef<HTMLDivElement>(null);
|
|
const triggerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isMulti = multiple;
|
|
const currentValue = value;
|
|
|
|
const selectedLabels = isMulti
|
|
? (Array.isArray(currentValue) ? currentValue : [])
|
|
: options.find((o) => o.value === currentValue)?.label || '';
|
|
|
|
function handleClick() {
|
|
setIsOpen(!isOpen);
|
|
}
|
|
|
|
function handleSelect(optionValue: string) {
|
|
if (isMulti) {
|
|
const newValues = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
if (newValues.includes(optionValue)) {
|
|
onChange(newValues.filter((v) => v !== optionValue));
|
|
} else {
|
|
onChange([...newValues, optionValue]);
|
|
}
|
|
} else {
|
|
onChange(optionValue);
|
|
}
|
|
setIsOpen(false);
|
|
}
|
|
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (triggerRef.current && !triggerRef.current.contains(e.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<div className="relative" ref={ref}>
|
|
<div
|
|
ref={triggerRef}
|
|
onClick={handleClick}
|
|
className={`
|
|
flex items-center justify-between px-3 py-2 rounded-lg border
|
|
${disabled ? 'bg-surface-50 dark:bg-surface-800 border-surface-200 dark:border-surface-700 text-surface-300' : 'bg-white dark:bg-surface-800 border-surface-300 dark:border-surface-600 cursor-pointer hover:border-surface-400 dark:hover:border-surface-500'}
|
|
transition-all duration-200
|
|
min-w-[200px] max-w-full
|
|
`}
|
|
>
|
|
<span className={selectedLabels.length === 0 ? 'text-surface-400' : 'text-surface-900 dark:text-surface-200 truncate'}>
|
|
{placeholder || 'Select...'}
|
|
{isMulti && Array.isArray(selectedLabels) && selectedLabels.length > 0 && <span className="text-surface-400"> ({selectedLabels.length})</span>}
|
|
{!isMulti && selectedLabels && <span className="text-surface-900 dark:text-surface-200"> {selectedLabels}</span>}
|
|
</span>
|
|
<svg
|
|
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<polyline points="4 8 8 12" />
|
|
</svg>
|
|
</div>
|
|
|
|
{isOpen && (
|
|
<div className="absolute z-50 mt-1 w-full bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700 shadow-lg max-h-60 overflow-y-auto">
|
|
{options.map((option) => {
|
|
const isSelected = isMulti
|
|
? Array.isArray(currentValue) && currentValue.includes(option.value)
|
|
: currentValue === option.value;
|
|
|
|
return (
|
|
<div
|
|
key={option.value}
|
|
onClick={() => handleSelect(option.value)}
|
|
className={`
|
|
px-3 py-2 cursor-pointer hover:bg-surface-50 dark:hover:bg-surface-700
|
|
${isSelected ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' : 'text-surface-800 dark:text-surface-300'}
|
|
`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{isMulti && (
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={() => {}}
|
|
className="w-4 h-4"
|
|
readOnly
|
|
/>
|
|
)}
|
|
<span className="flex-1">{option.label}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
Select.displayName = 'Select';
|
|
|
|
export { Select };
|