diff --git a/src/components/TablePagination/TablePagination.stories.mdx b/src/components/TablePagination/TablePagination.stories.mdx
index 6679f2546..ed6f13b13 100644
--- a/src/components/TablePagination/TablePagination.stories.mdx
+++ b/src/components/TablePagination/TablePagination.stories.mdx
@@ -1,20 +1,56 @@
-import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/blocks";
import TablePagination from "./TablePagination";
import MainTable from "../MainTable";
-
+
export const Template = (args) => ;
### TablePagination
-This is an HOC [React](https://reactjs.org/) component for applying pagination to input data for direct child components.
-This component is un-opinionated about the structure of the input data and can be used with any child component that displays
-a list of data. However, the styling and behaviour of this component were designed to work nicely with the ```MainTable``` component.
+This is an HOC [React](https://reactjs.org/) component for applying pagination to direct children components. This component is un-opinionated about
+the structure of the input data and can be used with any child component that displays a list of data. However, the styling and behaviour of this component were designed
+to work nicely with the `MainTable` component. To use this component, simply wrap a child component with it and provide the data that you want
+to paginate to the `data` prop. This component will then pass the paged data to all direct child components via a child prop specified by `dataForwardProp`.
+The component may be externally controlled, see following sections for detailed explanation.
-To use this component, simply wrap a child component with it and provide the data that you want to paginate to the ```data``` prop.
-This component will then pass the paged data to all direct child components via a child prop specified by ```dataForwardProp```.
+#### Externally controlled pagination
+
+For externally controlled mode, you will be responsible for the pagination logic and therefore the component will be purely presentational.
+The pagination behaviour is controlled outside of this component. Note the data injection to child components is essentially a passthrough in this case.
+To enable externally controlled mode for this component, set the `externallyControlled` prop to `true`. From there, it is your responsibility
+to ensure that the following props `totalItems`, `currentPage`, `pageSize`, `onPageChange` and `onPageSizeChange` are set properly.
+You can refer to the props table below on how to set these props.
+
+#### Un-controlled pagination
+
+In this mode, the component assumes that the input data is not paginated. The component will implement the pagination logic and apply it to the input data
+then inject the paged data into direct child components. This is the default mode of operations for the component where `externallyControlled` prop is set
+to `false`.
### Props
@@ -26,7 +62,13 @@ This component will then pass the paged data to all direct child componen
{Template.bind({})}
@@ -39,8 +81,14 @@ This component will then pass the paged data to all direct child componen
{Template.bind({})}
@@ -53,8 +101,14 @@ This component will then pass the paged data to all direct child componen
Hello there
+ data: [
+ { id: "row-1" },
+ { id: "row-2" },
+ { id: "row-3" },
+ { id: "row-4" },
+ { id: "row-5" },
+ ],
+ description: Hello there,
}}
>
{Template.bind({})}
@@ -134,6 +188,7 @@ This component will then pass the paged data to all direct child componen
);
}}
+
@@ -210,5 +265,6 @@ This component will then pass the paged data to all direct child componen
);
}}
+
diff --git a/src/components/TablePagination/TablePagination.test.tsx b/src/components/TablePagination/TablePagination.test.tsx
index 225b5faf7..22fadff79 100644
--- a/src/components/TablePagination/TablePagination.test.tsx
+++ b/src/components/TablePagination/TablePagination.test.tsx
@@ -62,7 +62,7 @@ describe("", () => {
expect(currentPageInput).toHaveValue(1);
});
- it("should paginate correctly in incrementing or decrementing directions", async () => {
+ it("should paginate correctly in locally controlled mode", async () => {
render();
const incButton = screen.getByRole("button", { name: "Next page" });
const decButton = screen.getByRole("button", { name: "Previous page" });
@@ -87,4 +87,150 @@ describe("", () => {
await userEvent.click(decButton);
expect(currentPageInput).toHaveValue(2);
});
+
+ it("should paginate correctly in externally controlled mode", async () => {
+ const totalItems = 100;
+ let currentPage = 1;
+ let pageSize = 10;
+ const handlePageChange = (page: number) => {
+ currentPage = page;
+ };
+ const handlePageSizeChange = (size: number) => {
+ currentPage = 1;
+ pageSize = size;
+ };
+ const { rerender } = render(
+
+ );
+
+ const incButton = screen.getByRole("button", { name: "Next page" });
+ const decButton = screen.getByRole("button", { name: "Previous page" });
+ const currentPageInput = screen.getByRole("spinbutton", {
+ name: "Page number",
+ });
+ const pageSizeSelector = screen.getByRole("combobox", {
+ name: "Items per page",
+ });
+
+ expect(currentPageInput).toHaveValue(1);
+ await userEvent.click(decButton);
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(1);
+
+ await userEvent.click(incButton);
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(2);
+
+ await userEvent.selectOptions(pageSizeSelector, "20");
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(1);
+
+ fireEvent.change(currentPageInput, { target: { value: 5 } });
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(5);
+
+ await userEvent.click(incButton);
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(5);
+
+ await userEvent.click(decButton);
+ rerender(
+
+ );
+ expect(currentPageInput).toHaveValue(4);
+ });
+
+ it("should throw an error if pageSize is not in pageLimits when externally controlled", () => {
+ // Don't print out massive error logs for this test
+ console.error = () => "";
+ expect(() =>
+ render(
+
+ )
+ ).toThrow(
+ "pageSize must be a valid option in pageLimits, pageLimits is set to [10,20,50]"
+ );
+ });
});
diff --git a/src/components/TablePagination/TablePagination.tsx b/src/components/TablePagination/TablePagination.tsx
index c6ec1f35d..e9de99ab3 100644
--- a/src/components/TablePagination/TablePagination.tsx
+++ b/src/components/TablePagination/TablePagination.tsx
@@ -1,59 +1,19 @@
import React, {
- ChangeEvent,
- Children,
HTMLAttributes,
PropsWithChildren,
- ReactElement,
ReactNode,
- RefObject,
- cloneElement,
- useEffect,
- useRef,
useState,
} from "react";
-import classnames from "classnames";
-import { usePagination } from "hooks";
-import Select from "components/Select";
-import TablePaginationControls from "./TablePaginationControls";
import "./TablePagination.scss";
+import TablePaginationControls from "./TablePaginationControls";
+import {
+ DEFAULT_PAGE_LIMITS,
+ generatePagingOptions,
+ renderChildren,
+} from "./utils";
+import { usePagination } from "hooks";
-/**
- * Determine if we are working with a small screen.
- * 'small screen' in this case is relative to the width of the description div
- */
-const figureSmallScreen = (descriptionRef: RefObject) => {
- const descriptionElement = descriptionRef.current;
- if (!descriptionElement) {
- return true;
- }
- return descriptionElement.getBoundingClientRect().width < 230;
-};
-
-/**
- * Iterate direct react child components and override the value of the prop specified by @param dataForwardProp
- * for those child components.
- * @param children - react node children to iterate
- * @param dataForwardProp - the name of the prop from the children components to override
- * @param data - actual data to be passed to the prop specified by @param dataForwardProp
- */
-const renderChildren = (
- children: ReactNode,
- dataForwardProp: string,
- data: unknown[]
-) => {
- return Children.map(children, (child) => {
- return cloneElement(child as ReactElement, {
- [dataForwardProp]: data,
- });
- });
-};
-
-const DEFAULT_PAGE_LIMITS = [50, 100, 200];
-const generatePagingOptions = (pageLimits: number[]) => {
- return pageLimits.map((limit) => ({ value: limit, label: `${limit}/page` }));
-};
-
-export type Props = PropsWithChildren<{
+export type BasePaginationProps = {
/**
* list of data elements to be paginated. This component is un-opinionated about
* the structure of the data but it should be identical to the data structure
@@ -62,7 +22,7 @@ export type Props = PropsWithChildren<{
data: unknown[];
/**
* prop name of the child table component that receives paginated data.
- * default value is set to @constant rows, which is the data prop for the @func MainTable component
+ * default value is set to `rows`, which is the data prop for the `MainTable` component
*/
dataForwardProp?: string;
/**
@@ -85,93 +45,145 @@ export type Props = PropsWithChildren<{
* place the pagination component above or below the table?
*/
position?: "above" | "below";
-}> &
+};
+
+export type ExternalControlProps = BasePaginationProps & {
+ /**
+ * Whether the component will be controlled via external state.
+ */
+ externallyControlled?: true;
+ /**
+ * the total number of items available within the data. This prop is only relevant
+ * and will be required if `externallyControlled` is set to `true`.
+ */
+ totalItems: number;
+ /**
+ * the current page that's showing. This prop is only relevant and will be required
+ * if `externallyControlled` is set to `true`.
+ */
+ currentPage: number;
+ /**
+ * size per page. This prop is only relevant and will be required if
+ * `externallyControlled` is set to `true`.
+ */
+ pageSize: number;
+ /**
+ * callback indicating a page change event to the parent component.
+ * This prop is only relevant and will be required if `externallyControlled` is set
+ * to `true`.
+ */
+ onPageChange: (page: number) => void;
+ /**
+ * callback indicating a page size change event to the parent component.
+ * This prop is only relevant and will be required if `externallyControlled` is set
+ * to `true`.
+ */
+ onPageSizeChange: (pageSize: number) => void;
+};
+
+export type InternalControlProps = BasePaginationProps & {
+ /**
+ * Whether the component will be controlled via external state.
+ */
+ externallyControlled?: false;
+};
+
+export type Props = PropsWithChildren<
+ BasePaginationProps & (ExternalControlProps | InternalControlProps)
+> &
HTMLAttributes;
-const TablePagination = ({
- data,
- className,
- itemName = "item",
- description,
- position = "above",
- dataForwardProp = "rows",
- pageLimits = DEFAULT_PAGE_LIMITS,
- children,
- ...divProps
-}: Props) => {
- const descriptionRef = useRef(null);
- const [isSmallScreen, setSmallScreen] = useState(false);
- const [pageSize, setPageSize] = useState(() => {
+const TablePagination = (props: Props) => {
+ const {
+ data,
+ dataForwardProp = "rows",
+ itemName = "item",
+ className,
+ description,
+ pageLimits = DEFAULT_PAGE_LIMITS,
+ position = "above",
+ externallyControlled,
+ children,
+ ...divProps
+ } = props;
+
+ // Safety check to ensure pageSize is a valid option in
+ // pageLimits if the component is externally controlled
+ if (externallyControlled) {
+ let pageSizeFound = false;
+ for (const limit of pageLimits) {
+ if (limit === Number(props.pageSize)) {
+ pageSizeFound = true;
+ break;
+ }
+ }
+
+ if (!pageSizeFound) {
+ throw new Error(
+ `pageSize must be a valid option in pageLimits, pageLimits is set to [${pageLimits}]`
+ );
+ }
+ }
+
+ const [internalPageSize, setInternalPageSize] = useState(() => {
return generatePagingOptions(pageLimits)[0].value;
});
- const { paginate, currentPage, pageData, totalItems } = usePagination(data, {
- itemsPerPage: pageSize,
+ const {
+ paginate,
+ currentPage: internalCurrentPage,
+ pageData: internalData,
+ } = usePagination(externallyControlled ? [] : data, {
+ itemsPerPage: internalPageSize,
autoResetPage: true,
});
- useEffect(() => {
- const handleResize = () => {
- setSmallScreen(figureSmallScreen(descriptionRef));
- };
- window.addEventListener("resize", handleResize);
- return () => {
- window.removeEventListener("resize", handleResize);
- };
- }, [isSmallScreen]);
+ const controlData = externallyControlled ? data : internalData;
+ const controlPageSize = externallyControlled
+ ? props.pageSize
+ : internalPageSize;
+ const controlTotalItems = externallyControlled
+ ? props.totalItems
+ : data.length;
+ const controlCurrentPage = externallyControlled
+ ? props.currentPage
+ : internalCurrentPage;
- const handlePageSizeChange = (e: ChangeEvent) => {
- paginate(1);
- setPageSize(parseInt(e.target.value));
- };
-
- const getDescription = () => {
- if (description) {
- return description;
+ const handlePageChange = (page: number) => {
+ if (externallyControlled) {
+ props.onPageChange(page);
+ return;
}
- const visibleCount = pageData.length;
-
- if (isSmallScreen) {
- return `${visibleCount} out of ${totalItems}`;
- }
+ paginate(page);
+ };
- if (visibleCount === totalItems && visibleCount > 1) {
- return `Showing all ${totalItems} ${itemName}s`;
+ const handlePageSizeChange = (pageSize: number) => {
+ if (externallyControlled) {
+ props.onPageSizeChange(pageSize);
+ return;
}
- return `Showing ${visibleCount} out of ${totalItems} ${itemName}${
- totalItems !== 1 ? "s" : ""
- }`;
+ paginate(1);
+ setInternalPageSize(pageSize);
};
- const totalPages = Math.ceil(data.length / pageSize);
- const clonedChildren = renderChildren(children, dataForwardProp, pageData);
+ const clonedChildren = renderChildren(children, dataForwardProp, controlData);
return (
<>
{position === "below" && clonedChildren}
-