diff --git a/src/components/BookCard.css b/src/components/BookCard.css index 93f4b43..0052604 100644 --- a/src/components/BookCard.css +++ b/src/components/BookCard.css @@ -74,11 +74,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 c2ea984..473d9be 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"; @@ -50,16 +51,61 @@ 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 three subjects", () => { + const bookWithThreeSubjects: Book = { + ...fullBook, + subjects: ["Fantasy", "Magic", "Adventure"], + }; + + render(); + + const subjects = screen.getByRole("list", { name: "Subjects, showing 3 of 3" }); + expect(subjects).toHaveTextContent("Fantasy"); + expect(subjects).toHaveTextContent("Magic"); + expect(subjects).toHaveTextContent("Adventure"); + 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"], + subjects: ["Fantasy", "Magic", "Adventure", "Dragons"], }; render(); - expect(screen.getByRole("list", { name: "Subjects, showing 5 of 6" })).toBeInTheDocument(); - expect(screen.getByText("+1 more")).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 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 3 of 4" })).toBeInTheDocument(); + expect(screen.queryByText("Dragons")).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 3 more subjects" })); + rerender(); + + expect(screen.getByRole("list", { name: "Subjects, showing 3 of 6" })).toBeInTheDocument(); expect(screen.queryByText("Earthsea")).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Show 3 more subjects" })).toBeInTheDocument(); }); }); diff --git a/src/components/BookCard.tsx b/src/components/BookCard.tsx index cb55999..8c02bc0 100644 --- a/src/components/BookCard.tsx +++ b/src/components/BookCard.tsx @@ -1,9 +1,10 @@ +import { useState } from "react"; import { coverUrl } from "../api/openLibrary"; 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; @@ -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 ( @@ -44,8 +49,24 @@ export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCar {visibleSubjects.map((subject) => (
  • {subject}
  • ))} - {remainingSubjectCount > 0 && ( -
  • +{remainingSubjectCount} more
  • + {book.subjects.length > MAX_VISIBLE_SUBJECTS && ( +
  • + +
  • )} )}