@automattic/vip-design-system 2.18.0 → 2.18.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/build/system/Pagination/Pagination.d.ts +10 -20
- package/build/system/Pagination/Pagination.js +50 -68
- package/build/system/Pagination/Pagination.stories.js +13 -11
- package/build/system/Pagination/Pagination.test.js +4 -4
- package/build/system/Pagination/PaginationLayout.d.ts +27 -0
- package/build/system/Pagination/PaginationLayout.js +63 -0
- package/build/system/Pagination/SimplePagination.d.ts +26 -0
- package/build/system/Pagination/SimplePagination.js +76 -0
- package/build/system/Pagination/SimplePagination.stories.d.ts +13 -0
- package/build/system/Pagination/SimplePagination.stories.js +130 -0
- package/build/system/Pagination/SimplePagination.test.d.ts +2 -0
- package/build/system/Pagination/SimplePagination.test.js +171 -0
- package/build/system/Pagination/index.d.ts +3 -1
- package/build/system/Pagination/index.js +2 -1
- package/build/system/index.d.ts +2 -2
- package/build/system/index.js +2 -2
- package/package.json +1 -1
- package/src/system/Pagination/Pagination.stories.tsx +13 -10
- package/src/system/Pagination/Pagination.test.tsx +4 -6
- package/src/system/Pagination/Pagination.tsx +36 -71
- package/src/system/Pagination/PaginationLayout.tsx +93 -0
- package/src/system/Pagination/SimplePagination.stories.tsx +127 -0
- package/src/system/Pagination/SimplePagination.test.tsx +120 -0
- package/src/system/Pagination/SimplePagination.tsx +97 -0
- package/src/system/Pagination/index.ts +3 -1
- package/src/system/index.ts +2 -1
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** @jsxImportSource theme-ui */
|
|
2
2
|
|
|
3
|
-
import classNames from 'classnames';
|
|
4
3
|
import { forwardRef } from 'react';
|
|
5
4
|
import { BiDotsHorizontalRounded } from 'react-icons/bi';
|
|
6
5
|
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';
|
|
7
|
-
import { Flex
|
|
6
|
+
import { Flex } from 'theme-ui';
|
|
8
7
|
|
|
8
|
+
import { PaginationLayout, PaginationLayoutProps } from './PaginationLayout';
|
|
9
9
|
import {
|
|
10
|
-
containerStyles,
|
|
11
10
|
navigationStyles,
|
|
12
11
|
pageButtonStyles,
|
|
13
12
|
activePageButtonStyles,
|
|
@@ -19,13 +18,7 @@ import { Button } from '../Button';
|
|
|
19
18
|
import { Select } from '../NewForm';
|
|
20
19
|
import { Text } from '../Text';
|
|
21
20
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
export interface PaginationProps {
|
|
25
|
-
/** Whether to show the items-per-page dropdown selector.
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
displayItemsPerPageSelector?: boolean;
|
|
21
|
+
export interface PaginationProps extends PaginationLayoutProps {
|
|
29
22
|
/** The currently active page number (1-based). */
|
|
30
23
|
currentPage: number;
|
|
31
24
|
/** Total number of items across all pages. Used to compute totalPages if not provided. */
|
|
@@ -42,20 +35,15 @@ export interface PaginationProps {
|
|
|
42
35
|
hasNextPage?: boolean;
|
|
43
36
|
/** The maximum page number that can be reached. Used for open-ended pagination without totalPages. */
|
|
44
37
|
maxReachablePage?: number;
|
|
45
|
-
/**
|
|
46
|
-
* @default
|
|
38
|
+
/** When true, shows a compact dropdown page selector instead of individual page buttons.
|
|
39
|
+
* @default false
|
|
47
40
|
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @default
|
|
41
|
+
compact?: boolean;
|
|
42
|
+
/** Display variant. Use 'compact' for dropdown page selector. Equivalent to the `compact` prop.
|
|
43
|
+
* @default 'full'
|
|
44
|
+
* @deprecated Use the `compact` prop instead, or `SimplePagination` for cursor-based navigation.
|
|
51
45
|
*/
|
|
52
|
-
|
|
53
|
-
/** Additional CSS class name for the pagination container. */
|
|
54
|
-
className?: string;
|
|
55
|
-
/** Theme UI style overrides. */
|
|
56
|
-
sx?: ThemeUIStyleObject;
|
|
57
|
-
/** Optional content rendered between the items-per-page selector and page navigation. */
|
|
58
|
-
children?: React.ReactNode;
|
|
46
|
+
variant?: 'full' | 'compact';
|
|
59
47
|
}
|
|
60
48
|
|
|
61
49
|
export type PageNumberItem = number | 'ellipsis';
|
|
@@ -132,28 +120,6 @@ export function getPageNumbers(
|
|
|
132
120
|
];
|
|
133
121
|
}
|
|
134
122
|
|
|
135
|
-
const ItemsPerPageSelect = ( {
|
|
136
|
-
itemsPerPage,
|
|
137
|
-
pageSizeOptions,
|
|
138
|
-
onItemsPerPageChange,
|
|
139
|
-
}: {
|
|
140
|
-
itemsPerPage: number;
|
|
141
|
-
pageSizeOptions: number[];
|
|
142
|
-
onItemsPerPageChange: ( size: number ) => void;
|
|
143
|
-
} ) => (
|
|
144
|
-
<Select
|
|
145
|
-
id="items-per-page"
|
|
146
|
-
aria-label="Items per page"
|
|
147
|
-
separator={ false }
|
|
148
|
-
value={ itemsPerPage }
|
|
149
|
-
options={ pageSizeOptions.map( size => ( {
|
|
150
|
-
value: size,
|
|
151
|
-
label: `${ size.toString() } / page`,
|
|
152
|
-
} ) ) }
|
|
153
|
-
onChange={ option => onItemsPerPageChange( Number( option?.value ) ) }
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
|
|
157
123
|
const PageNumbers = ( {
|
|
158
124
|
currentPage,
|
|
159
125
|
totalPages,
|
|
@@ -235,12 +201,11 @@ const CompactPageSelector = ( {
|
|
|
235
201
|
|
|
236
202
|
/**
|
|
237
203
|
* A pagination control for navigating through paged content.
|
|
238
|
-
*
|
|
204
|
+
* Shows page-number buttons by default, or a compact dropdown when `compact` is true.
|
|
239
205
|
*/
|
|
240
206
|
export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
241
207
|
(
|
|
242
208
|
{
|
|
243
|
-
displayItemsPerPageSelector = false,
|
|
244
209
|
currentPage,
|
|
245
210
|
totalItems,
|
|
246
211
|
totalPages,
|
|
@@ -249,8 +214,10 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
249
214
|
onItemsPerPageChange,
|
|
250
215
|
hasNextPage,
|
|
251
216
|
maxReachablePage,
|
|
252
|
-
|
|
253
|
-
|
|
217
|
+
compact = false,
|
|
218
|
+
variant,
|
|
219
|
+
displayItemsPerPageSelector,
|
|
220
|
+
pageSizeOptions,
|
|
254
221
|
className,
|
|
255
222
|
sx,
|
|
256
223
|
children,
|
|
@@ -258,47 +225,45 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
258
225
|
},
|
|
259
226
|
ref
|
|
260
227
|
) => {
|
|
228
|
+
const isCompact = compact || variant === 'compact';
|
|
229
|
+
|
|
261
230
|
const resolvedTotalPages =
|
|
262
231
|
totalPages ??
|
|
263
232
|
( totalItems !== undefined ? Math.ceil( totalItems / itemsPerPage ) : undefined );
|
|
264
233
|
|
|
265
234
|
const isFirstPage = currentPage <= 1;
|
|
266
|
-
|
|
267
|
-
|
|
235
|
+
let isLastPage: boolean;
|
|
236
|
+
if ( resolvedTotalPages !== undefined ) {
|
|
237
|
+
isLastPage = currentPage >= resolvedTotalPages;
|
|
238
|
+
} else {
|
|
239
|
+
isLastPage = hasNextPage === false;
|
|
240
|
+
}
|
|
268
241
|
|
|
269
242
|
return (
|
|
270
|
-
<
|
|
243
|
+
<PaginationLayout
|
|
271
244
|
ref={ ref }
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
245
|
+
displayItemsPerPageSelector={ displayItemsPerPageSelector }
|
|
246
|
+
itemsPerPage={ itemsPerPage }
|
|
247
|
+
pageSizeOptions={ pageSizeOptions }
|
|
248
|
+
onItemsPerPageChange={ onItemsPerPageChange }
|
|
249
|
+
className={ className }
|
|
250
|
+
sx={ sx }
|
|
275
251
|
{ ...rest }
|
|
276
252
|
>
|
|
277
|
-
<Box>
|
|
278
|
-
{ displayItemsPerPageSelector && (
|
|
279
|
-
<ItemsPerPageSelect
|
|
280
|
-
itemsPerPage={ itemsPerPage }
|
|
281
|
-
pageSizeOptions={ pageSizeOptions }
|
|
282
|
-
onItemsPerPageChange={ onItemsPerPageChange }
|
|
283
|
-
/>
|
|
284
|
-
) }
|
|
285
|
-
</Box>
|
|
286
253
|
<Box sx={ { flex: 1 } }>{ children }</Box>
|
|
287
254
|
<Flex sx={ navigationStyles }>
|
|
288
|
-
{
|
|
289
|
-
<
|
|
255
|
+
{ isCompact ? (
|
|
256
|
+
<CompactPageSelector
|
|
290
257
|
currentPage={ currentPage }
|
|
291
258
|
totalPages={ resolvedTotalPages }
|
|
292
|
-
hasNextPage={ hasNextPage }
|
|
293
259
|
maxReachablePage={ maxReachablePage }
|
|
294
260
|
onPageChange={ onPageChange }
|
|
295
261
|
/>
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
{ variant === 'compact' && (
|
|
299
|
-
<CompactPageSelector
|
|
262
|
+
) : (
|
|
263
|
+
<PageNumbers
|
|
300
264
|
currentPage={ currentPage }
|
|
301
265
|
totalPages={ resolvedTotalPages }
|
|
266
|
+
hasNextPage={ hasNextPage }
|
|
302
267
|
maxReachablePage={ maxReachablePage }
|
|
303
268
|
onPageChange={ onPageChange }
|
|
304
269
|
/>
|
|
@@ -322,7 +287,7 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
322
287
|
<MdChevronRight size={ 20 } />
|
|
323
288
|
</Button>
|
|
324
289
|
</Flex>
|
|
325
|
-
</
|
|
290
|
+
</PaginationLayout>
|
|
326
291
|
);
|
|
327
292
|
}
|
|
328
293
|
);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { forwardRef } from 'react';
|
|
5
|
+
import { ThemeUIStyleObject } from 'theme-ui';
|
|
6
|
+
|
|
7
|
+
import { containerStyles } from './styles';
|
|
8
|
+
import { Box } from '../Box';
|
|
9
|
+
import { Select } from '../NewForm';
|
|
10
|
+
|
|
11
|
+
export interface PaginationLayoutProps {
|
|
12
|
+
/** Whether to show the items-per-page dropdown selector.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
displayItemsPerPageSelector?: boolean;
|
|
16
|
+
/** Number of items displayed per page. */
|
|
17
|
+
itemsPerPage?: number;
|
|
18
|
+
/** Available page size options for the items-per-page selector.
|
|
19
|
+
* @default [20, 50, 100]
|
|
20
|
+
*/
|
|
21
|
+
pageSizeOptions?: number[];
|
|
22
|
+
/** Callback fired when the user changes the items-per-page value. */
|
|
23
|
+
onItemsPerPageChange?: ( itemsPerPage: number ) => void;
|
|
24
|
+
/** Additional CSS class name for the pagination container. */
|
|
25
|
+
className?: string;
|
|
26
|
+
/** Theme UI style overrides. */
|
|
27
|
+
sx?: ThemeUIStyleObject;
|
|
28
|
+
/** Slot for variant-specific content (page numbers, arrows, etc.). */
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ItemsPerPageSelect = ( {
|
|
33
|
+
itemsPerPage,
|
|
34
|
+
pageSizeOptions,
|
|
35
|
+
onItemsPerPageChange,
|
|
36
|
+
}: {
|
|
37
|
+
itemsPerPage: number;
|
|
38
|
+
pageSizeOptions: number[];
|
|
39
|
+
onItemsPerPageChange: ( size: number ) => void;
|
|
40
|
+
} ) => (
|
|
41
|
+
<Select
|
|
42
|
+
id="items-per-page"
|
|
43
|
+
aria-label="Items per page"
|
|
44
|
+
separator={ false }
|
|
45
|
+
value={ itemsPerPage }
|
|
46
|
+
options={ pageSizeOptions.map( size => ( {
|
|
47
|
+
value: size,
|
|
48
|
+
label: `${ size.toString() } / page`,
|
|
49
|
+
} ) ) }
|
|
50
|
+
onChange={ option => onItemsPerPageChange( Number( option?.value ) ) }
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Shared layout wrapper for pagination components.
|
|
56
|
+
* Renders the nav landmark, optional items-per-page selector, and variant content.
|
|
57
|
+
*/
|
|
58
|
+
export const PaginationLayout = forwardRef< HTMLElement, PaginationLayoutProps >(
|
|
59
|
+
(
|
|
60
|
+
{
|
|
61
|
+
displayItemsPerPageSelector = false,
|
|
62
|
+
itemsPerPage,
|
|
63
|
+
pageSizeOptions = [ 20, 50, 100 ],
|
|
64
|
+
onItemsPerPageChange,
|
|
65
|
+
className,
|
|
66
|
+
sx,
|
|
67
|
+
children,
|
|
68
|
+
...rest
|
|
69
|
+
},
|
|
70
|
+
ref
|
|
71
|
+
) => (
|
|
72
|
+
<nav
|
|
73
|
+
ref={ ref }
|
|
74
|
+
aria-label="Pagination"
|
|
75
|
+
className={ classNames( 'vip-pagination-component', className ) }
|
|
76
|
+
sx={ { ...containerStyles, ...sx } }
|
|
77
|
+
{ ...rest }
|
|
78
|
+
>
|
|
79
|
+
<Box>
|
|
80
|
+
{ displayItemsPerPageSelector && itemsPerPage && onItemsPerPageChange && (
|
|
81
|
+
<ItemsPerPageSelect
|
|
82
|
+
itemsPerPage={ itemsPerPage }
|
|
83
|
+
pageSizeOptions={ pageSizeOptions }
|
|
84
|
+
onItemsPerPageChange={ onItemsPerPageChange }
|
|
85
|
+
/>
|
|
86
|
+
) }
|
|
87
|
+
</Box>
|
|
88
|
+
{ children }
|
|
89
|
+
</nav>
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
PaginationLayout.displayName = 'PaginationLayout';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { SimplePagination } from './SimplePagination';
|
|
9
|
+
import { Badge } from '../Badge';
|
|
10
|
+
import { Flex } from '../Flex';
|
|
11
|
+
import { Text } from '../Text';
|
|
12
|
+
|
|
13
|
+
import type { StoryObj, Meta } from '@storybook/react-vite';
|
|
14
|
+
|
|
15
|
+
const meta: Meta< typeof SimplePagination > = {
|
|
16
|
+
title: 'SimplePagination',
|
|
17
|
+
component: SimplePagination,
|
|
18
|
+
parameters: {
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component: `
|
|
22
|
+
A pagination control with only previous/next arrow buttons.
|
|
23
|
+
Designed for cursor-based pagination APIs with custom param names (e.g., \`after\`/\`before\`).
|
|
24
|
+
|
|
25
|
+
For page-number based pagination, see \`Pagination\`.
|
|
26
|
+
|
|
27
|
+
## Component Properties
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
|
|
36
|
+
type Story = StoryObj< typeof SimplePagination >;
|
|
37
|
+
|
|
38
|
+
const pageTokens = [ 'start', 'abc123', 'def456', 'ghi789', 'jkl012', 'end' ];
|
|
39
|
+
|
|
40
|
+
const SimplePaginationWithState = ( {
|
|
41
|
+
initialIndex = 0,
|
|
42
|
+
displayItemsPerPageSelector = false,
|
|
43
|
+
}: {
|
|
44
|
+
initialIndex?: number;
|
|
45
|
+
displayItemsPerPageSelector?: boolean;
|
|
46
|
+
} ) => {
|
|
47
|
+
const [ index, setIndex ] = useState( initialIndex );
|
|
48
|
+
const [ itemsPerPage, setItemsPerPage ] = useState( 20 );
|
|
49
|
+
|
|
50
|
+
const hasPreviousPage = index > 0;
|
|
51
|
+
const hasNextPage = index < pageTokens.length - 1;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<SimplePagination
|
|
55
|
+
hasNextPage={ hasNextPage }
|
|
56
|
+
hasPreviousPage={ hasPreviousPage }
|
|
57
|
+
nextParam={ hasNextPage ? { param: 'after', value: pageTokens[ index + 1 ] } : undefined }
|
|
58
|
+
previousParam={
|
|
59
|
+
hasPreviousPage ? { param: 'before', value: pageTokens[ index ] } : undefined
|
|
60
|
+
}
|
|
61
|
+
onNavigate={ param => {
|
|
62
|
+
if ( param === 'after' ) {
|
|
63
|
+
setIndex( i => i + 1 );
|
|
64
|
+
}
|
|
65
|
+
if ( param === 'before' ) {
|
|
66
|
+
setIndex( i => i - 1 );
|
|
67
|
+
}
|
|
68
|
+
} }
|
|
69
|
+
displayItemsPerPageSelector={ displayItemsPerPageSelector }
|
|
70
|
+
itemsPerPage={ displayItemsPerPageSelector ? itemsPerPage : undefined }
|
|
71
|
+
onItemsPerPageChange={
|
|
72
|
+
displayItemsPerPageSelector
|
|
73
|
+
? size => {
|
|
74
|
+
setItemsPerPage( size );
|
|
75
|
+
setIndex( 0 );
|
|
76
|
+
}
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<Flex sx={ { justifyContent: 'center', alignItems: 'center', verticalAlign: 'middle' } }>
|
|
81
|
+
<Badge variant="gold" sx={ { mr: 2 } }>
|
|
82
|
+
DEBUG
|
|
83
|
+
</Badge>
|
|
84
|
+
<Text>
|
|
85
|
+
Index: { index } — token: { pageTokens[ index ] }
|
|
86
|
+
</Text>
|
|
87
|
+
</Flex>
|
|
88
|
+
</SimplePagination>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Default: Story = {
|
|
93
|
+
render: () => <SimplePaginationWithState />,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const FirstPage: Story = {
|
|
97
|
+
render: () => <SimplePaginationWithState />,
|
|
98
|
+
parameters: {
|
|
99
|
+
docs: {
|
|
100
|
+
description: {
|
|
101
|
+
story: 'On the first page — Previous button is disabled.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const LastPage: Story = {
|
|
108
|
+
render: () => <SimplePaginationWithState initialIndex={ pageTokens.length - 1 } />,
|
|
109
|
+
parameters: {
|
|
110
|
+
docs: {
|
|
111
|
+
description: {
|
|
112
|
+
story: 'On the last page — Next button is disabled.',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const WithPageSize: Story = {
|
|
119
|
+
render: () => <SimplePaginationWithState displayItemsPerPageSelector />,
|
|
120
|
+
parameters: {
|
|
121
|
+
docs: {
|
|
122
|
+
description: {
|
|
123
|
+
story: 'With an items-per-page selector.',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { axe } from 'jest-axe';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
|
|
8
|
+
import { SimplePagination } from './SimplePagination';
|
|
9
|
+
|
|
10
|
+
const defaultProps = {
|
|
11
|
+
hasNextPage: true,
|
|
12
|
+
hasPreviousPage: true,
|
|
13
|
+
nextParam: { param: 'after', value: 'cursor_abc' },
|
|
14
|
+
previousParam: { param: 'before', value: 'cursor_xyz' },
|
|
15
|
+
onNavigate: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe( '<SimplePagination />', () => {
|
|
19
|
+
beforeEach( () => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
it( 'renders a nav landmark with aria-label', () => {
|
|
24
|
+
render( <SimplePagination { ...defaultProps } /> );
|
|
25
|
+
|
|
26
|
+
expect( screen.getByRole( 'navigation', { name: 'Pagination' } ) ).toBeInTheDocument();
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'renders prev and next arrow buttons', () => {
|
|
30
|
+
render( <SimplePagination { ...defaultProps } /> );
|
|
31
|
+
|
|
32
|
+
expect( screen.getByRole( 'button', { name: 'Previous page' } ) ).toBeInTheDocument();
|
|
33
|
+
expect( screen.getByRole( 'button', { name: 'Next page' } ) ).toBeInTheDocument();
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
it( 'does not render page number buttons', () => {
|
|
37
|
+
render( <SimplePagination { ...defaultProps } /> );
|
|
38
|
+
|
|
39
|
+
expect( screen.queryByRole( 'button', { name: /Go to page/ } ) ).not.toBeInTheDocument();
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'disables previous button when hasPreviousPage is false', () => {
|
|
43
|
+
render( <SimplePagination { ...defaultProps } hasPreviousPage={ false } /> );
|
|
44
|
+
|
|
45
|
+
expect( screen.getByRole( 'button', { name: 'Previous page' } ) ).toBeDisabled();
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'disables next button when hasNextPage is false', () => {
|
|
49
|
+
render( <SimplePagination { ...defaultProps } hasNextPage={ false } /> );
|
|
50
|
+
|
|
51
|
+
expect( screen.getByRole( 'button', { name: 'Next page' } ) ).toBeDisabled();
|
|
52
|
+
} );
|
|
53
|
+
|
|
54
|
+
it( 'disables previous button when previousParam is undefined', () => {
|
|
55
|
+
render( <SimplePagination { ...defaultProps } previousParam={ undefined } /> );
|
|
56
|
+
|
|
57
|
+
expect( screen.getByRole( 'button', { name: 'Previous page' } ) ).toBeDisabled();
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
it( 'disables next button when nextParam is undefined', () => {
|
|
61
|
+
render( <SimplePagination { ...defaultProps } nextParam={ undefined } /> );
|
|
62
|
+
|
|
63
|
+
expect( screen.getByRole( 'button', { name: 'Next page' } ) ).toBeDisabled();
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'calls onNavigate with correct param and value when clicking next', async () => {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
render( <SimplePagination { ...defaultProps } /> );
|
|
69
|
+
|
|
70
|
+
await user.click( screen.getByRole( 'button', { name: 'Next page' } ) );
|
|
71
|
+
|
|
72
|
+
expect( defaultProps.onNavigate ).toHaveBeenCalledWith( 'after', 'cursor_abc' );
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'calls onNavigate with correct param and value when clicking previous', async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
render( <SimplePagination { ...defaultProps } /> );
|
|
78
|
+
|
|
79
|
+
await user.click( screen.getByRole( 'button', { name: 'Previous page' } ) );
|
|
80
|
+
|
|
81
|
+
expect( defaultProps.onNavigate ).toHaveBeenCalledWith( 'before', 'cursor_xyz' );
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
it( 'renders children content', () => {
|
|
85
|
+
render(
|
|
86
|
+
<SimplePagination { ...defaultProps }>
|
|
87
|
+
<span>Showing results</span>
|
|
88
|
+
</SimplePagination>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect( screen.getByText( 'Showing results' ) ).toBeInTheDocument();
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'renders items-per-page selector when enabled', () => {
|
|
95
|
+
render(
|
|
96
|
+
<SimplePagination
|
|
97
|
+
{ ...defaultProps }
|
|
98
|
+
displayItemsPerPageSelector
|
|
99
|
+
itemsPerPage={ 20 }
|
|
100
|
+
onItemsPerPageChange={ jest.fn() }
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect( screen.getByRole( 'combobox' ) ).toBeInTheDocument();
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
it( 'has no accessibility violations', async () => {
|
|
108
|
+
const { container } = render( <SimplePagination { ...defaultProps } /> );
|
|
109
|
+
|
|
110
|
+
expect( await axe( container ) ).toHaveNoViolations();
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
it( 'has no accessibility violations with both buttons disabled', async () => {
|
|
114
|
+
const { container } = render(
|
|
115
|
+
<SimplePagination { ...defaultProps } hasNextPage={ false } hasPreviousPage={ false } />
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect( await axe( container ) ).toHaveNoViolations();
|
|
119
|
+
} );
|
|
120
|
+
} );
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import { MdChevronLeft, MdChevronRight } from 'react-icons/md';
|
|
5
|
+
import { Flex } from 'theme-ui';
|
|
6
|
+
|
|
7
|
+
import { PaginationLayout, PaginationLayoutProps } from './PaginationLayout';
|
|
8
|
+
import { navigationStyles, arrowButtonStyles } from './styles';
|
|
9
|
+
import { Box } from '../Box';
|
|
10
|
+
import { Button } from '../Button';
|
|
11
|
+
|
|
12
|
+
/** A navigation parameter for SimplePagination. */
|
|
13
|
+
export interface SimpleNavigationParam {
|
|
14
|
+
/** The query parameter name (e.g., 'after', 'before'). */
|
|
15
|
+
param: string;
|
|
16
|
+
/** The parameter token value. */
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SimplePaginationProps extends PaginationLayoutProps {
|
|
21
|
+
/** Whether there is a next page available. */
|
|
22
|
+
hasNextPage?: boolean;
|
|
23
|
+
/** Whether there is a previous page available. */
|
|
24
|
+
hasPreviousPage?: boolean;
|
|
25
|
+
/** Navigation parameter for the next page. */
|
|
26
|
+
nextParam?: SimpleNavigationParam;
|
|
27
|
+
/** Navigation parameter for the previous page. */
|
|
28
|
+
previousParam?: SimpleNavigationParam;
|
|
29
|
+
/** Callback fired when the user navigates. Receives the param name and value. */
|
|
30
|
+
onNavigate: ( param: string, value: string ) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A pagination control with only previous/next arrow buttons.
|
|
35
|
+
* Designed for cursor-based pagination APIs with custom param names (e.g., `after`/`before`).
|
|
36
|
+
*/
|
|
37
|
+
export const SimplePagination = forwardRef< HTMLElement, SimplePaginationProps >(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
hasNextPage,
|
|
41
|
+
hasPreviousPage,
|
|
42
|
+
nextParam,
|
|
43
|
+
previousParam,
|
|
44
|
+
onNavigate,
|
|
45
|
+
displayItemsPerPageSelector,
|
|
46
|
+
itemsPerPage,
|
|
47
|
+
pageSizeOptions,
|
|
48
|
+
onItemsPerPageChange,
|
|
49
|
+
className,
|
|
50
|
+
sx,
|
|
51
|
+
children,
|
|
52
|
+
...rest
|
|
53
|
+
},
|
|
54
|
+
ref
|
|
55
|
+
) => {
|
|
56
|
+
const isPrevDisabled = ! hasPreviousPage || ! previousParam?.value;
|
|
57
|
+
const isNextDisabled = ! hasNextPage || ! nextParam?.value;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<PaginationLayout
|
|
61
|
+
ref={ ref }
|
|
62
|
+
displayItemsPerPageSelector={ displayItemsPerPageSelector }
|
|
63
|
+
itemsPerPage={ itemsPerPage }
|
|
64
|
+
pageSizeOptions={ pageSizeOptions }
|
|
65
|
+
onItemsPerPageChange={ onItemsPerPageChange }
|
|
66
|
+
className={ className }
|
|
67
|
+
sx={ sx }
|
|
68
|
+
{ ...rest }
|
|
69
|
+
>
|
|
70
|
+
<Box sx={ { flex: 1 } }>{ children }</Box>
|
|
71
|
+
<Flex sx={ navigationStyles }>
|
|
72
|
+
<Button
|
|
73
|
+
aria-label="Previous page"
|
|
74
|
+
disabled={ isPrevDisabled }
|
|
75
|
+
onClick={ () =>
|
|
76
|
+
previousParam && onNavigate( previousParam.param, previousParam.value )
|
|
77
|
+
}
|
|
78
|
+
sx={ arrowButtonStyles }
|
|
79
|
+
>
|
|
80
|
+
<MdChevronLeft size={ 20 } />
|
|
81
|
+
</Button>
|
|
82
|
+
|
|
83
|
+
<Button
|
|
84
|
+
aria-label="Next page"
|
|
85
|
+
disabled={ isNextDisabled }
|
|
86
|
+
onClick={ () => nextParam && onNavigate( nextParam.param, nextParam.value ) }
|
|
87
|
+
sx={ arrowButtonStyles }
|
|
88
|
+
>
|
|
89
|
+
<MdChevronRight size={ 20 } />
|
|
90
|
+
</Button>
|
|
91
|
+
</Flex>
|
|
92
|
+
</PaginationLayout>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
SimplePagination.displayName = 'SimplePagination';
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { Pagination, getPageNumbers } from './Pagination';
|
|
2
|
-
export
|
|
2
|
+
export { SimplePagination } from './SimplePagination';
|
|
3
|
+
export type { PaginationProps, PageNumberItem } from './Pagination';
|
|
4
|
+
export type { SimplePaginationProps, SimpleNavigationParam } from './SimplePagination';
|
package/src/system/index.ts
CHANGED
|
@@ -50,7 +50,7 @@ import * as Form from './NewForm';
|
|
|
50
50
|
import { NewTooltip } from './NewTooltip';
|
|
51
51
|
import { Notice } from './Notice';
|
|
52
52
|
import { OptionRow } from './OptionRow';
|
|
53
|
-
import { Pagination } from './Pagination';
|
|
53
|
+
import { SimplePagination, Pagination } from './Pagination';
|
|
54
54
|
import { Progress } from './Progress';
|
|
55
55
|
import { ScreenReaderText } from './ScreenReaderText';
|
|
56
56
|
import { Skeleton } from './Skeleton';
|
|
@@ -99,6 +99,7 @@ export {
|
|
|
99
99
|
Flex,
|
|
100
100
|
Notice,
|
|
101
101
|
OptionRow,
|
|
102
|
+
SimplePagination,
|
|
102
103
|
Pagination,
|
|
103
104
|
Heading,
|
|
104
105
|
Hr,
|