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 && (
+
+
+
)}
)}