use radios for items!

This commit is contained in:
Matt Dunn-Rankin 2020-04-25 15:25:51 -07:00
parent 26244ea776
commit 48da0903ca
4 changed files with 146 additions and 40 deletions

View file

@ -16,6 +16,7 @@
"apollo-server-core": "^2.12.0", "apollo-server-core": "^2.12.0",
"apollo-server-env": "^2.4.3", "apollo-server-env": "^2.4.3",
"dataloader": "^2.0.0", "dataloader": "^2.0.0",
"emotion": "^10.0.27",
"emotion-theming": "^10.0.27", "emotion-theming": "^10.0.27",
"graphql": "^15.0.0", "graphql": "^15.0.0",
"immer": "^6.0.3", "immer": "^6.0.3",

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { css } from "emotion";
import { CSSTransition, TransitionGroup } from "react-transition-group"; import { CSSTransition, TransitionGroup } from "react-transition-group";
import { import {
Box, Box,
@ -8,6 +9,7 @@ import {
PseudoBox, PseudoBox,
Skeleton, Skeleton,
Tooltip, Tooltip,
useTheme,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import "./ItemList.css"; import "./ItemList.css";
@ -39,7 +41,11 @@ function ItemList({ items, outfitState, dispatchToOutfit }) {
); );
} }
function ItemListSkeleton({ count }) { export function ItemListContainer({ children }) {
return <Flex direction="column">{children}</Flex>;
}
export function ItemListSkeleton({ count }) {
return ( return (
<Flex direction="column"> <Flex direction="column">
{Array.from({ length: count }).map((_, i) => ( {Array.from({ length: count }).map((_, i) => (
@ -51,28 +57,39 @@ function ItemListSkeleton({ count }) {
); );
} }
function Item({ item, outfitState, dispatchToOutfit }) { export function Item({ item, outfitState, dispatchToOutfit }) {
const { wornItemIds, allItemIds } = outfitState; const { allItemIds } = outfitState;
const isWorn = wornItemIds.includes(item.id);
const isInOutfit = allItemIds.includes(item.id); const isInOutfit = allItemIds.includes(item.id);
const theme = useTheme();
return ( return (
<PseudoBox <Box
role="group" role="group"
mb="1"
mt="1"
p="1"
rounded="lg"
d="flex" d="flex"
alignItems="center" alignItems="center"
cursor="pointer" cursor="pointer"
onClick={() => border="1px"
dispatchToOutfit({ borderColor="transparent"
type: isWorn ? "unwearItem" : "wearItem", className={
itemId: item.id, "item-container " +
}) css`
input:active + & {
border-color: ${theme.colors.green["800"]};
}
input:focus + & {
border-style: dotted;
border-color: ${theme.colors.gray["400"]};
}
`
} }
> >
<ItemThumbnail src={item.thumbnailUrl} isWorn={isWorn} /> <ItemThumbnail src={item.thumbnailUrl} />
<Box width="3" /> <Box width="3" />
<ItemName isWorn={isWorn}>{item.name}</ItemName> <ItemName>{item.name}</ItemName>
<Box flexGrow="1" /> <Box flexGrow="1" />
{isInOutfit && ( {isInOutfit && (
<Tooltip label="Remove" placement="top"> <Tooltip label="Remove" placement="top">
@ -105,7 +122,7 @@ function Item({ item, outfitState, dispatchToOutfit }) {
/> />
</Tooltip> </Tooltip>
)} )}
</PseudoBox> </Box>
); );
} }
@ -119,53 +136,61 @@ function ItemSkeleton() {
); );
} }
function ItemThumbnail({ src, isWorn }) { function ItemThumbnail({ src }) {
const theme = useTheme();
return ( return (
<PseudoBox <Box
rounded="lg" rounded="lg"
boxShadow="md" boxShadow="md"
border="1px" border="1px"
borderColor={isWorn ? "green.700" : "green.700"} borderColor="green.700"
opacity={isWorn ? 1 : 0.7}
width="50px" width="50px"
height="50px" height="50px"
overflow="hidden" overflow="hidden"
transition="all 0.15s" transition="all 0.15s"
transformOrigin="center" transformOrigin="center"
transform={isWorn ? null : "scale(0.8)"} transform="scale(0.8)"
_groupHover={ className={css`
!isWorn && { .item-container:hover & {
opacity: 0.9, opacity: 0.9;
transform: "scale(0.9)", transform: scale(0.9);
borderColor: "green.600", bordercolor: ${theme.colors.green["600"]};
} }
input:checked + .item-container & {
opacity: 1;
transform: none;
} }
`}
> >
<Image src={src} /> <Image src={src} />
</PseudoBox> </Box>
); );
} }
function ItemName({ children, isWorn }) { function ItemName({ children }) {
const theme = useTheme();
return ( return (
<PseudoBox <Box
fontSize="md" fontSize="md"
fontWeight={isWorn && "bold"}
color="green.800" color="green.800"
transition="all 0.15s" transition="all 0.15s"
opacity={isWorn ? 1 : 0.8} className={css`
_groupHover={ .item-container:hover & {
!isWorn && { opacity: 0.9;
color: "green.800", font-weight: ${theme.fontWeights.medium};
fontWeight: "medium",
opacity: 0.9,
} }
input:checked + .item-container & {
opacity: 1;
font-weight: ${theme.fontWeights.bold};
} }
`}
> >
{children} {children}
</PseudoBox> </Box>
); );
} }
export default ItemList; export default ItemList;
export { ItemListSkeleton };

View file

@ -8,11 +8,12 @@ import {
IconButton, IconButton,
PseudoBox, PseudoBox,
Skeleton, Skeleton,
VisuallyHidden,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { CSSTransition, TransitionGroup } from "react-transition-group"; import { CSSTransition, TransitionGroup } from "react-transition-group";
import { Delay, Heading1, Heading2 } from "./util"; import { Delay, Heading1, Heading2 } from "./util";
import ItemList, { ItemListSkeleton } from "./ItemList"; import { ItemListContainer, Item, ItemListSkeleton } from "./ItemList";
import "./ItemsPanel.css"; import "./ItemsPanel.css";
@ -48,7 +49,8 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
> >
<Box mb="10"> <Box mb="10">
<Heading2>{zoneLabel}</Heading2> <Heading2>{zoneLabel}</Heading2>
<ItemList <ItemRadioList
name={zoneLabel}
items={items} items={items}
outfitState={outfitState} outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
@ -63,6 +65,66 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
); );
} }
function ItemRadioList({ name, items, outfitState, dispatchToOutfit }) {
const onChange = (e) => {
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 (
<ItemListContainer>
<TransitionGroup component={null}>
{items.map((item) => (
<CSSTransition
key={item.id}
classNames="item-list-row"
timeout={500}
onExit={(e) => {
e.style.height = e.offsetHeight + "px";
}}
>
<label>
<VisuallyHidden
as="input"
type="radio"
name={name}
value={item.id}
checked={outfitState.wornItemIds.includes(item.id)}
onChange={onChange}
onClick={onToggle}
onKeyUp={(e) => {
if (e.key === " ") {
onToggle(e);
}
}}
/>
<Item
item={item}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</label>
</CSSTransition>
))}
</TransitionGroup>
</ItemListContainer>
);
}
function OutfitHeading({ outfitState, dispatchToOutfit }) { function OutfitHeading({ outfitState, dispatchToOutfit }) {
return ( return (
<Box> <Box>

View file

@ -3974,6 +3974,16 @@ create-ecdh@^4.0.0:
bn.js "^4.1.0" bn.js "^4.1.0"
elliptic "^6.0.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: create-hash@^1.1.0, create-hash@^1.1.2:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" 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" "@emotion/weak-memoize" "0.2.5"
hoist-non-react-statics "^3.3.0" 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: encodeurl@~1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"