diff --git a/src/app/WardrobePage/ItemsAndSearchPanels.js b/src/app/WardrobePage/ItemsAndSearchPanels.js index 469780c..da90fa8 100644 --- a/src/app/WardrobePage/ItemsAndSearchPanels.js +++ b/src/app/WardrobePage/ItemsAndSearchPanels.js @@ -19,7 +19,7 @@ import SearchPanel from "./SearchPanel"; * state and refs. */ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) { - const [searchQuery, setSearchQuery] = React.useState(""); + const [searchQuery, setSearchQuery] = React.useState(null); const scrollContainerRef = React.useRef(); const searchQueryRef = React.useRef(); const firstSearchResultRef = React.useRef(); @@ -44,7 +44,7 @@ function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) { > z.isCommonlyUsedByItems); + + let zoneLabels = itemZones.map((z) => z.label); + zoneLabels = [...new Set(zoneLabels)]; + zoneLabels.sort(); + const onMoveFocusDownToResults = (e) => { if (firstSearchResultRef.current) { firstSearchResultRef.current.focus(); @@ -34,22 +56,45 @@ function SearchToolbar({ } }; + const renderSuggestion = React.useCallback( + (zoneLabel, { isHighlighted }) => ( + {zoneLabel} + ), + [] + ); + const focusBorderColor = useColorModeValue("green.600", "green.400"); return ( { - if (value.includes("hat")) setSuggestions(["Zone: Hat"]); - else setSuggestions([]); + onSuggestionsFetchRequested={({ value }) => + setSuggestions(getSuggestions(value, zoneLabels)) + } + onSuggestionsClearRequested={() => setSuggestions([])} + onSuggestionSelected={(e, { suggestion }) => { + const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1]; + onChange({ + ...query, + value: valueWithoutLastWord, + filterToZoneLabel: suggestion, + }); }} - onSuggestionsClearRequested={() => {}} - renderSuggestion={() => "Hat"} + getSuggestionValue={(zl) => zl} + shouldRenderSuggestions={() => query?.filterToZoneLabel == null} + renderSuggestion={renderSuggestion} renderInputComponent={(props) => ( - - - + {query?.filterToZoneLabel ? ( + + + {query.filterToZoneLabel} + + ) : ( + + + + )} {query && ( @@ -59,7 +104,9 @@ function SearchToolbar({ variant="ghost" colorScheme="green" aria-label="Clear search" - onClick={() => onChange("")} + onClick={() => { + onChange(null); + }} // Big style hacks here! height="calc(100% - 2px)" marginRight="2px" @@ -69,18 +116,40 @@ function SearchToolbar({ )} inputProps={{ - placeholder: "Search for items to add…", + // placeholder: "Search for items to add…", "aria-label": "Search for items to add…", focusBorderColor: focusBorderColor, - value: query, + value: query?.value || "", ref: searchQueryRef, - onChange: (e) => onChange(e.target.value), + minWidth: 0, + // HACK: Chakra isn't noticing the InputLeftElement swapping out + // for the InputLeftAddon, so the padding isn't updating. + paddingLeft: query?.filterToZoneLabel ? "1rem" : "2.5rem", + onChange: (e, { newValue, method }) => { + // The Autosuggest tries to change the _entire_ value of the element + // when navigating suggestions, which isn't actually what we want. + // Only accept value changes that are typed by the user! + if (method === "type") { + onChange({ ...query, value: newValue }); + } + }, onKeyDown: (e) => { if (e.key === "Escape") { - onChange(""); + if (suggestions.length > 0) { + setSuggestions([]); + return; + } + onChange(null); e.target.blur(); } else if (e.key === "ArrowDown") { + if (suggestions.length > 0) { + return; + } onMoveFocusDownToResults(e); + } else if (e.key === "Backspace") { + if (query.value === "") { + onChange({ ...query, filterToZoneLabel: null }); + } } }, }} @@ -88,4 +157,17 @@ function SearchToolbar({ ); } +function getSuggestions(value, zoneLabels) { + const words = value.split(/\s+/); + const lastWord = words[words.length - 1]; + if (lastWord.length < 2) { + return []; + } + + const matchingZoneLabels = zoneLabels.filter((zl) => + zl.toLowerCase().includes(lastWord.toLowerCase()) + ); + return matchingZoneLabels; +} + export default SearchToolbar; diff --git a/src/app/apolloClient.js b/src/app/apolloClient.js index 6ffa7fc..207d564 100644 --- a/src/app/apolloClient.js +++ b/src/app/apolloClient.js @@ -10,6 +10,11 @@ import cachedZones from "./cached-data/zones.json"; const typePolicies = { Query: { fields: { + allZones: (_, { toReference }) => { + return cachedZones.map((z) => + toReference({ __typename: "Zone", id: z.id }, true) + ); + }, items: (_, { args, toReference }) => { return args.ids.map((id) => toReference({ __typename: "Item", id }, true)