add pagination component
This commit is contained in:
parent
576a4c442f
commit
89055fe192
95
resources/assets/src/components/Pagination.tsx
Normal file
95
resources/assets/src/components/Pagination.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React from 'react'
|
||||
import PaginationItem from './PaginationItem'
|
||||
|
||||
interface Props {
|
||||
page: number
|
||||
totalPages: number
|
||||
onChange(page: number): void | Promise<void>
|
||||
}
|
||||
|
||||
export const labels = {
|
||||
first: '«',
|
||||
prev: '‹',
|
||||
next: '›',
|
||||
last: '»',
|
||||
}
|
||||
|
||||
const Pagination: React.FC<Props> = props => {
|
||||
const { page, totalPages, onChange } = props
|
||||
|
||||
return (
|
||||
<ul className="pagination">
|
||||
<PaginationItem disabled={page === 1} onClick={() => onChange(1)}>
|
||||
{labels.first}
|
||||
</PaginationItem>
|
||||
<PaginationItem disabled={page === 1} onClick={() => onChange(page - 1)}>
|
||||
{labels.prev}
|
||||
</PaginationItem>
|
||||
{totalPages < 8 ? (
|
||||
Array.from({ length: totalPages }).map((_, i) => (
|
||||
<PaginationItem
|
||||
key={i}
|
||||
active={page === i + 1}
|
||||
onClick={() => onChange(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</PaginationItem>
|
||||
))
|
||||
) : page < 3 || totalPages - page < 2 ? (
|
||||
<>
|
||||
{[1, 2].map(p => (
|
||||
<PaginationItem
|
||||
key={p}
|
||||
active={page === p}
|
||||
onClick={() => onChange(p)}
|
||||
>
|
||||
{p}
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem disabled>...</PaginationItem>
|
||||
{[totalPages - 1, totalPages].map(p => (
|
||||
<PaginationItem
|
||||
key={p}
|
||||
active={page === p}
|
||||
onClick={() => onChange(p)}
|
||||
>
|
||||
{p}
|
||||
</PaginationItem>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PaginationItem onClick={() => onChange(1)}>1</PaginationItem>
|
||||
<PaginationItem disabled>...</PaginationItem>
|
||||
{[page - 1, page, page + 1].map(p => (
|
||||
<PaginationItem
|
||||
key={p}
|
||||
active={page === p}
|
||||
onClick={() => onChange(p)}
|
||||
>
|
||||
{p}
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem disabled>...</PaginationItem>
|
||||
<PaginationItem onClick={() => onChange(totalPages)}>
|
||||
{totalPages}
|
||||
</PaginationItem>
|
||||
</>
|
||||
)}
|
||||
<PaginationItem
|
||||
disabled={page === totalPages}
|
||||
onClick={() => onChange(page + 1)}
|
||||
>
|
||||
{labels.next}
|
||||
</PaginationItem>
|
||||
<PaginationItem
|
||||
disabled={page === totalPages}
|
||||
onClick={() => onChange(totalPages)}
|
||||
>
|
||||
{labels.last}
|
||||
</PaginationItem>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pagination
|
33
resources/assets/src/components/PaginationItem.tsx
Normal file
33
resources/assets/src/components/PaginationItem.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
onClick?(): void
|
||||
}
|
||||
|
||||
const PaginationItem: React.FC<Props> = props => {
|
||||
const classes = ['page-item']
|
||||
if (props.active) {
|
||||
classes.push('active')
|
||||
}
|
||||
if (props.disabled) {
|
||||
classes.push('disabled')
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled && props.onClick) {
|
||||
props.onClick()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={classes.join(' ')} onClick={handleClick}>
|
||||
<a href="#" className="page-link" aria-disabled={props.disabled}>
|
||||
{props.children}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationItem
|
177
resources/assets/tests/components/Pagination.test.tsx
Normal file
177
resources/assets/tests/components/Pagination.test.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import Pagination, { labels } from '@/components/Pagination'
|
||||
|
||||
describe('first page', () => {
|
||||
it('enabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={2} totalPages={3} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.first))
|
||||
expect(mock).toBeCalledWith(1)
|
||||
})
|
||||
|
||||
it('disabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={1} totalPages={3} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.first))
|
||||
expect(mock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('last page', () => {
|
||||
it('enabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={2} totalPages={3} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.last))
|
||||
expect(mock).toBeCalledWith(3)
|
||||
})
|
||||
|
||||
it('disabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={3} totalPages={3} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.last))
|
||||
expect(mock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('previous page', () => {
|
||||
it('enabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={3} totalPages={5} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.prev))
|
||||
expect(mock).toBeCalledWith(2)
|
||||
})
|
||||
|
||||
it('disabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={1} totalPages={5} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.prev))
|
||||
expect(mock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('next page', () => {
|
||||
it('enabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={3} totalPages={5} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.next))
|
||||
expect(mock).toBeCalledWith(4)
|
||||
})
|
||||
|
||||
it('disabled', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={5} totalPages={5} onChange={mock} />,
|
||||
)
|
||||
fireEvent.click(getByText(labels.next))
|
||||
expect(mock).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('middle pages', () => {
|
||||
it('pages count less than 8', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText } = render(
|
||||
<Pagination page={1} totalPages={7} onChange={mock} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('1'))
|
||||
expect(mock).toBeCalledWith(1)
|
||||
|
||||
fireEvent.click(getByText('2'))
|
||||
expect(mock).toBeCalledWith(2)
|
||||
|
||||
fireEvent.click(getByText('3'))
|
||||
expect(mock).toBeCalledWith(3)
|
||||
|
||||
fireEvent.click(getByText('4'))
|
||||
expect(mock).toBeCalledWith(4)
|
||||
|
||||
fireEvent.click(getByText('5'))
|
||||
expect(mock).toBeCalledWith(5)
|
||||
|
||||
fireEvent.click(getByText('6'))
|
||||
expect(mock).toBeCalledWith(6)
|
||||
|
||||
fireEvent.click(getByText('7'))
|
||||
expect(mock).toBeCalledWith(7)
|
||||
})
|
||||
|
||||
describe('pages count greater than or equals to 8', () => {
|
||||
it('edge', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText, queryByText } = render(
|
||||
<Pagination page={2} totalPages={10} onChange={mock} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('1'))
|
||||
expect(mock).toBeCalledWith(1)
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryByText('2')).toBeInTheDocument()
|
||||
expect(queryByText('...')).toBeInTheDocument()
|
||||
expect(queryByText('9')).toBeInTheDocument()
|
||||
expect(queryByText('10')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText('2'))
|
||||
expect(mock).toBeCalledWith(2)
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryByText('2')).toBeInTheDocument()
|
||||
expect(queryByText('...')).toBeInTheDocument()
|
||||
expect(queryByText('9')).toBeInTheDocument()
|
||||
expect(queryByText('10')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText('9'))
|
||||
expect(mock).toBeCalledWith(9)
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryByText('2')).toBeInTheDocument()
|
||||
expect(queryByText('...')).toBeInTheDocument()
|
||||
expect(queryByText('9')).toBeInTheDocument()
|
||||
expect(queryByText('10')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText('10'))
|
||||
expect(mock).toBeCalledWith(10)
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryByText('2')).toBeInTheDocument()
|
||||
expect(queryByText('...')).toBeInTheDocument()
|
||||
expect(queryByText('9')).toBeInTheDocument()
|
||||
expect(queryByText('10')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('middle', () => {
|
||||
const mock = jest.fn()
|
||||
const { getByText, queryByText, queryAllByText } = render(
|
||||
<Pagination page={4} totalPages={10} onChange={mock} />,
|
||||
)
|
||||
|
||||
expect(queryByText('1')).toBeInTheDocument()
|
||||
expect(queryAllByText('...')).toHaveLength(2)
|
||||
expect(queryByText('3')).toBeInTheDocument()
|
||||
expect(queryByText('4')).toBeInTheDocument()
|
||||
expect(queryByText('5')).toBeInTheDocument()
|
||||
expect(queryByText('10')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(getByText('5'))
|
||||
expect(mock).toBeCalledWith(5)
|
||||
|
||||
fireEvent.click(getByText('1'))
|
||||
expect(mock).toBeCalledWith(1)
|
||||
|
||||
fireEvent.click(getByText('10'))
|
||||
expect(mock).toBeCalledWith(10)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user