diff --git a/src/components/NavigationControls.css b/src/components/NavigationControls.css new file mode 100644 index 0000000..1b3019d --- /dev/null +++ b/src/components/NavigationControls.css @@ -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; +} diff --git a/src/components/NavigationControls.test.tsx b/src/components/NavigationControls.test.tsx new file mode 100644 index 0000000..b37a86e --- /dev/null +++ b/src/components/NavigationControls.test.tsx @@ -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( + {}} + 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( + , + ); + 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( + , + ); + const previous = screen.getByRole("button", { name: "Show previous book" }); + expect(previous).toBeDisabled(); + await user.click(previous); + expect(onPrevious).not.toHaveBeenCalled(); + + rerender( + , + ); + 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( + {}} + onNext={() => {}} + />, + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/NavigationControls.tsx b/src/components/NavigationControls.tsx new file mode 100644 index 0000000..58f9b25 --- /dev/null +++ b/src/components/NavigationControls.tsx @@ -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 ( + + ); +} diff --git a/src/components/__snapshots__/NavigationControls.test.tsx.snap b/src/components/__snapshots__/NavigationControls.test.tsx.snap new file mode 100644 index 0000000..8ec68b3 --- /dev/null +++ b/src/components/__snapshots__/NavigationControls.test.tsx.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`NavigationControls > matches the snapshot 1`] = ` +
+ +
+`;