@purpurds/pagination 5.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LICENSE.txt +73 -0
- package/dist/navigation.d.ts +2 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/pagination-page-selector.d.ts +17 -0
- package/dist/pagination-page-selector.d.ts.map +1 -0
- package/dist/pagination-page-size-selector.d.ts +13 -0
- package/dist/pagination-page-size-selector.d.ts.map +1 -0
- package/dist/pagination-page-trigger.d.ts +14 -0
- package/dist/pagination-page-trigger.d.ts.map +1 -0
- package/dist/pagination-pages.d.ts +13 -0
- package/dist/pagination-pages.d.ts.map +1 -0
- package/dist/pagination-step-trigger.d.ts +14 -0
- package/dist/pagination-step-trigger.d.ts.map +1 -0
- package/dist/pagination-truncation-separator.d.ts +11 -0
- package/dist/pagination-truncation-separator.d.ts.map +1 -0
- package/dist/pagination.cjs.js +134 -0
- package/dist/pagination.cjs.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.es.js +4041 -0
- package/dist/pagination.es.js.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-page-options.hook.d.ts +8 -0
- package/dist/use-page-options.hook.d.ts.map +1 -0
- package/dist/use-page-size-options.hook.d.ts +8 -0
- package/dist/use-page-size-options.hook.d.ts.map +1 -0
- package/dist/use-pagination-page-selector-events.hook.d.ts +10 -0
- package/dist/use-pagination-page-selector-events.hook.d.ts.map +1 -0
- package/dist/use-pagination-pages.hook.d.ts +7 -0
- package/dist/use-pagination-pages.hook.d.ts.map +1 -0
- package/package.json +69 -0
- package/src/global.d.ts +4 -0
- package/src/navigation.ts +3 -0
- package/src/pagination-page-selector.module.scss +26 -0
- package/src/pagination-page-selector.test.tsx +78 -0
- package/src/pagination-page-selector.tsx +105 -0
- package/src/pagination-page-size-selector.module.scss +34 -0
- package/src/pagination-page-size-selector.test.tsx +73 -0
- package/src/pagination-page-size-selector.tsx +78 -0
- package/src/pagination-page-trigger.module.scss +41 -0
- package/src/pagination-page-trigger.test.tsx +172 -0
- package/src/pagination-page-trigger.tsx +93 -0
- package/src/pagination-pages.module.scss +8 -0
- package/src/pagination-pages.test.tsx +95 -0
- package/src/pagination-pages.tsx +72 -0
- package/src/pagination-step-trigger.module.scss +27 -0
- package/src/pagination-step-trigger.test.tsx +106 -0
- package/src/pagination-step-trigger.tsx +92 -0
- package/src/pagination-truncation-separator.module.scss +8 -0
- package/src/pagination-truncation-separator.test.tsx +25 -0
- package/src/pagination-truncation-separator.tsx +29 -0
- package/src/pagination.module.scss +105 -0
- package/src/pagination.stories.tsx +197 -0
- package/src/pagination.test.tsx +76 -0
- package/src/pagination.tsx +177 -0
- package/src/types.ts +43 -0
- package/src/use-page-options.hook.ts +21 -0
- package/src/use-page-size-options.hook.ts +21 -0
- package/src/use-pagination-page-selector-events.hook.ts +59 -0
- package/src/use-pagination-pages.hook.ts +41 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as matchers from "@testing-library/jest-dom/matchers";
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { PaginationTruncationSeparator } from "./pagination-truncation-separator";
|
|
7
|
+
|
|
8
|
+
expect.extend(matchers);
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("PaginationTruncationSeparator", () => {
|
|
16
|
+
it("renders without crashing", () => {
|
|
17
|
+
render(<PaginationTruncationSeparator />);
|
|
18
|
+
expect(screen.getByTestId(Selectors.PAGINATION_TRUNCATION_SEPARATOR)).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByTestId(Selectors.PAGINATION_TRUNCATION_SEPARATOR)).toHaveTextContent("...");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const Selectors = {
|
|
24
|
+
PAGINATION_TRUNCATION_SEPARATOR: "purpur-pagination-truncation-separator",
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Paragraph } from "@purpurds/paragraph";
|
|
3
|
+
import c from "classnames/bind";
|
|
4
|
+
|
|
5
|
+
import styles from "./pagination-truncation-separator.module.scss";
|
|
6
|
+
const cx = c.bind(styles);
|
|
7
|
+
|
|
8
|
+
export type PaginationTruncationSeparatorProps = {
|
|
9
|
+
["data-testid"]?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const rootClassName = "purpur-pagination-truncation-separator";
|
|
14
|
+
|
|
15
|
+
export const PaginationTruncationSeparator = ({
|
|
16
|
+
["data-testid"]: dataTestId = "purpur-pagination-truncation-separator",
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: PaginationTruncationSeparatorProps) => {
|
|
20
|
+
const classes = cx([className, rootClassName]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<li className={classes} data-testid={dataTestId} {...props}>
|
|
24
|
+
<Paragraph>...</Paragraph>
|
|
25
|
+
</li>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
PaginationTruncationSeparator.displayName = "PaginationTruncationSeparator";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
@import "@purpurds/tokens/breakpoint/variables";
|
|
2
|
+
|
|
3
|
+
.purpur-pagination {
|
|
4
|
+
$root: &;
|
|
5
|
+
align-items: center;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column-reverse;
|
|
8
|
+
gap: var(--purpur-spacing-300);
|
|
9
|
+
|
|
10
|
+
@media screen and (min-width: $purpur-breakpoint-lg) {
|
|
11
|
+
align-items: center;
|
|
12
|
+
display: grid;
|
|
13
|
+
gap: var(--purpur-spacing-100);
|
|
14
|
+
grid-template-areas: "pagesize pagination empty";
|
|
15
|
+
grid-template-columns: 1fr 3fr 1fr;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&__page-size-select-container {
|
|
19
|
+
flex: 0 0 auto;
|
|
20
|
+
width: 100%;
|
|
21
|
+
|
|
22
|
+
@media screen and (min-width: $purpur-breakpoint-lg) {
|
|
23
|
+
grid-area: pagesize;
|
|
24
|
+
width: auto;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&__pagination-container {
|
|
29
|
+
align-items: center;
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
gap: var(--purpur-spacing-200);
|
|
32
|
+
margin: 0 auto;
|
|
33
|
+
max-width: calc(3 * var(--purpur-spacing-1200));
|
|
34
|
+
width: 100%;
|
|
35
|
+
|
|
36
|
+
@media screen and (min-width: $purpur-breakpoint-md) {
|
|
37
|
+
gap: var(--purpur-spacing-400);
|
|
38
|
+
max-width: none;
|
|
39
|
+
width: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media screen and (min-width: $purpur-breakpoint-lg) {
|
|
43
|
+
grid-area: pagination;
|
|
44
|
+
justify-self: center;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&__step-trigger-label {
|
|
49
|
+
border: 0;
|
|
50
|
+
clip: rect(0, 0, 0, 0);
|
|
51
|
+
display: block;
|
|
52
|
+
height: 1px;
|
|
53
|
+
margin: -1px;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
padding: 0;
|
|
56
|
+
position: absolute;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
width: 1px;
|
|
59
|
+
|
|
60
|
+
@media screen and (min-width: $purpur-breakpoint-lg) {
|
|
61
|
+
clip: auto;
|
|
62
|
+
display: inline;
|
|
63
|
+
height: auto;
|
|
64
|
+
margin: 0;
|
|
65
|
+
overflow: visible;
|
|
66
|
+
position: static;
|
|
67
|
+
white-space: normal;
|
|
68
|
+
width: auto;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__step-trigger {
|
|
73
|
+
flex: 0 0 auto;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&__page-trigger-container {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex: 1 1 auto;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#{$root}__pages {
|
|
83
|
+
display: none;
|
|
84
|
+
|
|
85
|
+
&--visible {
|
|
86
|
+
display: flex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@media screen and (min-width: $purpur-breakpoint-md) {
|
|
90
|
+
display: flex;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#{$root}__page-selector {
|
|
95
|
+
display: none;
|
|
96
|
+
|
|
97
|
+
&--visible {
|
|
98
|
+
display: flex;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media screen and (min-width: $purpur-breakpoint-md) {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React, { forwardRef } from "react";
|
|
2
|
+
import { Skeleton } from "@purpurds/skeleton";
|
|
3
|
+
import { useArgs } from "@storybook/preview-api";
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
import "@purpurds/autocomplete/styles";
|
|
7
|
+
import "@purpurds/icon/styles";
|
|
8
|
+
import "@purpurds/label/styles";
|
|
9
|
+
import "@purpurds/listbox/styles";
|
|
10
|
+
import "@purpurds/paragraph/styles";
|
|
11
|
+
import "@purpurds/select/styles";
|
|
12
|
+
import "@purpurds/skeleton/styles";
|
|
13
|
+
import "@purpurds/text-field/styles";
|
|
14
|
+
import { Pagination, PaginationProps } from "./pagination";
|
|
15
|
+
import { PaginationInformation } from "./types";
|
|
16
|
+
|
|
17
|
+
type CustomTestLinkProps = {
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
href?: string;
|
|
20
|
+
target?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const CustomTestLink = forwardRef(({ children, href, target, ...props }: CustomTestLinkProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<a href={href} target={target} {...props}>
|
|
26
|
+
{children}
|
|
27
|
+
</a>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const meta = {
|
|
32
|
+
title: "Components/Pagination",
|
|
33
|
+
component: Pagination,
|
|
34
|
+
argTypes: {
|
|
35
|
+
asLink: {
|
|
36
|
+
table: {
|
|
37
|
+
type: { summary: "Whether or not the pages should be rendered as links or buttons" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
hrefGetter: {
|
|
41
|
+
table: {
|
|
42
|
+
type: {
|
|
43
|
+
summary: `hrefGetter: (hrefInformation: HrefInformation) => string;
|
|
44
|
+
- Function that returns a url for a step when asLink is true.
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
navigationFunction: {
|
|
50
|
+
table: {
|
|
51
|
+
type: {
|
|
52
|
+
summary: `(navigationInformation: NavigationInformation) => void
|
|
53
|
+
- Optional function to be used when navigating from javascript.
|
|
54
|
+
Defaults to a function that changes window.location.href.
|
|
55
|
+
Will only be called in link mode from the page select dropdown or the page size select.`,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
anchorTagElement: {
|
|
60
|
+
table: {
|
|
61
|
+
type: {
|
|
62
|
+
summary:
|
|
63
|
+
"A React.ForwardRefExoticComponent<React.AnchorHTMLAttributes<HTMLAnchorElement>> that will be used as the link if asLink is true.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
onPageChange: {
|
|
68
|
+
table: {
|
|
69
|
+
type: {
|
|
70
|
+
summary: `onPageChange: (paginationChangeInformation: PaginationInformation) => void
|
|
71
|
+
- Function that will be called when the page changes. Mandatory for keeping up with the currentPage in button mode.
|
|
72
|
+
Optional when asLink is true, can be used to run some code on page change.`,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
parameters: {
|
|
78
|
+
design: [
|
|
79
|
+
{
|
|
80
|
+
name: "Pagination",
|
|
81
|
+
type: "figma",
|
|
82
|
+
url: "https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=33818-15953",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
} satisfies Meta<typeof Pagination>;
|
|
87
|
+
|
|
88
|
+
export default meta;
|
|
89
|
+
type Story = StoryObj<typeof Pagination>;
|
|
90
|
+
|
|
91
|
+
export const Showcase: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
className: "myClassName",
|
|
94
|
+
currentPage: 1,
|
|
95
|
+
pageSize: 10,
|
|
96
|
+
pageSizeLabel: "Items per page",
|
|
97
|
+
nextButtonText: "Next",
|
|
98
|
+
outOfLabel: "of",
|
|
99
|
+
previousButtonText: "Previous",
|
|
100
|
+
pageSelectorAutocompleteId: "pageSelectorAutocompleteId",
|
|
101
|
+
pageSelectorListBoxLabel: "Select a page",
|
|
102
|
+
pageSelectorNoOptionsText: "Page does not exist",
|
|
103
|
+
totalItems: 145,
|
|
104
|
+
},
|
|
105
|
+
render: ({ ...args }) => {
|
|
106
|
+
const [{ currentPage }, updateArgs]: [
|
|
107
|
+
PaginationProps,
|
|
108
|
+
(newArgs: Partial<PaginationProps>) => void,
|
|
109
|
+
(argNames?: (keyof PaginationProps)[] | undefined) => void
|
|
110
|
+
] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
|
|
111
|
+
const onPageChange = ({ currentPage }: PaginationInformation) => {
|
|
112
|
+
updateArgs({ currentPage });
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div>
|
|
117
|
+
<Skeleton style={{ height: "25rem", width: "100%", marginBottom: "2rem" }} />
|
|
118
|
+
<Pagination {...args} currentPage={currentPage} onPageChange={onPageChange} />
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const AsLinks: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
asLink: true,
|
|
127
|
+
className: "myClassName",
|
|
128
|
+
currentPage: 1,
|
|
129
|
+
pageSize: 10,
|
|
130
|
+
pageSizeLabel: "Items per page",
|
|
131
|
+
hrefGetter: ({ page, pageSize }) =>
|
|
132
|
+
`?path=/story/components-pagination--as-links&args=currentPage:${page};pageSize:${pageSize}`,
|
|
133
|
+
nextButtonText: "Next",
|
|
134
|
+
outOfLabel: "of",
|
|
135
|
+
previousButtonText: "Previous",
|
|
136
|
+
pageSelectorListBoxLabel: "Select a page",
|
|
137
|
+
pageSelectorNoOptionsText: "Page does not exist",
|
|
138
|
+
totalItems: 145,
|
|
139
|
+
},
|
|
140
|
+
render: ({ ...args }) => {
|
|
141
|
+
const [{ currentPage }, updateArgs]: [
|
|
142
|
+
PaginationProps,
|
|
143
|
+
(newArgs: Partial<PaginationProps>) => void,
|
|
144
|
+
(argNames?: (keyof PaginationProps)[] | undefined) => void
|
|
145
|
+
] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
|
|
146
|
+
const onPageChange = ({ currentPage }: PaginationInformation) => {
|
|
147
|
+
updateArgs({ currentPage });
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<Skeleton style={{ height: "25rem", width: "100%", marginBottom: "2rem" }} />
|
|
153
|
+
<Pagination {...args} currentPage={currentPage} onPageChange={onPageChange} />
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const AsCustomLinks: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
anchorTagElement: CustomTestLink,
|
|
162
|
+
asLink: true,
|
|
163
|
+
className: "myClassName",
|
|
164
|
+
currentPage: 5,
|
|
165
|
+
hrefGetter: ({ page, pageSize }) =>
|
|
166
|
+
`?path=/story/components-pagination--as-custom-links&args=currentPage:${page};pageSize:${pageSize}`,
|
|
167
|
+
navigationFunction: function ({ currentPage, pageSize, url }) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.log(currentPage, pageSize, url);
|
|
170
|
+
},
|
|
171
|
+
nextButtonText: "Next",
|
|
172
|
+
outOfLabel: "of",
|
|
173
|
+
previousButtonText: "Previous",
|
|
174
|
+
pageSize: 10,
|
|
175
|
+
pageSizeLabel: "Items per page",
|
|
176
|
+
pageSelectorListBoxLabel: "Select a page",
|
|
177
|
+
pageSelectorNoOptionsText: "Page does not exist",
|
|
178
|
+
totalItems: 145,
|
|
179
|
+
},
|
|
180
|
+
render: ({ ...args }) => {
|
|
181
|
+
const [{ currentPage }, updateArgs]: [
|
|
182
|
+
PaginationProps,
|
|
183
|
+
(newArgs: Partial<PaginationProps>) => void,
|
|
184
|
+
(argNames?: (keyof PaginationProps)[] | undefined) => void
|
|
185
|
+
] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
|
|
186
|
+
const onPageChange = ({ currentPage }: PaginationInformation) => {
|
|
187
|
+
updateArgs({ currentPage });
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div>
|
|
192
|
+
<Skeleton style={{ height: "25rem", width: "100%", marginBottom: "2rem" }} />
|
|
193
|
+
<Pagination {...args} currentPage={currentPage} onPageChange={onPageChange} />
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import * as matchers from "@testing-library/jest-dom/matchers";
|
|
3
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { Pagination } from "./pagination";
|
|
7
|
+
|
|
8
|
+
expect.extend(matchers);
|
|
9
|
+
|
|
10
|
+
const defaultArgs = {
|
|
11
|
+
currentPage: 1,
|
|
12
|
+
pageSize: 10,
|
|
13
|
+
pageSizeLabel: "Items per page",
|
|
14
|
+
nextButtonText: "Next",
|
|
15
|
+
outOfLabel: "of",
|
|
16
|
+
previousButtonText: "Previous",
|
|
17
|
+
pageSelectorListBoxLabel: "Select a page",
|
|
18
|
+
pageSelectorNoOptionsText: "Page does not exist",
|
|
19
|
+
totalItems: 145,
|
|
20
|
+
onPageChange: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
cleanup();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("Pagination", () => {
|
|
28
|
+
it("should render", () => {
|
|
29
|
+
render(<Pagination {...defaultArgs} />);
|
|
30
|
+
const pagination = screen.getByTestId(Selectors.PAGINATION);
|
|
31
|
+
expect(pagination).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should have a previous page button", () => {
|
|
35
|
+
render(<Pagination {...defaultArgs} />);
|
|
36
|
+
const previousButton = screen.getByTestId(Selectors.PREVIOUS_BUTTON);
|
|
37
|
+
expect(previousButton).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should have a next page button", () => {
|
|
41
|
+
render(<Pagination {...defaultArgs} />);
|
|
42
|
+
const nextButton = screen.getByTestId(Selectors.NEXT_BUTTON);
|
|
43
|
+
expect(nextButton).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should have a page size selector", () => {
|
|
47
|
+
render(<Pagination {...defaultArgs} />);
|
|
48
|
+
const pageSizeSelector = screen.getByTestId(Selectors.PAGE_SIZE_SELECTOR);
|
|
49
|
+
expect(pageSizeSelector).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should always show regular page buttons if total number of pages is less than or equal to 3", () => {
|
|
53
|
+
render(<Pagination {...defaultArgs} pageSize={75} />);
|
|
54
|
+
const paginationPages = screen.getByTestId(Selectors.PAGINATION_PAGES);
|
|
55
|
+
const paginationPageSelector = screen.getByTestId(Selectors.PAGINATION_PAGE_SELECTOR);
|
|
56
|
+
expect(paginationPages).toHaveClass("purpur-pagination__pages--visible");
|
|
57
|
+
expect(paginationPageSelector).not.toHaveClass("purpur-pagination__page-selector--visible");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should show pagination page selector if total number of pages is greater than 3 (shown on small screen sizes)", () => {
|
|
61
|
+
render(<Pagination {...defaultArgs} pageSize={10} />);
|
|
62
|
+
const paginationPages = screen.getByTestId(Selectors.PAGINATION_PAGES);
|
|
63
|
+
const paginationPageSelector = screen.getByTestId(Selectors.PAGINATION_PAGE_SELECTOR);
|
|
64
|
+
expect(paginationPages).not.toHaveClass("purpur-pagination__pages--visible");
|
|
65
|
+
expect(paginationPageSelector).toHaveClass("purpur-pagination__page-selector--visible");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const Selectors = {
|
|
70
|
+
PAGINATION: "purpur-pagination",
|
|
71
|
+
PREVIOUS_BUTTON: "purpur-pagination-previous-step-trigger",
|
|
72
|
+
NEXT_BUTTON: "purpur-pagination-next-step-trigger",
|
|
73
|
+
PAGE_SIZE_SELECTOR: "purpur-pagination-page-size-selector",
|
|
74
|
+
PAGINATION_PAGES: "purpur-pagination-pages",
|
|
75
|
+
PAGINATION_PAGE_SELECTOR: "purpur-pagination-page-selector",
|
|
76
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React, { ForwardedRef, forwardRef } from "react";
|
|
2
|
+
import { IconArrowLeft, IconArrowRight } from "@purpurds/icon";
|
|
3
|
+
import c from "classnames/bind";
|
|
4
|
+
|
|
5
|
+
import { navigateToPage } from "./navigation";
|
|
6
|
+
import styles from "./pagination.module.scss";
|
|
7
|
+
import { PaginationPageSelector } from "./pagination-page-selector";
|
|
8
|
+
import { PaginationPageSizeSelector } from "./pagination-page-size-selector";
|
|
9
|
+
import { PaginationPages } from "./pagination-pages";
|
|
10
|
+
import { PaginationStepTrigger } from "./pagination-step-trigger";
|
|
11
|
+
import { LinkProps, NoLinkProps, PaginationInformation } from "./types";
|
|
12
|
+
import { usePaginationPages } from "./use-pagination-pages.hook";
|
|
13
|
+
|
|
14
|
+
const cx = c.bind(styles);
|
|
15
|
+
|
|
16
|
+
export type PaginationTexts = {
|
|
17
|
+
nextButtonText: string;
|
|
18
|
+
outOfLabel: string;
|
|
19
|
+
pageSelectorListBoxLabel: string;
|
|
20
|
+
pageSelectorNoOptionsText: string;
|
|
21
|
+
previousButtonText: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type PageSizeProps = {
|
|
25
|
+
availablePageSizes?: number[];
|
|
26
|
+
pageSize?: number;
|
|
27
|
+
pageSizeLabel: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type PaginationProps = {
|
|
31
|
+
["data-testid"]?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
currentPage: number;
|
|
34
|
+
pageSelectorAutocompleteId?: string;
|
|
35
|
+
totalItems: number;
|
|
36
|
+
} & PaginationTexts &
|
|
37
|
+
PageSizeProps &
|
|
38
|
+
(LinkProps | NoLinkProps);
|
|
39
|
+
|
|
40
|
+
const rootClassName = "purpur-pagination";
|
|
41
|
+
|
|
42
|
+
export const Pagination = forwardRef(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
["data-testid"]: dataTestId = "purpur-pagination",
|
|
46
|
+
anchorTagElement = "a",
|
|
47
|
+
asLink,
|
|
48
|
+
availablePageSizes = [10, 25, 50, 75],
|
|
49
|
+
className,
|
|
50
|
+
currentPage = 1,
|
|
51
|
+
hrefGetter,
|
|
52
|
+
navigationFunction,
|
|
53
|
+
nextButtonText,
|
|
54
|
+
onPageChange,
|
|
55
|
+
outOfLabel,
|
|
56
|
+
pageSelectorAutocompleteId,
|
|
57
|
+
pageSelectorListBoxLabel,
|
|
58
|
+
pageSelectorNoOptionsText,
|
|
59
|
+
pageSize = 10,
|
|
60
|
+
pageSizeLabel,
|
|
61
|
+
previousButtonText,
|
|
62
|
+
totalItems,
|
|
63
|
+
...props
|
|
64
|
+
}: PaginationProps,
|
|
65
|
+
ref: ForwardedRef<HTMLDivElement>
|
|
66
|
+
) => {
|
|
67
|
+
const classes = cx([className, rootClassName]);
|
|
68
|
+
const [_pageSize, _setPageSize] = React.useState(pageSize);
|
|
69
|
+
const { pages, numberOfPages } = usePaginationPages(totalItems, _pageSize, currentPage);
|
|
70
|
+
|
|
71
|
+
const _onPageChange = (page: number): void => {
|
|
72
|
+
onPageChange?.({ currentPage: page, pageSize });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const _hrefGetter = (page: number): string => {
|
|
76
|
+
return hrefGetter?.({ page, pageSize }) ?? "";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const _onPageSizeChange = (newPageSize: number): void => {
|
|
80
|
+
_setPageSize(newPageSize);
|
|
81
|
+
onPageChange?.({ currentPage: 1, pageSize: newPageSize });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const _navigationFunction = (page: number, url: string): void => {
|
|
85
|
+
if (navigationFunction) {
|
|
86
|
+
navigationFunction({ currentPage: page, pageSize: _pageSize, url });
|
|
87
|
+
} else {
|
|
88
|
+
navigateToPage(url);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const linkProps = asLink
|
|
93
|
+
? { asLink: true as const, hrefGetter: _hrefGetter }
|
|
94
|
+
: {
|
|
95
|
+
asLink: undefined as never,
|
|
96
|
+
hrefGetter: undefined as never,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const ContainerTag = asLink ? "nav" : "div";
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<ContainerTag className={classes} data-testid={dataTestId} ref={ref} {...props}>
|
|
103
|
+
<div className={cx(`${rootClassName}__page-size-select-container`)}>
|
|
104
|
+
<PaginationPageSizeSelector
|
|
105
|
+
{...(asLink
|
|
106
|
+
? { asLink: true, hrefGetter, navigationFunction }
|
|
107
|
+
: { asLink: undefined as never, hrefGetter: undefined as never })}
|
|
108
|
+
availablePageSizes={availablePageSizes}
|
|
109
|
+
className={cx(`${rootClassName}__page-size-selector`)}
|
|
110
|
+
data-testid={`${dataTestId}-page-size-selector`}
|
|
111
|
+
onPageSizeChange={_onPageSizeChange}
|
|
112
|
+
pageSize={_pageSize}
|
|
113
|
+
pageSizeLabel={pageSizeLabel}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
<div className={cx(`${rootClassName}__pagination-container`)}>
|
|
117
|
+
<PaginationStepTrigger
|
|
118
|
+
{...linkProps}
|
|
119
|
+
anchorTagElement={anchorTagElement}
|
|
120
|
+
className={cx(`${rootClassName}__step-trigger`)}
|
|
121
|
+
data-testid={`${dataTestId}-previous-step-trigger`}
|
|
122
|
+
disabled={currentPage === 1}
|
|
123
|
+
onPageChange={_onPageChange}
|
|
124
|
+
pageToNavigateTo={currentPage - 1}
|
|
125
|
+
>
|
|
126
|
+
<IconArrowLeft size="sm" />
|
|
127
|
+
<span className={cx(`${rootClassName}__step-trigger-label`)}>{previousButtonText}</span>
|
|
128
|
+
</PaginationStepTrigger>
|
|
129
|
+
<div className={cx(`${rootClassName}__page-trigger-container`)}>
|
|
130
|
+
<PaginationPageSelector
|
|
131
|
+
{...linkProps}
|
|
132
|
+
className={cx([
|
|
133
|
+
`${rootClassName}__page-selector`,
|
|
134
|
+
{ [`${rootClassName}__page-selector--visible`]: pages.length > 3 },
|
|
135
|
+
])}
|
|
136
|
+
currentPage={currentPage}
|
|
137
|
+
data-testid={`${dataTestId}-page-selector`}
|
|
138
|
+
navigationFunction={_navigationFunction}
|
|
139
|
+
numberOfPages={numberOfPages}
|
|
140
|
+
onPageChange={_onPageChange}
|
|
141
|
+
outOfLabel={outOfLabel}
|
|
142
|
+
pageSelectorAutocompleteId={pageSelectorAutocompleteId}
|
|
143
|
+
pageSelectorListBoxLabel={pageSelectorListBoxLabel}
|
|
144
|
+
pageSelectorNoOptionsText={pageSelectorNoOptionsText}
|
|
145
|
+
/>
|
|
146
|
+
<PaginationPages
|
|
147
|
+
{...linkProps}
|
|
148
|
+
anchorTagElement={anchorTagElement}
|
|
149
|
+
className={cx([
|
|
150
|
+
`${rootClassName}__pages`,
|
|
151
|
+
{ [`${rootClassName}__pages--visible`]: pages.length <= 3 },
|
|
152
|
+
])}
|
|
153
|
+
currentPage={currentPage}
|
|
154
|
+
data-testid={`${dataTestId}-pages`}
|
|
155
|
+
onPageChange={_onPageChange}
|
|
156
|
+
pages={pages}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<PaginationStepTrigger
|
|
160
|
+
{...linkProps}
|
|
161
|
+
anchorTagElement={anchorTagElement}
|
|
162
|
+
className={cx(`${rootClassName}__step-trigger`)}
|
|
163
|
+
data-testid={`${dataTestId}-next-step-trigger`}
|
|
164
|
+
disabled={currentPage === numberOfPages}
|
|
165
|
+
onPageChange={_onPageChange}
|
|
166
|
+
pageToNavigateTo={currentPage + 1}
|
|
167
|
+
>
|
|
168
|
+
<span className={cx(`${rootClassName}__step-trigger-label`)}>{nextButtonText}</span>
|
|
169
|
+
<IconArrowRight size="sm" />
|
|
170
|
+
</PaginationStepTrigger>
|
|
171
|
+
</div>
|
|
172
|
+
</ContainerTag>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
Pagination.displayName = "Pagination";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type PaginationInformation = {
|
|
2
|
+
pageSize: number;
|
|
3
|
+
currentPage: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type NavigationInformation = PaginationInformation & {
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type HrefInformation = {
|
|
11
|
+
pageSize: number;
|
|
12
|
+
page: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type LinkProps = {
|
|
16
|
+
anchorTagElement?: Link;
|
|
17
|
+
asLink: true;
|
|
18
|
+
hrefGetter: (hrefInformation: HrefInformation) => string;
|
|
19
|
+
navigationFunction?: (navigationInformation: NavigationInformation) => void;
|
|
20
|
+
onPageChange?: (paginationChangeInformation: PaginationInformation) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type NoLinkProps = {
|
|
24
|
+
anchorTagElement?: never;
|
|
25
|
+
asLink?: never;
|
|
26
|
+
hrefGetter?: never;
|
|
27
|
+
navigationFunction?: never;
|
|
28
|
+
onPageChange: (paginationChangeInformation: PaginationInformation) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type InternalLinkProps = {
|
|
32
|
+
asLink: true;
|
|
33
|
+
hrefGetter: (page: number) => string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type InternalNoLinkProps = {
|
|
37
|
+
asLink?: never;
|
|
38
|
+
hrefGetter?: never;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type Link =
|
|
42
|
+
| React.ForwardRefExoticComponent<React.AnchorHTMLAttributes<HTMLAnchorElement>>
|
|
43
|
+
| "a";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Option } from "@purpurds/autocomplete";
|
|
3
|
+
|
|
4
|
+
type UsePageOptionsHook = {
|
|
5
|
+
options: Option[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const usePageOptions = (numberOfPages: number): UsePageOptionsHook => {
|
|
9
|
+
const options = useMemo(
|
|
10
|
+
() =>
|
|
11
|
+
Array.from({ length: numberOfPages }, (_, i) => ({
|
|
12
|
+
id: `${i + 1}`,
|
|
13
|
+
label: `${i + 1}`,
|
|
14
|
+
})),
|
|
15
|
+
[numberOfPages]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
options,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { SelectOption } from "@purpurds/select";
|
|
3
|
+
|
|
4
|
+
type UsePageSizeOptionsHook = {
|
|
5
|
+
options: SelectOption[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const usePageSizeOptions = (availablePageSizes: number[]): UsePageSizeOptionsHook => {
|
|
9
|
+
const options = useMemo(
|
|
10
|
+
() =>
|
|
11
|
+
availablePageSizes.map((pageSize: number) => ({
|
|
12
|
+
label: `${pageSize}`,
|
|
13
|
+
value: `${pageSize}`,
|
|
14
|
+
})),
|
|
15
|
+
[availablePageSizes]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
options,
|
|
20
|
+
};
|
|
21
|
+
};
|