Skip to content

Commit

Permalink
refactor: pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
lisalupi committed Dec 4, 2024
1 parent 6740685 commit e751e39
Show file tree
Hide file tree
Showing 12 changed files with 3,438 additions and 903 deletions.
9 changes: 9 additions & 0 deletions .changeset/dirty-fishes-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@ultraviolet/ui": minor
---

Refactor and enhancement of `<Pagination />` :
- Number of results on the bottom left
- Number of items to display
- Styling

152 changes: 152 additions & 0 deletions packages/ui/src/components/Pagination/PaginationButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import styled from '@emotion/styled'
import React, { useCallback, useMemo } from 'react'
import { Button } from '../Button'
import { getPageNumbers } from './getPageNumbers'

const PageNumbersContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.space['1']};
margin: 0 ${({ theme }) => theme.space['1']};
`

const PageSwitchContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.space['1']};
`

const StyledContainer = styled.div`
display: flex;
`
const PageButton = styled(Button)`
width: ${({ theme }) => theme.space[6]};
`

const Ellipsis = styled(PageButton)`
&:hover {
background: none;
cursor: default;
}
&:active, &:focus {
box-shadow: none;
cursor: default;
background: none;
}
`
type PaginationButtonsProps = {
page: number
disabled: boolean
onChange: (newPage: number) => void
pageCount: number
pageTabCount?: number
className?: string
'data-testid'?: string
}

export const PaginationButtons = ({
page,
disabled,
onChange,
pageCount,
pageTabCount,
'data-testid': dataTestId,
className,
}: PaginationButtonsProps) => {
const goToNextPage = useCallback(() => {
onChange(page + 1)
}, [onChange, page])

const goToPreviousPage = useCallback(() => {
onChange(page - 1)
}, [onChange, page])

const pageNumbersToDisplay = useMemo(
() => (pageCount > 1 ? getPageNumbers(page, pageCount, pageTabCount) : [1]),
[page, pageCount, pageTabCount],
)

const handlePageClick = useCallback(
(pageNumber: number) => () => {
onChange(pageNumber)
},
[onChange],
)

return (
<StyledContainer className={className} data-testid={dataTestId}>
<PageSwitchContainer>
<Button
aria-label="Back"
disabled={page <= 1 || disabled}
variant="outlined"
sentiment="primary"
onClick={goToPreviousPage}
icon="arrow-left"
/>
</PageSwitchContainer>
<PageNumbersContainer>
{pageNumbersToDisplay.map((pageNumber, index) => {
if (
index === 0 ||
pageNumbersToDisplay[index - 1] === pageNumber - 1
) {
return (
<PageButton
aria-label={`Page ${pageNumber}`}
aria-current={pageNumber === page}
key={`pagination-page-${pageNumber}`}
disabled={disabled}
variant="outlined"
sentiment={pageNumber === page ? 'primary' : 'neutral'}
onClick={handlePageClick(pageNumber)}
type="button"
>
{pageNumber}
</PageButton>
)
}

return (
<React.Fragment key={pageNumber}>
<Ellipsis
aria-label="ellipsis"
key={`ellipsis-page-${pageNumber}`}
disabled={disabled}
variant="ghost"
sentiment="neutral"
type="button"
tabIndex={-1}
>
...
</Ellipsis>
<PageButton
aria-label={`Page ${pageNumber}`}
aria-current={pageNumber === page}
key={`pagination-page-${pageNumber}`}
disabled={disabled}
variant="outlined"
sentiment={pageNumber === page ? 'primary' : 'neutral'}
onClick={handlePageClick(pageNumber)}
type="button"
>
{pageNumber}
</PageButton>
</React.Fragment>
)
})}
</PageNumbersContainer>
<PageSwitchContainer>
<Button
aria-label="Next"
disabled={page >= pageCount || disabled}
variant="outlined"
sentiment="primary"
onClick={goToNextPage}
icon="arrow-right"
/>
</PageSwitchContainer>
</StyledContainer>
)
}
73 changes: 73 additions & 0 deletions packages/ui/src/components/Pagination/PerPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import styled from '@emotion/styled'
import type { Dispatch, SetStateAction } from 'react'
import { SelectInputV2 } from '../SelectInputV2'
import { Stack } from '../Stack'
import { Text } from '../Text'

const optionsItemsPerPage = [
{
value: '10',
label: '10',
},
{
value: '25',
label: '25',
},
{
value: '50',
label: '50',
},
{
value: '100',
label: '100',
},
]

const StyledSelectInput = styled(SelectInputV2)`
width: fit-content;
min-width: none;
`

type PerPageProps = {
perPage: number
onChangePerPage?: (perPage: number) => void
perPageText?: string
setPerPage: Dispatch<SetStateAction<number>>
numberOfItemsText?: string
page: number
numberOfItems: number
}

export const PerPage = ({
perPage,
onChangePerPage,
perPageText,
setPerPage,
numberOfItemsText,
page,
numberOfItems,
}: PerPageProps) => {
const handleChange = (value: string) => {
const intValue = parseInt(value, 10)
onChangePerPage?.(intValue)
setPerPage(intValue)
}

return (
<Stack direction="row" gap="2" alignItems="center">
<Text as="span" variant="body" sentiment="neutral" prominence="weak">
{perPageText ?? 'Items per page'}
</Text>
<StyledSelectInput
value={perPage.toString()}
options={optionsItemsPerPage}
onChange={handleChange}
name="select-items-per-page"
/>
<Text as="span" variant="body" sentiment="neutral" prominence="weak">
{(page - 1) * perPage + 1}-{Math.min(page * perPage, numberOfItems)}{' '}
{numberOfItemsText ?? `of ${numberOfItems} items"`}
</Text>
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { StoryFn } from '@storybook/react'
import { useState } from 'react'
import { Pagination } from '..'
import { Badge } from '../../Badge'

export const Controlled: StoryFn = props => {
const [page, setPage] = useState(1)

return <Pagination {...props} onChange={setPage} page={page} pageCount={10} />
}

Controlled.args = {
value: 40,
labelDescription: (
<Badge sentiment="primary" size="small">
New
</Badge>
),
label: 'Label',
}

Controlled.parameters = {
docs: {
description: {
story: 'Define number of elements to show per page',
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import styled from '@emotion/styled'
import type { StoryFn } from '@storybook/react'
import { useState } from 'react'
import { Pagination } from '..'
import { Badge } from '../../Badge'
import { Stack } from '../../Stack'

const NUMBER_OF_ITEMS = 134
const StyledList = styled.ul`
height: 210px;
overflow-y: auto;
border: ${({ theme }) => theme.colors.neutral.border} 1px solid;
padding: ${({ theme }) => theme.space[1]};
`
export const PerPage: StoryFn = props => {
const [page, setPage] = useState(1)
const [perPage, setPerPage] = useState(10)
const computeNumberOfPages = Math.ceil(NUMBER_OF_ITEMS / perPage)

return (
<Stack gap={1}>
<StyledList>
{Array.from({ length: perPage }).map((_, index) => {
const itemNumber = perPage * (page - 1) + index + 1
if (itemNumber <= NUMBER_OF_ITEMS)
return <li key={itemNumber}>Item #{itemNumber}</li>

Check failure on line 26 in packages/ui/src/components/Pagination/__stories__/PerPage.stories.tsx

View workflow job for this annotation

GitHub Actions / lint

Expected { after 'if' condition

return null
})}
</StyledList>
<Pagination
{...props}
onChange={setPage}
page={page}
perPage={perPage}
pageCount={computeNumberOfPages}
onChangePerPage={setPerPage}
numberOfItems={NUMBER_OF_ITEMS}
/>
</Stack>
)
}

PerPage.args = {
value: 40,
labelDescription: (
<Badge sentiment="primary" size="small">
New
</Badge>
),
label: 'Label',
numberOfItemsText: `of ${NUMBER_OF_ITEMS} items`,
}

PerPage.parameters = {
docs: {
description: {
story: 'Define number of elements to show per page',
},
},
}

PerPage.decorators = [
StoryComponent => (
<div style={{ height: '500px' }}>
<StoryComponent />
</div>
),
]
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { Template } from './Template.stories'

export const Playground = Template.bind({})

Playground.args = {
page: 1,
pageCount: 1,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export default {
export { Playground } from './Playground.stories'
export { Basic } from './Basic.stories'
export { Disabled } from './Disabled.stories'
export { Controlled } from './Controlled.stories'
export { PerPage } from './PerPage.stories'
Loading

0 comments on commit e751e39

Please sign in to comment.