PosePicker is a radio!

This commit is contained in:
Matt Dunn-Rankin 2020-05-02 22:04:20 -07:00
parent 117a802835
commit 752828b4b0

View file

@ -11,6 +11,7 @@ import {
PopoverArrow, PopoverArrow,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
VisuallyHidden,
useTheme, useTheme,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
@ -23,17 +24,18 @@ import twemojiSick from "../images/twemoji/sick.svg";
import twemojiMasc from "../images/twemoji/masc.svg"; import twemojiMasc from "../images/twemoji/masc.svg";
import twemojiFem from "../images/twemoji/fem.svg"; import twemojiFem from "../images/twemoji/fem.svg";
import { OutfitLayers } from "./OutfitPreview"; import { OutfitLayers } from "./OutfitPreview";
import { safeImageUrl } from "./util";
function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) { function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) {
const theme = useTheme(); const theme = useTheme();
const { speciesId, colorId } = outfitState; const { speciesId, colorId } = outfitState;
const { loading, error, poses } = useAvailablePoses({ const { loading, error, poses, selectPose } = usePoses({
speciesId, speciesId,
colorId, colorId,
}); });
const checkedInputRef = React.useRef();
if (loading) { if (loading) {
return null; return null;
} }
@ -52,10 +54,11 @@ function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) {
return ( return (
<Popover <Popover
placement="top-end" placement="bottom-end"
usePortal usePortal
onOpen={onLockFocus} onOpen={onLockFocus}
onClose={onUnlockFocus} onClose={onUnlockFocus}
initialFocusRef={checkedInputRef}
> >
{({ isOpen }) => ( {({ isOpen }) => (
<> <>
@ -92,7 +95,16 @@ function PosePicker({ outfitState, onLockFocus, onUnlockFocus }) {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent> <PopoverContent>
<Box p="4"> <Box p="4">
<table width="100%" borderSpacing="8px"> <table
width="100%"
borderSpacing="8px"
onChange={(e) => {
const [emotion, genderPresentation] = e.target.value.split(
"-"
);
selectPose({ emotion, genderPresentation });
}}
>
<thead> <thead>
<tr> <tr>
<th /> <th />
@ -161,31 +173,96 @@ function Cell({ children, as }) {
); );
} }
const EMOTION_STRINGS = {
HAPPY: "Happy",
SAD: "Sad",
SICK: "Sick",
};
const GENDER_PRESENTATION_STRINGS = {
MASCULINE: "Masculine",
FEMININE: "Feminine",
};
function PoseButton({ pose, speciesId }) { function PoseButton({ pose, speciesId }) {
const theme = useTheme();
if (!pose) { if (!pose) {
return null; return null;
} }
const genderPresentationStr =
GENDER_PRESENTATION_STRINGS[pose.genderPresentation];
const emotionStr = EMOTION_STRINGS[pose.emotion];
return ( return (
<Box <Box
rounded="full" as="label"
boxShadow="md" cursor="pointer"
overflow="hidden" onClick={(e) => {
width="50px" // HACK: We need the timeout to beat the popover's focus stealing!
height="50px" const input = e.currentTarget.querySelector("input");
title={window.location.hostname.includes("localhost") && `#${pose.id}`} setTimeout(() => input.focus(), 0);
}}
> >
<Button variant="unstyled" width="100%" height="100%"> <VisuallyHidden
as="input"
type="radio"
aria-label={`${emotionStr} and ${genderPresentationStr}`}
name="pose"
value={`${pose.emotion}-${pose.genderPresentation}`}
checked={pose.isSelected}
/>
<Box
rounded="full"
boxShadow="md"
overflow="hidden"
width="50px"
height="50px"
title={window.location.hostname.includes("localhost") && `#${pose.id}`}
position="relative"
className={css`
transform: scale(0.8);
opacity: 0.8;
transition: all 0.2s;
input:checked + & {
transform: scale(1);
opacity: 1;
}
`}
>
<Box <Box
width="100%" rounded="full"
height="100%" position="absolute"
top="0"
bottom="0"
left="0"
right="0"
zIndex="2"
className={css`
border: 0px solid ${theme.colors.green["600"]};
transition: border-width 0.2s;
input:checked + * & {
border-width: 1px;
}
input:focus + * & {
border-width: 3px;
}
`}
/>
<Box
width="50px"
height="50px"
transform={ transform={
transformsBySpeciesId[speciesId] || transformsBySpeciesId.default transformsBySpeciesId[speciesId] || transformsBySpeciesId.default
} }
> >
<OutfitLayers visibleLayers={getVisibleLayers(pose, [])} /> <OutfitLayers visibleLayers={getVisibleLayers(pose, [])} />
</Box> </Box>
</Button> </Box>
</Box> </Box>
); );
} }
@ -194,7 +271,12 @@ function EmojiImage({ src, "aria-label": ariaLabel }) {
return <Image src={src} aria-label={ariaLabel} width="16px" height="16px" />; return <Image src={src} aria-label={ariaLabel} width="16px" height="16px" />;
} }
function useAvailablePoses({ speciesId, colorId }) { function usePoses({ speciesId, colorId }) {
const [selectedPose, selectPose] = React.useState({
emotion: "HAPPY",
genderPresentation: "FEMININE",
});
const { loading, error, data } = useQuery( const { loading, error, data } = useQuery(
gql` gql`
query PosePicker($speciesId: ID!, $colorId: ID!) { query PosePicker($speciesId: ID!, $colorId: ID!) {
@ -212,21 +294,24 @@ function useAvailablePoses({ speciesId, colorId }) {
); );
const petAppearances = data?.petAppearances || []; const petAppearances = data?.petAppearances || [];
const findAppearanceFor = (e, gp) => const buildPose = (e, gp) => ({
petAppearances.find( ...petAppearances.find(
(pa) => pa.emotion === e && pa.genderPresentation === gp (pa) => pa.emotion === e && pa.genderPresentation === gp
); ),
isSelected:
selectedPose.emotion === e && selectedPose.genderPresentation === gp,
});
const poses = { const poses = {
happyMasc: findAppearanceFor("HAPPY", "MASCULINE"), happyMasc: buildPose("HAPPY", "MASCULINE"),
sadMasc: findAppearanceFor("SAD", "MASCULINE"), sadMasc: buildPose("SAD", "MASCULINE"),
sickMasc: findAppearanceFor("SICK", "MASCULINE"), sickMasc: buildPose("SICK", "MASCULINE"),
happyFem: findAppearanceFor("HAPPY", "FEMININE"), happyFem: buildPose("HAPPY", "FEMININE"),
sadFem: findAppearanceFor("SAD", "FEMININE"), sadFem: buildPose("SAD", "FEMININE"),
sickFem: findAppearanceFor("SICK", "FEMININE"), sickFem: buildPose("SICK", "FEMININE"),
}; };
return { loading, error, poses }; return { loading, error, poses, selectPose };
} }
const transformsBySpeciesId = { const transformsBySpeciesId = {