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
26 changes: 26 additions & 0 deletions src/components/BookJumpList.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.book-jump-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
max-width: 24rem;
color: #243b53;
font-weight: 600;
}

.book-jump-list select {
width: 100%;
min-height: 2.75rem;
padding: 0.625rem 0.75rem;
border: 1px solid #d5dde5;
border-radius: 0.5rem;
background-color: #ffffff;
color: #1f2933;
font: inherit;
font-weight: 400;
}

.book-jump-list select:focus-visible {
outline: 0.1875rem solid #2c3e50;
outline-offset: 0.125rem;
}
57 changes: 57 additions & 0 deletions src/components/BookJumpList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { BookJumpList } from "./BookJumpList";

// Inline fixture data keeps the tests independent of the API and network.
const titles = ["The Hobbit", "Dune", "Pride and Prejudice"];

describe("BookJumpList", () => {
it('renders a native select associated with the label "Jump to book"', () => {
// Render the component with controlled test props.
render(<BookJumpList titles={titles} currentIndex={0} onSelect={vi.fn()} />);

// Find the select by its accessible role and label.
const select = screen.getByRole("combobox", { name: "Jump to book" });

// Verify that the accessible control is a native HTML select element.
expect(select.tagName).toBe("SELECT");
});

it("renders one option per title", () => {
render(<BookJumpList titles={titles} currentIndex={0} onSelect={vi.fn()} />);

// Verify that every fixture title is rendered as an option in the same order.
expect(screen.getAllByRole("option")).toHaveLength(titles.length);
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual(titles);
});

it("selects the option at currentIndex", () => {
render(<BookJumpList titles={titles} currentIndex={1} onSelect={vi.fn()} />);

// currentIndex 1 should select the second title, "Dune".
expect(screen.getByRole("option", { name: "Dune" })).toHaveProperty("selected", true);
});

it("calls onSelect with the selected zero-based index", () => {
// Create a mock function so we can inspect how onSelect is called.
const onSelect = vi.fn();
render(<BookJumpList titles={titles} currentIndex={0} onSelect={onSelect} />);

// Simulate the user selecting the option whose value is index 2.
fireEvent.change(screen.getByRole("combobox", { name: "Jump to book" }), {
target: { value: "2" },
});

// Verify that the component reports the selected index to its parent.
expect(onSelect).toHaveBeenCalledWith(2);
});

it("matches the rendered snapshot", () => {
const { container } = render(
<BookJumpList titles={titles} currentIndex={1} onSelect={vi.fn()} />,
);

// Detect unexpected changes to the component's rendered HTML structure.
expect(container).toMatchSnapshot();
});
});
28 changes: 28 additions & 0 deletions src/components/BookJumpList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "./BookJumpList.css";

interface BookJumpListProps {
titles: string[]; // Titles available for direct navigation.
currentIndex: number; // Zero-based index of the currently selected book.
onSelect: (index: number) => void; // Notifies the parent when the user selects another book.
}

export function BookJumpList({ titles, currentIndex, onSelect }: BookJumpListProps) {
return (
<>
<label className="book-jump-list" htmlFor="book-jump-list">
Jump to book
<select
id="book-jump-list"
value={currentIndex}
onChange={(event) => onSelect(Number(event.target.value))}
>
{titles.map((title, index) => (
<option key={`${index}-${title}`} value={index}>
{title}
</option>
))}
</select>
</label>
</>
);
}
31 changes: 31 additions & 0 deletions src/components/__snapshots__/BookJumpList.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`BookJumpList > matches the rendered snapshot 1`] = `
<div>
<label
class="book-jump-list"
for="book-jump-list"
>
Jump to book
<select
id="book-jump-list"
>
<option
value="0"
>
The Hobbit
</option>
<option
value="1"
>
Dune
</option>
<option
value="2"
>
Pride and Prejudice
</option>
</select>
</label>
</div>
`;