diff --git a/app/components/GameCard.jsx b/app/components/GameCard.jsx index 5bcd33c7..1b720bd1 100644 --- a/app/components/GameCard.jsx +++ b/app/components/GameCard.jsx @@ -8,13 +8,13 @@ import { Text, Heading, Skeleton, - Tag, } from '@chakra-ui/react'; import { LuTrash2 } from 'react-icons/lu'; import usePlaceholder from '../hooks/usePlaceholder'; import Card from './Card'; import CardLink from './CardLink'; +import TagList from './TagList'; export const GameCardSkeleton = () => ( @@ -144,27 +144,7 @@ function GameCard({ {name} - {tags && ( - - {tags.map((tag) => ( - - {tag.name} - - ))} - - )} + {tags?.length > 0 && } ); } diff --git a/app/components/TagList.jsx b/app/components/TagList.jsx new file mode 100644 index 00000000..02b46530 --- /dev/null +++ b/app/components/TagList.jsx @@ -0,0 +1,42 @@ +import { Flex, Tag } from '@chakra-ui/react'; +import { Link } from 'react-router'; + +/** + * A game's tags as a list, so screen readers, copy-paste and crawlers get + * separate items instead of one run-on word (#201). With `linked`, each + * tag links to the games list filtered by it. + */ +const TagList = ({ tags, size = 'sm', fontSize = '0.6rem', linked = false, ...rest }) => ( + + {tags.map((tag) => { + const chip = ( + + {tag.name} + + ); + return ( +
  • + {linked ? {chip} : chip} +
  • + ); + })} +
    +); + +export default TagList; diff --git a/app/routes/game.$id.jsx b/app/routes/game.$id.jsx index 1e7b111a..a6dfe54c 100644 --- a/app/routes/game.$id.jsx +++ b/app/routes/game.$id.jsx @@ -8,12 +8,11 @@ import { Button, useDisclosure, IconButton, - Tag, - Link as ChakraLink, List, Flex, Dialog, Portal, + Link as ChakraLink, } from '@chakra-ui/react'; import { LuPlus, LuPencil, LuExternalLink } from 'react-icons/lu'; @@ -37,6 +36,7 @@ import Markdown from '../components/Markdown'; import MotionGallery from '../components/MotionGallery'; import ActionMenu from '../components/ActionMenu'; import { ogUrl } from '../utils/meta'; +import TagList from '../components/TagList'; const uuidRegex = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; @@ -211,29 +211,8 @@ const Game = () => { /> - {tags && ( - - {tags.map((tag) => ( - - - {tag.name} - - - ))} - + {tags?.length > 0 && ( + )} {about && ( diff --git a/e2e/games.spec.js b/e2e/games.spec.js index 2e4a6556..b01eb410 100644 --- a/e2e/games.spec.js +++ b/e2e/games.spec.js @@ -42,3 +42,18 @@ test('the games tag filter works with the keyboard', async ({ page }) => { await expect(tag).toHaveAttribute('aria-pressed', 'false'); await expect(page).not.toHaveURL(/tags=/); }); + +// #201: tags were bare chips in a
    , so text tools read "soloplatformaction". +test('game tags are lists of separate items', async ({ page }) => { + await page.goto('/games'); + const tagList = page.locator('main').getByRole('list', { name: 'Tags' }).first(); + await expect(tagList).toBeVisible(); + const names = await tagList.getByRole('listitem').allInnerTexts(); + expect(names.length).toBeGreaterThan(1); + const text = await tagList.innerText(); + for (const name of names) expect(text.split(/\s*\n\s*/)).toContain(name.trim()); + + await tagList.locator('xpath=ancestor::*[.//a[starts-with(@href, "/game/")]][1]').locator('a[href^="/game/"]').first().click(); + // The game's own tags (related game cards below have their own lists). + await expect(page.locator('main').getByRole('list', { name: 'Tags' }).first()).toBeVisible(); +});