impress-2020/src/app/WardrobePage/SearchToolbar.js

184 lines
5.5 KiB
JavaScript
Raw Normal View History

import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import {
Box,
IconButton,
Input,
InputGroup,
InputLeftAddon,
InputLeftElement,
InputRightElement,
useColorModeValue,
} from "@chakra-ui/core";
import { CloseIcon, SearchIcon } from "@chakra-ui/icons";
2020-09-01 19:11:33 -07:00
import { css } from "emotion";
2020-09-01 17:59:04 -07:00
import Autosuggest from "react-autosuggest";
/**
* SearchToolbar is rendered above both the ItemsPanel and the SearchPanel,
* and contains the search field where the user types their query.
*
* It has some subtle keyboard interaction support, like DownArrow to go to the
* first search result, and Escape to clear the search and go back to the
* ItemsPanel. (The SearchPanel can also send focus back to here, with Escape
* from anywhere, or UpArrow from the first result!)
*/
function SearchToolbar({
query,
searchQueryRef,
firstSearchResultRef,
onChange,
}) {
2020-09-01 17:59:04 -07:00
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();
e.preventDefault();
}
};
const renderSuggestion = React.useCallback(
(zoneLabel, { isHighlighted }) => (
<Box fontWeight={isHighlighted ? "bold" : "normal"}>{zoneLabel}</Box>
),
[]
);
const focusBorderColor = useColorModeValue("green.600", "green.400");
return (
2020-09-01 17:59:04 -07:00
<Autosuggest
suggestions={suggestions}
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,
});
2020-09-01 17:59:04 -07:00
}}
getSuggestionValue={(zl) => zl}
2020-09-01 19:11:33 -07:00
shouldRenderSuggestions={() => query.filterToZoneLabel == null}
renderSuggestion={renderSuggestion}
2020-09-01 17:59:04 -07:00
renderInputComponent={(props) => (
<InputGroup>
2020-09-01 19:11:33 -07:00
{query.filterToZoneLabel ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{query.filterToZoneLabel}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
)}
2020-09-01 17:59:04 -07:00
<Input {...props} />
{query && (
<InputRightElement>
<IconButton
icon={<CloseIcon />}
color="gray.400"
variant="ghost"
colorScheme="green"
aria-label="Clear search"
onClick={() => {
onChange(null);
}}
2020-09-01 17:59:04 -07:00
// Big style hacks here!
height="calc(100% - 2px)"
marginRight="2px"
/>
</InputRightElement>
)}
</InputGroup>
)}
inputProps={{
// placeholder: "Search for items to add…",
2020-09-01 17:59:04 -07:00
"aria-label": "Search for items to add…",
focusBorderColor: focusBorderColor,
2020-09-01 19:11:33 -07:00
value: query.value || "",
2020-09-01 17:59:04 -07:00
ref: searchQueryRef,
minWidth: 0,
// HACK: Chakra isn't noticing the InputLeftElement swapping out
2020-09-01 19:11:33 -07:00
// for the InputLeftAddon, so the styles aren't updating...
// Hard override!
className: css`
padding-left: ${query.filterToZoneLabel
? "1rem"
: "2.5rem"} !important;
border-bottom-left-radius: ${query.filterToZoneLabel
? "0"
: "0.25rem"} !important;
border-top-left-radius: ${query.filterToZoneLabel
? "0"
: "0.25rem"} !important;
`,
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 });
}
},
2020-09-01 17:59:04 -07:00
onKeyDown: (e) => {
if (e.key === "Escape") {
if (suggestions.length > 0) {
setSuggestions([]);
return;
}
onChange(null);
e.target.blur();
} else if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
return;
}
onMoveFocusDownToResults(e);
2020-09-01 19:11:33 -07:00
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({ ...query, filterToZoneLabel: null });
}
2020-09-01 17:59:04 -07:00
},
}}
/>
);
}
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;