@instructure/ui-pagination 8.51.0 → 8.51.1-snapshot-5

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.
@@ -0,0 +1,210 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import React from 'react'
26
+
27
+ import { render } from '@testing-library/react'
28
+ import '@testing-library/jest-dom'
29
+
30
+ import { Pagination } from '../index'
31
+
32
+ describe('<Pagination />', () => {
33
+ describe('with minimal config', () => {
34
+ it('should render the correct pages - 1', () => {
35
+ const { container } = render(
36
+ <Pagination
37
+ variant="compact"
38
+ labelNext="Next Page"
39
+ labelPrev="Previous Page"
40
+ totalPageNumber={9}
41
+ />
42
+ )
43
+ expect(container.firstChild).toHaveTextContent('12...9Next Page')
44
+ })
45
+ it('should render the correct pages - 2', () => {
46
+ const { container } = render(
47
+ <Pagination
48
+ variant="compact"
49
+ labelNext="Next Page"
50
+ labelPrev="Previous Page"
51
+ totalPageNumber={9}
52
+ currentPage={5}
53
+ />
54
+ )
55
+ expect(container.firstChild).toHaveTextContent(
56
+ 'Previous Page1...456...9Next Page'
57
+ )
58
+ })
59
+ it('should render the correct pages - 3', () => {
60
+ const { container } = render(
61
+ <Pagination
62
+ variant="compact"
63
+ labelNext="Next Page"
64
+ labelPrev="Previous Page"
65
+ totalPageNumber={9}
66
+ currentPage={5}
67
+ siblingCount={1}
68
+ boundaryCount={3}
69
+ />
70
+ )
71
+ expect(container.firstChild).toHaveTextContent(
72
+ 'Previous Page123456789Next Page'
73
+ )
74
+ })
75
+ it('should render the correct pages - 4', () => {
76
+ const { container } = render(
77
+ <Pagination
78
+ variant="compact"
79
+ labelNext="Next Page"
80
+ labelPrev="Previous Page"
81
+ totalPageNumber={9}
82
+ currentPage={5}
83
+ siblingCount={1}
84
+ boundaryCount={2}
85
+ />
86
+ )
87
+ expect(container.firstChild).toHaveTextContent(
88
+ 'Previous Page12...456...89Next Page'
89
+ )
90
+ })
91
+ it('should render the correct pages - 5', () => {
92
+ const { container } = render(
93
+ <Pagination
94
+ variant="compact"
95
+ labelNext="Next Page"
96
+ labelPrev="Previous Page"
97
+ totalPageNumber={9}
98
+ currentPage={5}
99
+ siblingCount={100}
100
+ />
101
+ )
102
+ expect(container.firstChild).toHaveTextContent(
103
+ 'Previous Page123456789Next Page'
104
+ )
105
+ })
106
+ it('should render the correct pages - 6', () => {
107
+ const { container } = render(
108
+ <Pagination
109
+ variant="compact"
110
+ labelNext="Next Page"
111
+ labelPrev="Previous Page"
112
+ totalPageNumber={9}
113
+ currentPage={5}
114
+ boundaryCount={100}
115
+ />
116
+ )
117
+ expect(container.firstChild).toHaveTextContent(
118
+ 'Previous Page123456789Next Page'
119
+ )
120
+ })
121
+ it('should render the correct pages - 7', () => {
122
+ const { container } = render(
123
+ <Pagination
124
+ variant="compact"
125
+ labelNext="Next Page"
126
+ labelPrev="Previous Page"
127
+ totalPageNumber={9}
128
+ currentPage={1}
129
+ boundaryCount={3}
130
+ siblingCount={1}
131
+ />
132
+ )
133
+ expect(container.firstChild).toHaveTextContent('123...789Next Page')
134
+ })
135
+ it('should render the correct ellipsis', () => {
136
+ const { container } = render(
137
+ <Pagination
138
+ variant="compact"
139
+ labelNext="Next Page"
140
+ labelPrev="Previous Page"
141
+ totalPageNumber={9}
142
+ currentPage={1}
143
+ boundaryCount={3}
144
+ siblingCount={1}
145
+ ellipsis="<->"
146
+ />
147
+ )
148
+ expect(container.firstChild).toHaveTextContent('123<->789Next Page')
149
+ })
150
+ it('should render custom buttons', () => {
151
+ const pageMap = ['A-G', 'H-J', 'K-M', 'N-Q', 'R-Z']
152
+ const { container } = render(
153
+ <Pagination
154
+ variant="full"
155
+ labelNext="Next Page"
156
+ labelPrev="Previous Page"
157
+ totalPageNumber={5}
158
+ currentPage={1}
159
+ renderPageIndicator={(page) => pageMap[page - 1]}
160
+ />
161
+ )
162
+ expect(container.firstChild).toHaveTextContent('A-GH-JK-MN-QR-Z')
163
+ })
164
+ it('should render huge "totalPageNumber"s properly', () => {
165
+ const { container } = render(
166
+ <Pagination
167
+ variant="compact"
168
+ labelNext="Next Page"
169
+ labelPrev="Previous Page"
170
+ totalPageNumber={1000000000000000}
171
+ currentPage={5678}
172
+ />
173
+ )
174
+ expect(container.firstChild).toHaveTextContent(
175
+ 'Previous Page1...567756785679...1000000000000000Next Page'
176
+ )
177
+ })
178
+ it('should render first and last buttons', () => {
179
+ const { container } = render(
180
+ <Pagination
181
+ variant="compact"
182
+ labelNext="Next Page"
183
+ labelPrev="Previous Page"
184
+ labelFirst="First Page"
185
+ labelLast="Last Page"
186
+ totalPageNumber={100}
187
+ currentPage={5}
188
+ withFirstAndLastButton
189
+ />
190
+ )
191
+ expect(container.firstChild).toHaveTextContent(
192
+ 'First PagePrevious Page1...456...100Next PageLast Page'
193
+ )
194
+ })
195
+ it('should render every page if boundary and sibling counts are big enough', () => {
196
+ const { container } = render(
197
+ <Pagination
198
+ variant="compact"
199
+ labelNext="Next Page"
200
+ labelPrev="Previous Page"
201
+ totalPageNumber={10}
202
+ currentPage={1}
203
+ siblingCount={5}
204
+ boundaryCount={4}
205
+ />
206
+ )
207
+ expect(container.firstChild).toHaveTextContent('12345678910Next Page')
208
+ })
209
+ })
210
+ })
@@ -62,7 +62,9 @@ const childrenArray = (props: PaginationProps) => {
62
62
  }
63
63
 
64
64
  function propsHaveCompactView(props: PaginationProps) {
65
- return props.variant === 'compact' && childrenArray(props).length > 5
65
+ if (props.children)
66
+ return props.variant === 'compact' && childrenArray(props).length > 5
67
+ return props.variant === 'compact' && props.totalPageNumber! > 5
66
68
  }
67
69
 
68
70
  type ArrowConfig = {
@@ -96,7 +98,13 @@ class Pagination extends Component<PaginationProps> {
96
98
  currentPage: number,
97
99
  numberOfPages: number
98
100
  ) => `Select page (${currentPage} of ${numberOfPages})`,
99
- shouldHandleFocus: true
101
+ shouldHandleFocus: true,
102
+ totalPageNumber: 0,
103
+ currentPage: 1,
104
+ siblingCount: 1,
105
+ boundaryCount: 1,
106
+ ellipsis: '...',
107
+ renderPageIndicator: (page: number) => page
100
108
  }
101
109
 
102
110
  static Page = PaginationButton
@@ -110,6 +118,7 @@ class Pagination extends Component<PaginationProps> {
110
118
  private _lastButton: HTMLButtonElement | null = null
111
119
 
112
120
  ref: Element | null = null
121
+ currentPageRef: PaginationButton | null = null
113
122
 
114
123
  constructor(props: PaginationProps) {
115
124
  super(props)
@@ -167,6 +176,18 @@ class Pagination extends Component<PaginationProps> {
167
176
  ) {
168
177
  this.props.makeStyles?.()
169
178
 
179
+ // set focus on the currently active page
180
+ if (
181
+ this.props.currentPage !== prevProps.currentPage &&
182
+ document.activeElement !== this._firstButton &&
183
+ document.activeElement !== this._prevButton &&
184
+ document.activeElement !== this._nextButton &&
185
+ document.activeElement !== this._lastButton
186
+ ) {
187
+ // @ts-expect-error fix typing
188
+ this.currentPageRef?.ref?.focus?.()
189
+ }
190
+
170
191
  if (
171
192
  !this.props.shouldHandleFocus ||
172
193
  (!propsHaveCompactView(prevProps) && !propsHaveCompactView(this.props))
@@ -243,7 +264,25 @@ class Pagination extends Component<PaginationProps> {
243
264
  )
244
265
  }
245
266
 
267
+ renderDefaultPageInput = () => {
268
+ const { currentPage, totalPageNumber } = this.props
269
+ return (
270
+ <PaginationPageInput
271
+ numberOfPages={totalPageNumber!}
272
+ currentPageIndex={currentPage! - 1}
273
+ onChange={(_e, nextPageIndex) =>
274
+ this.props.onPageChange?.(nextPageIndex + 1, currentPage!)
275
+ }
276
+ screenReaderLabel={this.props.screenReaderLabelNumberInput!}
277
+ label={this.props.labelNumberInput}
278
+ disabled={this.props.disabled}
279
+ inputRef={this.handleInputRef}
280
+ />
281
+ )
282
+ }
283
+
246
284
  renderPageInput(currentPageIndex: number) {
285
+ if (!this.props.children) return this.renderDefaultPageInput()
247
286
  return (
248
287
  <PaginationPageInput
249
288
  numberOfPages={this.childPages.length}
@@ -267,7 +306,113 @@ class Pagination extends Component<PaginationProps> {
267
306
  this.childPages[pageIndex].props.onClick?.(event)
268
307
  }
269
308
 
309
+ handleNavigation = (nextIndex: number, previousIndex: number) => {
310
+ const { onPageChange } = this.props
311
+ if (typeof onPageChange === 'function') {
312
+ onPageChange(nextIndex, previousIndex)
313
+ }
314
+ }
315
+
316
+ renderPagesInInterval = (from: number, to: number, currentPage: number) => {
317
+ if (to - from > 1000)
318
+ throw new Error('Pagination: too many pages (more than 1000)')
319
+ const pages = []
320
+ for (let i = from; i <= to; i++) {
321
+ pages.push(
322
+ <Pagination.Page
323
+ ref={(e) => (i === currentPage ? (this.currentPageRef = e) : null)}
324
+ key={i}
325
+ onClick={() => this.handleNavigation(i, currentPage)}
326
+ current={i === currentPage}
327
+ >
328
+ {this.props.renderPageIndicator?.(i, currentPage)}
329
+ </Pagination.Page>
330
+ )
331
+ }
332
+ return pages
333
+ }
334
+
335
+ renderDefaultPages = () => {
336
+ const {
337
+ ellipsis,
338
+ currentPage,
339
+ totalPageNumber,
340
+ siblingCount,
341
+ boundaryCount,
342
+ variant
343
+ } = this.props
344
+ const pages: any = []
345
+ if (
346
+ totalPageNumber! <= 2 * boundaryCount! ||
347
+ totalPageNumber! <= 1 + siblingCount! + boundaryCount! ||
348
+ variant === 'full'
349
+ ) {
350
+ return this.renderPagesInInterval(1, totalPageNumber!, currentPage!)
351
+ }
352
+
353
+ if (currentPage! > boundaryCount! + siblingCount! + 1) {
354
+ pages.push(this.renderPagesInInterval(1, boundaryCount!, currentPage!))
355
+ pages.push(ellipsis)
356
+ if (
357
+ currentPage! - siblingCount! >
358
+ totalPageNumber! - boundaryCount! + 1
359
+ ) {
360
+ pages.push(
361
+ this.renderPagesInInterval(
362
+ totalPageNumber! - boundaryCount! + 1,
363
+ totalPageNumber!,
364
+ currentPage!
365
+ )
366
+ )
367
+ return pages
368
+ }
369
+ pages.push(
370
+ this.renderPagesInInterval(
371
+ currentPage! - siblingCount!,
372
+ currentPage!,
373
+ currentPage!
374
+ )
375
+ )
376
+ } else {
377
+ pages.push(
378
+ this.renderPagesInInterval(
379
+ 1,
380
+ Math.max(currentPage!, boundaryCount!),
381
+ currentPage!
382
+ )
383
+ )
384
+ }
385
+
386
+ if (currentPage! < totalPageNumber! - (siblingCount! + boundaryCount!)) {
387
+ pages.push(
388
+ this.renderPagesInInterval(
389
+ Math.max(currentPage!, boundaryCount!) + 1,
390
+ currentPage! + siblingCount!,
391
+ currentPage!
392
+ )
393
+ )
394
+ pages.push(ellipsis)
395
+ pages.push(
396
+ this.renderPagesInInterval(
397
+ totalPageNumber! - boundaryCount! + 1,
398
+ totalPageNumber!,
399
+ currentPage!
400
+ )
401
+ )
402
+ } else {
403
+ pages.push(
404
+ this.renderPagesInInterval(
405
+ currentPage! + 1,
406
+ totalPageNumber!,
407
+ currentPage!
408
+ )
409
+ )
410
+ }
411
+ return pages
412
+ }
413
+
270
414
  renderPages(currentPageIndex: number) {
415
+ if (!this.props.children) return this.renderDefaultPages()
271
416
  const allPages = this.childPages
272
417
  let visiblePages = allPages
273
418
 
@@ -354,12 +499,61 @@ class Pagination extends Component<PaginationProps> {
354
499
  }
355
500
  }
356
501
 
502
+ renderDefaultArrowButton = (direction: PaginationArrowDirections) => {
503
+ if (
504
+ !this.withFirstAndLastButton &&
505
+ (direction === 'first' || direction === 'last')
506
+ ) {
507
+ return null
508
+ }
509
+ // We don't display the arrows in "compact" variant under 6 items
510
+ if (!(propsHaveCompactView(this.props) || this.inputMode)) {
511
+ return null
512
+ }
513
+ const { totalPageNumber, currentPage } = this.props
514
+ const { label, shouldEnableIcon, handleButtonRef } = this.getArrowVariant(
515
+ direction,
516
+ currentPage! - 1,
517
+ totalPageNumber!
518
+ )
519
+
520
+ const disabled = this.props.disabled || !shouldEnableIcon
521
+ const onClick = () => {
522
+ if (direction === 'first') {
523
+ this.handleNavigation(1, currentPage!)
524
+ }
525
+ if (direction === 'prev') {
526
+ this.handleNavigation(Math.max(currentPage! - 1, 1), currentPage!)
527
+ }
528
+ if (direction === 'next') {
529
+ this.handleNavigation(
530
+ Math.min(currentPage! + 1, totalPageNumber!),
531
+ currentPage!
532
+ )
533
+ }
534
+ if (direction === 'last') {
535
+ this.handleNavigation(totalPageNumber!, currentPage!)
536
+ }
537
+ }
538
+
539
+ return shouldEnableIcon || this.showDisabledButtons ? (
540
+ <PaginationArrowButton
541
+ direction={direction}
542
+ data-direction={direction}
543
+ label={label}
544
+ onClick={onClick}
545
+ disabled={disabled}
546
+ buttonRef={handleButtonRef}
547
+ />
548
+ ) : null
549
+ }
550
+
357
551
  renderArrowButton(
358
552
  direction: PaginationArrowDirections,
359
553
  currentPageIndex: number
360
554
  ) {
555
+ if (!this.props.children) return this.renderDefaultArrowButton(direction)
361
556
  const { childPages } = this
362
-
363
557
  // We don't display the arrows in "compact" variant under 6 items
364
558
  if (!(propsHaveCompactView(this.props) || this.inputMode)) {
365
559
  return null
@@ -394,8 +588,6 @@ class Pagination extends Component<PaginationProps> {
394
588
  }
395
589
 
396
590
  render() {
397
- if (!this.props.children) return null
398
-
399
591
  const currentPageIndex = fastFindIndex(
400
592
  this.childPages,
401
593
  (p) => p && p.props && p.props.current
@@ -146,6 +146,46 @@ type PaginationOwnProps = {
146
146
  * Set this property to `false` to prevent this behavior.
147
147
  */
148
148
  shouldHandleFocus?: boolean
149
+
150
+ /**
151
+ * The total number of pages
152
+ */
153
+ totalPageNumber?: number
154
+
155
+ /**
156
+ * The current page number
157
+ */
158
+ currentPage?: number
159
+
160
+ /**
161
+ * The number of pages to display before and after the current page
162
+ */
163
+ siblingCount?: number
164
+
165
+ /**
166
+ * The number of always visible pages at the beginning and end
167
+ * of the pagination component
168
+ * */
169
+ boundaryCount?: number
170
+
171
+ /**
172
+ * Called when page number is changed
173
+ */
174
+ onPageChange?: (next: number, prev: number) => void
175
+
176
+ /**
177
+ * Renders the visible pages
178
+ */
179
+ renderPageIndicator?: (
180
+ pageIndex: number,
181
+ currentPage: number
182
+ ) => React.ReactNode
183
+
184
+ /**
185
+ * The ellipsis
186
+ * (e.g. "...")
187
+ */
188
+ ellipsis?: React.ReactNode
149
189
  }
150
190
 
151
191
  type PropKeys = keyof PaginationOwnProps
@@ -154,7 +194,8 @@ type AllowedPropKeys = Readonly<Array<PropKeys>>
154
194
 
155
195
  type PaginationProps = PaginationOwnProps &
156
196
  WithStyleProps<null, PaginationStyle> &
157
- OtherHTMLAttributes<PaginationOwnProps> & WithDeterministicIdProps
197
+ OtherHTMLAttributes<PaginationOwnProps> &
198
+ WithDeterministicIdProps
158
199
 
159
200
  type PaginationStyle = ComponentStyle<'pagination' | 'pages'>
160
201
 
@@ -175,7 +216,14 @@ const propTypes: PropValidators<PropKeys> = {
175
216
  as: PropTypes.elementType,
176
217
  elementRef: PropTypes.func,
177
218
  inputRef: PropTypes.func,
178
- shouldHandleFocus: PropTypes.bool
219
+ shouldHandleFocus: PropTypes.bool,
220
+ totalPageNumber: PropTypes.number,
221
+ currentPage: PropTypes.number,
222
+ siblingCount: PropTypes.number,
223
+ boundaryCount: PropTypes.number,
224
+ onPageChange: PropTypes.func,
225
+ renderPageIndicator: PropTypes.func,
226
+ ellipsis: PropTypes.node
179
227
  }
180
228
 
181
229
  const allowedProps: AllowedPropKeys = [
@@ -195,7 +243,14 @@ const allowedProps: AllowedPropKeys = [
195
243
  'as',
196
244
  'elementRef',
197
245
  'inputRef',
198
- 'shouldHandleFocus'
246
+ 'shouldHandleFocus',
247
+ 'totalPageNumber',
248
+ 'currentPage',
249
+ 'onPageChange',
250
+ 'siblingCount',
251
+ 'boundaryCount',
252
+ 'renderPageIndicator',
253
+ 'ellipsis'
199
254
  ]
200
255
 
201
256
  type PaginationSnapshot = {