Skip to content

Commit

Permalink
feat: implement externally controlled mode for TablePagination
Browse files Browse the repository at this point in the history
Signed-off-by: Mason Hu <mason.hu@canonical.com>
  • Loading branch information
mas-who committed Feb 5, 2024
1 parent e9c2743 commit 8fc31de
Show file tree
Hide file tree
Showing 7 changed files with 530 additions and 156 deletions.
80 changes: 68 additions & 12 deletions src/components/TablePagination/TablePagination.stories.mdx
Original file line number Diff line number Diff line change
@@ -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";

<Meta title="TablePagination" component={TablePagination} />
<Meta
title="TablePagination"
component={TablePagination}
argTypes={{
totalItems: {
if: { arg: 'externallyControlled', truthy: true },
},
totalItems: {
if: { arg: 'externallyControlled', truthy: true },
},
currentPage: {
if: { arg: 'externallyControlled', truthy: true },
},
pageSize: {
if: { arg: 'externallyControlled', truthy: true },
},
onPageChange: {
if: { arg: 'externallyControlled', truthy: true },
},
onPageSizeChange: {
if: { arg: 'externallyControlled', truthy: true },
},
}}
/>

export const Template = (args) => <TablePagination {...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 <b>direct</b> 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 <b>direct</b> 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

Expand All @@ -26,7 +62,13 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="Default"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
}}
>
{Template.bind({})}
Expand All @@ -39,8 +81,14 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="CustomPageLimit"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
pageLimits: [1, 2, 3]
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
pageLimits: [1, 2, 3],
}}
>
{Template.bind({})}
Expand All @@ -53,8 +101,14 @@ This component will then pass the paged data to all <b>direct</b> child componen
<Story
name="CustomDisplayTitle"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
description: <b>Hello there</b>
data: [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
],
description: <b>Hello there</b>,
}}
>
{Template.bind({})}
Expand Down Expand Up @@ -134,6 +188,7 @@ This component will then pass the paged data to all <b>direct</b> child componen
</TablePagination>
);
}}

</Story>
</Canvas>

Expand Down Expand Up @@ -210,5 +265,6 @@ This component will then pass the paged data to all <b>direct</b> child componen
</TablePagination>
);
}}

</Story>
</Canvas>
148 changes: 147 additions & 1 deletion src/components/TablePagination/TablePagination.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("<TablePagination />", () => {
expect(currentPageInput).toHaveValue(1);
});

it("should paginate correctly in incrementing or decrementing directions", async () => {
it("should paginate correctly in locally controlled mode", async () => {
render(<TablePagination data={dummyData} pageLimits={[1, 2, 5]} />);
const incButton = screen.getByRole("button", { name: "Next page" });
const decButton = screen.getByRole("button", { name: "Previous page" });
Expand All @@ -87,4 +87,150 @@ describe("<TablePagination />", () => {
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(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);

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(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
expect(currentPageInput).toHaveValue(1);

await userEvent.click(incButton);
rerender(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
expect(currentPageInput).toHaveValue(2);

await userEvent.selectOptions(pageSizeSelector, "20");
rerender(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
expect(currentPageInput).toHaveValue(1);

fireEvent.change(currentPageInput, { target: { value: 5 } });
rerender(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
expect(currentPageInput).toHaveValue(5);

await userEvent.click(incButton);
rerender(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
expect(currentPageInput).toHaveValue(5);

await userEvent.click(decButton);
rerender(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={totalItems}
currentPage={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
/>
);
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(
<TablePagination
data={dummyData}
pageLimits={[10, 20, 50]}
externallyControlled
totalItems={100}
currentPage={1}
pageSize={5}
onPageChange={jest.fn()}
onPageSizeChange={jest.fn()}
/>
)
).toThrow(
"pageSize must be a valid option in pageLimits, pageLimits is set to [10,20,50]"
);
});
});
Loading

0 comments on commit 8fc31de

Please sign in to comment.