diff --git a/packages/ui/src/components/Pagination/Pagination.test.tsx b/packages/ui/src/components/Pagination/Pagination.test.tsx index 8367cd51c..0e86694d6 100644 --- a/packages/ui/src/components/Pagination/Pagination.test.tsx +++ b/packages/ui/src/components/Pagination/Pagination.test.tsx @@ -205,6 +205,96 @@ describe("Pagination", () => { ); }); + describe("getPageUrl", () => { + it("should render anchor elements when getPageUrl is provided", () => { + render( + undefined} + totalPages={5} + getPageUrl={(page) => `/blog?page=${page}`} + />, + ); + + const links = screen.getAllByRole("link"); + expect(links.length).toBeGreaterThan(0); + }); + + it("should set correct href on page links", () => { + render( + undefined} + totalPages={5} + getPageUrl={(page) => `/blog?page=${page}`} + />, + ); + + const links = screen.getAllByRole("link"); + const hrefs = links.map((link) => link.getAttribute("href")); + + expect(hrefs).toContain("/blog?page=1"); + expect(hrefs).toContain("/blog?page=3"); + }); + + it("should not render previous as link on first page", () => { + render( + undefined} + totalPages={5} + getPageUrl={(page) => `/blog?page=${page}`} + />, + ); + + const prevButton = previousButton(); + expect(prevButton.tagName).toBe("BUTTON"); + expect(prevButton).toBeDisabled(); + }); + + it("should not render next as link on last page", () => { + render( + undefined} + totalPages={5} + getPageUrl={(page) => `/blog?page=${page}`} + />, + ); + + const nextBtn = nextButton(); + expect(nextBtn.tagName).toBe("BUTTON"); + expect(nextBtn).toBeDisabled(); + }); + + it("should render previous and next as links on middle pages", () => { + render( + undefined} + totalPages={5} + getPageUrl={(page) => `/blog?page=${page}`} + />, + ); + + const links = screen.getAllByRole("link"); + const hrefs = links.map((link) => link.getAttribute("href")); + + expect(hrefs).toContain("/blog?page=2"); + expect(hrefs).toContain("/blog?page=4"); + }); + + it("should render buttons when getPageUrl is not provided", () => { + render( undefined} totalPages={5} />); + + const links = screen.queryAllByRole("link"); + expect(links).toHaveLength(0); + + const btns = screen.getAllByRole("button"); + expect(btns.length).toBeGreaterThan(0); + }); + }); + it("should throw an error if totalPages is not a positive integer", () => { expect(() => render( undefined} totalPages={-1} />)).toThrow( "Invalid props: totalPages must be a positive integer", diff --git a/packages/ui/src/components/Pagination/Pagination.tsx b/packages/ui/src/components/Pagination/Pagination.tsx index a4fd4c9ee..173b99358 100644 --- a/packages/ui/src/components/Pagination/Pagination.tsx +++ b/packages/ui/src/components/Pagination/Pagination.tsx @@ -48,18 +48,43 @@ export interface BasePaginationProps extends ComponentProps<"nav">, ThemingProps layout?: "navigation" | "pagination" | "table"; currentPage: number; nextLabel?: string; - onPageChange: (page: number) => void; previousLabel?: string; showIcons?: boolean; } -export interface DefaultPaginationProps extends BasePaginationProps { +interface DefaultPaginationSharedProps extends BasePaginationProps { layout?: "navigation" | "pagination"; renderPaginationButton?: (props: PaginationButtonProps) => ReactNode; totalPages: number; } + +/** + * Client-side pagination: uses `onPageChange` callback for navigation. + */ +interface ClientSidePaginationProps extends DefaultPaginationSharedProps { + onPageChange: (page: number) => void; + getPageUrl?: never; +} + +/** + * Anchor-based pagination: uses `getPageUrl` to render `` elements for SEO. + * `onPageChange` is optional — if omitted, anchor links handle navigation natively. + */ +interface AnchorPaginationProps extends DefaultPaginationSharedProps { + /** + * A function that returns a URL for a given page number. When provided, pagination buttons + * render as `` elements instead of ` ); @@ -50,34 +88,39 @@ export const PaginationButton = forwardRef - {children} - - ); -} +export const PaginationNavigation = forwardRef( + ({ children, className, disabled = false, theme: customTheme, clearTheme, applyTheme, ...props }, ref) => { + const provider = useThemeProvider(); + const theme = useResolveTheme( + [paginationTheme, provider.theme?.pagination, customTheme], + [get(provider.clearTheme, "pagination"), clearTheme], + [get(provider.applyTheme, "pagination"), applyTheme], + ); + + const mergedClassName = twMerge(disabled && theme.pages.selector.disabled, className); + + if ("href" in props && props.href && !disabled) { + const { href, ...anchorProps } = props; + return ( + } href={href} className={mergedClassName} {...anchorProps}> + {children} + + ); + } + + const { href: _, ...buttonProps } = props as PaginationNavigationAsButton; + return ( + + ); + }, +); PaginationNavigation.displayName = "PaginationNavigation";