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

feat(Pagination): Advance Pagination Component #424

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 22 additions & 132 deletions src/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,164 +11,54 @@ export default {

export const Default: Story<PaginationProps> = (args) => {
return (
<Pagination {...args}>
<Button className="join-item">1</Button>
<Button className="join-item" active>
2
</Button>
<Button className="join-item">3</Button>
<Button className="join-item">4</Button>
</Pagination>
<Pagination {...args} onPageChange={(currentPage: number) => console.log(currentPage)} />
)
}

Default.args = {}
Default.args = { totalCount: 101 }

export const Sizes: Story<PaginationProps> = (args) => {
return (
<div className="flex flex-col gap-2 items-center">
<Pagination {...args}>
<Button size="xs" className="join-item">
1
</Button>
<Button size="xs" className="join-item" active>
2
</Button>
<Button size="xs" className="join-item">
3
</Button>
<Button size="xs" className="join-item">
4
</Button>
</Pagination>

<Pagination {...args}>
<Button size="sm" className="join-item">
1
</Button>
<Button size="sm" className="join-item" active>
2
</Button>
<Button size="sm" className="join-item">
3
</Button>
<Button size="sm" className="join-item">
4
</Button>
</Pagination>

<Pagination {...args}>
<Button size="md" className="join-item">
1
</Button>
<Button size="md" className="join-item" active>
2
</Button>
<Button size="md" className="join-item">
3
</Button>
<Button size="md" className="join-item">
4
</Button>
</Pagination>

<Pagination {...args}>
<Button size="lg" className="join-item">
1
</Button>
<Button size="lg" className="join-item" active>
2
</Button>
<Button size="lg" className="join-item">
3
</Button>
<Button size="lg" className="join-item">
4
</Button>
</Pagination>
<div className='flex flex-col gap-4'>
<Pagination {...args} size='xs' />
<Pagination {...args} size='sm' />
<Pagination {...args} size='md' />
<Pagination {...args} size='lg' />
</div>
)
}

Sizes.args = {}
Sizes.args = { totalCount: 101 }

export const DisabledButton: Story<PaginationProps> = (args) => {
export const ShowTotal: Story<PaginationProps> = (args) => {
return (
<Pagination {...args}>
<Button className="join-item">1</Button>
<Button className="join-item">2</Button>
<Button className="join-item" disabled>
...
</Button>
<Button className="join-item">99</Button>
<Button className="join-item">100</Button>
</Pagination>
<div className='flex flex-col gap-4'>
<Pagination {...args} showTotal={(total: number) => `Total ${total} items`} />
<Pagination {...args} showTotal={(total: number, range: number[]) => `${range[0]}-${range[1]} of ${total} items`} />
</div>
)
}

DisabledButton.args = {}
ShowTotal.args = { totalCount: 101 }


export const ExtraSmallButtons: Story<PaginationProps> = (args) => {
return (
<Pagination {...args}>
<Button className="join-item">«</Button>
<Button className="join-item">Page 22</Button>
<Button className="join-item">»</Button>
</Pagination>
<div className='flex flex-col gap-4'>
<Pagination {...args} simple />
<Pagination {...args} simple={(currentPage: number) => `page ${currentPage}`} />
</div>
)
}

ExtraSmallButtons.args = {}
ExtraSmallButtons.args = { totalCount: 101, previousLabel: "«", nextLabel: "»" }

export const NextPrevOutlineButtonsWithEqualWidth: Story<PaginationProps> = (
args
) => {
return (
<Pagination {...args}>
<Button variant="outline" className="join-item">
Previous page
</Button>
<Button variant="outline" className="join-item">
Next
</Button>
</Pagination>
)
}

NextPrevOutlineButtonsWithEqualWidth.args = {
className: 'grid grid-cols-2',
}

export const UsingRadioInputs: Story<PaginationProps> = (args) => {
return (
<Pagination {...args}>
<input
className="join-item btn btn-square"
type="radio"
name="options"
aria-label="1"
defaultChecked={true}
/>
<input
className="join-item btn btn-square"
type="radio"
name="options"
aria-label="2"
/>
<input
className="join-item btn btn-square"
type="radio"
name="options"
aria-label="3"
/>
<input
className="join-item btn btn-square"
type="radio"
name="options"
aria-label="4"
/>
</Pagination>
<Pagination {...args} />
)
}

UsingRadioInputs.args = {}
NextPrevOutlineButtonsWithEqualWidth.args = { totalCount: 101, previousLabel: "previous", nextLabel: "next", simple: true, variant: 'outline', className: 'grid grid-cols-2' }
78 changes: 39 additions & 39 deletions src/Pagination/Pagination.test.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { render } from '@testing-library/react'

import Button from '../Button'
import Pagination from './'

const testid = 'pagination'
const buttons = [
<Button key="1">Button 1</Button>,
<Button key="2">Button 2</Button>,
<Button key="3">Button 3</Button>,
]

describe('Pagination', () => {
it('Should render a group of buttons', () => {
render(<Pagination>{buttons}</Pagination>)

expect(screen.getAllByRole('button')).toHaveLength(3)
expect(screen.getByText('Button 1')).toBeInTheDocument()
expect(screen.getByText('Button 2')).toBeInTheDocument()
expect(screen.getByText('Button 3')).toBeInTheDocument()
it('Should render Pagination', () => {
render(<Pagination totalCount={101} />)
})

it('Should apply additional class namess', () => {
render(
<Pagination className="custom-class" data-testid={testid}>
{buttons}
</Pagination>
)
expect(screen.getByTestId(testid)).toHaveClass('join', 'custom-class')
it('Should apply additional root class names', () => {
const { container } = render(<Pagination totalCount={101} classNameRoot="custom-class" />)
expect(container.firstChild).toHaveClass('custom-class')
})

it('Should apply vertical style when vertical prop is true', () => {
render(
<Pagination vertical data-testid={testid}>
{buttons}
</Pagination>
)
expect(screen.getByTestId(testid)).toHaveClass('join-vertical')
it('Should apply additional class names', () => {
const { container } = render(<Pagination totalCount={101} className="custom-class" />)
expect(container.firstChild?.lastChild).toHaveClass('custom-class')
})

it('Should allow passing extra props', () => {
render(<Pagination data-testid={testid}>{buttons}</Pagination>)
expect(screen.getByTestId(testid)).toBeInTheDocument()
it('Should show total correctly', () => {
const { container } = render(<Pagination totalCount={101} showTotal={(total: number) => `Total ${total} items`} />)
expect(container.firstChild?.firstChild).toHaveTextContent("Total 101 items")
})

it('Should forward the ref to the root element', () => {
const ref = React.createRef<HTMLDivElement>()
render(<Pagination ref={ref}>{buttons}</Pagination>)
expect(ref.current).toBeInTheDocument()
it('Should show total correctly 2', () => {
const { container } = render(<Pagination totalCount={101} currentPage={1} showTotal={(total: number, range: number[]) => `${range[0]}-${range[1]} of ${total} items`} />)
expect(container.firstChild?.firstChild).toHaveTextContent("1-10 of 101 items")
})
it('Should simple correctly', () => {
const { container } = render(<Pagination totalCount={101} simple />)
expect(container.firstChild?.lastChild?.childNodes).toHaveLength(2)
})
it('Should simple correctly 2', () => {
const { container } = render(<Pagination totalCount={101} simple={(currentPage) => `page ${currentPage}`} />)
expect(container.firstChild?.lastChild?.childNodes).toHaveLength(3)
})
it('Should apply size class correctly', () => {
const { container } = render(<Pagination totalCount={101} size='sm' />)
expect(container.firstChild?.lastChild?.firstChild).toHaveClass('btn-sm')
})
it('Should apply variant class correctly', () => {
const { container } = render(<Pagination totalCount={101} variant='outline' />)
expect(container.firstChild?.lastChild?.firstChild).toHaveClass('btn-outline')
})
it('Should apply previous label correctly', () => {
const { container } = render(<Pagination totalCount={101} previousLabel="previous" />)
expect(container.firstChild?.lastChild?.firstChild).toHaveTextContent("previous")
})
it('Should apply next label correctly', () => {
const { container } = render(<Pagination totalCount={101} nextLabel="next" />)
expect(container.firstChild?.lastChild?.lastChild).toHaveTextContent("next")
})
})
106 changes: 103 additions & 3 deletions src/Pagination/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,105 @@
import Join, { JoinProps } from '../Join'
import React from 'react';
import Button, { ButtonProps } from '../Button';
import Join from '../Join'
import { DOTS, usePagination } from './usePagination';
import clsx from 'clsx';

export type PaginationProps = JoinProps
const Pagination = Join
export type PaginationProps = {
onPageChange?: (currentPage: number) => void;
currentPage?: number;
pageSize?: number;
totalCount: number;
siblingCount?: number;
classNameRoot?: string;
className?: string;
size?: ButtonProps['size'];
variant?: ButtonProps['variant'];
buttonProps?: ButtonProps;
previousLabel?: React.ReactNode;
nextLabel?: React.ReactNode;
showTotal?: (total: number, range: number[]) => React.ReactNode;
simple?: boolean | ((currentPage: number) => React.ReactNode);
}

function Pagination(props: PaginationProps) {
const {
totalCount,
siblingCount = 1,
currentPage: currentPageDefault = 1,
pageSize = 10,
previousLabel = "<",
nextLabel = ">",
simple,
onPageChange,
classNameRoot,
className,
showTotal,
size,
variant,
buttonProps: buttonPropsDefault,
} = props;

const buttonProps = React.useMemo(() => ({ ...buttonPropsDefault, size, variant }), [buttonPropsDefault, size, variant])

const [currentPage, setCurrentPage] = React.useState(currentPageDefault)

const paginationRange = usePagination({
currentPage,
totalCount,
siblingCount,
pageSize
});
if (currentPage === 0 || paginationRange.length < 2) {
return null;
}

const onClick = (pageNumber: number) => {
if (pageNumber === currentPage) {
return
}
setCurrentPage(pageNumber)
onPageChange?.(pageNumber);
}

const onNext = () => {
const newCurrentPage = currentPage + 1
setCurrentPage(newCurrentPage)
onPageChange?.(newCurrentPage)
};

const onPrevious = () => {
const newCurrentPage = currentPage - 1
setCurrentPage(newCurrentPage)
onPageChange?.(newCurrentPage)
};

function renderButtons() {
if (simple) {
if (typeof simple === 'function') {
return (
<Button {...buttonProps} className='join-item'>{simple(currentPage)}</Button>
)
}
return null
}
return paginationRange.map((pageNumber, idx) => {
return (
<Button {...buttonProps} onClick={() => onClick(pageNumber as number)} key={idx} className='join-item' color={currentPage === pageNumber ? 'primary' : undefined} disabled={pageNumber === DOTS}>{pageNumber}</Button>
)
})

}

const lastPage = paginationRange[paginationRange.length - 1];
return (
<div className={clsx("flex items-center gap-4", classNameRoot)}>
{showTotal?.(totalCount, [(currentPage - 1) * pageSize + 1, Math.min(currentPage * pageSize, totalCount)])}
<Join className={className}>
<Button {...buttonProps} className='join-item' onClick={onPrevious} disabled={currentPage === 1}>{previousLabel}</Button>
{renderButtons()}
<Button {...buttonProps} className='join-item' onClick={onNext} disabled={currentPage === lastPage}>{nextLabel}</Button>
</Join>
</div>
)
}
export default Pagination
Loading