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
35 changes: 35 additions & 0 deletions src/components/NavigationControls.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.navigation-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin: 1rem 0 0;
}

.navigation-progress {
margin: 0;
color: #616e7c;
font-variant-numeric: tabular-nums;
}

.navigation-controls button {
padding: 0.5rem 1rem;
border: 1px solid #cbd2d9;
border-radius: 0.375rem;
background-color: #ffffff;
color: #1f2933;
}

.navigation-controls button:hover:not(:disabled) {
background-color: #f5f7fa;
}

.navigation-controls button:focus-visible {
outline: 2px solid #2c3e50;
outline-offset: 2px;
}

.navigation-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
80 changes: 80 additions & 0 deletions src/components/NavigationControls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { NavigationControls } from "./NavigationControls";

describe("NavigationControls", () => {
it("reports the current position", () => {
render(
<NavigationControls
currentIndex={2}
totalCount={10}
onPrevious={() => {}}
onNext={() => {}}
/>,
);
expect(screen.getByText("3 / 10")).toBeInTheDocument();
});

it("calls the matching handler when a button is clicked", async () => {
const user = userEvent.setup();
const onPrevious = vi.fn();
const onNext = vi.fn();
render(
<NavigationControls
currentIndex={2}
totalCount={10}
onPrevious={onPrevious}
onNext={onNext}
/>,
);
await user.click(screen.getByRole("button", { name: "Show previous book" }));
await user.click(screen.getByRole("button", { name: "Show next book" }));
expect(onPrevious).toHaveBeenCalledTimes(1);
expect(onNext).toHaveBeenCalledTimes(1);
});

it("disables the buttons at the boundaries", async () => {
const user = userEvent.setup();
const onPrevious = vi.fn();
const onNext = vi.fn();
const { rerender } = render(
<NavigationControls
currentIndex={0}
totalCount={10}
onPrevious={onPrevious}
onNext={onNext}
/>,
);
const previous = screen.getByRole("button", { name: "Show previous book" });
expect(previous).toBeDisabled();
await user.click(previous);
expect(onPrevious).not.toHaveBeenCalled();

rerender(
<NavigationControls
currentIndex={9}
totalCount={10}
onPrevious={onPrevious}
onNext={onNext}
/>,
);
const next = screen.getByRole("button", { name: "Show next book" });
expect(next).toBeDisabled();
await user.click(next);
expect(onNext).not.toHaveBeenCalled();
expect(screen.getByText("10 / 10")).toBeInTheDocument();
});

it("matches the snapshot", () => {
const { container } = render(
<NavigationControls
currentIndex={2}
totalCount={10}
onPrevious={() => {}}
onNext={() => {}}
/>,
);
expect(container).toMatchSnapshot();
});
});
39 changes: 39 additions & 0 deletions src/components/NavigationControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import "./NavigationControls.css";

interface NavigationControlsProps {
currentIndex: number;
totalCount: number;
onPrevious: () => void;
onNext: () => void;
}

export function NavigationControls({
currentIndex,
totalCount,
onPrevious,
onNext,
}: NavigationControlsProps) {
return (
<nav className="navigation-controls" aria-label="Book navigation">
<button
type="button"
onClick={onPrevious}
disabled={currentIndex === 0}
aria-label="Show previous book"
>
Previous
</button>
<p className="navigation-progress" aria-live="polite">
{currentIndex + 1} / {totalCount}
</p>
<button
type="button"
onClick={onNext}
disabled={currentIndex >= totalCount - 1}
aria-label="Show next book"
>
Next
</button>
</nav>
);
}
31 changes: 31 additions & 0 deletions src/components/__snapshots__/NavigationControls.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[`NavigationControls > matches the snapshot 1`] = `
<div>
<nav
aria-label="Book navigation"
class="navigation-controls"
>
<button
aria-label="Show previous book"
type="button"
>
Previous
</button>
<p
aria-live="polite"
class="navigation-progress"
>
3
/
10
</p>
<button
aria-label="Show next book"
type="button"
>
Next
</button>
</nav>
</div>
`;