Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/pagination #5525

Merged
196 changes: 85 additions & 111 deletions packages/react/src/Pagination/model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
164 changes: 164 additions & 0 deletions packages/react/src/__tests__/Pagination/PaginationModel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,170 @@ function last(array: Array<any>, 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')
Expand Down
Loading