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,
background = null,
boxShadow = null,
...props
}) {
const [suggestions, setSuggestions] = React.useState([]);
const [advancedSearchIsOpen, setAdvancedSearchIsOpen] = React.useState(false);
@ -182,139 +183,144 @@ function SearchToolbar({
const focusBorderColor = useColorModeValue("green.600", "green.400");
return (
<Autosuggest
suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
onSuggestionsFetchRequested={({ value }) => {
// HACK: I'm not sure why, but apparently this gets called with value
// set to the _chosen suggestion_ after choosing it? Has that
// always happened? Idk? Let's just, gate around it, I guess?
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
// query value. Or, if it was from Advanced Search, leave it alone!
value: advancedSearchIsOpen
? query.value
: removeLastWord(query.value),
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
filterToCurrentUserOwnsOrWants:
suggestion.userOwnsOrWants || query.filterToCurrentUserOwnsOrWants,
});
}}
getSuggestionValue={(zl) => zl}
alwaysRenderSuggestions={true}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(inputProps) => (
<InputGroup boxShadow={boxShadow} borderRadius="md">
{queryFilterText ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
)}
<Input
background={background}
autoFocus={autoFocus}
{...inputProps}
/>
<InputRightElement
width="auto"
justifyContent="flex-end"
paddingRight="2px"
paddingY="2px"
>
{!searchQueryIsEmpty(query) && (
<Tooltip label="Clear">
<Box {...props}>
<Autosuggest
suggestions={advancedSearchIsOpen ? allSuggestions : suggestions}
onSuggestionsFetchRequested={({ value }) => {
// HACK: I'm not sure why, but apparently this gets called with value
// set to the _chosen suggestion_ after choosing it? Has that
// always happened? Idk? Let's just, gate around it, I guess?
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
// query value. Or, if it was from Advanced Search, leave it alone!
value: advancedSearchIsOpen
? query.value
: removeLastWord(query.value),
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
filterToCurrentUserOwnsOrWants:
suggestion.userOwnsOrWants ||
query.filterToCurrentUserOwnsOrWants,
});
}}
getSuggestionValue={(zl) => zl}
alwaysRenderSuggestions={true}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(inputProps) => (
<InputGroup boxShadow={boxShadow} borderRadius="md">
{queryFilterText ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
)}
<Input
background={background}
autoFocus={autoFocus}
{...inputProps}
/>
<InputRightElement
width="auto"
justifyContent="flex-end"
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
icon={<CloseIcon fontSize="0.6em" />}
icon={
advancedSearchIsOpen ? (
<ChevronUpIcon fontSize="1.5em" />
) : (
<ChevronDownIcon fontSize="1.5em" />
)
}
color="gray.400"
variant="ghost"
height="100%"
marginLeft="1"
aria-label="Clear search"
onClick={() => {
setSuggestions([]);
onChange(emptySearchQuery);
}}
aria-label="Open advanced search"
onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
/>
</Tooltip>
)}
<Tooltip label="Advanced search">
<IconButton
icon={
advancedSearchIsOpen ? (
<ChevronUpIcon fontSize="1.5em" />
) : (
<ChevronDownIcon fontSize="1.5em" />
)
}
color="gray.400"
variant="ghost"
height="100%"
aria-label="Open advanced search"
onClick={() => setAdvancedSearchIsOpen((isOpen) => !isOpen)}
/>
</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;
</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 });
}
onChange(emptySearchQuery);
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;
},
onKeyDown: (e) => {
if (e.key === "Escape") {
if (suggestions.length > 0) {
setSuggestions([]);
return;
}
onChange(emptySearchQuery);
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;
}
onMoveFocusDownToResults(e);
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
filterToCurrentUserOwnsOrWants: null,
});
}
},
}}
/>
},
}}
/>
</Box>
);
}

View file

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

View file

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