Skip to content

Commit

Permalink
feat(pagination): improves pagination behaviour
Browse files Browse the repository at this point in the history
Length of pagination stays the same. End buttons disable when appropriate. Option to hide if only on
page.

fix #316
  • Loading branch information
stuarthendren committed Jan 27, 2023
1 parent 2da5c13 commit 923458e
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 58 deletions.
67 changes: 61 additions & 6 deletions src/components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,72 @@
import { Meta, Story } from '@storybook/react'
import React, { useState } from 'react'
import { Pagination } from '.'
import { Pagination, PaginationProps } from '.'
import { Divider } from '../Divider'
import { Label } from '../Label'
import { Radio, RadioGroup } from '../RadioGroup'
import { Stack } from '../Stack'

export default {
title: 'Components/Pagination',
component: Pagination,
} as Meta

export const Default: Story = (args) => <Pagination count={10} {...args} />

export const Controlled: Story = (args) => {
const Template: Story<PaginationProps> = (args) => {
const [page, setPage] = useState(1)
return <Pagination count={10} page={page} onPageChange={setPage} {...args} />
return <Pagination {...args} page={page} onPageChange={setPage} />
}

export const Default = Template.bind({})
Default.args = {
totalPages: 10,
}

export const ManyPages = Template.bind({})
ManyPages.args = {
totalPages: 100,
}

export const BoundaryCondition = Template.bind({})
BoundaryCondition.args = {
totalPages: 14,
}

export const ManyPages: Story = () => <Pagination count={100} page={75} />
export const ChangePadding = Template.bind({})
ChangePadding.args = {
totalPages: 20,
boundaryCount: 3,
siblingCount: 2,
}

export const LayoutOptions = Template.bind({})
LayoutOptions.args = {
totalPages: 20,
align: 'center',
spacing: 'small',
}

export const Single: Story<PaginationProps> = (args) => {
const [single, setSingle] = useState('show')

return (
<Stack>
<Label>Single Prop</Label>
<RadioGroup
orientation="horizontal"
value={single}
onValueChange={setSingle}
>
<Radio value="show" label="show" />
<Radio value="hide" label="hide" />
<Radio value="none" label="none" />
</RadioGroup>
<Divider />
<Pagination
totalPages={1}
page={1}
single={single as PaginationProps['single']}
/>
<Divider />
</Stack>
)
}
4 changes: 2 additions & 2 deletions src/components/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { renderDark, renderLight } from '../../test'
import { Default } from './Pagination.stories'

it('renders light without error', () => {
const { asFragment } = renderLight(<Default />)
const { asFragment } = renderLight(<Default totalPages={10} />)
expect(asFragment()).toBeDefined()
})

it('renders dark without error', () => {
const { asFragment } = renderDark(<Default />)
const { asFragment } = renderDark(<Default totalPages={20} />)
expect(asFragment()).toBeDefined()
})
151 changes: 101 additions & 50 deletions src/components/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import React, { ComponentProps, useCallback, useMemo } from 'react'
import { styled } from '../../stitches.config'
import { Box } from '../Box'
import { Button } from '../Button'
import { ChevronLeft, ChevronRight } from '../Icons'
import { Inline } from '../Inline'

const CommonButton = styled(Button, {
width: '1em',
Expand All @@ -13,11 +13,19 @@ const UnclickableButton = styled(Button, {
pointerEvents: 'none',
})

export interface PaginationProps {
type InlineProps = ComponentProps<typeof Inline>

export interface PaginationProps
extends Pick<InlineProps, 'align' | 'spacing'> {
/**
The total number of pages
@deprecated use totalPages
*/
count: number
count?: number
/**
The total number of pages
*/
totalPages: number
/**
Callback fired when the page is changed
*/
Expand All @@ -34,89 +42,132 @@ export interface PaginationProps {
Number of always visible pages before and after the current page.
*/
siblingCount?: number
/**
What to do if only one page
*/
single?: 'show' | 'hide' | 'none'
}

export const Pagination: React.FC<PaginationProps> = ({
count,
totalPages,
onPageChange,
page: pageOptional,
boundaryCount: boundaryCountOptional,
siblingCount: siblingCountOptional,
single = 'hide',
...inlineProps
}) => {
if (totalPages == undefined && count != undefined) {
totalPages = count
}

const page = pageOptional ?? 1
const boundaryCount = boundaryCountOptional ?? 1
const boundaryCount = boundaryCountOptional ?? 2
const siblingCount = siblingCountOptional ?? 3
const handlePageChange =
onPageChange ??
(() => {
// do nothing
})
const pages: number[] = []
for (let currentPage = 1; currentPage < count + 1; currentPage++) {
if (
Math.abs(currentPage - 1) <= boundaryCount ||
Math.abs(currentPage - count) <= boundaryCount ||
Math.abs(currentPage - page) <= siblingCount
) {
pages.push(currentPage)

const handlePrevious = useCallback(() => {
onPageChange?.(Math.max(0, page - 1))
}, [onPageChange, page])
const handleNext = useCallback(() => {
onPageChange?.(Math.min(totalPages, page + 1))
}, [onPageChange, page, totalPages])
const handleSet = useCallback(
(newPage: number) => {
onPageChange?.(newPage)
},
[onPageChange]
)

const items = useMemo(() => {
const pages: number[] = []
const maxPages = Math.min(
2 * (boundaryCount + siblingCount) + 1,
totalPages
)
for (let currentPage = 1; currentPage < totalPages + 1; currentPage++) {
if (
totalPages <= maxPages + 2 ||
(page <= boundaryCount + siblingCount + 1 &&
currentPage < maxPages - boundaryCount + 2) ||
(page >= totalPages - boundaryCount - siblingCount - 1 &&
totalPages - currentPage <= maxPages - boundaryCount) ||
Math.abs(currentPage) <= boundaryCount ||
Math.abs(currentPage - totalPages) < boundaryCount ||
Math.abs(currentPage - page) <= siblingCount
) {
pages.push(currentPage)
}
}
}
const items: (
| { type: 'page'; currentPage: number }
| { type: 'ellipsis' }
)[] = []
let prevPage: number | null = null
for (const page of pages) {
if (prevPage != null && page - prevPage > 1) {
items.push({ type: 'ellipsis' })
const items = []
let prevPage: number | null = null
for (const page of pages) {
if (prevPage != null && page - prevPage > 1) {
items.push({ type: 'ellipsis', currentPage: page - 1 })
}
items.push({ type: 'page', currentPage: page })
prevPage = page
}
items.push({ type: 'page', currentPage: page })
prevPage = page
}
return items
}, [boundaryCount, totalPages, page, siblingCount])

const visibility = useMemo(() => {
if (totalPages <= 1) {
switch (single) {
case 'show':
return {}
case 'hide':
return { visibility: 'hidden' }
case 'none':
return { display: 'none' }
default:
return {}
}
}
return {}
}, [single, totalPages])

return (
<Box css={{ display: 'flex', justifyContent: 'flex-start', gap: '$3' }}>
<ControlButton onClick={() => handlePageChange(Math.max(0, page - 1))}>
<Inline {...inlineProps} css={{ ...visibility }}>
<ControlButton disabled={page === 1} onClick={handlePrevious}>
<ChevronLeft />
</ControlButton>
{items.map((item, i) => {
if (item.type === 'page') {
const { currentPage } = item
{items.map((item) => {
const { type, currentPage } = item
if (type === 'page') {
if (currentPage === page) {
return <SelectedPage key={i} page={currentPage} />
return <SelectedPage key={currentPage} page={currentPage} />
} else {
return (
<PageButton
key={i}
key={currentPage}
page={currentPage}
onClick={() => {
handlePageChange(currentPage)
}}
onSetPage={handleSet}
/>
)
}
} else if (item.type === 'ellipsis') {
return <Ellipsis key={i} />
} else if (type === 'ellipsis') {
return <Ellipsis key={currentPage} />
} else {
return null
}
})}
<ControlButton
onClick={() => handlePageChange(Math.min(count, page + 1))}
>
<ControlButton disabled={page === totalPages} onClick={handleNext}>
<ChevronRight />
</ControlButton>
</Box>
</Inline>
)
}

const PageButton: React.FC<
{
React.ComponentProps<typeof Button> & {
page: number
} & React.ComponentProps<typeof Button>
> = ({ page, ...props }) => {
onSetPage: (page: number) => void
}
> = ({ page, onSetPage, ...props }) => {
const onClick = useCallback(() => onSetPage(page), [onSetPage, page])
return (
<CommonButton variant="tertiary" {...props}>
<CommonButton variant="tertiary" {...props} onClick={onClick}>
{page}
</CommonButton>
)
Expand Down

0 comments on commit 923458e

Please sign in to comment.