show incompatible items in the outfit list

This was a subtle little thing for a while! If you switch species/color, such that an item doesn't fit the pet anymore, we used to just hide it. Now, we show it in a list, so that you can understand what went wrong, and have the option to remove it.
This commit is contained in:
Emi Matchu 2020-09-01 17:17:45 -07:00
parent b7c958c39b
commit 6d968cc385
3 changed files with 111 additions and 60 deletions

View file

@ -52,7 +52,7 @@ function Item({
isWorn, isWorn,
isInOutfit, isInOutfit,
dispatchToOutfit, dispatchToOutfit,
hideSimpleZones = false, isDisabled = false,
}) { }) {
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false); const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
@ -65,15 +65,16 @@ function Item({
return ( return (
<> <>
<ItemContainer> <ItemContainer isDisabled={isDisabled}>
<Box flex="0 0 auto" marginRight="3"> <Box flex="0 0 auto" marginRight="3">
<ItemThumbnail <ItemThumbnail
src={safeImageUrl(item.thumbnailUrl)} src={safeImageUrl(item.thumbnailUrl)}
isWorn={isWorn} isWorn={isWorn}
isDisabled={isDisabled}
/> />
</Box> </Box>
<Box flex="1 1 0" minWidth="0"> <Box flex="1 1 0" minWidth="0">
<ItemName id={itemNameId} isWorn={isWorn}> <ItemName id={itemNameId} isWorn={isWorn} isDisabled={isDisabled}>
{item.name} {item.name}
</ItemName> </ItemName>
<Wrap spacing="2" marginTop="1" opacity="0.7"> <Wrap spacing="2" marginTop="1" opacity="0.7">
@ -147,7 +148,7 @@ function Item({
*/ */
function ItemSkeleton() { function ItemSkeleton() {
return ( return (
<ItemContainer isFocusable={false}> <ItemContainer isDisabled>
<Skeleton width="50px" height="50px" /> <Skeleton width="50px" height="50px" />
<Box width="3" /> <Box width="3" />
<Skeleton height="1.5rem" width="12rem" /> <Skeleton height="1.5rem" width="12rem" />
@ -162,7 +163,7 @@ function ItemSkeleton() {
* styles - including for its children, who sometimes reference it as an * styles - including for its children, who sometimes reference it as an
* .item-container parent! * .item-container parent!
*/ */
function ItemContainer({ children, isFocusable = true }) { function ItemContainer({ children, isDisabled = false }) {
const theme = useTheme(); const theme = useTheme();
const focusBackgroundColor = useColorModeValue( const focusBackgroundColor = useColorModeValue(
@ -187,12 +188,12 @@ function ItemContainer({ children, isFocusable = true }) {
borderRadius="lg" borderRadius="lg"
d="flex" d="flex"
alignItems="center" alignItems="center"
cursor={isFocusable ? "pointer" : undefined} cursor={isDisabled ? undefined : "pointer"}
border="1px" border="1px"
borderColor="transparent" borderColor="transparent"
className={cx([ className={cx([
"item-container", "item-container",
isFocusable && !isDisabled &&
css` css`
&:hover, &:hover,
input:focus + & { input:focus + & {
@ -218,7 +219,7 @@ function ItemContainer({ children, isFocusable = true }) {
* ItemThumbnail shows a small preview image for the item, including some * ItemThumbnail shows a small preview image for the item, including some
* hover/focus and worn/unworn states. * hover/focus and worn/unworn states.
*/ */
function ItemThumbnail({ src, isWorn }) { function ItemThumbnail({ src, isWorn, isDisabled }) {
const theme = useTheme(); const theme = useTheme();
const colorMode = useColorMode(); const colorMode = useColorMode();
@ -243,16 +244,18 @@ function ItemThumbnail({ src, isWorn }) {
{ {
transform: "scale(0.8)", transform: "scale(0.8)",
}, },
!isWorn && { !isDisabled &&
[containerHasFocus]: { !isWorn && {
opacity: "0.9", [containerHasFocus]: {
transform: "scale(0.9)", opacity: "0.9",
transform: "scale(0.9)",
},
},
!isDisabled &&
isWorn && {
opacity: 1,
transform: "none",
}, },
},
isWorn && {
opacity: 1,
transform: "none",
},
])} ])}
> >
<Box <Box
@ -266,11 +269,12 @@ function ItemThumbnail({ src, isWorn }) {
{ {
borderColor: `${borderColor} !important`, borderColor: `${borderColor} !important`,
}, },
!isWorn && { !isDisabled &&
[containerHasFocus]: { !isWorn && {
borderColor: `${focusBorderColor} !important`, [containerHasFocus]: {
borderColor: `${focusBorderColor} !important`,
},
}, },
},
])} ])}
> >
<Image width="100%" height="100%" src={src} alt="" /> <Image width="100%" height="100%" src={src} alt="" />
@ -283,7 +287,7 @@ function ItemThumbnail({ src, isWorn }) {
* ItemName shows the item's name, including some hover/focus and worn/unworn * ItemName shows the item's name, including some hover/focus and worn/unworn
* states. * states.
*/ */
function ItemName({ children, ...props }) { function ItemName({ children, isDisabled, ...props }) {
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -293,17 +297,20 @@ function ItemName({ children, ...props }) {
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
className={css` className={
${containerHasFocus} { !isDisabled &&
opacity: 0.9; css`
font-weight: ${theme.fontWeights.medium}; ${containerHasFocus} {
} opacity: 0.9;
font-weight: ${theme.fontWeights.medium};
}
input:checked + .item-container & { input:checked + .item-container & {
opacity: 1; opacity: 1;
font-weight: ${theme.fontWeights.bold}; font-weight: ${theme.fontWeights.bold};
} }
`} `
}
{...props} {...props}
> >
{children} {children}

View file

@ -8,9 +8,10 @@ import {
Flex, Flex,
IconButton, IconButton,
Skeleton, Skeleton,
Tooltip,
VisuallyHidden, VisuallyHidden,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { EditIcon } from "@chakra-ui/icons"; import { EditIcon, QuestionIcon } from "@chakra-ui/icons";
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";
@ -30,7 +31,7 @@ import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
* full width of the container, it doesn't look like it! * full width of the container, it doesn't look like it!
*/ */
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
const { zonesAndItems } = outfitState; const { zonesAndItems, incompatibleItems } = outfitState;
return ( return (
<Box> <Box>
@ -55,6 +56,24 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
/> />
</CSSTransition> </CSSTransition>
))} ))}
{incompatibleItems.length > 0 && (
<ItemZoneGroup
zoneLabel="Incompatible"
afterHeader={
<Tooltip
label="These items don't fit this pet"
placement="top"
openDelay={100}
>
<QuestionIcon fontSize="sm" />
</Tooltip>
}
items={incompatibleItems}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
isDisabled
/>
)}
</TransitionGroup> </TransitionGroup>
)} )}
</Flex> </Flex>
@ -70,7 +89,14 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
* the Item component (which will visually reflect the radio's state). This * the Item component (which will visually reflect the radio's state). This
* makes the list screen-reader- and keyboard-accessible! * makes the list screen-reader- and keyboard-accessible!
*/ */
function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) { function ItemZoneGroup({
zoneLabel,
items,
outfitState,
dispatchToOutfit,
isDisabled = false,
afterHeader = null,
}) {
// onChange is fired when the radio button becomes checked, not unchecked! // onChange is fired when the radio button becomes checked, not unchecked!
const onChange = (e) => { const onChange = (e) => {
const itemId = e.target.value; const itemId = e.target.value;
@ -93,38 +119,52 @@ function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
return ( return (
<Box mb="10"> <Box mb="10">
<Heading2 mx="1">{zoneLabel}</Heading2> <Heading2 display="flex" alignItems="center" mx="1">
{zoneLabel}
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
</Heading2>
<ItemListContainer> <ItemListContainer>
<TransitionGroup component={null}> <TransitionGroup component={null}>
{items.map((item) => { {items.map((item) => {
const itemNameId = const itemNameId =
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`; zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
const itemNode = (
<Item
item={item}
itemNameId={itemNameId}
isWorn={
!isDisabled && outfitState.wornItemIds.includes(item.id)
}
isInOutfit={outfitState.allItemIds.includes(item.id)}
dispatchToOutfit={dispatchToOutfit}
isDisabled={isDisabled}
/>
);
return ( return (
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}> <CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
<label> {isDisabled ? (
<VisuallyHidden itemNode
as="input" ) : (
type="radio" <label>
aria-labelledby={itemNameId} <VisuallyHidden
name={zoneLabel} as="input"
value={item.id} type="radio"
checked={outfitState.wornItemIds.includes(item.id)} aria-labelledby={itemNameId}
onChange={onChange} name={zoneLabel}
onClick={onClick} value={item.id}
onKeyUp={(e) => { checked={outfitState.wornItemIds.includes(item.id)}
if (e.key === " ") { onChange={onChange}
onClick(e); onClick={onClick}
} onKeyUp={(e) => {
}} if (e.key === " ") {
/> onClick(e);
<Item }
item={item} }}
itemNameId={itemNameId} />
isWorn={outfitState.wornItemIds.includes(item.id)} {itemNode}
isInOutfit={outfitState.allItemIds.includes(item.id)} </label>
dispatchToOutfit={dispatchToOutfit} )}
/>
</label>
</CSSTransition> </CSSTransition>
); );
})} })}

View file

@ -81,11 +81,15 @@ function useOutfitState() {
wornItemIds, wornItemIds,
closetedItemIds closetedItemIds
); );
const incompatibleItems = items
.filter((i) => i.appearanceOn.layers.length === 0)
.sort((a, b) => a.name.localeCompare(b.name));
const url = buildOutfitUrl(state); const url = buildOutfitUrl(state);
const outfitState = { const outfitState = {
zonesAndItems, zonesAndItems,
incompatibleItems,
name, name,
wornItemIds, wornItemIds,
closetedItemIds, closetedItemIds,