import React, {useEffect, useRef, useState} from 'react'; const normal = (text) => text.trim().toLowerCase(); // Parts that start with what was typed first (an exact match before any other), // then parts that contain it. With nothing typed, all of them, with the // suggested one (what this crop is usually harvested for) at the top. function matching(parts, text, suggestedId) { const term = normal(text); if (!term) return [...parts].sort((a, b) => (b.id === suggestedId) - (a.id === suggestedId)); const rank = (part) => { const name = normal(part.name); if (name === term) return 0; if (name.startsWith(term)) return 1; return name.includes(term) ? 2 : 3; }; return parts.filter((part) => rank(part) < 3).sort((a, b) => rank(a) - rank(b)); } // One big question, "What part did you harvest?", with a text box that // completes the answer from the plant parts we have ("fruit", "leaf", ...), in // the same style as the crop picker. Choosing one calls onChoose(part); the // dialog then moves on to how much. // // A combobox with a listbox of matches: Up and Down move through them, Enter // chooses the highlighted one (or the top match if none is highlighted), and // Escape closes the list before it would close the dialog. export default function PlantPartPicker({id, parts, suggestedId, onChoose}) { const [term, setTerm] = useState(''); const [open, setOpen] = useState(true); // the list starts open, showing every part const [activeIndex, setActiveIndex] = useState(-1); const list = useRef(null); const listId = `${id}-results`; const results = matching(parts, term, suggestedId); const showing = open && results.length > 0; const noMatch = term.trim() !== '' && results.length === 0; useEffect(() => { const option = activeIndex >= 0 && list.current && list.current.children[activeIndex]; if (option && option.scrollIntoView) option.scrollIntoView({block: 'nearest'}); }, [activeIndex]); function onKeyDown(event) { if (event.key === 'ArrowDown') { event.preventDefault(); if (showing) setActiveIndex((activeIndex + 1) % results.length); else setOpen(true); } else if (event.key === 'ArrowUp' && showing) { event.preventDefault(); setActiveIndex(activeIndex <= 0 ? results.length - 1 : activeIndex - 1); } else if (event.key === 'Enter') { // Never submit a form from here: choose a part, or do nothing. event.preventDefault(); if (showing) onChoose(results[activeIndex >= 0 ? activeIndex : 0]); } else if (event.key === 'Escape' && showing) { // Close the list; a second Escape then closes the dialog. event.stopPropagation(); setOpen(false); } } const active = activeIndex >= 0 ? results[activeIndex] : null; return (