81 lines
2.2 KiB
TypeScript
81 lines
2.2 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
type SelectOption<T extends string> = {
|
|
value: T;
|
|
label: string;
|
|
};
|
|
|
|
type SelectProps<T extends string> = {
|
|
value: T;
|
|
options: SelectOption<T>[];
|
|
onChange: (value: T) => void;
|
|
};
|
|
|
|
export function Select<T extends string>({
|
|
value,
|
|
options,
|
|
onChange,
|
|
}: SelectProps<T>) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
|
|
const selected =
|
|
options.find((option) => option.value === value) ??
|
|
options[0];
|
|
|
|
useEffect(() => {
|
|
function handleClick(event: MouseEvent) {
|
|
if (
|
|
ref.current &&
|
|
!ref.current.contains(event.target as Node)
|
|
) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('mousedown', handleClick);
|
|
|
|
return () => {
|
|
window.removeEventListener('mousedown', handleClick);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((current) => !current)}
|
|
className="flex w-full items-center justify-between rounded-xl border border-white/10 bg-slate-950 px-3 py-3 text-left text-sm text-slate-200 outline-none transition hover:border-blue-500/30 focus:border-blue-500/40"
|
|
>
|
|
<span>{selected.label}</span>
|
|
<span className="text-xs text-slate-500">▼</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute z-50 mt-2 w-full overflow-hidden rounded-xl border border-white/10 bg-slate-950 shadow-2xl">
|
|
{options.map((option) => {
|
|
const active = option.value === value;
|
|
|
|
return (
|
|
<button
|
|
key={option.value}
|
|
type="button"
|
|
onClick={() => {
|
|
onChange(option.value);
|
|
setOpen(false);
|
|
}}
|
|
className={`block w-full px-3 py-2 text-left text-sm transition ${
|
|
active
|
|
? 'bg-blue-500/15 text-blue-200'
|
|
: 'text-slate-300 hover:bg-white/5 hover:text-white'
|
|
}`}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |