diff --git a/src/components/Pagination/Pagination.mdx b/src/components/Pagination/Pagination.mdx index b683f1ca05..7888a0b4ea 100644 --- a/src/components/Pagination/Pagination.mdx +++ b/src/components/Pagination/Pagination.mdx @@ -9,10 +9,10 @@ import Link from "../Link/Link"; # Pagination -| Component Version | DS Version | -| ----------------- | ---------- | -| Added | `0.0.10` | -| Latest | `3.0.0` | +| Component Version | DS Version | +| ----------------- | ------------ | +| Added | `0.0.10` | +| Latest | `Prerelease` | ## Table of Contents diff --git a/src/components/Pagination/Pagination.stories.tsx b/src/components/Pagination/Pagination.stories.tsx index f0c0ea7ecc..3fd86c5cd2 100644 --- a/src/components/Pagination/Pagination.stories.tsx +++ b/src/components/Pagination/Pagination.stories.tsx @@ -55,7 +55,7 @@ export const UnchangingURL: Story = { onPageChange: (selectedPage) => { console.log(`Current page: ${selectedPage}`); }, - pageCount: 100, + pageCount: 10, }, name: "Pagination with Unchanging URL", render: (args) => , diff --git a/src/components/Pagination/Pagination.test.tsx b/src/components/Pagination/Pagination.test.tsx index d49391be4f..3bda86ec15 100644 --- a/src/components/Pagination/Pagination.test.tsx +++ b/src/components/Pagination/Pagination.test.tsx @@ -52,30 +52,38 @@ describe("Pagination", () => { expect(screen.getAllByRole("link")).toHaveLength(7); }); - it("does not render the Previous link on the first page", () => { + it("renders the disabled Previous link on the first page", () => { render( ); // Pagination should show: - // 1 2 3 4 5 Next + // Previous 1 2 3 4 5 Next const links = screen.getAllByRole("link"); - expect(links).toHaveLength(6); - expect(screen.queryByText("Previous")).not.toBeInTheDocument(); + expect(links).toHaveLength(7); + expect(screen.queryByText("Previous")).toBeInTheDocument(); + expect(screen.getByText("Previous").parentElement).toHaveAttribute( + "aria-disabled", + "true" + ); expect(screen.getByText("Next")).toBeInTheDocument(); }); - it("does not render the Next link on the last page", () => { + it("renders the disabled Next link on the last page", () => { render( ); // Pagination should show: - // Previous 1 2 3 4 5 + // Previous 1 2 3 4 5 Next const links = screen.getAllByRole("link"); - expect(links).toHaveLength(6); + expect(links).toHaveLength(7); expect(screen.getByText("Previous")).toBeInTheDocument(); - expect(screen.queryByText("Next")).not.toBeInTheDocument(); + expect(screen.queryByText("Next")).toBeInTheDocument(); + expect(screen.getByText("Next").parentElement).toHaveAttribute( + "aria-disabled", + "true" + ); }); it("renders an ellipsis at the end of the list", () => { @@ -83,15 +91,15 @@ describe("Pagination", () => { ); // Pagination should show: - // 1 2 3 4 5 ... 10 Next + // Previous 1 2 3 4 5 ... 10 Next const listitem = screen.getAllByRole("listitem"); - expect(listitem).toHaveLength(8); + expect(listitem).toHaveLength(9); // The ellipsis is not a link - expect(screen.getAllByRole("link")).toHaveLength(7); - expect(listitem[4].textContent).toEqual("5"); - expect(listitem[5].textContent).toEqual("..."); - expect(listitem[6].textContent).toEqual("10"); + expect(screen.getAllByRole("link")).toHaveLength(8); + expect(listitem[5].textContent).toEqual("5"); + expect(listitem[6].textContent).toEqual("..."); + expect(listitem[7].textContent).toEqual("10"); expect(screen.getByText("...")).toBeInTheDocument(); }); @@ -100,12 +108,12 @@ describe("Pagination", () => { ); // Pagination should show: - // Previous 1 ... 6 7 8 9 10 + // Previous 1 ... 6 7 8 9 10 Next const listitem = screen.getAllByRole("listitem"); - expect(listitem).toHaveLength(8); + expect(listitem).toHaveLength(9); // The ellipsis is not a link - expect(screen.getAllByRole("link")).toHaveLength(7); + expect(screen.getAllByRole("link")).toHaveLength(8); expect(listitem[1].textContent).toEqual("1"); expect(listitem[2].textContent).toEqual("..."); expect(listitem[3].textContent).toEqual("6"); @@ -361,7 +369,7 @@ describe("Pagination", () => { /> ); // Pagination should show: - // Previous 1 ... 6 7 8 9 10 + // Previous 1 ... 6 7 8 9 10 Next links = screen.getAllByRole("link"); // Page 6 @@ -376,9 +384,10 @@ describe("Pagination", () => { ); let links = screen.getAllByRole("link"); - let page1 = links[0].getAttribute("aria-current"); - let page2 = links[1].getAttribute("aria-current"); - let page3 = links[2].getAttribute("aria-current"); + // links[0] is "Previous" + let page1 = links[1].getAttribute("aria-current"); + let page2 = links[2].getAttribute("aria-current"); + let page3 = links[3].getAttribute("aria-current"); // Only the current page has `aria-current="page"` for accessibility. expect(page1).toEqual("page"); @@ -390,7 +399,7 @@ describe("Pagination", () => { ); links = screen.getAllByRole("link"); - // links[0] is now "Previous" + // links[0] is "Previous" page1 = links[1].getAttribute("aria-current"); page2 = links[2].getAttribute("aria-current"); page3 = links[3].getAttribute("aria-current"); diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index 574db18133..5e89c105ed 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -6,9 +6,11 @@ import { } from "@chakra-ui/react"; import React, { forwardRef, useState, useRef } from "react"; +import Text from "../Text/Text"; import Link from "../Link/Link"; import List from "../List/List"; import { range } from "../../utils/utils"; +import Icon from "../Icons/Icon"; export interface PaginationProps { /** Additional className. */ @@ -123,76 +125,152 @@ export const Pagination: ChakraComponent< handlePageClick(e, nextPageNumber); } }; + + // Returns the "Previous" or "Next" link element, disabled according to current page number. + const getPreviousNextElement = ({ + pageNumber, + text, + isDisabled, + }: { + pageNumber: number; + text: "Previous" | "Next"; + isDisabled: boolean; + }) => { + const isPrevious = text === "Previous"; + const disabledStyles = isDisabled ? styles.disabledElement : {}; + + return ( + + {!isPrevious && ( + + {text} + + )} + + + {isPrevious && ( + + {text} + + )} + + ); + }; + /** * All `Link` components have similar attributes but we need to differentiate - * between the "previous", "next", and regular number links. + * between the "previous"/"next", and regular number links. * 1. If `getPageHref` is passed, this means that the page refreshes and the * URL changes. In this case, the parent component returns the `href` URL * and the `onClick` callback is undefined. * 2. Otherwise, we stay on the same page by setting the `href` attribute to * "#" and call the `onPageChange` prop through the `onClick` callback. */ - const getLinkElement = ( - type: "items" | "previous" | "next", - item?: number - ) => { + const getItemElement = (item?: number) => { const isSelectedPage = selectedPage === item; - // The current page link has different styles. + + // Styles for the current page link. const currentStyles = isSelectedPage ? { - color: "ui.typography.body", - pointerEvent: "none", + display: "flex", + justifyContent: "center", + alignItems: "center", + pointerEvents: "none", + border: "1px solid", + borderRadius: "4px", + borderColor: "ui.link.primary", + bg: "ui.link.primary-05", + color: "ui.link.primary", + _visited: { + color: "ui.link.primary", + }, _dark: { - color: "dark.ui.typography.body", + bg: "transparent", + borderColor: "dark.ui.link.primary", + color: "dark.ui.link.primary", + _visited: { + color: "dark.ui.link.primary", + }, }, } : {}; - const allAttrs = { - items: { - href: changeUrls ? getPageHref(item as number) : "#", - attributes: { - "aria-label": `Page ${item}`, - "aria-current": isSelectedPage ? "page" : null, - onClick: changeUrls + + return ( + - ) => handlePageClick(e, item as number), - } as any, - text: item, - }, - previous: { - href: changeUrls ? getPageHref(previousPageNumber) : "#", - attributes: { - "aria-label": "Previous page", - onClick: changeUrls ? undefined : previousPage, - }, - text: "Previous", - }, - next: { - href: changeUrls ? getPageHref(nextPageNumber) : "#", - attributes: { - "aria-label": "Next page", - onClick: changeUrls ? undefined : nextPage, - }, - text: "Next", - }, - }; - const linkAttrs = allAttrs[type]; - return ( - handlePageClick(e, item as number) + } __css={{ ...styles.link, ...currentStyles, + ...{ + minWidth: { base: "44px", md: "32px" }, + height: { base: "44px", md: "32px" }, + // Remove padding at 360px so all page numbers (up to 4 digits) can still fit. + padding: ["0", "inherit"], + "@media (min-width: 360px)": { + padding: { base: "2px 8px", md: "4px 8px" }, + }, + }, }} > - {linkAttrs.text} + {item} ); }; @@ -222,7 +300,8 @@ export const Pagination: ChakraComponent< // one number before the current page. selected - 1, // If the current page is near the end, show the last five items. - pageCount - 4 + // If the page number has 4 digits, show only the last four items. + pageCount > 999 ? pageCount - 3 : pageCount - 4 ) ); // Where should the middle range of numbers end at? @@ -238,6 +317,8 @@ export const Pagination: ChakraComponent< 5 ) ); + //} + const itemList = pageCount < 4 ? // Get a short array with 2 or 3 items: [1, 2] or [1, 2, 3] @@ -257,24 +338,37 @@ export const Pagination: ChakraComponent< // Always display the last page. pageCount, ]; + // If it's a number, render an `li` element with a link page item, // otherwise return the `li` with the ellipse for a break. const pageLiItems = itemList.map((item) => { const itemElement = - typeof item === "number" ? getLinkElement("items", item) : "..."; + typeof item === "number" ? getItemElement(item) : "..."; return
  • {itemElement}
  • ; }); return pageLiItems; }; - // Don't display the previous link when you're on the first page. - const previousLiLink = selectedPage !== 1 && ( -
  • {getLinkElement("previous")}
  • + // Disable the previous link when you're on the first page. + const previousLiLink = ( +
  • + {getPreviousNextElement({ + pageNumber: previousPageNumber, + text: "Previous", + isDisabled: selectedPage === 1, + })} +
  • ); - // Don't display the next link when you're on the last page. - const nextLiLink = selectedPage !== pageCount && ( -
  • {getLinkElement("next")}
  • + // Disable the next link when you're on the last page. + const nextLiLink = ( +
  • + {getPreviousNextElement({ + pageNumber: nextPageNumber, + text: "Next", + isDisabled: selectedPage === pageCount, + })} +
  • ); return ( diff --git a/src/components/Pagination/__snapshots__/Pagination.test.tsx.snap b/src/components/Pagination/__snapshots__/Pagination.test.tsx.snap index c8c227bfe2..ee4cd300c6 100644 --- a/src/components/Pagination/__snapshots__/Pagination.test.tsx.snap +++ b/src/components/Pagination/__snapshots__/Pagination.test.tsx.snap @@ -11,11 +11,44 @@ exports[`Pagination Rendering renders the UI snapshot correctly 1`] = ` className="css-1xdhyk6" id="firstPage-list" > +
  • + + + + + Previous + + +
  • - Next + + Next + + +
  • @@ -121,6 +170,7 @@ exports[`Pagination Rendering renders the UI snapshot correctly 2`] = ` >
  • - Previous + + + + Previous +
  • +
  • + + + Next + + + + +
  • `; @@ -229,6 +327,7 @@ exports[`Pagination Rendering renders the UI snapshot correctly 3`] = ` >
  • - Previous + + + + Previous +
  • - Next + + Next + + +
  • @@ -337,11 +473,44 @@ exports[`Pagination Rendering renders the UI snapshot correctly 4`] = ` className="css-1xdhyk6" id="chakra-list" > +
  • + + + + + Previous + + +
  • - Next + + Next + + +
  • @@ -446,11 +631,44 @@ exports[`Pagination Rendering renders the UI snapshot correctly 5`] = ` className="css-1xdhyk6" id="props-list" > +
  • + + + + + Previous + + +
  • - Next + + Next + + +
  • diff --git a/src/components/Pagination/paginationChangelogData.ts b/src/components/Pagination/paginationChangelogData.ts index 1743bd0694..83ad8e453d 100644 --- a/src/components/Pagination/paginationChangelogData.ts +++ b/src/components/Pagination/paginationChangelogData.ts @@ -9,6 +9,13 @@ import { ChangelogData } from "../../utils/ComponentChangelogTable"; export const changelogData: ChangelogData[] = [ + { + date: "Prerelease", + version: "Prelease", + type: "Update", + affects: ["Styles"], + notes: ["Component styling."], + }, { date: "2024-03-14", version: "3.0.0", diff --git a/src/theme/components/pagination.ts b/src/theme/components/pagination.ts index 28936f33ae..019cbb0d41 100644 --- a/src/theme/components/pagination.ts +++ b/src/theme/components/pagination.ts @@ -1,12 +1,16 @@ import { createMultiStyleConfigHelpers } from "@chakra-ui/styled-system"; const { defineMultiStyleConfig, definePartsStyle } = - createMultiStyleConfigHelpers(["link"]); + createMultiStyleConfigHelpers([ + "link", + "previousNextElement", + "disabledElement", + ]); const Pagination = defineMultiStyleConfig({ baseStyle: definePartsStyle({ - alignItems: "stretch", display: "flex", + minWidth: { base: "320px", md: "unset" }, width: "100%", link: { lineHeight: "1.15", @@ -14,9 +18,56 @@ const Pagination = defineMultiStyleConfig({ _hover: { textDecoration: "none", }, + _visited: { + color: "ui.link.primary", + }, + _dark: { + _visited: { + color: "dark.ui.link.primary", + }, + }, }, ul: { + display: "flex", + justifyContent: "center", + alignItems: "center", marginBottom: "0", + justifyItems: "center", + }, + previousNextElement: { + alignItems: "center", + color: "ui.link.primary", + _hover: { + textDecoration: "none", + svg: { fill: "ui.link.secondary" }, + color: "ui.link.secondary", + }, + _visited: { + color: "ui.link.primary", + svg: { + fill: "ui.link.primary", + }, + }, + svg: { + fill: "ui.link.primary", + }, + _dark: { + color: "dark.ui.link.primary", + svg: { fill: "dark.ui.link.primary" }, + _visited: { + color: "dark.ui.link.primary", + svg: { fill: "dark.ui.link.primary" }, + }, + }, + }, + disabledElement: { + color: "ui.disabled.primary", + pointerEvents: "none", + svg: { fill: "ui.disabled.primary" }, + _dark: { + color: "ui.disabled.secondary", + svg: { fill: "ui.disabled.secondary" }, + }, }, }), });