83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
type Option = {
|
|
value: string;
|
|
label: string;
|
|
children?: Option[];
|
|
};
|
|
|
|
type CascaderProps = {
|
|
options: Option[];
|
|
defaultValue?: string[];
|
|
onChange?: (value: string[]) => void;
|
|
placeholder?: string;
|
|
};
|
|
|
|
const Cascader: React.FC<CascaderProps> = ({ options, defaultValue = [], onChange, placeholder = 'Select' }) => {
|
|
const [visible, setVisible] = useState(false);
|
|
const [selected, setSelected] = useState<string[]>(defaultValue);
|
|
const [menus, setMenus] = useState<Option[][]>([options]);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setVisible(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
const handleItemClick = (option: Option, level: number) => {
|
|
const newSelected = selected.slice(0, level).concat(option.value);
|
|
setSelected(newSelected);
|
|
if (option.children) {
|
|
setMenus(menus.slice(0, level + 1).concat([option.children]));
|
|
} else {
|
|
setMenus(menus.slice(0, level + 1));
|
|
setVisible(false);
|
|
onChange?.(newSelected);
|
|
}
|
|
};
|
|
|
|
const getDisplayText = () => {
|
|
if (selected.length === 0) return placeholder;
|
|
let currentOptions = options;
|
|
return selected.map(val => {
|
|
const opt = currentOptions.find(o => o.value === val);
|
|
currentOptions = opt?.children || [];
|
|
return opt?.label || '';
|
|
}).join(' / ');
|
|
};
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative inline-block">
|
|
<div
|
|
className="border border-gray-300 rounded px-3 py-1 cursor-pointer"
|
|
onClick={() => setVisible(!visible)}
|
|
>
|
|
{getDisplayText()}
|
|
</div>
|
|
{visible && (
|
|
<div className="absolute z-10 bg-white border border-gray-300 rounded shadow-lg mt-1 flex">
|
|
{menus.map((menu, level) => (
|
|
<ul key={level} className="list-none m-0 p-0 min-w-[150px] border-r border-gray-200 last:border-r-0">
|
|
{menu.map(option => (
|
|
<li
|
|
key={option.value}
|
|
className={`px-3 py-1 cursor-pointer hover:bg-gray-100 ${selected[level] === option.value ? 'bg-gray-200' : ''}`}
|
|
onClick={() => handleItemClick(option, level)}
|
|
>
|
|
{option.label}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Cascader; |