diff --git a/packages/react/src/Pagination/model.tsx b/packages/react/src/Pagination/model.tsx index 5a65d5f207b..70e8ad82770 100644 --- a/packages/react/src/Pagination/model.tsx +++ b/packages/react/src/Pagination/model.tsx @@ -13,134 +13,108 @@ export function buildPaginationModel( return [prev, next] } - // The number of pages shown in the middle window; the current page is always in the middle - // and we show surroundingPageCount pages on either side - const middleWindowCount = surroundingPageCount + 1 + surroundingPageCount - - // The total number of pages shown including the margin pages - const totalPagesShown = marginPageCount + middleWindowCount + marginPageCount - - // Only needs ellipsis if there are more pages than we can display - const needEllipsis = pageCount > totalPagesShown - - // Display the ellipsis at the start of the list of pages if the current page is further away - // than surroundingPageCount + marginPageCount from the first page. - // NOTE: we expand ellipses that collapse only one page later on. - - // Example: - // surroundingPageCount: 2 - // marginPageCount: 1 - // [1, ..., 3, 4, _5_, 6, 7] - const hasStartEllipsis = currentPage > surroundingPageCount + marginPageCount + 1 - - // Display the ellipsis at the end of the list of pages if the current page is further away - // than surroundingPageCount - marginPageCount from the last page. - // NOTE: we expand ellipses that collapse only one page later on - - // Example: - // surroundingPageCount: 2 - // marginPageCount: 1 - // [1, ..., 9, 10, _11_, 12, 13, ..., 15] - const hasEndEllipsis = currentPage < pageCount - surroundingPageCount - marginPageCount - - let state: PaginationState = 'noEllipsis' - if (needEllipsis) { - if (hasStartEllipsis && hasEndEllipsis) { - state = 'bothEllipsis' - } else if (hasStartEllipsis) { - state = 'startEllipsis' - } else if (hasEndEllipsis) { - state = 'endEllipsis' - } + if (pageCount <= 0) { + return [prev, {...next, disabled: true}] } const pages: PageType[] = [] - switch (state) { - case 'noEllipsis': { - // [1, 2, 3, 4] - addPages(1, pageCount) - break - } - case 'startEllipsis': { - addPages(1, marginPageCount, true) - // To keep the number of pages shown consistent, add the middleWindowCount - // and marginPageCount instead of overlapping them. - - // middleWindowCount: 5 - // marginPageCount: 1 - // [1, ..., 9, 10, _11_, 12, 13, 14, 15] - // [1, ..., 9, 10, 11, _12_, 13, 14, 15] - // [1, ..., 9, 10, 11, 12, _13_, 14, 15] - // [1, ..., 9, 10, 11, 12, 13, _14_, 15] - // [1, ..., 9, 10, 11, 12, 13, 14, _15_] - - addEllipsis(marginPageCount, pageCount - middleWindowCount - marginPageCount) - - addPages(pageCount - middleWindowCount - marginPageCount, pageCount) - break - } - case 'endEllipsis': { - // To keep the number of pages shown consistent, add the middleWindowCount - // and marginPageCount instead of overlapping them. + // number of pages shown on each side of the current page + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // standardGap: 3 + const standardGap = surroundingPageCount + marginPageCount - // middleWindowCount: 5 - // marginPageCount: 1 - // [1, 2, 3, 4, _5_, 6, 7, ..., 15] - // [1, 2, 3, _4_, 5, 6, 7, ..., 15] - // [1, 2, _3_, 4, 5, 6, 7, ..., 15] - // [1, _2_, 3, 4, 5, 6, 7, ..., 15] - // [_1_, 2, 3, 4, 5, 6, 7, ..., 15] + // the maximum number of pages that can be shown at a given time + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // maxVisiblePages: 7 + const maxVisiblePages = standardGap + standardGap + 1 - addPages(1, middleWindowCount + marginPageCount + 1, true) + // if the number of pages is less than the maximum number of pages that can be shown just return all of them + if (pageCount <= maxVisiblePages) { + addPages(1, pageCount, false) + return [prev, ...pages, next] + } - addEllipsis(middleWindowCount + marginPageCount + 1, pageCount - marginPageCount + 1) + // startGap is the number of pages hidden by the start ellipsis + // startOffset is the number of pages to offset at the start to compensate + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // startGap: 5 + // startOffset: 0 + // when the margin and the surrounding windows overlap. + // [1, _2_, 3, 4, 5, 6, ..., 15] + // startGap = 0 + // startOffset: -3 <-- + let startGap = 0 + let startOffset = 0 + + // When there is overlap + if (currentPage - standardGap - 1 <= 1) { + startOffset = currentPage - standardGap - 2 + } else { + startGap = currentPage - standardGap - 1 + } - addPages(pageCount - marginPageCount + 1, pageCount) - break - } - case 'bothEllipsis': { - // There is no window overlap in this case, it will always have this shape: - // middleWindowCount: 5 - // marginPageCount: 1 - // [1, ..., 4, 5, 6, _7_, 8, 9, ..., 15] + // These are equivalent to startGap and startOffset but at the end of the list + let endGap = 0 + let endOffset = 0 - addPages(1, marginPageCount, true) + // When there is overlap + if (pageCount - currentPage - standardGap <= 1) { + endOffset = pageCount - currentPage - standardGap - 1 + } else { + endGap = pageCount - currentPage - standardGap + } - addEllipsis(marginPageCount, currentPage - surroundingPageCount) + const hasStartEllipsis = startGap > 0 + const hasEndEllipsis = endGap > 0 - addPages(currentPage - surroundingPageCount, currentPage + surroundingPageCount, true) + // add pages "before" the start ellipsis (if any) + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // marginPageCount: 1 + // addPages(1, 1, true) + addPages(1, marginPageCount, hasStartEllipsis) - addEllipsis(currentPage + surroundingPageCount, pageCount - marginPageCount + 1) + if (hasStartEllipsis) { + addEllipsis(marginPageCount) + } - addPages(pageCount - marginPageCount + 1, pageCount) - break - } + // add middle pages + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // marginPageCount: 1 + // surroundingPageCount: 2 + // startGap: 5 + // startOffset: 0 + // endGap: 3 + // endOffset: 0 + // addPages(7, 11, true) + addPages( + marginPageCount + startGap + endOffset + 1, + pageCount - startOffset - endGap - marginPageCount, + hasEndEllipsis, + ) + + if (hasEndEllipsis) { + addEllipsis(pageCount - startOffset - endGap - marginPageCount) } - return [prev, ...pages, next] + // add pages "after" the start ellipsis (if any) + // [1, ..., 7, 8, _9_, 10, 11, ..., 15] + // marginPageCount: 1 + // surroundingPageCount: 2 + // startGap: 5 + // startOffset: 0 + // endGap: 3 + // endOffset: 0 + // addPages(15, 15) + addPages(pageCount - marginPageCount + 1, pageCount) - function addEllipsis(previousPage: number, nextPage: number): void { - // If there's only one page between the previous and next page, we don't need an ellipsis - // as it will take the same visual space as the page. + return [prev, ...pages, next] - // Example: - // surroundingPageCount: 2 - // marginPageCount: 1 - // [1, 2, 3, 4, _5_, 6, 7] <- no ellipsis, we render page 2 instead. - if (previousPage + 2 === nextPage) { - pages.push({ - type: 'NUM', - num: previousPage + 1, - selected: previousPage + 1 === currentPage, - precedesBreak: false, - }) - } else { - pages.push({ - type: 'BREAK', - num: previousPage + 1, - }) - } + function addEllipsis(previousPage: number): void { + pages.push({ + type: 'BREAK', + num: previousPage + 1, + }) } function addPages(start: number, end: number, precedesBreak: boolean = false): void { diff --git a/packages/react/src/__tests__/Pagination/PaginationModel.test.tsx b/packages/react/src/__tests__/Pagination/PaginationModel.test.tsx index dbeda7dbcef..a7ca5a88c1e 100644 --- a/packages/react/src/__tests__/Pagination/PaginationModel.test.tsx +++ b/packages/react/src/__tests__/Pagination/PaginationModel.test.tsx @@ -14,6 +14,170 @@ function last(array: Array, count = 1) { } describe('Pagination model', () => { + it('correctly handles negative pages', () => { + const model = buildPaginationModel(-10, 1, true, 1, 2) + expect(first(model).type).toEqual('PREV') + expect(first(model).disabled).toBe(true) + expect(last(model).type).toEqual('NEXT') + expect(last(model).disabled).toBe(true) + expect(model.length).toBe(2) + }) + + it('correctly handles zero pages', () => { + const model = buildPaginationModel(0, 1, true, 1, 2) + expect(first(model).type).toEqual('PREV') + expect(first(model).disabled).toBe(true) + expect(last(model).type).toEqual('NEXT') + expect(last(model).disabled).toBe(true) + expect(model.length).toBe(2) + }) + + it('correctly handles 1 page', () => { + const model = buildPaginationModel(1, 1, true, 1, 2) + expect(first(model).type).toEqual('PREV') + expect(first(model).disabled).toBe(true) + expect(last(model).type).toEqual('NEXT') + expect(last(model).disabled).toBe(true) + expect(model.length).toBe(3) + }) + + it('correctly handles zero margin pages', () => { + const model = buildPaginationModel(6, 2, true, 0, 2) + + const expected = [ + { + type: 'PREV', + num: 1, + disabled: false, + }, + { + type: 'NUM', + num: 1, + selected: false, + precedesBreak: false, + }, + { + type: 'NUM', + num: 2, + selected: true, + precedesBreak: false, + }, + { + type: 'NUM', + num: 3, + selected: false, + precedesBreak: false, + }, + { + type: 'NUM', + num: 4, + selected: false, + precedesBreak: false, + }, + { + type: 'NUM', + num: 5, + selected: false, + precedesBreak: false, + }, + { + type: 'NUM', + num: 6, + selected: false, + precedesBreak: true, + }, + { + type: 'BREAK', + num: 7, + }, + { + type: 'NEXT', + num: 3, + disabled: false, + }, + ] + + expect(model).toMatchObject(expected) + }) + + it('correctly handles zero surrounding pages', () => { + const model = buildPaginationModel(7, 4, true, 1, 0) + + const expected = [ + { + type: 'PREV', + num: 3, + disabled: false, + }, + { + type: 'NUM', + num: 1, + selected: false, + precedesBreak: true, + }, + { + type: 'BREAK', + num: 2, + }, + { + type: 'NUM', + num: 4, + selected: true, + precedesBreak: true, + }, + { + type: 'BREAK', + num: 5, + }, + { + type: 'NUM', + num: 7, + selected: false, + precedesBreak: false, + }, + { + type: 'NEXT', + num: 5, + disabled: false, + }, + ] + + expect(model).toMatchObject(expected) + }) + + it('correctly handles zero margin and surrounding pages', () => { + const model = buildPaginationModel(50, 3, true, 0, 0) + + const expected = [ + { + type: 'PREV', + num: 2, + disabled: false, + }, + { + type: 'BREAK', + num: 1, + }, + { + type: 'NUM', + num: 3, + selected: true, + precedesBreak: true, + }, + { + type: 'BREAK', + num: 4, + }, + { + type: 'NEXT', + num: 4, + disabled: false, + }, + ] + + expect(model).toMatchObject(expected) + }) + it('sets disabled on prev links', () => { const model1 = buildPaginationModel(10, 1, true, 1, 2) expect(first(model1).type).toEqual('PREV')