Add search footer to layout, behind a feature flag

Yeah it looks cute as a starting point! Definitely a lot to do here tho 😳
This commit is contained in:
Emi Matchu 2021-06-21 14:48:08 -07:00
parent 59fe02a4cc
commit ea33741594
4 changed files with 202 additions and 133 deletions

View file

@ -0,0 +1,46 @@
import React from "react";
import * as Sentry from "@sentry/react";
import { Box, Flex } from "@chakra-ui/react";
import SearchToolbar, { emptySearchQuery } from "./SearchToolbar";
import { MajorErrorMessage, TestErrorSender, useLocalStorage } from "../util";
/**
* SearchFooter appears on large screens only, to let you search for new items
* while still keeping the rest of the item screen open!
*/
function SearchFooter() {
const [canUseSearchFooter, setCanUseSearchFooter] = useLocalStorage(
"DTIFeatureFlagCanUseSearchFooter",
false
);
React.useEffect(() => {
if (window.location.search.includes("feature-flag-can-use-search-footer")) {
setCanUseSearchFooter(true);
}
}, [setCanUseSearchFooter]);
const [query, setQuery] = React.useState(emptySearchQuery);
// TODO: Show the new footer to other users, too!
if (!canUseSearchFooter) {
return null;
}
return (
<Box paddingX="4" paddingY="4">
<Sentry.ErrorBoundary fallback={MajorErrorMessage}>
<TestErrorSender />
<Flex as="label" align="center">
<Box fontWeight="600" flex="0 0 auto">
Add new items:
</Box>
<Box width="4" />
<SearchToolbar query={query} onChange={setQuery} flex="0 1 100%" />
</Flex>
</Sentry.ErrorBoundary>
</Box>
);
}
export default SearchFooter;

View file

@ -53,6 +53,7 @@ function SearchToolbar({
showItemsLabel = false, showItemsLabel = false,
background = null, background = null,
boxShadow = null, boxShadow = null,
...props
}) { }) {
const [suggestions, setSuggestions] = React.useState([]); const [suggestions, setSuggestions] = React.useState([]);
const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false); const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false);
@ -182,139 +183,144 @@ function SearchToolbar({
const focusBorderColor = useColorModeValue("green.600", "green.400"); const focusBorderColor = useColorModeValue("green.600", "green.400");
return ( return (
<Autosuggest <Box {...props}>
suggestions={advancedSearchIsOpen ? allSuggestions : suggestions} <Autosuggest
onSuggestionsFetchRequested={({ value }) => { suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
// HACK: I'm not sure why, but apparently this gets called with value onSuggestionsFetchRequested={({ value }) => {
// set to the _chosen suggestion_ after choosing it? Has that // HACK: I'm not sure why, but apparently this gets called with value
// always happened? Idk? Let's just, gate around it, I guess? // set to the _chosen suggestion_ after choosing it? Has that
if (typeof value === "string") { // always happened? Idk? Let's just, gate around it, I guess?
setSuggestions(getSuggestions(value, query, zoneLabels, isLoggedIn)); if (typeof value === "string") {
} setSuggestions(
}} getSuggestions(value, query, zoneLabels, isLoggedIn)
onSuggestionSelected={(e, { suggestion }) => { );
onChange({ }
...query, }}
// If the suggestion was from typing, remove the last word of the onSuggestionSelected={(e, { suggestion }) => {
// query value. Or, if it was from Advanced Search, leave it alone! onChange({
value: advancedSearchIsOpen ...query,
? query.value // If the suggestion was from typing, remove the last word of the
: removeLastWord(query.value), // query value. Or, if it was from Advanced Search, leave it alone!
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel, value: advancedSearchIsOpen
filterToItemKind: suggestion.itemKind || query.filterToItemKind, ? query.value
filterToCurrentUserOwnsOrWants: : removeLastWord(query.value),
suggestion.userOwnsOrWants || query.filterToCurrentUserOwnsOrWants, filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
}); filterToItemKind: suggestion.itemKind || query.filterToItemKind,
}} filterToCurrentUserOwnsOrWants:
getSuggestionValue={(zl) => zl} suggestion.userOwnsOrWants ||
alwaysRenderSuggestions={true} query.filterToCurrentUserOwnsOrWants,
renderSuggestion={renderSuggestion} });
renderSuggestionsContainer={renderSuggestionsContainer} }}
renderInputComponent={(inputProps) => ( getSuggestionValue={(zl) => zl}
<InputGroup boxShadow={boxShadow} borderRadius="md"> alwaysRenderSuggestions={true}
{queryFilterText ? ( renderSuggestion={renderSuggestion}
<InputLeftAddon> renderSuggestionsContainer={renderSuggestionsContainer}
<SearchIcon color="gray.400" marginRight="3" /> renderInputComponent={(inputProps) => (
<Box fontSize="sm">{queryFilterText}</Box> <InputGroup boxShadow={boxShadow} borderRadius="md">
</InputLeftAddon> {queryFilterText ? (
) : ( <InputLeftAddon>
<InputLeftElement> <SearchIcon color="gray.400" marginRight="3" />
<SearchIcon color="gray.400" /> <Box fontSize="sm">{queryFilterText}</Box>
</InputLeftElement> </InputLeftAddon>
)} ) : (
<Input <InputLeftElement>
background={background} <SearchIcon color="gray.400" />
autoFocus={autoFocus} </InputLeftElement>
{...inputProps} )}
/> <Input
<InputRightElement background={background}
width="auto" autoFocus={autoFocus}
justifyContent="flex-end" {...inputProps}
paddingRight="2px" />
paddingY="2px" <InputRightElement
> width="auto"
{!searchQueryIsEmpty(query) && ( justifyContent="flex-end"
<Tooltip label="Clear"> paddingRight="2px"
paddingY="2px"
>
{!searchQueryIsEmpty(query) && (
<Tooltip label="Clear">
<IconButton
icon={<CloseIcon fontSize="0.6em" />}
color="gray.400"
variant="ghost"
height="100%"
marginLeft="1"
aria-label="Clear search"
onClick={() => {
setSuggestions([]);
onChange(emptySearchQuery);
}}
/>
</Tooltip>
)}
<Tooltip label="Advanced search">
<IconButton <IconButton
icon={<CloseIcon fontSize="0.6em" />} icon={
advancedSearchIsOpen ? (
<ChevronUpIcon fontSize="1.5em" />
) : (
<ChevronDownIcon fontSize="1.5em" />
)
}
color="gray.400" color="gray.400"
variant="ghost" variant="ghost"
height="100%" height="100%"
marginLeft="1" aria-label="Open advanced search"
aria-label="Clear search" onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
onClick={() => {
setSuggestions([]);
onChange(emptySearchQuery);
}}
/> />
</Tooltip> </Tooltip>
)} </InputRightElement>
<Tooltip label="Advanced search"> </InputGroup>
<IconButton )}
icon={ inputProps={{
advancedSearchIsOpen ? ( placeholder: "Search all items…",
<ChevronUpIcon fontSize="1.5em" /> focusBorderColor: focusBorderColor,
) : ( value: query.value || "",
<ChevronDownIcon fontSize="1.5em" /> ref: searchQueryRef,
) minWidth: 0,
} "data-test-id": "item-search-input",
color="gray.400" onChange: (e, { newValue, method }) => {
variant="ghost" // The Autosuggest tries to change the _entire_ value of the element
height="100%" // when navigating suggestions, which isn't actually what we want.
aria-label="Open advanced search" // Only accept value changes that are typed by the user!
onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)} if (method === "type") {
/> onChange({ ...query, value: newValue });
</Tooltip>
</InputRightElement>
</InputGroup>
)}
inputProps={{
placeholder: "Search all items…",
focusBorderColor: focusBorderColor,
value: query.value || "",
ref: searchQueryRef,
minWidth: 0,
"data-test-id": "item-search-input",
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") {
if (suggestions.length > 0) {
setSuggestions([]);
return;
} }
onChange(emptySearchQuery); },
e.target.blur(); onKeyDown: (e) => {
} else if (e.key === "Enter") { if (e.key === "Escape") {
// Pressing Enter doesn't actually submit because it's all on if (suggestions.length > 0) {
// debounce, but it can be a declaration that the query is done, so setSuggestions([]);
// filter suggestions should go away! return;
if (suggestions.length > 0) { }
setSuggestions([]); onChange(emptySearchQuery);
return; e.target.blur();
} else if (e.key === "Enter") {
// Pressing Enter doesn't actually submit because it's all on
// debounce, but it can be a declaration that the query is done, so
// filter suggestions should go away!
if (suggestions.length > 0) {
setSuggestions([]);
return;
}
} else if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
return;
}
onMoveFocusDownToResults(e);
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
filterToCurrentUserOwnsOrWants: null,
});
} }
} else if (e.key === "ArrowDown") { },
if (suggestions.length > 0) { }}
return; />
} </Box>
onMoveFocusDownToResults(e);
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
filterToCurrentUserOwnsOrWants: null,
});
}
},
}}
/>
); );
} }

View file

@ -1,8 +1,15 @@
import React from "react"; import React from "react";
import { Box, Grid, useColorModeValue } from "@chakra-ui/react"; import { Box, Grid, useColorModeValue, useToken } from "@chakra-ui/react";
import { useCommonStyles } from "../util";
function WardrobePageLayout({ previewAndControls, itemsAndSearch }) { function WardrobePageLayout({
previewAndControls = null,
itemsAndMaybeSearchPanel = null,
searchFooter = null,
}) {
const itemsAndSearchBackground = useColorModeValue("white", "gray.900"); const itemsAndSearchBackground = useColorModeValue("white", "gray.900");
const searchBackground = useCommonStyles().bodyBackground;
const searchShadowColorValue = useToken("colors", "gray.400");
return ( return (
<Box <Box
@ -18,12 +25,13 @@ function WardrobePageLayout({ previewAndControls, itemsAndSearch }) {
<Grid <Grid
templateAreas={{ templateAreas={{
base: `"previewAndControls" base: `"previewAndControls"
"itemsAndSearch"`, "itemsAndMaybeSearchPanel"`,
md: `"previewAndControls itemsAndSearch"`, md: `"previewAndControls itemsAndMaybeSearchPanel"
"searchFooter searchFooter"`,
}} }}
templateRows={{ templateRows={{
base: "minmax(100px, 45%) minmax(300px, 55%)", base: "minmax(100px, 45%) minmax(300px, 55%)",
md: "100%", md: "minmax(300px, 1fr) auto",
}} }}
templateColumns={{ templateColumns={{
base: "100%", base: "100%",
@ -40,8 +48,15 @@ function WardrobePageLayout({ previewAndControls, itemsAndSearch }) {
> >
{previewAndControls} {previewAndControls}
</Box> </Box>
<Box gridArea="itemsAndSearch" bg={itemsAndSearchBackground}> <Box gridArea="itemsAndMaybeSearchPanel" bg={itemsAndSearchBackground}>
{itemsAndSearch} {itemsAndMaybeSearchPanel}
</Box>
<Box
gridArea="searchFooter"
bg={searchBackground}
boxShadow={`0 0 8px ${searchShadowColorValue}`}
>
{searchFooter}
</Box> </Box>
</Grid> </Grid>
</Box> </Box>

View file

@ -4,6 +4,7 @@ import { useToast } from "@chakra-ui/react";
import { loadable } from "../util"; import { loadable } from "../util";
import ItemsAndSearchPanels from "./ItemsAndSearchPanels"; import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
import SearchFooter from "./SearchFooter";
import SupportOnly from "./support/SupportOnly"; import SupportOnly from "./support/SupportOnly";
import useOutfitSaving from "./useOutfitSaving"; import useOutfitSaving from "./useOutfitSaving";
import useOutfitState, { OutfitStateContext } from "./useOutfitState"; import useOutfitState, { OutfitStateContext } from "./useOutfitState";
@ -104,7 +105,7 @@ function WardrobePage() {
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
/> />
} }
itemsAndSearch={ itemsAndMaybeSearchPanel={
<ItemsAndSearchPanels <ItemsAndSearchPanels
loading={loading} loading={loading}
outfitState={outfitState} outfitState={outfitState}
@ -112,6 +113,7 @@ function WardrobePage() {
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
/> />
} }
searchFooter={<SearchFooter />}
/> />
</OutfitStateContext.Provider> </OutfitStateContext.Provider>
); );