2020-04-25 23:51:07 -07:00
|
|
|
import React from "react";
|
|
|
|
import { css, cx } from "emotion";
|
|
|
|
import {
|
2020-08-31 23:27:21 -07:00
|
|
|
Badge,
|
2020-04-25 23:51:07 -07:00
|
|
|
Box,
|
|
|
|
Flex,
|
|
|
|
IconButton,
|
|
|
|
Image,
|
|
|
|
Skeleton,
|
|
|
|
Tooltip,
|
2020-09-01 01:18:24 -07:00
|
|
|
Wrap,
|
2020-08-12 00:37:31 -07:00
|
|
|
useColorModeValue,
|
2020-04-25 23:51:07 -07:00
|
|
|
useTheme,
|
|
|
|
} from "@chakra-ui/core";
|
2020-09-01 01:18:24 -07:00
|
|
|
import {
|
|
|
|
EditIcon,
|
|
|
|
DeleteIcon,
|
|
|
|
InfoIcon,
|
|
|
|
NotAllowedIcon,
|
|
|
|
} from "@chakra-ui/icons";
|
2020-07-31 16:57:37 -07:00
|
|
|
import loadable from "@loadable/component";
|
2020-04-25 23:51:07 -07:00
|
|
|
|
2020-07-20 21:41:26 -07:00
|
|
|
import { safeImageUrl } from "../util";
|
2020-07-31 16:57:37 -07:00
|
|
|
import SupportOnly from "./support/SupportOnly";
|
|
|
|
|
|
|
|
const LoadableItemSupportDrawer = loadable(() =>
|
|
|
|
import("./support/ItemSupportDrawer")
|
|
|
|
);
|
2020-05-02 15:41:02 -07:00
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
/**
|
|
|
|
* Item show a basic summary of an item, in the context of the current outfit!
|
|
|
|
*
|
|
|
|
* It also responds to the focus state of an `input` as its previous sibling.
|
|
|
|
* This will be an invisible radio/checkbox that controls the actual wear
|
|
|
|
* state.
|
|
|
|
*
|
|
|
|
* In fact, this component can't trigger wear or unwear events! When you click
|
|
|
|
* it in the app, you're actually clicking a <label> that wraps the radio or
|
|
|
|
* checkbox. We _do_ control the Remove button in here, though!
|
2020-08-05 00:25:25 -07:00
|
|
|
*
|
|
|
|
* NOTE: This component is memoized with React.memo. It's surpisingly expensive
|
|
|
|
* to re-render, because Chakra components are a lil bit expensive from
|
|
|
|
* their internal complexity, and we have a lot of them here. And it can
|
|
|
|
* add up when there's a lot of Items in the list. This contributes to
|
|
|
|
* wearing/unwearing items being noticeably slower on lower-power
|
|
|
|
* devices.
|
2020-04-25 23:51:07 -07:00
|
|
|
*/
|
2020-09-01 01:18:24 -07:00
|
|
|
function Item({
|
|
|
|
item,
|
|
|
|
itemNameId,
|
|
|
|
isWorn,
|
|
|
|
isInOutfit,
|
|
|
|
dispatchToOutfit,
|
2020-09-01 17:17:45 -07:00
|
|
|
isDisabled = false,
|
2020-09-01 01:18:24 -07:00
|
|
|
}) {
|
2020-07-31 16:57:37 -07:00
|
|
|
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
|
|
|
|
2020-09-01 01:18:24 -07:00
|
|
|
const occupiedZoneLabels = getZoneLabels(
|
|
|
|
item.appearanceOn.layers.map((l) => l.zone)
|
|
|
|
);
|
|
|
|
const restrictedZoneLabels = getZoneLabels(
|
|
|
|
item.appearanceOn.restrictedZones.filter((z) => z.isCommonlyUsedByItems)
|
|
|
|
);
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
return (
|
2020-07-31 16:57:37 -07:00
|
|
|
<>
|
2020-09-01 17:17:45 -07:00
|
|
|
<ItemContainer isDisabled={isDisabled}>
|
2020-08-04 22:48:14 -07:00
|
|
|
<Box flex="0 0 auto" marginRight="3">
|
2020-07-31 16:57:37 -07:00
|
|
|
<ItemThumbnail
|
|
|
|
src={safeImageUrl(item.thumbnailUrl)}
|
|
|
|
isWorn={isWorn}
|
2020-09-01 17:17:45 -07:00
|
|
|
isDisabled={isDisabled}
|
2020-07-31 16:57:37 -07:00
|
|
|
/>
|
|
|
|
</Box>
|
2020-08-31 23:54:54 -07:00
|
|
|
<Box flex="1 1 0" minWidth="0">
|
2020-09-01 17:17:45 -07:00
|
|
|
<ItemName id={itemNameId} isWorn={isWorn} isDisabled={isDisabled}>
|
2020-07-31 16:57:37 -07:00
|
|
|
{item.name}
|
|
|
|
</ItemName>
|
2020-09-01 02:51:03 -07:00
|
|
|
<Wrap spacing="2" marginTop="1" opacity="0.7">
|
|
|
|
{item.isNc ? (
|
2020-09-01 03:50:43 -07:00
|
|
|
<ItemBadgeTooltip label="Neocash">
|
|
|
|
<Badge colorScheme="purple">NC</Badge>
|
|
|
|
</ItemBadgeTooltip>
|
2020-09-01 02:51:03 -07:00
|
|
|
) : (
|
2020-09-01 03:15:19 -07:00
|
|
|
// The main purpose of the NP badge is alignment: if there are
|
|
|
|
// zone badges, we want them to start at the same rough position,
|
|
|
|
// even if there's an NC badge at the start. But if this view
|
|
|
|
// generally avoids zone badges, we'd rather have the NC badge be
|
|
|
|
// a little extra that might pop up and hide the NP case, rather
|
|
|
|
// than try to line things up like a table.
|
2020-09-01 03:50:43 -07:00
|
|
|
<ItemBadgeTooltip label="Neopoints">
|
|
|
|
<Badge>NP</Badge>
|
|
|
|
</ItemBadgeTooltip>
|
2020-09-01 02:51:03 -07:00
|
|
|
)}
|
2020-09-01 03:15:19 -07:00
|
|
|
{occupiedZoneLabels.map((zoneLabel) => (
|
2020-09-01 17:20:08 -07:00
|
|
|
<ZoneBadge
|
|
|
|
key={zoneLabel}
|
|
|
|
variant="occupies"
|
|
|
|
zoneLabel={zoneLabel}
|
|
|
|
/>
|
2020-09-01 03:15:19 -07:00
|
|
|
))}
|
|
|
|
{restrictedZoneLabels.map((zoneLabel) => (
|
2020-09-01 17:20:08 -07:00
|
|
|
<ZoneBadge
|
|
|
|
key={zoneLabel}
|
|
|
|
variant="restricts"
|
|
|
|
zoneLabel={zoneLabel}
|
|
|
|
/>
|
2020-09-01 03:15:19 -07:00
|
|
|
))}
|
2020-09-01 01:18:24 -07:00
|
|
|
</Wrap>
|
2020-07-31 16:57:37 -07:00
|
|
|
</Box>
|
2020-08-04 22:48:14 -07:00
|
|
|
<Box flex="0 0 auto">
|
2020-07-31 16:57:37 -07:00
|
|
|
<SupportOnly>
|
|
|
|
<ItemActionButton
|
|
|
|
icon={<EditIcon />}
|
2020-07-31 22:11:32 -07:00
|
|
|
label="Support"
|
2020-08-05 00:31:28 -07:00
|
|
|
onClick={(e) => {
|
|
|
|
setSupportDrawerIsOpen(true);
|
|
|
|
e.preventDefault();
|
|
|
|
}}
|
2020-07-31 16:57:37 -07:00
|
|
|
/>
|
|
|
|
</SupportOnly>
|
2020-06-24 21:12:02 -07:00
|
|
|
<ItemActionButton
|
2020-07-31 16:57:37 -07:00
|
|
|
icon={<InfoIcon />}
|
|
|
|
label="More info"
|
|
|
|
href={`https://impress.openneo.net/items/${
|
|
|
|
item.id
|
|
|
|
}-${item.name.replace(/ /g, "-")}`}
|
2020-08-05 00:31:28 -07:00
|
|
|
onClick={(e) => e.stopPropagation()}
|
2020-04-30 00:30:27 -07:00
|
|
|
/>
|
2020-07-31 16:57:37 -07:00
|
|
|
{isInOutfit && (
|
|
|
|
<ItemActionButton
|
|
|
|
icon={<DeleteIcon />}
|
|
|
|
label="Remove"
|
2020-08-05 00:31:28 -07:00
|
|
|
onClick={(e) => {
|
|
|
|
dispatchToOutfit({ type: "removeItem", itemId: item.id });
|
|
|
|
e.preventDefault();
|
|
|
|
}}
|
2020-07-31 16:57:37 -07:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
</ItemContainer>
|
|
|
|
<SupportOnly>
|
|
|
|
<LoadableItemSupportDrawer
|
|
|
|
item={item}
|
|
|
|
isOpen={supportDrawerIsOpen}
|
|
|
|
onClose={() => setSupportDrawerIsOpen(false)}
|
|
|
|
/>
|
|
|
|
</SupportOnly>
|
|
|
|
</>
|
2020-04-25 23:51:07 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-26 01:14:31 -07:00
|
|
|
/**
|
|
|
|
* ItemSkeleton is a placeholder for when an Item is loading.
|
|
|
|
*/
|
|
|
|
function ItemSkeleton() {
|
|
|
|
return (
|
2020-09-01 17:17:45 -07:00
|
|
|
<ItemContainer isDisabled>
|
2020-04-26 01:14:31 -07:00
|
|
|
<Skeleton width="50px" height="50px" />
|
|
|
|
<Box width="3" />
|
|
|
|
<Skeleton height="1.5rem" width="12rem" />
|
|
|
|
</ItemContainer>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
/**
|
|
|
|
* ItemContainer is the outermost element of an `Item`.
|
|
|
|
*
|
|
|
|
* It provides spacing, but also is responsible for a number of hover/focus/etc
|
|
|
|
* styles - including for its children, who sometimes reference it as an
|
|
|
|
* .item-container parent!
|
|
|
|
*/
|
2020-09-01 17:17:45 -07:00
|
|
|
function ItemContainer({ children, isDisabled = false }) {
|
2020-04-25 23:51:07 -07:00
|
|
|
const theme = useTheme();
|
|
|
|
|
2020-08-12 00:37:31 -07:00
|
|
|
const focusBackgroundColor = useColorModeValue(
|
|
|
|
theme.colors.gray["100"],
|
|
|
|
theme.colors.gray["700"]
|
|
|
|
);
|
|
|
|
|
|
|
|
const activeBorderColor = useColorModeValue(
|
|
|
|
theme.colors.green["400"],
|
|
|
|
theme.colors.green["500"]
|
|
|
|
);
|
|
|
|
|
|
|
|
const focusCheckedBorderColor = useColorModeValue(
|
|
|
|
theme.colors.green["800"],
|
|
|
|
theme.colors.green["300"]
|
|
|
|
);
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
p="1"
|
|
|
|
my="1"
|
2020-07-20 21:32:42 -07:00
|
|
|
borderRadius="lg"
|
2020-04-25 23:51:07 -07:00
|
|
|
d="flex"
|
|
|
|
alignItems="center"
|
2020-09-01 17:17:45 -07:00
|
|
|
cursor={isDisabled ? undefined : "pointer"}
|
2020-04-25 23:51:07 -07:00
|
|
|
border="1px"
|
|
|
|
borderColor="transparent"
|
|
|
|
className={cx([
|
|
|
|
"item-container",
|
2020-09-01 17:17:45 -07:00
|
|
|
!isDisabled &&
|
2020-08-31 17:41:28 -07:00
|
|
|
css`
|
|
|
|
&:hover,
|
|
|
|
input:focus + & {
|
|
|
|
background-color: ${focusBackgroundColor};
|
|
|
|
}
|
|
|
|
|
|
|
|
input:active + & {
|
|
|
|
border-color: ${activeBorderColor};
|
|
|
|
}
|
|
|
|
|
|
|
|
input:checked:focus + & {
|
|
|
|
border-color: ${focusCheckedBorderColor};
|
|
|
|
}
|
|
|
|
`,
|
2020-04-25 23:51:07 -07:00
|
|
|
])}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ItemThumbnail shows a small preview image for the item, including some
|
|
|
|
* hover/focus and worn/unworn states.
|
|
|
|
*/
|
2020-09-01 17:17:45 -07:00
|
|
|
function ItemThumbnail({ src, isWorn, isDisabled }) {
|
2020-04-25 23:51:07 -07:00
|
|
|
const theme = useTheme();
|
2020-08-12 00:37:31 -07:00
|
|
|
|
|
|
|
const borderColor = useColorModeValue(
|
2020-09-01 02:51:03 -07:00
|
|
|
theme.colors.green["700"],
|
2020-08-12 00:37:31 -07:00
|
|
|
"transparent"
|
|
|
|
);
|
|
|
|
|
|
|
|
const focusBorderColor = useColorModeValue(
|
2020-09-01 02:51:03 -07:00
|
|
|
theme.colors.green["600"],
|
2020-08-12 00:37:31 -07:00
|
|
|
"transparent"
|
|
|
|
);
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
width="50px"
|
|
|
|
height="50px"
|
|
|
|
transition="all 0.15s"
|
|
|
|
transformOrigin="center"
|
2020-09-01 02:27:16 -07:00
|
|
|
position="relative"
|
2020-04-25 23:51:07 -07:00
|
|
|
className={css([
|
2020-04-30 00:30:27 -07:00
|
|
|
{
|
|
|
|
transform: "scale(0.8)",
|
|
|
|
},
|
2020-09-01 17:17:45 -07:00
|
|
|
!isDisabled &&
|
|
|
|
!isWorn && {
|
|
|
|
[containerHasFocus]: {
|
|
|
|
opacity: "0.9",
|
|
|
|
transform: "scale(0.9)",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
!isDisabled &&
|
|
|
|
isWorn && {
|
|
|
|
opacity: 1,
|
|
|
|
transform: "none",
|
2020-04-25 23:51:07 -07:00
|
|
|
},
|
|
|
|
])}
|
|
|
|
>
|
2020-09-01 02:27:16 -07:00
|
|
|
<Box
|
|
|
|
borderRadius="lg"
|
|
|
|
boxShadow="md"
|
|
|
|
border="1px"
|
|
|
|
overflow="hidden"
|
|
|
|
width="100%"
|
|
|
|
height="100%"
|
|
|
|
className={css([
|
|
|
|
{
|
|
|
|
borderColor: `${borderColor} !important`,
|
|
|
|
},
|
2020-09-01 17:17:45 -07:00
|
|
|
!isDisabled &&
|
|
|
|
!isWorn && {
|
|
|
|
[containerHasFocus]: {
|
|
|
|
borderColor: `${focusBorderColor} !important`,
|
|
|
|
},
|
2020-09-01 02:27:16 -07:00
|
|
|
},
|
|
|
|
])}
|
|
|
|
>
|
|
|
|
<Image width="100%" height="100%" src={src} alt="" />
|
|
|
|
</Box>
|
2020-04-25 23:51:07 -07:00
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ItemName shows the item's name, including some hover/focus and worn/unworn
|
|
|
|
* states.
|
|
|
|
*/
|
2020-09-01 17:17:45 -07:00
|
|
|
function ItemName({ children, isDisabled, ...props }) {
|
2020-04-25 23:51:07 -07:00
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
fontSize="md"
|
|
|
|
transition="all 0.15s"
|
2020-08-31 23:54:54 -07:00
|
|
|
overflow="hidden"
|
|
|
|
whiteSpace="nowrap"
|
|
|
|
textOverflow="ellipsis"
|
2020-09-01 17:17:45 -07:00
|
|
|
className={
|
|
|
|
!isDisabled &&
|
|
|
|
css`
|
|
|
|
${containerHasFocus} {
|
|
|
|
opacity: 0.9;
|
|
|
|
font-weight: ${theme.fontWeights.medium};
|
|
|
|
}
|
2020-04-25 23:51:07 -07:00
|
|
|
|
2020-09-01 17:17:45 -07:00
|
|
|
input:checked + .item-container & {
|
|
|
|
opacity: 1;
|
|
|
|
font-weight: ${theme.fontWeights.bold};
|
|
|
|
}
|
|
|
|
`
|
|
|
|
}
|
2020-04-25 23:51:07 -07:00
|
|
|
{...props}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-06-24 21:12:02 -07:00
|
|
|
* ItemActionButton is one of a list of actions a user can take for this item.
|
2020-04-25 23:51:07 -07:00
|
|
|
*/
|
2020-06-24 21:12:02 -07:00
|
|
|
function ItemActionButton({ icon, label, href, onClick }) {
|
2020-04-25 23:51:07 -07:00
|
|
|
const theme = useTheme();
|
|
|
|
|
2020-08-12 00:37:31 -07:00
|
|
|
const focusBackgroundColor = useColorModeValue(
|
|
|
|
theme.colors.gray["300"],
|
|
|
|
theme.colors.gray["800"]
|
|
|
|
);
|
|
|
|
const focusColor = useColorModeValue(
|
|
|
|
theme.colors.gray["700"],
|
|
|
|
theme.colors.gray["200"]
|
|
|
|
);
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
return (
|
2020-06-24 21:12:02 -07:00
|
|
|
<Tooltip label={label} placement="top">
|
2020-04-25 23:51:07 -07:00
|
|
|
<IconButton
|
2020-06-24 21:12:02 -07:00
|
|
|
as={href ? "a" : "button"}
|
|
|
|
icon={icon}
|
|
|
|
aria-label={label}
|
2020-04-25 23:51:07 -07:00
|
|
|
variant="ghost"
|
|
|
|
color="gray.400"
|
2020-06-24 21:12:02 -07:00
|
|
|
href={href}
|
|
|
|
target={href ? "_blank" : null}
|
2020-08-05 00:31:28 -07:00
|
|
|
onClick={onClick}
|
2020-04-25 23:51:07 -07:00
|
|
|
className={css`
|
|
|
|
opacity: 0;
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
${containerHasFocus} {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:focus,
|
|
|
|
&:hover {
|
|
|
|
opacity: 1;
|
2020-08-12 00:37:31 -07:00
|
|
|
background-color: ${focusBackgroundColor};
|
|
|
|
color: ${focusColor};
|
2020-04-25 23:51:07 -07:00
|
|
|
}
|
2020-08-04 22:44:21 -07:00
|
|
|
|
|
|
|
/* On touch devices, always show the buttons! This avoids having to
|
|
|
|
* tap to reveal them (which toggles the item), or worse,
|
|
|
|
* accidentally tapping a hidden button without realizing! */
|
|
|
|
@media (hover: none) {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
2020-04-25 23:51:07 -07:00
|
|
|
`}
|
|
|
|
/>
|
|
|
|
</Tooltip>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ItemListContainer is a container for Item components! Wrap your Item
|
|
|
|
* components in this to ensure a consistent list layout.
|
|
|
|
*/
|
|
|
|
export function ItemListContainer({ children }) {
|
|
|
|
return <Flex direction="column">{children}</Flex>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-04-26 01:14:31 -07:00
|
|
|
* ItemListSkeleton is a placeholder for when an ItemListContainer and its
|
|
|
|
* Items are loading.
|
2020-04-25 23:51:07 -07:00
|
|
|
*/
|
|
|
|
export function ItemListSkeleton({ count }) {
|
|
|
|
return (
|
|
|
|
<ItemListContainer>
|
|
|
|
{Array.from({ length: count }).map((_, i) => (
|
2020-04-26 01:14:31 -07:00
|
|
|
<ItemSkeleton key={i} />
|
2020-04-25 23:51:07 -07:00
|
|
|
))}
|
|
|
|
</ItemListContainer>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-09-01 01:18:24 -07:00
|
|
|
/**
|
|
|
|
* getZoneLabels returns the set of labels for the given zones. Sometimes an
|
|
|
|
* item occupies multiple zones of the same name, so it's especially important
|
|
|
|
* to de-duplicate them here!
|
|
|
|
*/
|
|
|
|
function getZoneLabels(zones) {
|
|
|
|
let labels = zones.map((z) => z.label);
|
|
|
|
labels = new Set(labels);
|
|
|
|
labels = [...labels].sort();
|
|
|
|
return labels;
|
|
|
|
}
|
|
|
|
|
2020-09-01 03:50:43 -07:00
|
|
|
function ZoneBadge({ variant, zoneLabel }) {
|
|
|
|
// Shorten the label when necessary, to make the badges less bulky
|
|
|
|
const shorthand = zoneLabel
|
2020-09-01 01:18:24 -07:00
|
|
|
.replace("Background Item", "BG Item")
|
|
|
|
.replace("Foreground Item", "FG Item")
|
|
|
|
.replace("Lower-body", "Lower")
|
|
|
|
.replace("Upper-body", "Upper")
|
|
|
|
.replace("Transient", "Trans")
|
|
|
|
.replace("Biology", "Bio");
|
2020-09-01 03:50:43 -07:00
|
|
|
|
|
|
|
if (variant === "restricts") {
|
|
|
|
return (
|
|
|
|
<ItemBadgeTooltip
|
|
|
|
label={`Restricted: This item can't be worn with ${zoneLabel} items`}
|
|
|
|
>
|
|
|
|
<Badge>
|
|
|
|
<Box display="flex" alignItems="center">
|
2020-09-01 04:18:53 -07:00
|
|
|
{shorthand} <NotAllowedIcon marginLeft="1" />
|
2020-09-01 03:50:43 -07:00
|
|
|
</Box>
|
|
|
|
</Badge>
|
|
|
|
</ItemBadgeTooltip>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (shorthand !== zoneLabel) {
|
|
|
|
return (
|
|
|
|
<ItemBadgeTooltip label={zoneLabel}>
|
|
|
|
<Badge>{shorthand}</Badge>
|
|
|
|
</ItemBadgeTooltip>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <Badge>{shorthand}</Badge>;
|
|
|
|
}
|
|
|
|
|
|
|
|
function ItemBadgeTooltip({ label, children }) {
|
|
|
|
return (
|
|
|
|
<Tooltip
|
|
|
|
label={<Box textAlign="center">{label}</Box>}
|
|
|
|
placement="top"
|
|
|
|
openDelay={400}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</Tooltip>
|
|
|
|
);
|
2020-09-01 01:18:24 -07:00
|
|
|
}
|
|
|
|
|
2020-04-25 23:51:07 -07:00
|
|
|
/**
|
|
|
|
* containerHasFocus is a common CSS selector, for the case where our parent
|
|
|
|
* .item-container is hovered or the adjacent hidden radio/checkbox is
|
|
|
|
* focused.
|
|
|
|
*/
|
|
|
|
const containerHasFocus =
|
|
|
|
".item-container:hover &, input:focus + .item-container &";
|
2020-08-05 00:25:25 -07:00
|
|
|
|
|
|
|
export default React.memo(Item);
|