@automattic/vip-design-system 2.17.0 → 2.17.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 +2 -1
- package/build/system/Pagination/Pagination.js +54 -26
- package/build/system/Pagination/Pagination.stories.d.ts +1 -0
- package/build/system/Pagination/Pagination.stories.js +53 -0
- package/build/system/Pagination/Pagination.test.js +91 -29
- package/package.json +1 -1
- package/src/system/Pagination/Pagination.stories.tsx +43 -0
- package/src/system/Pagination/Pagination.test.tsx +61 -1
- package/src/system/Pagination/Pagination.tsx +62 -22
|
@@ -11,6 +11,7 @@ export interface PaginationProps {
|
|
|
11
11
|
onPageChange: (page: number) => void;
|
|
12
12
|
onItemsPerPageChange: (itemsPerPage: number) => void;
|
|
13
13
|
hasNextPage?: boolean;
|
|
14
|
+
maxReachablePage?: number;
|
|
14
15
|
variant?: PaginationVariant;
|
|
15
16
|
pageSizeOptions?: number[];
|
|
16
17
|
className?: string;
|
|
@@ -18,5 +19,5 @@ export interface PaginationProps {
|
|
|
18
19
|
children?: React.ReactNode;
|
|
19
20
|
}
|
|
20
21
|
export type PageNumberItem = number | 'ellipsis';
|
|
21
|
-
export declare function getPageNumbers(currentPage: number, totalPages?: number, hasNextPage?: boolean): PageNumberItem[];
|
|
22
|
+
export declare function getPageNumbers(currentPage: number, totalPages?: number, hasNextPage?: boolean, maxReachablePage?: number): PageNumberItem[];
|
|
22
23
|
export declare const Pagination: import("react").ForwardRefExoticComponent<PaginationProps & import("react").RefAttributes<HTMLElement>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _excluded = ["displayItemsPerPageSelector", "currentPage", "totalItems", "totalPages", "itemsPerPage", "onPageChange", "onItemsPerPageChange", "hasNextPage", "variant", "pageSizeOptions", "className", "sx", "children"];
|
|
1
|
+
var _excluded = ["displayItemsPerPageSelector", "currentPage", "totalItems", "totalPages", "itemsPerPage", "onPageChange", "onItemsPerPageChange", "hasNextPage", "maxReachablePage", "variant", "pageSizeOptions", "className", "sx", "children"];
|
|
2
2
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
3
3
|
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
|
|
4
4
|
/** @jsxImportSource theme-ui */
|
|
@@ -23,34 +23,60 @@ function range(start, end) {
|
|
|
23
23
|
return start + i;
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
/** Total number of visible items (page numbers + ellipsis indicators) in the pagination bar. */
|
|
28
|
+
var VISIBLE_PAGE_SLOTS = 8;
|
|
29
|
+
|
|
30
|
+
/** When currentPage <= this value, the "near start" layout is used (no leading ellipsis). */
|
|
31
|
+
var NEAR_START_THRESHOLD = 5;
|
|
32
|
+
|
|
33
|
+
/** Pages shown before the current page in the middle layout. */
|
|
34
|
+
var PAGES_BEFORE_CURRENT = 1;
|
|
35
|
+
|
|
36
|
+
/** Pages shown after the current page in the bounded middle layout. */
|
|
37
|
+
var PAGES_AFTER_CURRENT = 2;
|
|
38
|
+
export function getPageNumbers(currentPage, totalPages, hasNextPage, maxReachablePage) {
|
|
39
|
+
var _last;
|
|
40
|
+
// Resolve the last known page
|
|
27
41
|
var last;
|
|
28
|
-
if (totalPages
|
|
29
|
-
if (hasNextPage === false) {
|
|
30
|
-
last = currentPage;
|
|
31
|
-
} else {
|
|
32
|
-
last = undefined;
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
42
|
+
if (totalPages !== undefined) {
|
|
35
43
|
last = Math.max(1, Number(totalPages));
|
|
44
|
+
} else if (hasNextPage === false) {
|
|
45
|
+
last = currentPage;
|
|
36
46
|
}
|
|
37
47
|
if (last !== undefined && (!Number.isFinite(last) || last < 1)) {
|
|
38
48
|
return [];
|
|
39
49
|
}
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
|
|
51
|
+
// Effective end anchor: known last page, or capped reachable page
|
|
52
|
+
var end = (_last = last) != null ? _last : maxReachablePage !== undefined ? Math.max(currentPage, maxReachablePage) : undefined;
|
|
53
|
+
|
|
54
|
+
// Small page count — show all without ellipsis
|
|
55
|
+
if (end !== undefined && end <= VISIBLE_PAGE_SLOTS) {
|
|
56
|
+
return range(1, end);
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
//
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
return [1, 'ellipsis'].concat(range(currentPage - 1, currentPage + 2), ['ellipsis', last]);
|
|
59
|
+
// Near start
|
|
60
|
+
if (currentPage <= NEAR_START_THRESHOLD) {
|
|
61
|
+
if (end !== undefined) return [].concat(range(1, NEAR_START_THRESHOLD + 1), ['ellipsis', end]);
|
|
62
|
+
return [].concat(range(1, VISIBLE_PAGE_SLOTS - 1), ['ellipsis']);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
//
|
|
52
|
-
if (currentPage
|
|
53
|
-
|
|
65
|
+
// Near end (bounded only — open-ended has no "end zone")
|
|
66
|
+
if (last !== undefined && currentPage >= last - (NEAR_START_THRESHOLD - 1)) {
|
|
67
|
+
return [1, 'ellipsis'].concat(range(last - NEAR_START_THRESHOLD, last));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Middle
|
|
71
|
+
if (end !== undefined) {
|
|
72
|
+
var rangeEnd = Math.min(currentPage + PAGES_AFTER_CURRENT, end);
|
|
73
|
+
var middle = range(currentPage - PAGES_BEFORE_CURRENT, rangeEnd);
|
|
74
|
+
if (rangeEnd >= end) return [1, 'ellipsis'].concat(middle);
|
|
75
|
+
return [1, 'ellipsis'].concat(middle, ['ellipsis', end]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fully open-ended middle
|
|
79
|
+
return [1, 'ellipsis'].concat(range(currentPage - PAGES_BEFORE_CURRENT, currentPage + PAGES_AFTER_CURRENT + 1), ['ellipsis']);
|
|
54
80
|
}
|
|
55
81
|
var ItemsPerPageSelect = function ItemsPerPageSelect(_ref) {
|
|
56
82
|
var itemsPerPage = _ref.itemsPerPage,
|
|
@@ -76,8 +102,9 @@ var PageNumbers = function PageNumbers(_ref2) {
|
|
|
76
102
|
var currentPage = _ref2.currentPage,
|
|
77
103
|
totalPages = _ref2.totalPages,
|
|
78
104
|
hasNextPage = _ref2.hasNextPage,
|
|
105
|
+
maxReachablePage = _ref2.maxReachablePage,
|
|
79
106
|
onPageChange = _ref2.onPageChange;
|
|
80
|
-
var pages = getPageNumbers(currentPage, totalPages, hasNextPage);
|
|
107
|
+
var pages = getPageNumbers(currentPage, totalPages, hasNextPage, maxReachablePage);
|
|
81
108
|
return _jsx(_Fragment, {
|
|
82
109
|
children: pages.map(function (page, index) {
|
|
83
110
|
if (page === 'ellipsis') {
|
|
@@ -100,14 +127,12 @@ var PageNumbers = function PageNumbers(_ref2) {
|
|
|
100
127
|
var CompactPageSelector = function CompactPageSelector(_ref3) {
|
|
101
128
|
var currentPage = _ref3.currentPage,
|
|
102
129
|
totalPages = _ref3.totalPages,
|
|
130
|
+
maxReachablePage = _ref3.maxReachablePage,
|
|
103
131
|
onPageChange = _ref3.onPageChange;
|
|
104
132
|
var isOpenEnded = totalPages === undefined;
|
|
105
|
-
var
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return i + 1;
|
|
109
|
-
}) : Array.from({
|
|
110
|
-
length: totalPages
|
|
133
|
+
var upperBound = isOpenEnded ? maxReachablePage != null ? maxReachablePage : currentPage + 1 : totalPages;
|
|
134
|
+
var pageOptions = Array.from({
|
|
135
|
+
length: upperBound
|
|
111
136
|
}, function (_, i) {
|
|
112
137
|
return i + 1;
|
|
113
138
|
});
|
|
@@ -160,6 +185,7 @@ export var Pagination = /*#__PURE__*/forwardRef(function (_ref4, ref) {
|
|
|
160
185
|
onPageChange = _ref4.onPageChange,
|
|
161
186
|
onItemsPerPageChange = _ref4.onItemsPerPageChange,
|
|
162
187
|
hasNextPage = _ref4.hasNextPage,
|
|
188
|
+
maxReachablePage = _ref4.maxReachablePage,
|
|
163
189
|
_ref4$variant = _ref4.variant,
|
|
164
190
|
variant = _ref4$variant === void 0 ? 'full' : _ref4$variant,
|
|
165
191
|
_ref4$pageSizeOptions = _ref4.pageSizeOptions,
|
|
@@ -194,10 +220,12 @@ export var Pagination = /*#__PURE__*/forwardRef(function (_ref4, ref) {
|
|
|
194
220
|
currentPage: currentPage,
|
|
195
221
|
totalPages: resolvedTotalPages,
|
|
196
222
|
hasNextPage: hasNextPage,
|
|
223
|
+
maxReachablePage: maxReachablePage,
|
|
197
224
|
onPageChange: onPageChange
|
|
198
225
|
}), variant === 'compact' && _jsx(CompactPageSelector, {
|
|
199
226
|
currentPage: currentPage,
|
|
200
227
|
totalPages: resolvedTotalPages,
|
|
228
|
+
maxReachablePage: maxReachablePage,
|
|
201
229
|
onPageChange: onPageChange
|
|
202
230
|
}), _jsx(Button, {
|
|
203
231
|
"aria-label": "Previous page",
|
|
@@ -13,6 +13,7 @@ export declare const FewPages: Story;
|
|
|
13
13
|
export declare const MiddlePage: Story;
|
|
14
14
|
export declare const CustomPageSizes: Story;
|
|
15
15
|
export declare const WithItemsPerPageSelector: Story;
|
|
16
|
+
export declare const OpenEndedCursorBased: Story;
|
|
16
17
|
export declare const OpenEnded: Story;
|
|
17
18
|
export declare const OpenEndedCompact: Story;
|
|
18
19
|
export declare const OpenEndedLastPage: Story;
|
|
@@ -161,6 +161,59 @@ export var WithItemsPerPageSelector = {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
|
+
var CursorBasedPaginationWithState = function CursorBasedPaginationWithState() {
|
|
165
|
+
var _useState5 = useState(1),
|
|
166
|
+
currentPage = _useState5[0],
|
|
167
|
+
setCurrentPage = _useState5[1];
|
|
168
|
+
var _useState6 = useState(20),
|
|
169
|
+
itemsPerPage = _useState6[0],
|
|
170
|
+
setItemsPerPage = _useState6[1];
|
|
171
|
+
var _useState7 = useState(1),
|
|
172
|
+
maxVisited = _useState7[0],
|
|
173
|
+
setMaxVisited = _useState7[1];
|
|
174
|
+
var handlePageChange = function handlePageChange(page) {
|
|
175
|
+
setCurrentPage(page);
|
|
176
|
+
setMaxVisited(function (prev) {
|
|
177
|
+
return Math.max(prev, page);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
var hasNextPage = true; // Simulate always having a next page
|
|
181
|
+
var maxReachablePage = hasNextPage ? maxVisited + 1 : maxVisited;
|
|
182
|
+
return _jsx(Pagination, {
|
|
183
|
+
currentPage: currentPage,
|
|
184
|
+
itemsPerPage: itemsPerPage,
|
|
185
|
+
onPageChange: handlePageChange,
|
|
186
|
+
onItemsPerPageChange: function onItemsPerPageChange(size) {
|
|
187
|
+
setItemsPerPage(size);
|
|
188
|
+
setCurrentPage(1);
|
|
189
|
+
setMaxVisited(1);
|
|
190
|
+
},
|
|
191
|
+
hasNextPage: hasNextPage,
|
|
192
|
+
maxReachablePage: maxReachablePage,
|
|
193
|
+
displayItemsPerPageSelector: true,
|
|
194
|
+
children: _jsxs(Flex, {
|
|
195
|
+
sx: {
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
verticalAlign: 'middle'
|
|
199
|
+
},
|
|
200
|
+
children: [_jsx(Badge, {
|
|
201
|
+
variant: "gold",
|
|
202
|
+
sx: {
|
|
203
|
+
mr: 2
|
|
204
|
+
},
|
|
205
|
+
children: "DEBUG"
|
|
206
|
+
}), _jsxs(Text, {
|
|
207
|
+
children: ["Page ", currentPage, " \u2014 max reachable: ", maxReachablePage]
|
|
208
|
+
})]
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
export var OpenEndedCursorBased = {
|
|
213
|
+
render: function render() {
|
|
214
|
+
return _jsx(CursorBasedPaginationWithState, {});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
164
217
|
export var OpenEnded = {
|
|
165
218
|
render: function render() {
|
|
166
219
|
return _jsx(OpenEndedPaginationWithState, {});
|
|
@@ -7,7 +7,6 @@ function _extends() { _extends = Object.assign ? Object.assign.bind() : function
|
|
|
7
7
|
import { render, screen } from '@testing-library/react';
|
|
8
8
|
import userEvent from '@testing-library/user-event';
|
|
9
9
|
import { axe } from 'jest-axe';
|
|
10
|
-
import React from 'react';
|
|
11
10
|
import '@testing-library/jest-dom';
|
|
12
11
|
import { Pagination, getPageNumbers } from './Pagination';
|
|
13
12
|
import { jsx as _jsx } from "theme-ui/jsx-runtime";
|
|
@@ -253,6 +252,26 @@ describe('getPageNumbers', function () {
|
|
|
253
252
|
expect(getPageNumbers(10, 20)).toEqual([1, 'ellipsis', 9, 10, 11, 12, 'ellipsis', 20]);
|
|
254
253
|
});
|
|
255
254
|
});
|
|
255
|
+
describe('getPageNumbers (open-ended with maxReachablePage)', function () {
|
|
256
|
+
it('caps pages to maxReachablePage when near start', function () {
|
|
257
|
+
expect(getPageNumbers(1, undefined, true, 2)).toEqual([1, 2]);
|
|
258
|
+
});
|
|
259
|
+
it('shows all reachable pages when they fit', function () {
|
|
260
|
+
expect(getPageNumbers(3, undefined, true, 4)).toEqual([1, 2, 3, 4]);
|
|
261
|
+
});
|
|
262
|
+
it('shows ellipsis for large reachable ranges', function () {
|
|
263
|
+
expect(getPageNumbers(8, undefined, true, 9)).toEqual([1, 'ellipsis', 7, 8, 9]);
|
|
264
|
+
});
|
|
265
|
+
it('shows both ellipsis when end is far from current page', function () {
|
|
266
|
+
expect(getPageNumbers(8, undefined, true, 15)).toEqual([1, 'ellipsis', 7, 8, 9, 10, 'ellipsis', 15]);
|
|
267
|
+
});
|
|
268
|
+
it('returns all pages when maxReachablePage <= 8', function () {
|
|
269
|
+
expect(getPageNumbers(1, undefined, true, 8)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
270
|
+
});
|
|
271
|
+
it('does not affect behavior when maxReachablePage is undefined', function () {
|
|
272
|
+
expect(getPageNumbers(1, undefined, true)).toEqual([1, 2, 3, 4, 5, 6, 7, 'ellipsis']);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
256
275
|
describe('getPageNumbers (open-ended)', function () {
|
|
257
276
|
it('always returns 8 items when page >= 6', function () {
|
|
258
277
|
for (var cp = 6; cp <= 20; cp++) {
|
|
@@ -272,6 +291,49 @@ describe('getPageNumbers (open-ended)', function () {
|
|
|
272
291
|
expect(getPageNumbers(10, undefined, false)).toEqual([1, 'ellipsis', 5, 6, 7, 8, 9, 10]);
|
|
273
292
|
});
|
|
274
293
|
});
|
|
294
|
+
describe('<Pagination /> with maxReachablePage', function () {
|
|
295
|
+
var maxReachableProps = {
|
|
296
|
+
currentPage: 1,
|
|
297
|
+
itemsPerPage: 20,
|
|
298
|
+
hasNextPage: true,
|
|
299
|
+
maxReachablePage: 2,
|
|
300
|
+
onPageChange: jest.fn(),
|
|
301
|
+
onItemsPerPageChange: jest.fn()
|
|
302
|
+
};
|
|
303
|
+
it('only renders reachable page buttons', function () {
|
|
304
|
+
render(_jsx(Pagination, _extends({}, maxReachableProps)));
|
|
305
|
+
expect(screen.getByRole('button', {
|
|
306
|
+
name: 'Go to page 1'
|
|
307
|
+
})).toBeInTheDocument();
|
|
308
|
+
expect(screen.getByRole('button', {
|
|
309
|
+
name: 'Go to page 2'
|
|
310
|
+
})).toBeInTheDocument();
|
|
311
|
+
expect(screen.queryByRole('button', {
|
|
312
|
+
name: 'Go to page 3'
|
|
313
|
+
})).not.toBeInTheDocument();
|
|
314
|
+
expect(screen.queryByRole('button', {
|
|
315
|
+
name: 'Go to page 7'
|
|
316
|
+
})).not.toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
it('has no accessibility violations', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6() {
|
|
319
|
+
var _render3, container;
|
|
320
|
+
return _regeneratorRuntime().wrap(function _callee6$(_context6) {
|
|
321
|
+
while (1) switch (_context6.prev = _context6.next) {
|
|
322
|
+
case 0:
|
|
323
|
+
_render3 = render(_jsx(Pagination, _extends({}, maxReachableProps))), container = _render3.container;
|
|
324
|
+
_context6.t0 = expect;
|
|
325
|
+
_context6.next = 4;
|
|
326
|
+
return axe(container);
|
|
327
|
+
case 4:
|
|
328
|
+
_context6.t1 = _context6.sent;
|
|
329
|
+
(0, _context6.t0)(_context6.t1).toHaveNoViolations();
|
|
330
|
+
case 6:
|
|
331
|
+
case "end":
|
|
332
|
+
return _context6.stop();
|
|
333
|
+
}
|
|
334
|
+
}, _callee6);
|
|
335
|
+
})));
|
|
336
|
+
});
|
|
275
337
|
describe('<Pagination /> open-ended mode', function () {
|
|
276
338
|
var openEndedProps = {
|
|
277
339
|
currentPage: 5,
|
|
@@ -296,14 +358,14 @@ describe('<Pagination /> open-ended mode', function () {
|
|
|
296
358
|
name: 'Next page'
|
|
297
359
|
})).toBeDisabled();
|
|
298
360
|
});
|
|
299
|
-
it('calls onPageChange when clicking next in open-ended mode', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function
|
|
361
|
+
it('calls onPageChange when clicking next in open-ended mode', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee7() {
|
|
300
362
|
var user;
|
|
301
|
-
return _regeneratorRuntime().wrap(function
|
|
302
|
-
while (1) switch (
|
|
363
|
+
return _regeneratorRuntime().wrap(function _callee7$(_context7) {
|
|
364
|
+
while (1) switch (_context7.prev = _context7.next) {
|
|
303
365
|
case 0:
|
|
304
366
|
user = userEvent.setup();
|
|
305
367
|
render(_jsx(Pagination, _extends({}, openEndedProps)));
|
|
306
|
-
|
|
368
|
+
_context7.next = 4;
|
|
307
369
|
return user.click(screen.getByRole('button', {
|
|
308
370
|
name: 'Next page'
|
|
309
371
|
}));
|
|
@@ -311,9 +373,9 @@ describe('<Pagination /> open-ended mode', function () {
|
|
|
311
373
|
expect(openEndedProps.onPageChange).toHaveBeenCalledWith(6);
|
|
312
374
|
case 5:
|
|
313
375
|
case "end":
|
|
314
|
-
return
|
|
376
|
+
return _context7.stop();
|
|
315
377
|
}
|
|
316
|
-
},
|
|
378
|
+
}, _callee7);
|
|
317
379
|
})));
|
|
318
380
|
it('renders compact variant with "Page" but without "of Y"', function () {
|
|
319
381
|
render(_jsx(Pagination, _extends({}, openEndedProps, {
|
|
@@ -322,32 +384,12 @@ describe('<Pagination /> open-ended mode', function () {
|
|
|
322
384
|
expect(screen.getByText('Page')).toBeInTheDocument();
|
|
323
385
|
expect(screen.queryByText(/of \d+/)).not.toBeInTheDocument();
|
|
324
386
|
});
|
|
325
|
-
it('has no accessibility violations (open-ended full)', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function
|
|
326
|
-
var _render3, container;
|
|
327
|
-
return _regeneratorRuntime().wrap(function _callee7$(_context7) {
|
|
328
|
-
while (1) switch (_context7.prev = _context7.next) {
|
|
329
|
-
case 0:
|
|
330
|
-
_render3 = render(_jsx(Pagination, _extends({}, openEndedProps))), container = _render3.container;
|
|
331
|
-
_context7.t0 = expect;
|
|
332
|
-
_context7.next = 4;
|
|
333
|
-
return axe(container);
|
|
334
|
-
case 4:
|
|
335
|
-
_context7.t1 = _context7.sent;
|
|
336
|
-
(0, _context7.t0)(_context7.t1).toHaveNoViolations();
|
|
337
|
-
case 6:
|
|
338
|
-
case "end":
|
|
339
|
-
return _context7.stop();
|
|
340
|
-
}
|
|
341
|
-
}, _callee7);
|
|
342
|
-
})));
|
|
343
|
-
it('has no accessibility violations (open-ended compact)', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee8() {
|
|
387
|
+
it('has no accessibility violations (open-ended full)', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee8() {
|
|
344
388
|
var _render4, container;
|
|
345
389
|
return _regeneratorRuntime().wrap(function _callee8$(_context8) {
|
|
346
390
|
while (1) switch (_context8.prev = _context8.next) {
|
|
347
391
|
case 0:
|
|
348
|
-
_render4 = render(_jsx(Pagination, _extends({}, openEndedProps,
|
|
349
|
-
variant: "compact"
|
|
350
|
-
}))), container = _render4.container;
|
|
392
|
+
_render4 = render(_jsx(Pagination, _extends({}, openEndedProps))), container = _render4.container;
|
|
351
393
|
_context8.t0 = expect;
|
|
352
394
|
_context8.next = 4;
|
|
353
395
|
return axe(container);
|
|
@@ -360,4 +402,24 @@ describe('<Pagination /> open-ended mode', function () {
|
|
|
360
402
|
}
|
|
361
403
|
}, _callee8);
|
|
362
404
|
})));
|
|
405
|
+
it('has no accessibility violations (open-ended compact)', /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee9() {
|
|
406
|
+
var _render5, container;
|
|
407
|
+
return _regeneratorRuntime().wrap(function _callee9$(_context9) {
|
|
408
|
+
while (1) switch (_context9.prev = _context9.next) {
|
|
409
|
+
case 0:
|
|
410
|
+
_render5 = render(_jsx(Pagination, _extends({}, openEndedProps, {
|
|
411
|
+
variant: "compact"
|
|
412
|
+
}))), container = _render5.container;
|
|
413
|
+
_context9.t0 = expect;
|
|
414
|
+
_context9.next = 4;
|
|
415
|
+
return axe(container);
|
|
416
|
+
case 4:
|
|
417
|
+
_context9.t1 = _context9.sent;
|
|
418
|
+
(0, _context9.t0)(_context9.t1).toHaveNoViolations();
|
|
419
|
+
case 6:
|
|
420
|
+
case "end":
|
|
421
|
+
return _context9.stop();
|
|
422
|
+
}
|
|
423
|
+
}, _callee9);
|
|
424
|
+
})));
|
|
363
425
|
});
|
package/package.json
CHANGED
|
@@ -154,6 +154,49 @@ export const WithItemsPerPageSelector: Story = {
|
|
|
154
154
|
),
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
const CursorBasedPaginationWithState = () => {
|
|
158
|
+
const [ currentPage, setCurrentPage ] = useState( 1 );
|
|
159
|
+
const [ itemsPerPage, setItemsPerPage ] = useState( 20 );
|
|
160
|
+
const [ maxVisited, setMaxVisited ] = useState( 1 );
|
|
161
|
+
|
|
162
|
+
const handlePageChange = ( page: number ) => {
|
|
163
|
+
setCurrentPage( page );
|
|
164
|
+
setMaxVisited( prev => Math.max( prev, page ) );
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const hasNextPage = true; // Simulate always having a next page
|
|
168
|
+
const maxReachablePage = hasNextPage ? maxVisited + 1 : maxVisited;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Pagination
|
|
172
|
+
currentPage={ currentPage }
|
|
173
|
+
itemsPerPage={ itemsPerPage }
|
|
174
|
+
onPageChange={ handlePageChange }
|
|
175
|
+
onItemsPerPageChange={ size => {
|
|
176
|
+
setItemsPerPage( size );
|
|
177
|
+
setCurrentPage( 1 );
|
|
178
|
+
setMaxVisited( 1 );
|
|
179
|
+
} }
|
|
180
|
+
hasNextPage={ hasNextPage }
|
|
181
|
+
maxReachablePage={ maxReachablePage }
|
|
182
|
+
displayItemsPerPageSelector
|
|
183
|
+
>
|
|
184
|
+
<Flex sx={ { justifyContent: 'center', alignItems: 'center', verticalAlign: 'middle' } }>
|
|
185
|
+
<Badge variant="gold" sx={ { mr: 2 } }>
|
|
186
|
+
DEBUG
|
|
187
|
+
</Badge>
|
|
188
|
+
<Text>
|
|
189
|
+
Page { currentPage } — max reachable: { maxReachablePage }
|
|
190
|
+
</Text>
|
|
191
|
+
</Flex>
|
|
192
|
+
</Pagination>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const OpenEndedCursorBased: Story = {
|
|
197
|
+
render: () => <CursorBasedPaginationWithState />,
|
|
198
|
+
};
|
|
199
|
+
|
|
157
200
|
export const OpenEnded: Story = {
|
|
158
201
|
render: () => <OpenEndedPaginationWithState />,
|
|
159
202
|
};
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { render, screen } from '@testing-library/react';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import { axe } from 'jest-axe';
|
|
6
|
-
import React from 'react';
|
|
7
6
|
import '@testing-library/jest-dom';
|
|
8
7
|
|
|
9
8
|
import { Pagination, getPageNumbers } from './Pagination';
|
|
@@ -178,6 +177,41 @@ describe( 'getPageNumbers', () => {
|
|
|
178
177
|
} );
|
|
179
178
|
} );
|
|
180
179
|
|
|
180
|
+
describe( 'getPageNumbers (open-ended with maxReachablePage)', () => {
|
|
181
|
+
it( 'caps pages to maxReachablePage when near start', () => {
|
|
182
|
+
expect( getPageNumbers( 1, undefined, true, 2 ) ).toEqual( [ 1, 2 ] );
|
|
183
|
+
} );
|
|
184
|
+
|
|
185
|
+
it( 'shows all reachable pages when they fit', () => {
|
|
186
|
+
expect( getPageNumbers( 3, undefined, true, 4 ) ).toEqual( [ 1, 2, 3, 4 ] );
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
it( 'shows ellipsis for large reachable ranges', () => {
|
|
190
|
+
expect( getPageNumbers( 8, undefined, true, 9 ) ).toEqual( [ 1, 'ellipsis', 7, 8, 9 ] );
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
it( 'shows both ellipsis when end is far from current page', () => {
|
|
194
|
+
expect( getPageNumbers( 8, undefined, true, 15 ) ).toEqual( [
|
|
195
|
+
1,
|
|
196
|
+
'ellipsis',
|
|
197
|
+
7,
|
|
198
|
+
8,
|
|
199
|
+
9,
|
|
200
|
+
10,
|
|
201
|
+
'ellipsis',
|
|
202
|
+
15,
|
|
203
|
+
] );
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
it( 'returns all pages when maxReachablePage <= 8', () => {
|
|
207
|
+
expect( getPageNumbers( 1, undefined, true, 8 ) ).toEqual( [ 1, 2, 3, 4, 5, 6, 7, 8 ] );
|
|
208
|
+
} );
|
|
209
|
+
|
|
210
|
+
it( 'does not affect behavior when maxReachablePage is undefined', () => {
|
|
211
|
+
expect( getPageNumbers( 1, undefined, true ) ).toEqual( [ 1, 2, 3, 4, 5, 6, 7, 'ellipsis' ] );
|
|
212
|
+
} );
|
|
213
|
+
} );
|
|
214
|
+
|
|
181
215
|
describe( 'getPageNumbers (open-ended)', () => {
|
|
182
216
|
it( 'always returns 8 items when page >= 6', () => {
|
|
183
217
|
for ( let cp = 6; cp <= 20; cp++ ) {
|
|
@@ -210,6 +244,32 @@ describe( 'getPageNumbers (open-ended)', () => {
|
|
|
210
244
|
} );
|
|
211
245
|
} );
|
|
212
246
|
|
|
247
|
+
describe( '<Pagination /> with maxReachablePage', () => {
|
|
248
|
+
const maxReachableProps = {
|
|
249
|
+
currentPage: 1,
|
|
250
|
+
itemsPerPage: 20,
|
|
251
|
+
hasNextPage: true,
|
|
252
|
+
maxReachablePage: 2,
|
|
253
|
+
onPageChange: jest.fn(),
|
|
254
|
+
onItemsPerPageChange: jest.fn(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
it( 'only renders reachable page buttons', () => {
|
|
258
|
+
render( <Pagination { ...maxReachableProps } /> );
|
|
259
|
+
|
|
260
|
+
expect( screen.getByRole( 'button', { name: 'Go to page 1' } ) ).toBeInTheDocument();
|
|
261
|
+
expect( screen.getByRole( 'button', { name: 'Go to page 2' } ) ).toBeInTheDocument();
|
|
262
|
+
expect( screen.queryByRole( 'button', { name: 'Go to page 3' } ) ).not.toBeInTheDocument();
|
|
263
|
+
expect( screen.queryByRole( 'button', { name: 'Go to page 7' } ) ).not.toBeInTheDocument();
|
|
264
|
+
} );
|
|
265
|
+
|
|
266
|
+
it( 'has no accessibility violations', async () => {
|
|
267
|
+
const { container } = render( <Pagination { ...maxReachableProps } /> );
|
|
268
|
+
|
|
269
|
+
expect( await axe( container ) ).toHaveNoViolations();
|
|
270
|
+
} );
|
|
271
|
+
} );
|
|
272
|
+
|
|
213
273
|
describe( '<Pagination /> open-ended mode', () => {
|
|
214
274
|
const openEndedProps = {
|
|
215
275
|
currentPage: 5,
|
|
@@ -30,6 +30,7 @@ export interface PaginationProps {
|
|
|
30
30
|
onPageChange: ( page: number ) => void;
|
|
31
31
|
onItemsPerPageChange: ( itemsPerPage: number ) => void;
|
|
32
32
|
hasNextPage?: boolean;
|
|
33
|
+
maxReachablePage?: number;
|
|
33
34
|
variant?: PaginationVariant;
|
|
34
35
|
pageSizeOptions?: number[];
|
|
35
36
|
className?: string;
|
|
@@ -43,39 +44,72 @@ function range( start: number, end: number ): number[] {
|
|
|
43
44
|
return Array.from( { length: end - start + 1 }, ( _, i ) => start + i );
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** Total number of visible items (page numbers + ellipsis indicators) in the pagination bar. */
|
|
48
|
+
const VISIBLE_PAGE_SLOTS = 8;
|
|
49
|
+
|
|
50
|
+
/** When currentPage <= this value, the "near start" layout is used (no leading ellipsis). */
|
|
51
|
+
const NEAR_START_THRESHOLD = 5;
|
|
52
|
+
|
|
53
|
+
/** Pages shown before the current page in the middle layout. */
|
|
54
|
+
const PAGES_BEFORE_CURRENT = 1;
|
|
55
|
+
|
|
56
|
+
/** Pages shown after the current page in the bounded middle layout. */
|
|
57
|
+
const PAGES_AFTER_CURRENT = 2;
|
|
58
|
+
|
|
46
59
|
export function getPageNumbers(
|
|
47
60
|
currentPage: number,
|
|
48
61
|
totalPages?: number,
|
|
49
|
-
hasNextPage?: boolean
|
|
62
|
+
hasNextPage?: boolean,
|
|
63
|
+
maxReachablePage?: number
|
|
50
64
|
): PageNumberItem[] {
|
|
65
|
+
// Resolve the last known page
|
|
51
66
|
let last: number | undefined;
|
|
52
|
-
if ( totalPages
|
|
53
|
-
if ( hasNextPage === false ) {
|
|
54
|
-
last = currentPage;
|
|
55
|
-
} else {
|
|
56
|
-
last = undefined;
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
67
|
+
if ( totalPages !== undefined ) {
|
|
59
68
|
last = Math.max( 1, Number( totalPages ) );
|
|
69
|
+
} else if ( hasNextPage === false ) {
|
|
70
|
+
last = currentPage;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
if ( last !== undefined && ( ! Number.isFinite( last ) || last < 1 ) ) {
|
|
63
74
|
return [];
|
|
64
75
|
}
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
|
|
77
|
+
// Effective end anchor: known last page, or capped reachable page
|
|
78
|
+
const end =
|
|
79
|
+
last ??
|
|
80
|
+
( maxReachablePage !== undefined ? Math.max( currentPage, maxReachablePage ) : undefined );
|
|
81
|
+
|
|
82
|
+
// Small page count — show all without ellipsis
|
|
83
|
+
if ( end !== undefined && end <= VISIBLE_PAGE_SLOTS ) {
|
|
84
|
+
return range( 1, end );
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Near start
|
|
88
|
+
if ( currentPage <= NEAR_START_THRESHOLD ) {
|
|
89
|
+
if ( end !== undefined ) return [ ...range( 1, NEAR_START_THRESHOLD + 1 ), 'ellipsis', end ];
|
|
90
|
+
return [ ...range( 1, VISIBLE_PAGE_SLOTS - 1 ), 'ellipsis' ];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Near end (bounded only — open-ended has no "end zone")
|
|
94
|
+
if ( last !== undefined && currentPage >= last - ( NEAR_START_THRESHOLD - 1 ) ) {
|
|
95
|
+
return [ 1, 'ellipsis', ...range( last - NEAR_START_THRESHOLD, last ) ];
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
//
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
// Middle
|
|
99
|
+
if ( end !== undefined ) {
|
|
100
|
+
const rangeEnd = Math.min( currentPage + PAGES_AFTER_CURRENT, end );
|
|
101
|
+
const middle = range( currentPage - PAGES_BEFORE_CURRENT, rangeEnd );
|
|
102
|
+
if ( rangeEnd >= end ) return [ 1, 'ellipsis', ...middle ];
|
|
103
|
+
return [ 1, 'ellipsis', ...middle, 'ellipsis', end ];
|
|
74
104
|
}
|
|
75
105
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
// Fully open-ended middle
|
|
107
|
+
return [
|
|
108
|
+
1,
|
|
109
|
+
'ellipsis',
|
|
110
|
+
...range( currentPage - PAGES_BEFORE_CURRENT, currentPage + PAGES_AFTER_CURRENT + 1 ),
|
|
111
|
+
'ellipsis',
|
|
112
|
+
];
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
const ItemsPerPageSelect = ( {
|
|
@@ -104,14 +138,16 @@ const PageNumbers = ( {
|
|
|
104
138
|
currentPage,
|
|
105
139
|
totalPages,
|
|
106
140
|
hasNextPage,
|
|
141
|
+
maxReachablePage,
|
|
107
142
|
onPageChange,
|
|
108
143
|
}: {
|
|
109
144
|
currentPage: number;
|
|
110
145
|
totalPages?: number;
|
|
111
146
|
hasNextPage?: boolean;
|
|
147
|
+
maxReachablePage?: number;
|
|
112
148
|
onPageChange: ( page: number ) => void;
|
|
113
149
|
} ) => {
|
|
114
|
-
const pages = getPageNumbers( currentPage, totalPages, hasNextPage );
|
|
150
|
+
const pages = getPageNumbers( currentPage, totalPages, hasNextPage, maxReachablePage );
|
|
115
151
|
|
|
116
152
|
return (
|
|
117
153
|
<>
|
|
@@ -142,16 +178,17 @@ const PageNumbers = ( {
|
|
|
142
178
|
const CompactPageSelector = ( {
|
|
143
179
|
currentPage,
|
|
144
180
|
totalPages,
|
|
181
|
+
maxReachablePage,
|
|
145
182
|
onPageChange,
|
|
146
183
|
}: {
|
|
147
184
|
currentPage: number;
|
|
148
185
|
totalPages?: number;
|
|
186
|
+
maxReachablePage?: number;
|
|
149
187
|
onPageChange: ( page: number ) => void;
|
|
150
188
|
} ) => {
|
|
151
189
|
const isOpenEnded = totalPages === undefined;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
: Array.from( { length: totalPages }, ( _, i ) => i + 1 );
|
|
190
|
+
const upperBound: number = isOpenEnded ? maxReachablePage ?? currentPage + 1 : totalPages;
|
|
191
|
+
const pageOptions = Array.from( { length: upperBound }, ( _, i ) => i + 1 );
|
|
155
192
|
|
|
156
193
|
return (
|
|
157
194
|
<Flex sx={ compactTextStyles }>
|
|
@@ -187,6 +224,7 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
187
224
|
onPageChange,
|
|
188
225
|
onItemsPerPageChange,
|
|
189
226
|
hasNextPage,
|
|
227
|
+
maxReachablePage,
|
|
190
228
|
variant = 'full',
|
|
191
229
|
pageSizeOptions = [ 20, 50, 100 ],
|
|
192
230
|
className,
|
|
@@ -228,6 +266,7 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
228
266
|
currentPage={ currentPage }
|
|
229
267
|
totalPages={ resolvedTotalPages }
|
|
230
268
|
hasNextPage={ hasNextPage }
|
|
269
|
+
maxReachablePage={ maxReachablePage }
|
|
231
270
|
onPageChange={ onPageChange }
|
|
232
271
|
/>
|
|
233
272
|
) }
|
|
@@ -236,6 +275,7 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
|
|
|
236
275
|
<CompactPageSelector
|
|
237
276
|
currentPage={ currentPage }
|
|
238
277
|
totalPages={ resolvedTotalPages }
|
|
278
|
+
maxReachablePage={ maxReachablePage }
|
|
239
279
|
onPageChange={ onPageChange }
|
|
240
280
|
/>
|
|
241
281
|
) }
|