From 03724cf4faaef18036cd2510b1ab9f28c20b0524 Mon Sep 17 00:00:00 2001 From: Nathan Drake Date: Mon, 17 Feb 2025 15:52:54 -0600 Subject: [PATCH] feat: Add builitin formatter, and ability to pass custom children to page button --- src/Pagination/Pagination.test.tsx | 91 +++++++ src/Pagination/Pagination.test.tsx.snap | 258 +++++++++++++++++++ src/Pagination/Pagination.tsx | 16 +- src/types/types.ts | 6 +- stories/PaginationCustomChildren.stories.tsx | 87 +++++++ stories/PaginationTailwind.stories.tsx | 22 ++ 6 files changed, 476 insertions(+), 4 deletions(-) create mode 100644 stories/PaginationCustomChildren.stories.tsx diff --git a/src/Pagination/Pagination.test.tsx b/src/Pagination/Pagination.test.tsx index f6c0b4f..9896692 100644 --- a/src/Pagination/Pagination.test.tsx +++ b/src/Pagination/Pagination.test.tsx @@ -49,6 +49,28 @@ const setupPagination = ({ ); describe("Pagination", () => { + const mockFormat = jest.fn((number) => { + const parts = number.toString().split("."); + const wholeNumber = parts[0]; + + return wholeNumber.replace(/\B(?=(\d{3})+(?!\d))/g, ","); + }); + + beforeEach(() => { + const MockNumberFormat = jest.fn(() => ({ + format: mockFormat, + })) as jest.Mock & typeof Intl.NumberFormat; + + // Add required static methods + MockNumberFormat.supportedLocalesOf = jest.fn(); + + global.Intl.NumberFormat = MockNumberFormat; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it("renders correctly with basic setup of prevButton, pageButton and nextButton", () => { const { asFragment } = setupPagination({}); @@ -216,4 +238,73 @@ describe("Pagination", () => { expect(mockSetCurrentPage).toHaveBeenCalledWith(3); }); + + it("renders correctly with function as children", () => { + const { asFragment } = setupPagination({ + children: ( + <> + Previous +
+ `Page ${page}`} + /> +
+ Next + + ), + }); + + expect(asFragment()).toMatchSnapshot(); + const element = screen.getByTestId("function-children"); + expect(element.textContent).toBe("Page 6"); // Since currentPage is 5 in setup + }); + + it("uses default number formatter correctly", () => { + const { asFragment } = setupPagination({ + currentPage: 1000, + totalPages: 1200, + children: ( + <> + Previous +
+ +
+ Next + + ), + }); + + expect(asFragment()).toMatchSnapshot(); + const element = screen.getByTestId("default-formatter"); + // Should format 2 as "2" (currentPage is 1, so active page is 2) + expect(element.textContent).toBe("1,001"); + }); + + it("applies custom number formatter correctly", () => { + const romanNumerals = ["I", "II", "III", "IV", "V"]; + const customFormatter = (page: number) => romanNumerals[page - 1]; + + const { asFragment } = setupPagination({ + currentPage: 1, + totalPages: 5, + children: ( + <> + Previous +
+ +
+ Next + + ), + }); + + expect(asFragment()).toMatchSnapshot(); + const element = screen.getByTestId("custom-formatter"); + // Should format 2 as "II" (currentPage is 1, so active page is 2) + expect(element.textContent).toBe("II"); + }); }); diff --git a/src/Pagination/Pagination.test.tsx.snap b/src/Pagination/Pagination.test.tsx.snap index b0d4693..8d114fb 100644 --- a/src/Pagination/Pagination.test.tsx.snap +++ b/src/Pagination/Pagination.test.tsx.snap @@ -1,5 +1,75 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Pagination applies custom number formatter correctly 1`] = ` + +
+ +
+
  • + + I + +
  • +
  • + + II + +
  • +
  • + + III + +
  • +
  • + + IV + +
  • +
  • + + V + +
  • +
    + +
    +
    +`; + exports[`Pagination renders correctly with advanced setup of prevButton, pageButton and nextButton using Tailwind 1`] = `
    `; + +exports[`Pagination renders correctly with function as children 1`] = ` + +
    + +
    +
  • + + Page 1 + +
  • +
  • + ... +
  • +
  • + + Page 4 + +
  • +
  • + + Page 5 + +
  • +
  • + + Page 6 + +
  • +
  • + + Page 7 + +
  • +
  • + + Page 8 + +
  • +
  • + ... +
  • +
  • + + Page 10 + +
  • +
    + +
    +
    +`; + +exports[`Pagination uses default number formatter correctly 1`] = ` + +
    + +
    +
  • + + 1 + +
  • +
  • + ... +
  • +
  • + + 999 + +
  • +
  • + + 1,000 + +
  • +
  • + + 1,001 + +
  • +
  • + + 1,002 + +
  • +
  • + + 1,003 + +
  • +
  • + ... +
  • +
  • + + 1,200 + +
  • +
    + +
    +
    +`; diff --git a/src/Pagination/Pagination.tsx b/src/Pagination/Pagination.tsx index 48ad38f..4252e7c 100644 --- a/src/Pagination/Pagination.tsx +++ b/src/Pagination/Pagination.tsx @@ -102,17 +102,31 @@ const TruncableElement = ({ prev }: ITruncableElementProps) => { ) : null; }; +const pageFormatter = (page: number) => Intl.NumberFormat().format(page); + export const PageButton = ({ as = , + children, className, dataTestIdActive, dataTestIdInactive, + formatter = pageFormatter, activeClassName, inactiveClassName, renderExtraProps, }: PageButtonProps) => { const pagination: IPagination = React.useContext(PaginationContext); + const getPageContent = (page: number): React.ReactNode => { + if (!children) { + return formatter(page); + } + + return typeof children === "function" + ? (children as (page: number) => React.ReactNode)(page) + : children; + }; + const renderPageButton = (page: number) => (
  • - {page} + {getPageContent(page)}
  • ); diff --git a/src/types/types.ts b/src/types/types.ts index c61f797..f0be549 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -32,8 +32,6 @@ type IPagination = IUsePagination & { type ButtonProps = ButtonHTMLAttributes & { as?: React.ReactElement; - children?: string | React.ReactNode; - className?: string; dataTestId?: string; }; @@ -44,9 +42,11 @@ type PageButtonProps = ButtonProps & { as?: React.ReactElement; activeClassName?: string; inactiveClassName?: string; + children?: React.ReactNode | ((page: number) => React.ReactNode); dataTestIdActive?: string; dataTestIdInactive?: string; - renderExtraProps?: (pageNum: number) => {}; + formatter?: (page: number) => string; + renderExtraProps?: (pageNum: number) => Record; }; export { diff --git a/stories/PaginationCustomChildren.stories.tsx b/stories/PaginationCustomChildren.stories.tsx new file mode 100644 index 0000000..28b6d11 --- /dev/null +++ b/stories/PaginationCustomChildren.stories.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import clsx from "clsx"; +import { Meta, Story } from "@storybook/react"; +import { FiArrowLeft, FiArrowRight } from "react-icons/fi"; +import { Pagination, IPaginationProps } from "../src"; + +import "./tailwind_output.css"; + +const meta: Meta = { + title: "Pagination Custom Children", + component: Pagination, + parameters: { + controls: { expanded: true }, + }, +}; + +export default meta; + +const PaginationStory: Story = (args) => { + const [page, setPage] = React.useState(0); + + const handlePageChange = (page: number) => { + setPage(page); + }; + + return ( + + } + className={clsx( + "flex items-center mr-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-200", + { + "cursor-pointer": page !== 0, + "opacity-50": page === 0, + }, + )} + > + + Previous + + + + + + Next + + + + ); +}; + +export const Default = PaginationStory.bind({}); + +Default.args = { + totalPages: 1000, + edgePageCount: 2, + middlePagesSiblingCount: 1, + truncableText: "...", + truncableClassName: "w-10 px-0.5", +}; diff --git a/stories/PaginationTailwind.stories.tsx b/stories/PaginationTailwind.stories.tsx index 4946835..396c51d 100644 --- a/stories/PaginationTailwind.stories.tsx +++ b/stories/PaginationTailwind.stories.tsx @@ -83,3 +83,25 @@ Default.args = { truncableText: "...", truncableClassName: "w-10 px-0.5", }; + +export const BigNumbers = PaginationStory.bind({}); + +BigNumbers.args = { + totalPages: 1200, + currentPage: 1000, + edgePageCount: 2, + middlePagesSiblingCount: 1, + truncableText: "...", + truncableClassName: "w-10 px-0.5", +}; + +export const CustomFormatter = PaginationStory.bind({}); + +CustomFormatter.args = { + totalPages: 10, + edgePageCount: 2, + middlePagesSiblingCount: 1, + truncableText: "...", + truncableClassName: "", + formatter: (page: number) => `${page}.0`, +};