184 lines
4.7 KiB
TypeScript
184 lines
4.7 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
|
|
interface SelectProps {
|
|
value?: string;
|
|
onValueChange?: (value: string) => void;
|
|
disabled?: boolean;
|
|
className?: string;
|
|
placeholder?: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface SelectTriggerProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
interface SelectContentProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
interface SelectItemProps {
|
|
value: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
interface SelectValueProps {
|
|
placeholder?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function Select({
|
|
value,
|
|
onValueChange,
|
|
disabled = false,
|
|
className = '',
|
|
placeholder,
|
|
children
|
|
}: SelectProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [selectedValue, setSelectedValue] = useState(value || '');
|
|
const [selectedLabel, setSelectedLabel] = useState('');
|
|
const selectRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
setSelectedValue(value || '');
|
|
}, [value]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const handleSelect = (value: string, label: string) => {
|
|
setSelectedValue(value);
|
|
setSelectedLabel(label);
|
|
onValueChange?.(value);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div ref={selectRef} className={`relative ${className}`}>
|
|
{React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child)) {
|
|
if (child.type === SelectTrigger) {
|
|
return React.cloneElement(child as any, {
|
|
onClick: () => !disabled && setIsOpen(!isOpen),
|
|
disabled,
|
|
selectedValue: selectedValue,
|
|
selectedLabel: selectedLabel,
|
|
placeholder,
|
|
isOpen
|
|
});
|
|
}
|
|
if (child.type === SelectContent && isOpen) {
|
|
return React.cloneElement(child as any, {
|
|
onSelect: handleSelect
|
|
});
|
|
}
|
|
}
|
|
return child;
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SelectTrigger({
|
|
children,
|
|
className = '',
|
|
onClick,
|
|
disabled,
|
|
selectedValue,
|
|
selectedLabel,
|
|
placeholder,
|
|
isOpen
|
|
}: SelectTriggerProps & {
|
|
onClick?: () => void;
|
|
disabled?: boolean;
|
|
selectedValue?: string;
|
|
selectedLabel?: string;
|
|
placeholder?: string;
|
|
isOpen?: boolean;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
className={`w-full px-3 py-2 text-left border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${className}`}
|
|
>
|
|
<span className="flex items-center justify-between">
|
|
<span className={selectedValue ? 'text-gray-900' : 'text-gray-500'}>
|
|
{selectedLabel || placeholder || '请选择...'}
|
|
</span>
|
|
<svg
|
|
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function SelectContent({
|
|
children,
|
|
className = '',
|
|
onSelect
|
|
}: SelectContentProps & {
|
|
onSelect?: (value: string, label: string) => void;
|
|
}) {
|
|
return (
|
|
<div className={`absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto ${className}`}>
|
|
{React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child) && child.type === SelectItem) {
|
|
return React.cloneElement(child as any, {
|
|
onSelect
|
|
});
|
|
}
|
|
return child;
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SelectItem({
|
|
value,
|
|
children,
|
|
className = '',
|
|
onSelect
|
|
}: SelectItemProps & {
|
|
onSelect?: (value: string, label: string) => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelect?.(value, children as string)}
|
|
className={`w-full px-3 py-2 text-left hover:bg-gray-100 focus:bg-gray-100 focus:outline-none ${className}`}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export function SelectValue({
|
|
placeholder,
|
|
className = ''
|
|
}: SelectValueProps) {
|
|
return (
|
|
<span className={className}>
|
|
{placeholder}
|
|
</span>
|
|
);
|
|
}
|