185 lines
4.4 KiB
TypeScript
185 lines
4.4 KiB
TypeScript
|
|
import * as React from 'react';
|
||
|
|
|
||
|
|
// 创建Select上下文
|
||
|
|
const SelectContext = React.createContext<{
|
||
|
|
value: string;
|
||
|
|
setValue: (value: string) => void;
|
||
|
|
open: boolean;
|
||
|
|
setOpen: (open: boolean) => void;
|
||
|
|
} | null>(null);
|
||
|
|
|
||
|
|
// Select组件接口
|
||
|
|
export interface SelectProps {
|
||
|
|
defaultValue?: string;
|
||
|
|
value?: string;
|
||
|
|
onValueChange?: (value: string) => void;
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// SelectTrigger组件接口
|
||
|
|
export interface SelectTriggerProps {
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// SelectValue组件接口
|
||
|
|
export interface SelectValueProps {
|
||
|
|
placeholder?: string;
|
||
|
|
children?: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
// SelectContent组件接口
|
||
|
|
export interface SelectContentProps {
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
// SelectItem组件接口
|
||
|
|
export interface SelectItemProps {
|
||
|
|
value: string;
|
||
|
|
children: React.ReactNode;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Select组件 - 管理选择器的状态
|
||
|
|
*/
|
||
|
|
export const Select: React.FC<SelectProps> = ({
|
||
|
|
defaultValue = '',
|
||
|
|
value: valueProp,
|
||
|
|
onValueChange,
|
||
|
|
children,
|
||
|
|
className = ''
|
||
|
|
}) => {
|
||
|
|
const [valueState, setValueState] = React.useState(defaultValue);
|
||
|
|
const [open, setOpen] = React.useState(false);
|
||
|
|
|
||
|
|
const value = valueProp !== undefined ? valueProp : valueState;
|
||
|
|
const setValue = valueProp !== undefined && onValueChange
|
||
|
|
? onValueChange
|
||
|
|
: setValueState;
|
||
|
|
|
||
|
|
// 点击外部关闭下拉菜单
|
||
|
|
React.useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
const handleClickOutside = (event: MouseEvent) => {
|
||
|
|
const target = event.target as HTMLElement;
|
||
|
|
if (!target.closest(`.${className}`) && !target.closest('.select-trigger')) {
|
||
|
|
setOpen(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
document.addEventListener('mousedown', handleClickOutside);
|
||
|
|
return () => {
|
||
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}, [open, className]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<SelectContext.Provider value={{ value, setValue, open, setOpen }}>
|
||
|
|
<div className={className}>{children}</div>
|
||
|
|
</SelectContext.Provider>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SelectTrigger组件 - 选择器的触发器按钮
|
||
|
|
*/
|
||
|
|
export const SelectTrigger: React.FC<SelectTriggerProps> = ({ children, className = '' }) => {
|
||
|
|
const context = React.useContext(SelectContext);
|
||
|
|
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('SelectTrigger must be used within a Select component');
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleClick = () => {
|
||
|
|
context.setOpen(!context.open);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleClick}
|
||
|
|
className={`px-4 py-2 border rounded-md flex items-center justify-between w-full select-trigger ${className}`}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
<span className="ml-2">▼</span>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SelectValue组件 - 显示当前选中的值或占位符
|
||
|
|
*/
|
||
|
|
export const SelectValue: React.FC<SelectValueProps> = ({ placeholder, children }) => {
|
||
|
|
const context = React.useContext(SelectContext);
|
||
|
|
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('SelectValue must be used within a Select component');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (context.value && children) {
|
||
|
|
return <span>{children}</span>;
|
||
|
|
}
|
||
|
|
|
||
|
|
return placeholder ? (
|
||
|
|
<span className="text-gray-500">{placeholder}</span>
|
||
|
|
) : null;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SelectContent组件 - 包含选项的下拉菜单
|
||
|
|
*/
|
||
|
|
export const SelectContent: React.FC<SelectContentProps> = ({ children, className = '' }) => {
|
||
|
|
const context = React.useContext(SelectContext);
|
||
|
|
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('SelectContent must be used within a Select component');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!context.open) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`absolute mt-1 border rounded-md bg-white shadow-lg z-10 max-h-60 overflow-auto ${className}`}>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SelectItem组件 - 选择器中的单个选项
|
||
|
|
*/
|
||
|
|
export const SelectItem: React.FC<SelectItemProps> = ({
|
||
|
|
value,
|
||
|
|
children,
|
||
|
|
className = ''
|
||
|
|
}) => {
|
||
|
|
const context = React.useContext(SelectContext);
|
||
|
|
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('SelectItem must be used within a Select component');
|
||
|
|
}
|
||
|
|
|
||
|
|
const isSelected = context.value === value;
|
||
|
|
|
||
|
|
const handleClick = () => {
|
||
|
|
context.setValue(value);
|
||
|
|
context.setOpen(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleClick}
|
||
|
|
className={`block w-full text-left px-4 py-2 hover:bg-gray-100 ${isSelected ? 'bg-blue-50 text-blue-500' : ''} ${className}`}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default Select;
|