From 9fcc91c660ed5aa4f5353c16f7ab106e927857b8 Mon Sep 17 00:00:00 2001 From: Rachel Stiansen Date: Wed, 16 Sep 2026 18:58:56 +0200 Subject: [PATCH 1/2] feat: allow expanding long book subject lists (#39) --- src/components/BookCard.css | 18 +++++++++++ src/components/BookCard.test.tsx | 52 ++++++++++++++++++++++++++++++-- src/components/BookCard.tsx | 27 +++++++++++++++-- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/components/BookCard.css b/src/components/BookCard.css index 3ab6294..ee4733f 100644 --- a/src/components/BookCard.css +++ b/src/components/BookCard.css @@ -69,11 +69,29 @@ } .book-card-subjects .book-card-subjects-more { + padding: 0; background-color: #edf2f7; color: #52606d; font-weight: 600; } +.book-card-subjects-more button { + max-width: 100%; + border: 0; + border-radius: inherit; + background: transparent; + color: inherit; + padding: 0.25rem 0.625rem; + font: inherit; + overflow-wrap: anywhere; + cursor: pointer; +} + +.book-card-subjects-more button:focus-visible { + outline: 2px solid #243b53; + outline-offset: 2px; +} + @media (max-width: 32rem) { .book-card { grid-template-columns: 1fr; diff --git a/src/components/BookCard.test.tsx b/src/components/BookCard.test.tsx index e7d853f..33c868a 100644 --- a/src/components/BookCard.test.tsx +++ b/src/components/BookCard.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import type { Book } from "../types"; import { BookCard } from "./BookCard"; @@ -44,7 +45,25 @@ describe("BookCard", () => { expect(container).toMatchSnapshot(); }); - it("limits long subject lists and shows how many subjects remain", () => { + it("shows all subjects and no expand control for five subjects", () => { + const bookWithFiveSubjects: Book = { + ...fullBook, + subjects: ["Fantasy", "Magic", "Adventure", "Dragons", "Wizards"], + }; + + render(); + + const subjects = screen.getByRole("list", { name: "Subjects, showing 5 of 5" }); + expect(subjects).toHaveTextContent("Fantasy"); + expect(subjects).toHaveTextContent("Magic"); + expect(subjects).toHaveTextContent("Adventure"); + expect(subjects).toHaveTextContent("Dragons"); + expect(subjects).toHaveTextContent("Wizards"); + expect(screen.queryByRole("button", { name: /subjects/ })).not.toBeInTheDocument(); + }); + + it("expands and collapses long subject lists", async () => { + const user = userEvent.setup(); const bookWithManySubjects: Book = { ...fullBook, subjects: ["Fantasy", "Magic", "Adventure", "Dragons", "Wizards", "Earthsea"], @@ -53,7 +72,36 @@ describe("BookCard", () => { render(); expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); - expect(screen.getByText("+1 more")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Show 1 more subjects" })).toBeInTheDocument(); + expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Show 1 more subjects" })); + + expect(screen.getByRole("list", { name: "Subjects, showing 6 of 6" })).toHaveTextContent( + "Earthsea", + ); + expect(screen.getByRole("button", { name: "Show fewer subjects" })).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Show fewer subjects" })); + + expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); + expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); + }); + + it("resets the expanded subjects when the book changes", async () => { + const user = userEvent.setup(); + const bookWithManySubjects: Book = { + ...fullBook, + subjects: ["Fantasy", "Magic", "Adventure", "Dragons", "Wizards", "Earthsea"], + }; + const anotherBook = { ...minimalBook, subjects: bookWithManySubjects.subjects }; + const { rerender } = render(); + + await user.click(screen.getByRole("button", { name: "Show 1 more subjects" })); + rerender(); + + expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Show 1 more subjects" })).toBeInTheDocument(); }); }); diff --git a/src/components/BookCard.tsx b/src/components/BookCard.tsx index 7518ce4..3ab1902 100644 --- a/src/components/BookCard.tsx +++ b/src/components/BookCard.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { coverUrl } from "../api/openLibrary"; import type { Book } from "../types"; import { FavoriteButton } from "./FavoriteButton"; @@ -12,9 +13,13 @@ interface BookCardProps { } export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCardProps) { + const [subjectState, setSubjectState] = useState({ bookKey: book.key, isExpanded: false }); const imageUrl = coverUrl(book.coverId); const authors = book.authors.length > 0 ? book.authors.join(", ") : "Unknown author"; - const visibleSubjects = book.subjects.slice(0, MAX_VISIBLE_SUBJECTS); + const isSubjectsExpanded = subjectState.bookKey === book.key && subjectState.isExpanded; + const visibleSubjects = isSubjectsExpanded + ? book.subjects + : book.subjects.slice(0, MAX_VISIBLE_SUBJECTS); const remainingSubjectCount = book.subjects.length - visibleSubjects.length; return ( @@ -38,8 +43,24 @@ export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCar {visibleSubjects.map((subject) => (
  • {subject}
  • ))} - {remainingSubjectCount > 0 && ( -
  • +{remainingSubjectCount} more
  • + {book.subjects.length > MAX_VISIBLE_SUBJECTS && ( +
  • + +
  • )} )} From d7c8e33f8cec4e2deec26bbc42c9652cf94f8ff4 Mon Sep 17 00:00:00 2001 From: Rachel Stiansen Date: Wed, 16 Sep 2026 22:00:49 +0200 Subject: [PATCH 2/2] fix: limit collapsed subjects to three (#39) --- src/components/BookCard.test.tsx | 30 ++++++++++++++---------------- src/components/BookCard.tsx | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/BookCard.test.tsx b/src/components/BookCard.test.tsx index 33c868a..58d69f1 100644 --- a/src/components/BookCard.test.tsx +++ b/src/components/BookCard.test.tsx @@ -45,20 +45,18 @@ describe("BookCard", () => { expect(container).toMatchSnapshot(); }); - it("shows all subjects and no expand control for five subjects", () => { - const bookWithFiveSubjects: Book = { + it("shows all subjects and no expand control for three subjects", () => { + const bookWithThreeSubjects: Book = { ...fullBook, - subjects: ["Fantasy", "Magic", "Adventure", "Dragons", "Wizards"], + subjects: ["Fantasy", "Magic", "Adventure"], }; - render(); + render(); - const subjects = screen.getByRole("list", { name: "Subjects, showing 5 of 5" }); + const subjects = screen.getByRole("list", { name: "Subjects, showing 3 of 3" }); expect(subjects).toHaveTextContent("Fantasy"); expect(subjects).toHaveTextContent("Magic"); expect(subjects).toHaveTextContent("Adventure"); - expect(subjects).toHaveTextContent("Dragons"); - expect(subjects).toHaveTextContent("Wizards"); expect(screen.queryByRole("button", { name: /subjects/ })).not.toBeInTheDocument(); }); @@ -66,26 +64,26 @@ describe("BookCard", () => { const user = userEvent.setup(); const bookWithManySubjects: Book = { ...fullBook, - subjects: ["Fantasy", "Magic", "Adventure", "Dragons", "Wizards", "Earthsea"], + subjects: ["Fantasy", "Magic", "Adventure", "Dragons"], }; render(); - expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Subjects, showing 3 of 4" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Show 1 more subjects" })).toBeInTheDocument(); expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Show 1 more subjects" })); - expect(screen.getByRole("list", { name: "Subjects, showing 6 of 6" })).toHaveTextContent( - "Earthsea", + expect(screen.getByRole("list", { name: "Subjects, showing 4 of 4" })).toHaveTextContent( + "Dragons", ); expect(screen.getByRole("button", { name: "Show fewer subjects" })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Show fewer subjects" })); - expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); - expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Subjects, showing 3 of 4" })).toBeInTheDocument(); + expect(screen.queryByText("Dragons")).not.toBeInTheDocument(); }); it("resets the expanded subjects when the book changes", async () => { @@ -97,11 +95,11 @@ describe("BookCard", () => { const anotherBook = { ...minimalBook, subjects: bookWithManySubjects.subjects }; const { rerender } = render(); - await user.click(screen.getByRole("button", { name: "Show 1 more subjects" })); + await user.click(screen.getByRole("button", { name: "Show 3 more subjects" })); rerender(); - expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Subjects, showing 3 of 6" })).toBeInTheDocument(); expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Show 1 more subjects" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Show 3 more subjects" })).toBeInTheDocument(); }); }); diff --git a/src/components/BookCard.tsx b/src/components/BookCard.tsx index 3ab1902..37c315e 100644 --- a/src/components/BookCard.tsx +++ b/src/components/BookCard.tsx @@ -4,7 +4,7 @@ import type { Book } from "../types"; import { FavoriteButton } from "./FavoriteButton"; import "./BookCard.css"; -const MAX_VISIBLE_SUBJECTS = 5; +const MAX_VISIBLE_SUBJECTS = 3; interface BookCardProps { book: Book;