casualroom/apps/fenghuo/web/components/selector/duty-select.tsx

265 lines
8.2 KiB
TypeScript
Executable File

'use client';
import * as React from 'react';
import { Check, ChevronDown, X } from 'lucide-react';
import { cn } from '@nice/ui/lib/utils';
import { Button } from '@nice/ui/components/button';
import { Input } from '@nice/ui/components/input';
import { Popover, PopoverContent, PopoverTrigger } from '@nice/ui/components/popover';
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@nice/ui/components/command';
import { useTranslation } from '@nice/i18n';
interface DutySelectProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
allowClear?: boolean;
}
export function DutySelect({
value = '',
onChange,
placeholder = '请选择或输入职务',
disabled = false,
className,
allowClear = true,
}: DutySelectProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value);
const inputRef = React.useRef<HTMLInputElement>(null);
const isSelectingRef = React.useRef(false);
// 添加一个ref来跟踪是否是通过点击打开的
const isOpeningRef = React.useRef(false);
// 获取预定义职务选项
const dutyOptions = React.useMemo(() => {
const dutyEntries = Object.entries({});
return dutyEntries.map(([value, _]) => ({
value,
label: t(`duty.${value}`),
}));
}, [t]);
// 同步外部value到内部inputValue
React.useEffect(() => {
setInputValue(value);
}, [value]);
// 处理输入值变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputValue(newValue);
onChange?.(newValue);
// 输入时显示下拉选项
if (!open) {
setOpen(true);
}
};
// 处理选项选择
const handleSelect = (optionLabel: string) => {
isSelectingRef.current = true; // 标记正在选择
setInputValue(optionLabel);
onChange?.(optionLabel);
// 延迟重置标记和重新聚焦
setTimeout(() => {
isSelectingRef.current = false;
inputRef.current?.focus(); // 重新聚焦到输入框
}, 50);
};
// 清除值
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setInputValue('');
onChange?.('');
inputRef.current?.focus();
};
// 处理输入框获得焦点 - 显示下拉选项
const handleInputFocus = () => {
if (!disabled && dutyOptions.length > 0) {
// 延迟设置,避免与点击事件冲突
setTimeout(() => {
if (!isOpeningRef.current) {
setOpen(true);
}
isOpeningRef.current = false;
}, 0);
}
};
// 处理输入框点击
const handleInputClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (!disabled && dutyOptions.length > 0) {
isOpeningRef.current = true;
setOpen(true);
}
};
// 处理输入框失去焦点
const handleInputBlur = (e: React.FocusEvent) => {
// 如果正在选择选项,不要关闭菜单
if (isSelectingRef.current) {
return;
}
// 延迟关闭,避免点击选项时立即关闭
setTimeout(() => {
// 再次检查是否正在选择选项
if (isSelectingRef.current) {
return;
}
// 检查焦点是否移动到了下拉选项中
const activeElement = document.activeElement;
const popoverElement = document.querySelector('[data-radix-popover-content]');
if (!popoverElement?.contains(activeElement)) {
setOpen(false);
}
}, 150);
};
// 处理键盘事件
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (!open) {
setOpen(true);
}
}
if (e.key === 'Escape') {
setOpen(false);
inputRef.current?.blur();
}
};
// 处理下拉箭头点击
const handleChevronClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (disabled) return;
setOpen(!open);
inputRef.current?.focus();
};
// 过滤选项(根据输入值进行模糊匹配)
const filteredOptions = React.useMemo(() => {
if (!inputValue.trim()) return dutyOptions;
return dutyOptions.filter(option =>
option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
option.value.toLowerCase().includes(inputValue.toLowerCase())
);
}, [dutyOptions, inputValue]);
return (
<div className={cn('relative', className)}>
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<div className="relative">
<Input
ref={inputRef}
value={inputValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
onClick={handleInputClick} // 添加点击处理
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className={cn(
'pr-10',
disabled && 'cursor-not-allowed'
)}
autoComplete="off"
// 添加这个属性防止输入框点击时关闭 Popover
onMouseDown={(e) => e.stopPropagation()}
/>
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
{allowClear && inputValue && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-transparent cursor-pointer"
onClick={handleClear}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
tabIndex={2}
>
<X className="h-3 w-3 opacity-50 hover:opacity-100" />
</Button>
)}
<div
role="button"
tabIndex={-1}
onClick={handleChevronClick}
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className="flex items-center justify-center p-1 hover:bg-gray-100 rounded cursor-pointer"
>
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
</div>
</div>
</PopoverTrigger>
<PopoverContent
className="w-full p-0 max-h-[210px] overflow-y-auto"
align="start"
style={{ width: 'var(--radix-popover-trigger-width)' }}
onOpenAutoFocus={(e) => e.preventDefault()} // 防止内容获得焦点
>
<Command>
<CommandList>
{filteredOptions.length === 0 ? (
<CommandEmpty>
{inputValue.trim() ? '当前无匹配的职务,可手动输入职务' : '请输入内容搜索职务'}
</CommandEmpty>
) : (
<CommandGroup>
{filteredOptions.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => handleSelect(option.label)}
className={cn(
"cursor-pointer",
inputValue === option.label && "bg-accent text-accent-foreground"
)}
onMouseDown={(e) => {
// 防止鼠标按下时输入框失去焦点
e.preventDefault();
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
inputValue === option.label ? 'opacity-100' : 'opacity-0'
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
);
}