Skip to content

Commit

Permalink
feat: optionally scroll to the top after paginating
Browse files Browse the repository at this point in the history
  • Loading branch information
haideralsh committed Nov 17, 2023
1 parent 567eafe commit c495914
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 7 deletions.
30 changes: 30 additions & 0 deletions cypress/e2e/components/Pagination.spec.ts
Original file line number Diff line number Diff line change
@@ -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"');
});
});
});
59 changes: 58 additions & 1 deletion src/Pagination/Pagination.story.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -41,3 +45,56 @@ export const WithLessThan5Pages = () => <Pagination currentPage={3} totalPages={
WithLessThan5Pages.story = {
name: "with less than 5 pages",
};

export function ScrollAfterPagination() {
const [currentPage, setCurrentPage] = useState(1);
const [scrollTarget, setScrollTarget] = useState("none");
const ref = useRef(null);

const messages = {
none: {
page: "The page should not scroll after pagination.",
section: "",
},
topOfPage: {
page: "The page should scroll to the top after pagination.",
section: "",
},
topOfSection: {
page: "This part of the page should not be in the viewport after pagination.",
section: "The page should scroll to the top of this section after pagination.",
},
};

return (
<Flex gap="x2" flexDirection="column" alignItems="flex-end">
<Flex flexDirection="column" gap="x1" alignSelf="flex-start" mb="x2">
<Text fontSize="small" fontWeight="bold">
Scroll target after pagination
</Text>
<Switcher selected={scrollTarget} onChange={setScrollTarget} aria-label="scroll target">
<Switch value="none">None</Switch>
<Switch value="topOfPage">Top of page</Switch>
<Switch value="topOfSection">Top of section</Switch>
</Switcher>
</Flex>
<Box height="180px" width="100%">
<Heading1 data-testid="page-heading">{messages[scrollTarget].page}</Heading1>
</Box>
<Box ref={ref} p="x4" height="1400px" width="100%" bg="lightBlue">
<Heading1 data-testid="section-heading">{messages[scrollTarget].section}</Heading1>
</Box>
<Pagination
scrollToTopAfterPagination={scrollTarget !== "none"}
scrollTargetRef={scrollTarget === "topOfSection" ? ref : undefined}
currentPage={currentPage}
totalPages={7}
onNext={() => setCurrentPage((p) => p + 1)}
onPrevious={() => setCurrentPage((p) => p - 1)}
onSelectPage={(page) => {
setCurrentPage(Number(page));
}}
/>
</Flex>
);
}
48 changes: 42 additions & 6 deletions src/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -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 = "...";

Expand Down Expand Up @@ -36,6 +36,8 @@ type PaginationProps = FlexProps & {
nextAriaLabel?: string;
previousLabel?: ReactNode;
previousAriaLabel?: string;
scrollToTopAfterPagination?: boolean;
scrollTargetRef?: RefObject<HTMLElement>;
};

function Pagination({
Expand All @@ -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 (
<Flex as="nav" aria-label={ariaLabel || t("pagination navigation")} {...restProps}>
<PreviousButton
disabled={currentPage === 1}
onClick={onPrevious}
onClick={() => {
flushSync(() => {
onPrevious();
});

scrollToTop();
}}
ariaLabel={previousAriaLabel}
label={previousLabel}
/>
Expand All @@ -67,7 +87,6 @@ function Pagination({

if (page === SEPARATOR)
return (
// eslint-disable-next-line react/no-array-index-key
<Text key={`sep${index}`} py="x1" mr="x2" fontSize="small" lineHeight="smallTextBase">
{SEPARATOR}
</Text>
Expand All @@ -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}
</PageNumber>
);
})}
<NextButton disabled={currentPage === totalPages} onClick={onNext} ariaLabel={nextAriaLabel} label={nextLabel} />
<NextButton
disabled={currentPage === totalPages}
onClick={() => {
flushSync(() => {
onNext();
});

scrollToTop();
}}
ariaLabel={nextAriaLabel}
label={nextLabel}
/>
</Flex>
);
}
Expand Down

0 comments on commit c495914

Please sign in to comment.