Skip to content

Commit

Permalink
feat: Pagination 컴포넌트
Browse files Browse the repository at this point in the history
Co-authored-by: 이선재 <[email protected]>
  • Loading branch information
sounmind and Sunjae95 committed Jan 25, 2025
1 parent 24805e0 commit 723ad3d
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/components/Pagination/Pagination.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.container {
display: flex;
align-items: center;
gap: 10px;
font-family: Arial, sans-serif;
}

.button {
border: none;
background: transparent;
border-radius: 5px;
padding: 8px 0;
cursor: pointer;
font-size: 16px;
transition: background 0.2s ease;

&:disabled {
cursor: not-allowed;
opacity: 0.5;
color: #d9d9d9;
}
}

.pageInfo {
font-size: 16px;

> span {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 24px;
padding: 0 7.5px;
}

.currentPage {
color: var(--primary);
font-weight: var(--font-weight-bold);
}

.divider {
width: 8px;
}
}
33 changes: 33 additions & 0 deletions src/components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from "@storybook/react";
import Pagination from "./Pagination";
import { fn } from "@storybook/test";

const meta = {
component: Pagination,
} satisfies Meta<typeof Pagination>;

export default meta;

export const Default: StoryObj<typeof meta> = {
args: {
currentPage: 2,
totalPages: 10,
onPageChange: fn(),
},
};

export const Disabled1: StoryObj<typeof meta> = {
args: {
currentPage: 1,
totalPages: 10,
onPageChange: fn(),
},
};

export const Disabled2: StoryObj<typeof meta> = {
args: {
currentPage: 10,
totalPages: 10,
onPageChange: fn(),
},
};
78 changes: 78 additions & 0 deletions src/components/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, vi, beforeEach, test } from "vitest";
import Pagination from "./Pagination";

describe("Pagination Component", () => {
const onPageChange = vi.fn();

beforeEach(() => {
onPageChange.mockClear();
});

test("renders current page and total pages", () => {
render(
<Pagination currentPage={2} totalPages={5} onPageChange={onPageChange} />,
);

expect(screen.getByLabelText("Page 2 of 5")).toBeInTheDocument();
});

test("disables previous button on the first page", async () => {
render(
<Pagination currentPage={1} totalPages={5} onPageChange={onPageChange} />,
);
const prevButton = screen.getByRole("button", {
name: "Go to previous page",
});
expect(prevButton).toBeDisabled();
expect(prevButton).toHaveAttribute("aria-disabled", "true");

// Clicking the disabled button should not trigger onPageChange
await userEvent.click(prevButton);
expect(onPageChange).not.toHaveBeenCalled();
});

test("disables next button on the last page", async () => {
render(
<Pagination currentPage={5} totalPages={5} onPageChange={onPageChange} />,
);
const nextButton = screen.getByRole("button", { name: "Go to next page" });
expect(nextButton).toBeDisabled();
expect(nextButton).toHaveAttribute("aria-disabled", "true");

// Clicking the disabled button should not trigger onPageChange
await userEvent.click(nextButton);
expect(onPageChange).not.toHaveBeenCalled();
});

test("calls onPageChange with the previous page number", async () => {
render(
<Pagination currentPage={3} totalPages={5} onPageChange={onPageChange} />,
);
const prevButton = screen.getByRole("button", {
name: "Go to previous page",
});

await userEvent.click(prevButton);
expect(onPageChange).toHaveBeenCalledWith(2);
});

test("calls onPageChange with the next page number", async () => {
render(
<Pagination currentPage={3} totalPages={5} onPageChange={onPageChange} />,
);
const nextButton = screen.getByRole("button", { name: "Go to next page" });

await userEvent.click(nextButton);
expect(onPageChange).toHaveBeenCalledWith(4);
});

test("shows aria-current attribute for the current page", () => {
render(
<Pagination currentPage={3} totalPages={5} onPageChange={onPageChange} />,
);

expect(screen.getByLabelText("Current Page")).toHaveTextContent("3");
});
});
66 changes: 66 additions & 0 deletions src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import styles from "./Pagination.module.css";

interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}

const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
}) => {
const handlePrevClick = () => {
if (currentPage > 1) {
onPageChange(currentPage - 1);
}
};

const handleNextClick = () => {
if (currentPage < totalPages) {
onPageChange(currentPage + 1);
}
};

return (
<nav
className={styles.container}
role="navigation"
aria-label="Pagination Navigation"
>
<button
className={styles.button}
onClick={handlePrevClick}
disabled={currentPage === 1}
aria-disabled={currentPage === 1}
aria-label="Go to previous page"
>
&lt;
</button>

<p
className={styles.pageInfo}
aria-label={`Page ${currentPage} of ${totalPages}`}
>
<span aria-label="Current Page" className={styles.currentPage}>
{currentPage}
</span>
<span className={styles.divider}>/</span>
<span>{totalPages}</span>
</p>

<button
className={styles.button}
onClick={handleNextClick}
disabled={currentPage === totalPages}
aria-disabled={currentPage === totalPages}
aria-label="Go to next page"
>
&gt;
</button>
</nav>
);
};

export default Pagination;

0 comments on commit 723ad3d

Please sign in to comment.