Files
tabdeal-job-queue/frontend/src/components/SelectField.tsx

63 lines
1.9 KiB
TypeScript

import { ChevronDown } from "lucide-react";
import type { FocusEvent } from "react";
import { useMemo, useState } from "react";
export type SelectOption = {
value: string;
label: string;
};
type SelectFieldProps = {
label?: string;
value: string;
options: SelectOption[];
placeholder?: string;
onChange: (value: string) => void;
};
export function SelectField({ label, value, options, placeholder = "Select", onChange }: SelectFieldProps) {
const [open, setOpen] = useState(false);
const selectedOption = useMemo(() => options.find((option) => option.value === value), [options, value]);
const closeOnOutsideBlur = (event: FocusEvent<HTMLDivElement>) => {
const nextTarget = event.relatedTarget;
if (!(nextTarget instanceof Node) || !event.currentTarget.contains(nextTarget)) {
setOpen(false);
}
};
const control = (
<div className="select-field" onBlur={closeOnOutsideBlur}>
<button className={`select-trigger ${open ? "open" : ""}`} type="button" aria-haspopup="listbox" aria-expanded={open} onClick={() => setOpen(!open)}>
<span>{selectedOption?.label ?? placeholder}</span>
<ChevronDown size={16} />
</button>
{open && (
<div className="select-menu" role="listbox">
{options.map((option) => (
<button
className={option.value === value ? "select-option active" : "select-option"}
key={option.value}
role="option"
type="button"
aria-selected={option.value === value}
onClick={() => {
onChange(option.value);
setOpen(false);
}}
>
{option.label}
</button>
))}
</div>
)}
</div>
);
if (!label) return control;
return (
<label>
{label}
{control}
</label>
);
}