import { useId, useMemo, useRef, useState } from 'react';
import { ChevronDown } from 'lucide-react';
export interface PractitionerComboOption {
key: string;
name: string;
/** Role and specialty, when the record carries them. */
detail?: string;
/** The signed-in viewer. */
isYou?: boolean;
}
interface PractitionerComboboxProps {
label: string;
allLabel: string;
options: PractitionerComboOption[];
/** `undefined` is the all-practitioners state. */
selectedKey: string | undefined;
onSelect: (key: string | undefined) => void;
}
const termsOf = (query: string) => query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
/** The "(you)" mark: a tag to the eye, the words in parentheses to a screen reader. */
const YouTag = ({ id }: { id?: string }) => (
(you)
);
export const matchSentence = (matched: number, total: number, query: string) =>
`${matched} of ${total} ${total === 1 ? 'practitioner' : 'practitioners'} ${matched === 1 ? 'matches' : 'match'} “${query.trim()}”`;
export const noMatchSentence = (query: string) => `No practitioner matches “${query.trim()}”.`;
export const clearTheBoxSentence = (total: number) =>
`Clear the box to see ${total === 1 ? 'the one practitioner' : `all ${total}`}.`;
/** A type-ahead combobox: holds the current value, filters as you type, Escape restores. */
const PractitionerCombobox = ({ label, allLabel, options, selectedKey, onSelect }: PractitionerComboboxProps) => {
const baseId = useId();
const inputId = `${baseId}-input`;
const listId = `${baseId}-list`;
const youId = `${baseId}-you`;
const inputRef = useRef(null);
const [open, setOpen] = useState(false);
// null while not typing, so the field shows what it holds.
const [query, setQuery] = useState(null);
const [activeIndex, setActiveIndex] = useState(-1);
const selected = selectedKey ? options.find((option) => option.key === selectedKey) : undefined;
const typing = query !== null && query.trim() !== '';
const shown = useMemo<(PractitionerComboOption | null)[]>(() => {
if (!typing) return [null, ...options];
const terms = termsOf(query ?? '');
return options.filter((option) => {
const name = option.name.toLocaleLowerCase();
return terms.every((term) => name.includes(term));
});
}, [options, query, typing]);
const close = () => {
setOpen(false);
setQuery(null);
setActiveIndex(-1);
};
const choose = (option: PractitionerComboOption | null) => {
onSelect(option?.key);
close();
};
const optionId = (index: number) => `${baseId}-opt-${index}`;
return (
{
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) close();
}}
>
{/* The box only hands a pointer press to the input, which keyboard users reach directly. */}