diff --git a/package.json b/package.json
index 6a3d290..80d6ba5 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"apollo-server-core": "^2.12.0",
"apollo-server-env": "^2.4.3",
"dataloader": "^2.0.0",
+ "emotion": "^10.0.27",
"emotion-theming": "^10.0.27",
"graphql": "^15.0.0",
"immer": "^6.0.3",
diff --git a/src/ItemList.js b/src/ItemList.js
index aec43f8..78c93c8 100644
--- a/src/ItemList.js
+++ b/src/ItemList.js
@@ -1,78 +1,41 @@
import React from "react";
-import { CSSTransition, TransitionGroup } from "react-transition-group";
+import { css } from "emotion";
import {
Box,
Flex,
IconButton,
Image,
- PseudoBox,
Skeleton,
Tooltip,
+ useTheme,
} from "@chakra-ui/core";
import "./ItemList.css";
-function ItemList({ items, outfitState, dispatchToOutfit }) {
- return (
-
-
- {items.map((item) => (
- {
- e.style.height = e.offsetHeight + "px";
- }}
- >
-
-
-
-
- ))}
-
-
- );
+export function ItemListContainer({ children }) {
+ return {children};
}
-function ItemListSkeleton({ count }) {
+export function ItemListSkeleton({ count }) {
return (
{Array.from({ length: count }).map((_, i) => (
-
-
-
+
))}
);
}
-function Item({ item, outfitState, dispatchToOutfit }) {
- const { wornItemIds, allItemIds } = outfitState;
-
- const isWorn = wornItemIds.includes(item.id);
+export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) {
+ const { allItemIds } = outfitState;
const isInOutfit = allItemIds.includes(item.id);
+ const theme = useTheme();
return (
-
- dispatchToOutfit({
- type: isWorn ? "unwearItem" : "wearItem",
- itemId: item.id,
- })
- }
- >
-
+
+
- {item.name}
+ {item.name}
{isInOutfit && (
@@ -82,90 +45,129 @@ function Item({ item, outfitState, dispatchToOutfit }) {
variant="ghost"
color="gray.400"
onClick={(e) => {
- e.stopPropagation();
dispatchToOutfit({ type: "removeItem", itemId: item.id });
+ e.preventDefault();
}}
opacity="0"
transitionProperty="opacity color"
transitionDuration="0.2s"
- _groupHover={{
- opacity: 1,
- transitionDuration: "0.5s",
- }}
- _hover={{
- opacity: 1,
- color: "gray.800",
- backgroundColor: "gray.200",
- }}
- _focus={{
- opacity: 1,
- color: "gray.800",
- backgroundColor: "gray.200",
- }}
+ className={css`
+ &:hover,
+ &:focus,
+ input:focus + .item-container & {
+ opacity: 1;
+ color: ${theme.colors.gray["800"]};
+ backgroundcolor: ${theme.colors.gray["200"]};
+ }
+ `}
/>
)}
-
+
);
}
function ItemSkeleton() {
return (
-
-
-
-
+
+
+
+
+
+
+
+ );
+}
+
+function ItemContainer({ children }) {
+ const theme = useTheme();
+
+ return (
+
+ {children}
);
}
-function ItemThumbnail({ src, isWorn }) {
+function ItemThumbnail({ src }) {
+ const theme = useTheme();
return (
-
-
+
);
}
-function ItemName({ children, isWorn }) {
+function ItemName({ children, ...props }) {
+ const theme = useTheme();
+
return (
-
{children}
-
+
);
}
-
-export default ItemList;
-export { ItemListSkeleton };
diff --git a/src/ItemsPanel.js b/src/ItemsPanel.js
index 88c1213..5541e81 100644
--- a/src/ItemsPanel.js
+++ b/src/ItemsPanel.js
@@ -8,11 +8,12 @@ import {
IconButton,
PseudoBox,
Skeleton,
+ VisuallyHidden,
} from "@chakra-ui/core";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { Delay, Heading1, Heading2 } from "./util";
-import ItemList, { ItemListSkeleton } from "./ItemList";
+import { ItemListContainer, Item, ItemListSkeleton } from "./ItemList";
import "./ItemsPanel.css";
@@ -21,16 +22,20 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
return (
-
+
+
+
{loading &&
[1, 2, 3].map((i) => (
-
+
+
+
@@ -47,8 +52,11 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
}}
>
- {zoneLabel}
-
+ {zoneLabel}
+
+ {
+ const itemId = e.target.value;
+ dispatchToOutfit({ type: "wearItem", itemId });
+ };
+
+ const onToggle = (e) => {
+ // Clicking the radio button when already selected deselects it - this is
+ // how you can select none!
+ const itemId = e.target.value;
+ if (outfitState.wornItemIds.includes(itemId)) {
+ // We need the event handler to finish before this, so that simulated
+ // events don't just come back around and undo it - but we can't just
+ // solve that with `preventDefault`, because it breaks the radio's
+ // intended visual updates when we unwear. So, we `setTimeout` to do it
+ // after all event handlers resolve!
+ setTimeout(() => dispatchToOutfit({ type: "unwearItem", itemId }), 0);
+ }
+ };
+
+ return (
+
+
+ {items.map((item) => (
+ {
+ e.style.height = e.offsetHeight + "px";
+ }}
+ >
+
+
+ ))}
+
+
+ );
+}
+
function OutfitHeading({ outfitState, dispatchToOutfit }) {
return (
diff --git a/src/SearchPanel.js b/src/SearchPanel.js
index 1381d4d..695725e 100644
--- a/src/SearchPanel.js
+++ b/src/SearchPanel.js
@@ -1,17 +1,18 @@
import React from "react";
import gql from "graphql-tag";
-import { Box, Text } from "@chakra-ui/core";
+import { Box, Text, VisuallyHidden } from "@chakra-ui/core";
import { useQuery } from "@apollo/react-hooks";
import { Delay, Heading1, useDebounce } from "./util";
-import ItemList, { ItemListSkeleton } from "./ItemList";
+import { ItemListContainer, ItemListSkeleton, Item } from "./ItemList";
import { itemAppearanceFragment } from "./OutfitPreview";
function SearchPanel({
query,
outfitState,
dispatchToOutfit,
- getScrollParent,
+ firstSearchResultRef,
+ onMoveFocusUpToQuery,
}) {
return (
@@ -20,13 +21,20 @@ function SearchPanel({
query={query}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
- getScrollParent={getScrollParent}
+ firstSearchResultRef={firstSearchResultRef}
+ onMoveFocusUpToQuery={onMoveFocusUpToQuery}
/>
);
}
-function SearchResults({ query, outfitState, dispatchToOutfit }) {
+function SearchResults({
+ query,
+ outfitState,
+ dispatchToOutfit,
+ firstSearchResultRef,
+ onMoveFocusUpToQuery,
+}) {
const { speciesId, colorId } = outfitState;
const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true });
@@ -159,19 +167,72 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
);
}
+ const onChange = (e) => {
+ const itemId = e.target.value;
+ const willBeWorn = e.target.checked;
+ if (willBeWorn) {
+ dispatchToOutfit({ type: "wearItem", itemId });
+ } else {
+ dispatchToOutfit({ type: "unwearItem", itemId });
+ }
+ };
+
+ const goToPrevItem = (e) => {
+ const prevLabel = e.target.closest("label").previousSibling;
+ if (prevLabel) {
+ prevLabel.querySelector("input[type=checkbox]").focus();
+ e.preventDefault();
+ } else {
+ // If we're at the top of the list, move back up to the search box!
+ onMoveFocusUpToQuery(e);
+ }
+ };
+
+ const goToNextItem = (e) => {
+ const nextLabel = e.target.closest("label").nextSibling;
+ if (nextLabel) {
+ nextLabel.querySelector("input[type=checkbox]").focus();
+ e.preventDefault();
+ }
+ };
+
return (
-
+
+ {items.map((item, index) => (
+
+ ))}
+
{items && loading && }
);
}
-function ScrollTracker({ children, query, threshold, onScrolledToBottom }) {
+function ScrollTracker({ children, threshold, onScrolledToBottom }) {
const containerRef = React.useRef();
const scrollParent = React.useRef();
@@ -209,7 +270,7 @@ function ScrollTracker({ children, query, threshold, onScrolledToBottom }) {
};
}, [onScroll]);
- return {children};
+ return {children}
;
}
export default SearchPanel;
diff --git a/src/SpeciesColorPicker.js b/src/SpeciesColorPicker.js
index bd5b887..5358ad7 100644
--- a/src/SpeciesColorPicker.js
+++ b/src/SpeciesColorPicker.js
@@ -74,7 +74,6 @@ function SpeciesColorPicker({
if (allValidSpeciesColorPairs.has(pair)) {
dispatchToOutfit({ type: "changeColor", colorId: e.target.value });
} else {
- console.log(pair, Array.from(allValidSpeciesColorPairs));
const species = allSpecies.find((s) => s.id === speciesId);
const color = allColors.find((c) => c.id === colorId);
toast({
@@ -91,7 +90,6 @@ function SpeciesColorPicker({
if (allValidSpeciesColorPairs.has(pair)) {
dispatchToOutfit({ type: "changeSpecies", speciesId: e.target.value });
} else {
- console.log(pair, Array.from(allValidSpeciesColorPairs));
const species = allSpecies.find((s) => s.id === speciesId);
const color = allColors.find((c) => c.id === colorId);
toast({
diff --git a/src/WardrobePage.js b/src/WardrobePage.js
index cd74be8..d9ae9e3 100644
--- a/src/WardrobePage.js
+++ b/src/WardrobePage.js
@@ -21,6 +21,8 @@ function WardrobePage() {
const [searchQuery, setSearchQuery] = React.useState("");
const toast = useToast();
const searchContainerRef = React.useRef();
+ const searchQueryRef = React.useRef();
+ const firstSearchResultRef = React.useRef();
React.useEffect(() => {
if (error) {
@@ -72,27 +74,50 @@ function WardrobePage() {
-
+ {
+ if (firstSearchResultRef.current) {
+ firstSearchResultRef.current.focus();
+ e.preventDefault();
+ }
+ }}
+ />
{searchQuery ? (
-
+
{
+ if (searchQueryRef.current) {
+ searchQueryRef.current.focus();
+ e.preventDefault();
+ }
+ }}
/>
) : (
-
+
@@ -118,11 +148,14 @@ function SearchToolbar({ query, onChange }) {
focusBorderColor="green.600"
color="green.800"
value={query}
+ ref={queryRef}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") {
onChange("");
e.target.blur();
+ } else if (e.key === "ArrowDown") {
+ onMoveFocusDownToResults(e);
}
}}
/>
diff --git a/yarn.lock b/yarn.lock
index 78b0f1f..e0c7e9a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3974,6 +3974,16 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0"
elliptic "^6.0.0"
+create-emotion@^10.0.27:
+ version "10.0.27"
+ resolved "https://registry.yarnpkg.com/create-emotion/-/create-emotion-10.0.27.tgz#cb4fa2db750f6ca6f9a001a33fbf1f6c46789503"
+ integrity sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==
+ dependencies:
+ "@emotion/cache" "^10.0.27"
+ "@emotion/serialize" "^0.11.15"
+ "@emotion/sheet" "0.9.4"
+ "@emotion/utils" "0.11.3"
+
create-hash@^1.1.0, create-hash@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
@@ -4684,6 +4694,14 @@ emotion-theming@^10.0.27:
"@emotion/weak-memoize" "0.2.5"
hoist-non-react-statics "^3.3.0"
+emotion@^10.0.27:
+ version "10.0.27"
+ resolved "https://registry.yarnpkg.com/emotion/-/emotion-10.0.27.tgz#f9ca5df98630980a23c819a56262560562e5d75e"
+ integrity sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==
+ dependencies:
+ babel-plugin-emotion "^10.0.27"
+ create-emotion "^10.0.27"
+
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"