import React, {useEffect, useRef, useState} from 'react';
import {getJson} from '../api';
const SEARCH_DELAY = 250; // ms to wait for typing to pause before searching
const IDLE = {status: 'idle', results: []};
// Searches /crops/search.json as you type (the same search as the existing
// form's autosuggest, which puts crops you have planted first). `status` is
// 'idle', 'searching' (still showing the last results), 'done', or 'none' for a
// finished search that found nothing. Call `reset` to close the matches.
function useCropSearch(term) {
const [search, setSearch] = useState(IDLE);
useEffect(() => {
if (!term.trim()) {
setSearch(IDLE);
return undefined;
}
setSearch((current) => ({...current, status: 'searching'}));
const controller = new AbortController();
const timer = setTimeout(async () => {
try {
const {ok, data} = await getJson(`/crops/search.json?term=${encodeURIComponent(term)}`, {signal: controller.signal});
const results = ok && Array.isArray(data) ? data : [];
setSearch({status: ok && results.length === 0 ? 'none' : 'done', results});
} catch (error) {
if (error.name !== 'AbortError') setSearch(IDLE);
}
}, SEARCH_DELAY);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [term]);
return [search, () => setSearch(IDLE)];
}
function hint({status, results}) {
if (status === 'searching') return 'Searching…';
if (results.length > 0) {
return `${results.length} ${results.length === 1 ? 'crop' : 'crops'} found. Use the up and down arrow keys, then Enter, to choose one.`;
}
return 'Search for a crop, then choose it.';
}
function Options({id, results, activeIndex, listRef, onChoose, onHover}) {
return (
);
}
// Choose a crop by searching for it. Choosing one calls onChoose({id, name, slug}).
//
// 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 a dialog it is in.
//
// `label` and `placeholder` say what is being asked. `compact` is the small
// version for a toolbar: an ordinary box, its label and hint kept for screen
// readers only, and the matches floating over the page.
export default function CropPicker({
id,
onChoose,
label = 'What did you plant?',
placeholder = 'Start typing a crop name',
autoFocus = true,
compact = false,
}) {
const [term, setTerm] = useState('');
const [search, resetSearch] = useCropSearch(term);
const [activeIndex, setActiveIndex] = useState(-1);
const list = useRef(null);
const {results, status} = search;
const open = results.length > 0;
const activeCrop = activeIndex >= 0 ? results[activeIndex] : null;
// A new set of matches starts with none highlighted.
useEffect(() => setActiveIndex(-1), [results]);
// 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('');
resetSearch();
onChoose({id: crop.id, name: crop.name, slug: crop.slug});
}
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 || status === 'none')) {
// Close the list; a second Escape then closes the dialog.
event.stopPropagation();
resetSearch();
}
}
return (