147 lines
4 KiB
JavaScript
147 lines
4 KiB
JavaScript
|
import React from "react";
|
||
|
import {
|
||
|
Box,
|
||
|
Flex,
|
||
|
Icon,
|
||
|
IconButton,
|
||
|
Input,
|
||
|
InputGroup,
|
||
|
InputLeftElement,
|
||
|
InputRightElement,
|
||
|
} from "@chakra-ui/core";
|
||
|
|
||
|
import ItemsPanel from "./ItemsPanel";
|
||
|
import SearchPanel from "./SearchPanel";
|
||
|
|
||
|
/**
|
||
|
* ItemsAndSearchPanels manages the shared layout and state for:
|
||
|
* - ItemsPanel, which shows the items in the outfit now, and
|
||
|
* - SearchPanel, which helps you find new items to add.
|
||
|
*
|
||
|
* These panels don't share a _lot_ of concerns; they're mainly intertwined by
|
||
|
* the fact that they share the SearchToolbar at the top!
|
||
|
*
|
||
|
* We try to keep the search concerns in the search components, by avoiding
|
||
|
* letting any actual _logic_ live at the root here; and instead just
|
||
|
* performing some wiring to help them interact with each other via simple
|
||
|
* state and refs.
|
||
|
*/
|
||
|
function ItemsAndSearchPanels({ loading, outfitState, dispatchToOutfit }) {
|
||
|
const [searchQuery, setSearchQuery] = React.useState("");
|
||
|
const scrollContainerRef = React.useRef();
|
||
|
const searchQueryRef = React.useRef();
|
||
|
const firstSearchResultRef = React.useRef();
|
||
|
|
||
|
return (
|
||
|
<Flex direction="column" height="100%">
|
||
|
<Box px="5" py="3" boxShadow="sm">
|
||
|
<SearchToolbar
|
||
|
query={searchQuery}
|
||
|
searchQueryRef={searchQueryRef}
|
||
|
firstSearchResultRef={firstSearchResultRef}
|
||
|
onChange={setSearchQuery}
|
||
|
/>
|
||
|
</Box>
|
||
|
{searchQuery ? (
|
||
|
<Box
|
||
|
key="search-panel"
|
||
|
gridArea="items"
|
||
|
position="relative"
|
||
|
overflow="auto"
|
||
|
ref={scrollContainerRef}
|
||
|
>
|
||
|
<Box px="4" py="5">
|
||
|
<SearchPanel
|
||
|
query={searchQuery}
|
||
|
outfitState={outfitState}
|
||
|
dispatchToOutfit={dispatchToOutfit}
|
||
|
scrollContainerRef={scrollContainerRef}
|
||
|
searchQueryRef={searchQueryRef}
|
||
|
firstSearchResultRef={firstSearchResultRef}
|
||
|
/>
|
||
|
</Box>
|
||
|
</Box>
|
||
|
) : (
|
||
|
<Box
|
||
|
gridArea="items"
|
||
|
position="relative"
|
||
|
overflow="auto"
|
||
|
key="items-panel"
|
||
|
>
|
||
|
<Box px="4" py="5">
|
||
|
<ItemsPanel
|
||
|
loading={loading}
|
||
|
outfitState={outfitState}
|
||
|
dispatchToOutfit={dispatchToOutfit}
|
||
|
/>
|
||
|
</Box>
|
||
|
</Box>
|
||
|
)}
|
||
|
</Flex>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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,
|
||
|
}) {
|
||
|
const onMoveFocusDownToResults = (e) => {
|
||
|
if (firstSearchResultRef.current) {
|
||
|
firstSearchResultRef.current.focus();
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<InputGroup>
|
||
|
<InputLeftElement>
|
||
|
<Icon name="search" color="gray.400" />
|
||
|
</InputLeftElement>
|
||
|
<Input
|
||
|
placeholder="Search for items to add…"
|
||
|
focusBorderColor="green.600"
|
||
|
color="green.800"
|
||
|
value={query}
|
||
|
ref={searchQueryRef}
|
||
|
onChange={(e) => onChange(e.target.value)}
|
||
|
onKeyDown={(e) => {
|
||
|
if (e.key === "Escape") {
|
||
|
onChange("");
|
||
|
e.target.blur();
|
||
|
} else if (e.key === "ArrowDown") {
|
||
|
onMoveFocusDownToResults(e);
|
||
|
}
|
||
|
}}
|
||
|
/>
|
||
|
{query && (
|
||
|
<InputRightElement>
|
||
|
<IconButton
|
||
|
icon="close"
|
||
|
color="gray.400"
|
||
|
variant="ghost"
|
||
|
variantColor="green"
|
||
|
aria-label="Clear search"
|
||
|
onClick={() => onChange("")}
|
||
|
// Big style hacks here!
|
||
|
height="calc(100% - 2px)"
|
||
|
marginRight="2px"
|
||
|
/>
|
||
|
</InputRightElement>
|
||
|
)}
|
||
|
</InputGroup>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
export default ItemsAndSearchPanels;
|