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

View file

@ -8,9 +8,10 @@ import {
Flex,
IconButton,
Skeleton,
Tooltip,
VisuallyHidden,
} 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 { 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!
*/
function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
const { zonesAndItems } = outfitState;
const { zonesAndItems, incompatibleItems } = outfitState;
return (
<Box>
@ -55,6 +56,24 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
/>
</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>
)}
</Flex>
@ -70,7 +89,14 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
* the Item component (which will visually reflect the radio's state). This
* 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!
const onChange = (e) => {
const itemId = e.target.value;
@ -93,38 +119,52 @@ function ItemZoneGroup({ zoneLabel, items, outfitState, dispatchToOutfit }) {
return (
<Box mb="10">
<Heading2 mx="1">{zoneLabel}</Heading2>
<Heading2 display="flex" alignItems="center" mx="1">
{zoneLabel}
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
</Heading2>
<ItemListContainer>
<TransitionGroup component={null}>
{items.map((item) => {
const itemNameId =
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 (
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
<label>
<VisuallyHidden
as="input"
type="radio"
aria-labelledby={itemNameId}
name={zoneLabel}
value={item.id}
checked={outfitState.wornItemIds.includes(item.id)}
onChange={onChange}
onClick={onClick}
onKeyUp={(e) => {
if (e.key === " ") {
onClick(e);
}
}}
/>
<Item
item={item}
itemNameId={itemNameId}
isWorn={outfitState.wornItemIds.includes(item.id)}
isInOutfit={outfitState.allItemIds.includes(item.id)}
dispatchToOutfit={dispatchToOutfit}
/>
</label>
{isDisabled ? (
itemNode
) : (
<label>
<VisuallyHidden
as="input"
type="radio"
aria-labelledby={itemNameId}
name={zoneLabel}
value={item.id}
checked={outfitState.wornItemIds.includes(item.id)}
onChange={onChange}
onClick={onClick}
onKeyUp={(e) => {
if (e.key === " ") {
onClick(e);
}
}}
/>
{itemNode}
</label>
)}
</CSSTransition>
);
})}

View file

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