first draft of search zones
It doesn't affect the actual query yet, and it looks bad! But it exists!
This commit is contained in:
parent
d013dd6d89
commit
0088c3f193
3 changed files with 102 additions and 15 deletions
|
@ -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 }) {
|
|||
>
|
||||
<Box px="4" py="5">
|
||||
<SearchPanel
|
||||
query={searchQuery}
|
||||
query={searchQuery.value}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import React from "react";
|
||||
import gql from "graphql-tag";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftAddon,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
useColorModeValue,
|
||||
|
@ -27,6 +31,24 @@ function SearchToolbar({
|
|||
}) {
|
||||
const [suggestions, setSuggestions] = React.useState([]);
|
||||
|
||||
// NOTE: This query should always load ~instantly, from the client cache.
|
||||
const { data } = useQuery(gql`
|
||||
query SearchToolbarZones {
|
||||
allZones {
|
||||
id
|
||||
label
|
||||
depth
|
||||
isCommonlyUsedByItems
|
||||
}
|
||||
}
|
||||
`);
|
||||
const zones = data?.allZones || [];
|
||||
const itemZones = zones.filter((z) => 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 }) => (
|
||||
<Box fontWeight={isHighlighted ? "bold" : "normal"}>{zoneLabel}</Box>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const focusBorderColor = useColorModeValue("green.600", "green.400");
|
||||
|
||||
return (
|
||||
<Autosuggest
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={({ value }) => {
|
||||
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) => (
|
||||
<InputGroup>
|
||||
{query?.filterToZoneLabel ? (
|
||||
<InputLeftAddon>
|
||||
<SearchIcon color="gray.400" marginRight="3" />
|
||||
<Box fontSize="sm">{query.filterToZoneLabel}</Box>
|
||||
</InputLeftAddon>
|
||||
) : (
|
||||
<InputLeftElement>
|
||||
<SearchIcon color="gray.400" />
|
||||
</InputLeftElement>
|
||||
)}
|
||||
<Input {...props} />
|
||||
{query && (
|
||||
<InputRightElement>
|
||||
|
@ -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({
|
|||
</InputGroup>
|
||||
)}
|
||||
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;
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue