Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upstream generic pagination component for tables [WD-4258] #1013

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/components/TablePagination/TablePagination.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@import "~vanilla-framework/scss/settings";

.pagination {
align-items: baseline;
display: flex;
margin-top: 1.2rem;

.description {
flex-grow: 1;
}

.back {
margin: 0 $spv--large;

.p-icon--chevron-down {
rotate: 90deg;
}
}

.next {
margin: 0 $spv--large;

.p-icon--chevron-down {
rotate: 270deg;
}
}

.pagination-input {
margin-right: $spv--small;
min-width: 0;
width: 3rem;
}

.pagination-select {
margin-bottom: 0;
margin-left: $spv--x-large;
min-width: 0;
width: 7rem;
}
}
214 changes: 214 additions & 0 deletions src/components/TablePagination/TablePagination.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";

import TablePagination from "./TablePagination";
import MainTable from "../MainTable";

<Meta title="TablePagination" component={TablePagination} />

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.

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```.

### Props

<ArgsTable of={TablePagination} />

### Default

<Canvas>
<Story
name="Default"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
}}
>
{Template.bind({})}
</Story>
</Canvas>

### Custom page limit

<Canvas>
<Story
name="CustomPageLimit"
args={{
data: [{id: "row-1"}, {id: "row-2"}, {id: "row-3"}, {id: "row-4"}, {id: "row-5"}],
pageLimits: [1, 2, 3]
}}
>
{Template.bind({})}
</Story>
</Canvas>

### Custom display title

<Canvas>
<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>
}}
>
{Template.bind({})}
</Story>
</Canvas>

### Render above

<Canvas>
<Story
name="RenderAbove"
>
{() => {
const data = [
{
columns: [
{ content: "Ready", role: "rowheader" },
{ content: 1, className: "u-align--right" },
{ content: "1 GiB", className: "u-align--right" },
{ content: 2, className: "u-align--right" },
{ content: 42, className: "u-align--right" },
],
sortData: {
status: "ready",
cores: 2,
ram: 1,
disks: 2,
},
},
{
columns: [
{ content: "Idle", role: "rowheader" },
{ content: 1, className: "u-align--right" },
{ content: "1 GiB", className: "u-align--right" },
{ content: 2, className: "u-align--right" },
{ content: 23, className: "u-align--right" },
],
sortData: {
status: "idle",
cores: 1,
ram: 1,
disks: 2,
},
},
{
columns: [
{ content: "Waiting", role: "rowheader" },
{ content: 8, className: "u-align--right" },
{ content: "3.9 GiB", className: "u-align--right" },
{ content: 3, className: "u-align--right" },
{ content: 0, className: "u-align--right" },
],
sortData: {
status: "waiting",
cores: 8,
ram: 3.9,
disks: 3,
},
},
];

const headers = [
{ content: "Status", sortKey: "status" },
{ content: "Cores", sortKey: "cores", className: "u-align--right" },
{ content: "RAM", sortKey: "ram", className: "u-align--right" },
{ content: "Disks", sortKey: "disks", className: "u-align--right" },
{ content: "Networks", className: "u-align--right" },
];

return (
<TablePagination data={data} pageLimits={[1, 2, 3]}>
<MainTable
headers={headers}
rows={data}
sortabl
/>
</TablePagination>
);
}}
</Story>
</Canvas>

### Render below

<Canvas>
<Story
name="RenderBelow"
>
{() => {
const data = [
{
columns: [
{ content: "Ready", role: "rowheader" },
{ content: 1, className: "u-align--right" },
{ content: "1 GiB", className: "u-align--right" },
{ content: 2, className: "u-align--right" },
{ content: 42, className: "u-align--right" },
],
sortData: {
status: "ready",
cores: 2,
ram: 1,
disks: 2,
},
},
{
columns: [
{ content: "Idle", role: "rowheader" },
{ content: 1, className: "u-align--right" },
{ content: "1 GiB", className: "u-align--right" },
{ content: 2, className: "u-align--right" },
{ content: 23, className: "u-align--right" },
],
sortData: {
status: "idle",
cores: 1,
ram: 1,
disks: 2,
},
},
{
columns: [
{ content: "Waiting", role: "rowheader" },
{ content: 8, className: "u-align--right" },
{ content: "3.9 GiB", className: "u-align--right" },
{ content: 3, className: "u-align--right" },
{ content: 0, className: "u-align--right" },
],
sortData: {
status: "waiting",
cores: 8,
ram: 3.9,
disks: 3,
},
},
];

const headers = [
{ content: "Status", sortKey: "status" },
{ content: "Cores", sortKey: "cores", className: "u-align--right" },
{ content: "RAM", sortKey: "ram", className: "u-align--right" },
{ content: "Disks", sortKey: "disks", className: "u-align--right" },
{ content: "Networks", className: "u-align--right" },
];

return (
<TablePagination data={data} pageLimits={[1, 2, 3]} position="below">
<MainTable
headers={headers}
rows={data}
sortabl
/>
</TablePagination>
);
}}
</Story>
</Canvas>
90 changes: 90 additions & 0 deletions src/components/TablePagination/TablePagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable testing-library/no-node-access */
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import TablePagination from "./TablePagination";
import userEvent from "@testing-library/user-event";

const dummyData = [
{ id: "row-1" },
{ id: "row-2" },
{ id: "row-3" },
{ id: "row-4" },
{ id: "row-5" },
];

describe("<TablePagination />", () => {
// snapshot tests
it("renders table pagination and matches the snapshot", () => {
render(<TablePagination data={dummyData} />);

expect(screen.getByRole("navigation")).toMatchSnapshot();
});

// unit tests
it("renders default display title correctly when no pagination takes place", () => {
render(<TablePagination data={dummyData} />);

expect(screen.getByRole("navigation")).toHaveTextContent(
"Showing all 5 items"
);
});

it("renders default display title correctly when pagination takes place", () => {
render(<TablePagination data={dummyData} pageLimits={[1]} />);

expect(screen.getByRole("navigation")).toHaveTextContent(
"Showing 1 out of 5 items"
);
});

it("has correct per page setting when changed", () => {
render(<TablePagination data={dummyData} pageLimits={[2, 5, 10]} />);

expect(screen.getByRole("navigation")).toHaveTextContent("2/page");
userEvent.selectOptions(
screen.getByRole("combobox", { name: "Items per page" }),
"5"
);
expect(screen.getByRole("navigation")).toHaveTextContent("5/page");
});

it("resets to first page when page size is changed", () => {
render(<TablePagination data={dummyData} pageLimits={[2, 5, 10]} />);

expect(screen.getByRole("navigation")).toHaveTextContent("2/page");
userEvent.selectOptions(
screen.getByRole("combobox", { name: "Items per page" }),
"5"
);
const currentPageInput = screen.getByRole("spinbutton", {
name: "Page number",
});
expect(currentPageInput).toHaveValue(1);
edlerd marked this conversation as resolved.
Show resolved Hide resolved
});

it("should paginate correctly in incrementing or decrementing directions", 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" });
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);
expect(currentPageInput).toHaveValue(1);
await userEvent.click(incButton);
expect(currentPageInput).toHaveValue(2);
await userEvent.selectOptions(pageSizeSelector, "2");
expect(currentPageInput).toHaveValue(1);
await fireEvent.change(currentPageInput, { target: { value: 3 } });
expect(currentPageInput).toHaveValue(3);
await userEvent.click(incButton);
expect(currentPageInput).toHaveValue(3);
await userEvent.click(decButton);
expect(currentPageInput).toHaveValue(2);
});
});
Loading
Loading