diff --git a/cypress/e2e/components/Pagination.spec.ts b/cypress/e2e/components/Pagination.spec.ts new file mode 100644 index 000000000..307a42c26 --- /dev/null +++ b/cypress/e2e/components/Pagination.spec.ts @@ -0,0 +1,30 @@ +describe("Pagination", () => { + describe("with scrolling", () => { + beforeEach(() => { + cy.renderFromStorybook("pagination--scroll-after-pagination"); + }); + + it("does not scroll after pagination by default", () => { + cy.get('[aria-label="scroll target"]').contains("None").click(); + + cy.get('[aria-label="Pagination navigation"]').contains("Next").click(); + cy.isNotInViewport('[data-testid="page-heading"'); + }); + + it("scrolls to the top of the page after pagination when scrollToTopAfterPagination is true", () => { + cy.get('[aria-label="scroll target"]').contains("Top of page").click(); + + cy.get('[aria-label="Pagination navigation"]').contains("Next").click(); + cy.get('[aria-label="Pagination navigation"]').contains("Previous").click(); + cy.isInViewport('[data-testid="page-heading"'); + }); + + it("scrolls to the top of the target element when scrollToTopAfterPagination is true and a scrollTargetRef is present", () => { + cy.get('[aria-label="scroll target"]').contains("Top of section").click(); + + cy.get('[aria-label="Pagination navigation"]').contains("7").click(); + cy.isNotInViewport('[data-testid="page-heading"'); + cy.isInViewport('[data-testid="section-heading"'); + }); + }); +}); diff --git a/src/Pagination/Pagination.story.tsx b/src/Pagination/Pagination.story.tsx index cf2f2dfd5..8d78ee03f 100644 --- a/src/Pagination/Pagination.story.tsx +++ b/src/Pagination/Pagination.story.tsx @@ -1,5 +1,9 @@ -import React from "react"; +import React, { useRef, useState } from "react"; import { action } from "@storybook/addon-actions"; +import { Switch, Switcher } from "../Switcher"; +import { Flex } from "../Flex"; +import { Heading1, Text } from "../Type"; +import { Box } from "../Box"; import { Pagination } from "."; export default { @@ -41,3 +45,56 @@ export const WithLessThan5Pages = () => + + + Scroll target after pagination + + + None + Top of page + Top of section + + + + {messages[scrollTarget].page} + + + {messages[scrollTarget].section} + + setCurrentPage((p) => p + 1)} + onPrevious={() => setCurrentPage((p) => p - 1)} + onSelectPage={(page) => { + setCurrentPage(Number(page)); + }} + /> + + ); +} diff --git a/src/Pagination/Pagination.tsx b/src/Pagination/Pagination.tsx index f67ba3290..ab71dccb9 100644 --- a/src/Pagination/Pagination.tsx +++ b/src/Pagination/Pagination.tsx @@ -1,13 +1,13 @@ import React, { ReactNode, RefObject } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; +import { flushSync } from "react-dom"; import { Flex } from "../Flex"; import { Text } from "../Type"; import { FlexProps } from "../Flex/Flex"; import PageNumber from "./PageNumber"; import PreviousButton from "./PreviousButton"; import NextButton from "./NextButton"; -import { flushSync } from "react-dom"; const SEPARATOR = "..."; @@ -36,6 +36,8 @@ type PaginationProps = FlexProps & { nextAriaLabel?: string; previousLabel?: ReactNode; previousAriaLabel?: string; + scrollToTopAfterPagination?: boolean; + scrollTargetRef?: RefObject; }; function Pagination({ @@ -48,17 +50,35 @@ function Pagination({ nextLabel, previousAriaLabel, previousLabel, - previousLabel, + scrollToTopAfterPagination, + scrollTargetRef, "aria-label": ariaLabel, ...restProps }: PaginationProps) { const { t } = useTranslation(); + const scrollToTop = () => { + if (scrollToTopAfterPagination) { + const top = scrollTargetRef?.current ? window.scrollY + scrollTargetRef.current?.getBoundingClientRect()?.top : 0; + + window.scrollTo({ + top, + behavior: "smooth", + }); + } + }; + return ( { + flushSync(() => { + onPrevious(); + }); + + scrollToTop(); + }} ariaLabel={previousAriaLabel} label={previousLabel} /> @@ -67,7 +87,6 @@ function Pagination({ if (page === SEPARATOR) return ( - // eslint-disable-next-line react/no-array-index-key {SEPARATOR} @@ -80,13 +99,30 @@ function Pagination({ disabled={isCurrentPage} aria-label={isCurrentPage ? null : t("go to page", { count: Number(page) })} key={page} - onClick={() => onSelectPage(page)} + onClick={() => { + flushSync(() => { + onSelectPage(page); + }); + + scrollToTop(); + }} > {page} ); })} - + { + flushSync(() => { + onNext(); + }); + + scrollToTop(); + }} + ariaLabel={nextAriaLabel} + label={nextLabel} + /> ); }