Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/components/BookCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
54 changes: 50 additions & 4 deletions src/components/BookCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(<BookCard book={bookWithThreeSubjects} />);

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(<BookCard book={bookWithManySubjects} />);

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(<BookCard book={bookWithManySubjects} />);

await user.click(screen.getByRole("button", { name: "Show 3 more subjects" }));
rerender(<BookCard book={anotherBook} />);

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();
});
});
29 changes: 25 additions & 4 deletions src/components/BookCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
Expand Down Expand Up @@ -44,8 +49,24 @@ export function BookCard({ book, isFavorite = false, onFavoriteToggle }: BookCar
{visibleSubjects.map((subject) => (
<li key={subject}>{subject}</li>
))}
{remainingSubjectCount > 0 && (
<li className="book-card-subjects-more">+{remainingSubjectCount} more</li>
{book.subjects.length > MAX_VISIBLE_SUBJECTS && (
<li className="book-card-subjects-more">
<button
type="button"
onClick={() =>
setSubjectState({ bookKey: book.key, isExpanded: !isSubjectsExpanded })
}
aria-label={
isSubjectsExpanded
? "Show fewer subjects"
: `Show ${remainingSubjectCount} more subjects`
}
>
{isSubjectsExpanded
? "Show fewer subjects"
: `Show ${remainingSubjectCount} more subjects`}
</button>
</li>
)}
</ul>
)}
Expand Down