From 48da0903ca7d88154039df91b1130ee4d927bd28 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 15:25:51 -0700 Subject: [PATCH 1/7] use radios for items! --- package.json | 1 + src/ItemList.js | 101 +++++++++++++++++++++++++++++----------------- src/ItemsPanel.js | 66 +++++++++++++++++++++++++++++- yarn.lock | 18 +++++++++ 4 files changed, 146 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 6a3d290..80d6ba5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "apollo-server-core": "^2.12.0", "apollo-server-env": "^2.4.3", "dataloader": "^2.0.0", + "emotion": "^10.0.27", "emotion-theming": "^10.0.27", "graphql": "^15.0.0", "immer": "^6.0.3", diff --git a/src/ItemList.js b/src/ItemList.js index aec43f8..3d7af94 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -1,4 +1,5 @@ import React from "react"; +import { css } from "emotion"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { Box, @@ -8,6 +9,7 @@ import { PseudoBox, Skeleton, Tooltip, + useTheme, } from "@chakra-ui/core"; import "./ItemList.css"; @@ -39,7 +41,11 @@ function ItemList({ items, outfitState, dispatchToOutfit }) { ); } -function ItemListSkeleton({ count }) { +export function ItemListContainer({ children }) { + return {children}; +} + +export function ItemListSkeleton({ count }) { return ( {Array.from({ length: count }).map((_, i) => ( @@ -51,28 +57,39 @@ function ItemListSkeleton({ count }) { ); } -function Item({ item, outfitState, dispatchToOutfit }) { - const { wornItemIds, allItemIds } = outfitState; - - const isWorn = wornItemIds.includes(item.id); +export function Item({ item, outfitState, dispatchToOutfit }) { + const { allItemIds } = outfitState; const isInOutfit = allItemIds.includes(item.id); + const theme = useTheme(); return ( - - dispatchToOutfit({ - type: isWorn ? "unwearItem" : "wearItem", - itemId: item.id, - }) + border="1px" + borderColor="transparent" + className={ + "item-container " + + css` + input:active + & { + border-color: ${theme.colors.green["800"]}; + } + input:focus + & { + border-style: dotted; + border-color: ${theme.colors.gray["400"]}; + } + ` } > - + - {item.name} + {item.name} {isInOutfit && ( @@ -105,7 +122,7 @@ function Item({ item, outfitState, dispatchToOutfit }) { /> )} - + ); } @@ -119,53 +136,61 @@ function ItemSkeleton() { ); } -function ItemThumbnail({ src, isWorn }) { +function ItemThumbnail({ src }) { + const theme = useTheme(); return ( - - + ); } -function ItemName({ children, isWorn }) { +function ItemName({ children }) { + const theme = useTheme(); + return ( - {children} - + ); } export default ItemList; -export { ItemListSkeleton }; diff --git a/src/ItemsPanel.js b/src/ItemsPanel.js index 88c1213..99c1dde 100644 --- a/src/ItemsPanel.js +++ b/src/ItemsPanel.js @@ -8,11 +8,12 @@ import { IconButton, PseudoBox, Skeleton, + VisuallyHidden, } from "@chakra-ui/core"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { Delay, Heading1, Heading2 } from "./util"; -import ItemList, { ItemListSkeleton } from "./ItemList"; +import { ItemListContainer, Item, ItemListSkeleton } from "./ItemList"; import "./ItemsPanel.css"; @@ -48,7 +49,8 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { > {zoneLabel} - { + 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 ( + + + {items.map((item) => ( + { + e.style.height = e.offsetHeight + "px"; + }} + > + + + ))} + + + ); +} + function OutfitHeading({ outfitState, dispatchToOutfit }) { return ( diff --git a/yarn.lock b/yarn.lock index 78b0f1f..e0c7e9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3974,6 +3974,16 @@ create-ecdh@^4.0.0: bn.js "^4.1.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: version "1.2.0" 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" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" From 6794093e8ef2f40ac2a694df1e8557404bacad50 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 18:58:47 -0700 Subject: [PATCH 2/7] fix hover/focus beahvior for remove button --- src/ItemList.js | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/ItemList.js b/src/ItemList.js index 3d7af94..96c7412 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -64,7 +64,6 @@ export function Item({ item, outfitState, dispatchToOutfit }) { return ( )} From 047dbb632735bf4b6c2e6cb4644bd785064bff77 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 19:02:30 -0700 Subject: [PATCH 3/7] use ItemContainer in skeleton, too --- src/ItemList.js | 80 +++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/src/ItemList.js b/src/ItemList.js index 96c7412..0e946df 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -49,9 +49,7 @@ export function ItemListSkeleton({ count }) { return ( {Array.from({ length: count }).map((_, i) => ( - - - + ))} ); @@ -63,33 +61,7 @@ export function Item({ item, outfitState, dispatchToOutfit }) { const theme = useTheme(); return ( - + {item.name} @@ -120,16 +92,54 @@ export function Item({ item, outfitState, dispatchToOutfit }) { /> )} - + ); } function ItemSkeleton() { return ( - - - - + + + + + + + + ); +} + +function ItemContainer({ children }) { + const theme = useTheme(); + + return ( + + {children} ); } From 7ecbaf82c21da0a66c162d768a75c657c7158ed8 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 19:06:58 -0700 Subject: [PATCH 4/7] fix padding for item and adjacent containers --- src/ItemList.js | 3 +-- src/ItemsPanel.js | 18 ++++++++++++------ src/WardrobePage.js | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/ItemList.js b/src/ItemList.js index 0e946df..1a9d451 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -114,8 +114,7 @@ function ItemContainer({ children }) { return ( - + + + {loading && [1, 2, 3].map((i) => ( - + + + @@ -48,7 +52,9 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { }} > - {zoneLabel} + + {zoneLabel} + - + Date: Sat, 25 Apr 2020 19:10:55 -0700 Subject: [PATCH 5/7] fix a bug with VisuallyHidden radios The VisuallyHidden radios were position:absolute, which was leaking outside the scroll container. These keeps them inside their component, which will scroll them properly! --- src/ItemsPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ItemsPanel.js b/src/ItemsPanel.js index 5d6c7d1..178c02e 100644 --- a/src/ItemsPanel.js +++ b/src/ItemsPanel.js @@ -21,7 +21,7 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { const { zonesAndItems } = outfitState; return ( - + Date: Sat, 25 Apr 2020 20:06:51 -0700 Subject: [PATCH 6/7] checkboxes and keyboard nav for search view! --- src/ItemList.js | 38 +++----------------- src/ItemsPanel.js | 4 ++- src/SearchPanel.js | 87 ++++++++++++++++++++++++++++++++++++++------- src/WardrobePage.js | 39 ++++++++++++++++++-- 4 files changed, 118 insertions(+), 50 deletions(-) diff --git a/src/ItemList.js b/src/ItemList.js index 1a9d451..d38bf8b 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -1,12 +1,10 @@ import React from "react"; import { css } from "emotion"; -import { CSSTransition, TransitionGroup } from "react-transition-group"; import { Box, Flex, IconButton, Image, - PseudoBox, Skeleton, Tooltip, useTheme, @@ -14,33 +12,6 @@ import { import "./ItemList.css"; -function ItemList({ items, outfitState, dispatchToOutfit }) { - return ( - - - {items.map((item) => ( - { - e.style.height = e.offsetHeight + "px"; - }} - > - - - - - ))} - - - ); -} - export function ItemListContainer({ children }) { return {children}; } @@ -55,7 +26,7 @@ export function ItemListSkeleton({ count }) { ); } -export function Item({ item, outfitState, dispatchToOutfit }) { +export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) { const { allItemIds } = outfitState; const isInOutfit = allItemIds.includes(item.id); const theme = useTheme(); @@ -64,7 +35,7 @@ export function Item({ item, outfitState, dispatchToOutfit }) { - {item.name} + {item.name} {isInOutfit && ( @@ -175,7 +146,7 @@ function ItemThumbnail({ src }) { ); } -function ItemName({ children }) { +function ItemName({ children, ...props }) { const theme = useTheme(); return ( @@ -194,10 +165,9 @@ function ItemName({ children }) { font-weight: ${theme.fontWeights.bold}; } `} + {...props} > {children} ); } - -export default ItemList; diff --git a/src/ItemsPanel.js b/src/ItemsPanel.js index 178c02e..5541e81 100644 --- a/src/ItemsPanel.js +++ b/src/ItemsPanel.js @@ -21,7 +21,7 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) { const { zonesAndItems } = outfitState; return ( - + diff --git a/src/SearchPanel.js b/src/SearchPanel.js index 1381d4d..9ef441a 100644 --- a/src/SearchPanel.js +++ b/src/SearchPanel.js @@ -1,17 +1,18 @@ import React from "react"; import gql from "graphql-tag"; -import { Box, Text } from "@chakra-ui/core"; +import { Box, Text, VisuallyHidden } from "@chakra-ui/core"; import { useQuery } from "@apollo/react-hooks"; import { Delay, Heading1, useDebounce } from "./util"; -import ItemList, { ItemListSkeleton } from "./ItemList"; +import { ItemListContainer, ItemListSkeleton, Item } from "./ItemList"; import { itemAppearanceFragment } from "./OutfitPreview"; function SearchPanel({ query, outfitState, dispatchToOutfit, - getScrollParent, + firstSearchResultRef, + onMoveFocusUpToQuery, }) { return ( @@ -20,13 +21,20 @@ function SearchPanel({ query={query} outfitState={outfitState} dispatchToOutfit={dispatchToOutfit} - getScrollParent={getScrollParent} + firstSearchResultRef={firstSearchResultRef} + onMoveFocusUpToQuery={onMoveFocusUpToQuery} /> ); } -function SearchResults({ query, outfitState, dispatchToOutfit }) { +function SearchResults({ + query, + outfitState, + dispatchToOutfit, + firstSearchResultRef, + onMoveFocusUpToQuery, +}) { const { speciesId, colorId } = outfitState; const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true }); @@ -159,19 +167,74 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) { ); } + const onChange = (e) => { + const itemId = e.target.value; + const willBeWorn = e.target.checked; + if (willBeWorn) { + dispatchToOutfit({ type: "wearItem", itemId }); + } else { + dispatchToOutfit({ type: "unwearItem", itemId }); + } + }; + + const goToPrevItem = (e) => { + const prevLabel = e.target.closest("label").previousSibling; + if (prevLabel) { + prevLabel.querySelector("input[type=checkbox]").focus(); + e.preventDefault(); + } else { + // If we're at the top of the list, move back up to the search box! + onMoveFocusUpToQuery(e); + } + }; + + const goToNextItem = (e) => { + const nextLabel = e.target.closest("label").nextSibling; + if (nextLabel) { + nextLabel.querySelector("input[type=checkbox]").focus(); + e.preventDefault(); + } + }; + + console.log(firstSearchResultRef); return ( - + + {items.map((item, index) => ( + + ))} + {items && loading && } ); } -function ScrollTracker({ children, query, threshold, onScrolledToBottom }) { +function ScrollTracker({ children, threshold, onScrolledToBottom }) { const containerRef = React.useRef(); const scrollParent = React.useRef(); @@ -209,7 +272,7 @@ function ScrollTracker({ children, query, threshold, onScrolledToBottom }) { }; }, [onScroll]); - return {children}; + return
{children}
; } export default SearchPanel; diff --git a/src/WardrobePage.js b/src/WardrobePage.js index fbae50d..d9ae9e3 100644 --- a/src/WardrobePage.js +++ b/src/WardrobePage.js @@ -21,6 +21,8 @@ function WardrobePage() { const [searchQuery, setSearchQuery] = React.useState(""); const toast = useToast(); const searchContainerRef = React.useRef(); + const searchQueryRef = React.useRef(); + const firstSearchResultRef = React.useRef(); React.useEffect(() => { if (error) { @@ -72,13 +74,24 @@ function WardrobePage() {
- + { + if (firstSearchResultRef.current) { + firstSearchResultRef.current.focus(); + e.preventDefault(); + } + }} + /> {searchQuery ? ( { + if (searchQueryRef.current) { + searchQueryRef.current.focus(); + e.preventDefault(); + } + }} />
) : ( - + @@ -118,11 +148,14 @@ function SearchToolbar({ query, onChange }) { focusBorderColor="green.600" color="green.800" value={query} + ref={queryRef} onChange={(e) => onChange(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { onChange(""); e.target.blur(); + } else if (e.key === "ArrowDown") { + onMoveFocusDownToResults(e); } }} /> From ce8142cdbea739e7cd3cb9bf3b9ae22ee28995a9 Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Sat, 25 Apr 2020 20:21:51 -0700 Subject: [PATCH 7/7] fix bug with remove button in search --- src/ItemList.js | 2 +- src/SearchPanel.js | 2 -- src/SpeciesColorPicker.js | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ItemList.js b/src/ItemList.js index d38bf8b..78c93c8 100644 --- a/src/ItemList.js +++ b/src/ItemList.js @@ -45,8 +45,8 @@ export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) { variant="ghost" color="gray.400" onClick={(e) => { - e.stopPropagation(); dispatchToOutfit({ type: "removeItem", itemId: item.id }); + e.preventDefault(); }} opacity="0" transitionProperty="opacity color" diff --git a/src/SearchPanel.js b/src/SearchPanel.js index 9ef441a..695725e 100644 --- a/src/SearchPanel.js +++ b/src/SearchPanel.js @@ -196,7 +196,6 @@ function SearchResults({ } }; - console.log(firstSearchResultRef); return ( @@ -211,7 +210,6 @@ function SearchResults({ ref={index === 0 ? firstSearchResultRef : null} onChange={onChange} onKeyDown={(e) => { - console.log(e.key); if (e.key === "ArrowUp") { goToPrevItem(e); } else if (e.key === "ArrowDown") { diff --git a/src/SpeciesColorPicker.js b/src/SpeciesColorPicker.js index bd5b887..5358ad7 100644 --- a/src/SpeciesColorPicker.js +++ b/src/SpeciesColorPicker.js @@ -74,7 +74,6 @@ function SpeciesColorPicker({ if (allValidSpeciesColorPairs.has(pair)) { dispatchToOutfit({ type: "changeColor", colorId: e.target.value }); } else { - console.log(pair, Array.from(allValidSpeciesColorPairs)); const species = allSpecies.find((s) => s.id === speciesId); const color = allColors.find((c) => c.id === colorId); toast({ @@ -91,7 +90,6 @@ function SpeciesColorPicker({ if (allValidSpeciesColorPairs.has(pair)) { dispatchToOutfit({ type: "changeSpecies", speciesId: e.target.value }); } else { - console.log(pair, Array.from(allValidSpeciesColorPairs)); const species = allSpecies.find((s) => s.id === speciesId); const color = allColors.find((c) => c.id === colorId); toast({