import React, {useEffect, useRef, useState} from 'react'; import {getJson} from '../api'; function hint(searching, open, count) { if (searching) return 'Searching…'; if (open) return `${count} ${count === 1 ? 'crop' : 'crops'} found. Use the up and down arrow keys, then Enter, to choose one.`; return 'Search for a crop, then choose it.'; } // One big question, "What did you plant?", with search-as-you-type for the // answer, using the same /crops/search.json as the existing form's autosuggest // (which puts crops you have planted first). Choosing a crop calls onChoose(crop); // the dialog then shows it for confirmation. // // 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 CropPicker({id, onChoose}) { const [term, setTerm] = useState(''); const [results, setResults] = useState([]); const [noMatch, setNoMatch] = useState(false); // a search finished and found nothing const [searching, setSearching] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const list = useRef(null); const listId = `${id}-results`; const open = results.length > 0; useEffect(() => { setNoMatch(false); if (!term.trim()) { setResults([]); setSearching(false); return undefined; } setSearching(true); const controller = new AbortController(); const timer = setTimeout(async () => { try { const {ok, data} = await getJson(`/crops/search.json?term=${encodeURIComponent(term)}`, {signal: controller.signal}); const found = ok && Array.isArray(data) ? data : []; setResults(found); setActiveIndex(-1); setNoMatch(ok && found.length === 0); setSearching(false); } catch (error) { if (error.name !== 'AbortError') { setResults([]); setSearching(false); } } }, 250); return () => { clearTimeout(timer); controller.abort(); }; }, [term]); // Keep the highlighted match in view when the list scrolls. useEffect(() => { const option = activeIndex >= 0 && list.current && list.current.children[activeIndex]; if (option && option.scrollIntoView) option.scrollIntoView({block: 'nearest'}); }, [activeIndex]); function choose(crop) { setTerm(''); setResults([]); onChoose({id: crop.id, name: crop.name}); } function onKeyDown(event) { if (event.key === 'ArrowDown' && open) { event.preventDefault(); setActiveIndex((activeIndex + 1) % results.length); } else if (event.key === 'ArrowUp' && open) { event.preventDefault(); setActiveIndex(activeIndex <= 0 ? results.length - 1 : activeIndex - 1); } else if (event.key === 'Enter') { // Never submit a form from here: choose a crop, or do nothing. event.preventDefault(); if (open) choose(results[activeIndex >= 0 ? activeIndex : 0]); } else if (event.key === 'Escape' && (open || noMatch)) { // Close the list; a second Escape then closes the dialog. event.stopPropagation(); setResults([]); setNoMatch(false); } } const activeCrop = activeIndex >= 0 ? results[activeIndex] : null; return (