impress-2020/src/app/ItemsAndSearchPanels.js

147 lines
4 KiB
JavaScript
Raw Normal View History

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;