Files
fiberops-web/src/components/ui/select.tsx
2026-04-13 09:36:37 +08:00

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 };