From 89055fe19266c00bb42551e9a1cd93d6302f09ec Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 10 Feb 2020 15:59:56 +0800 Subject: [PATCH] add pagination component --- .../assets/src/components/Pagination.tsx | 95 ++++++++++ .../assets/src/components/PaginationItem.tsx | 33 ++++ .../tests/components/Pagination.test.tsx | 177 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 resources/assets/src/components/Pagination.tsx create mode 100644 resources/assets/src/components/PaginationItem.tsx create mode 100644 resources/assets/tests/components/Pagination.test.tsx diff --git a/resources/assets/src/components/Pagination.tsx b/resources/assets/src/components/Pagination.tsx new file mode 100644 index 00000000..4b24e531 --- /dev/null +++ b/resources/assets/src/components/Pagination.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import PaginationItem from './PaginationItem' + +interface Props { + page: number + totalPages: number + onChange(page: number): void | Promise +} + +export const labels = { + first: '«', + prev: '‹', + next: '›', + last: '»', +} + +const Pagination: React.FC = props => { + const { page, totalPages, onChange } = props + + return ( +
    + onChange(1)}> + {labels.first} + + onChange(page - 1)}> + {labels.prev} + + {totalPages < 8 ? ( + Array.from({ length: totalPages }).map((_, i) => ( + onChange(i + 1)} + > + {i + 1} + + )) + ) : page < 3 || totalPages - page < 2 ? ( + <> + {[1, 2].map(p => ( + onChange(p)} + > + {p} + + ))} + ... + {[totalPages - 1, totalPages].map(p => ( + onChange(p)} + > + {p} + + ))} + + ) : ( + <> + onChange(1)}>1 + ... + {[page - 1, page, page + 1].map(p => ( + onChange(p)} + > + {p} + + ))} + ... + onChange(totalPages)}> + {totalPages} + + + )} + onChange(page + 1)} + > + {labels.next} + + onChange(totalPages)} + > + {labels.last} + +
+ ) +} + +export default Pagination diff --git a/resources/assets/src/components/PaginationItem.tsx b/resources/assets/src/components/PaginationItem.tsx new file mode 100644 index 00000000..a240f89f --- /dev/null +++ b/resources/assets/src/components/PaginationItem.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +interface Props { + disabled?: boolean + active?: boolean + onClick?(): void +} + +const PaginationItem: React.FC = 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 ( +
  • + + {props.children} + +
  • + ) +} + +export default PaginationItem diff --git a/resources/assets/tests/components/Pagination.test.tsx b/resources/assets/tests/components/Pagination.test.tsx new file mode 100644 index 00000000..39771699 --- /dev/null +++ b/resources/assets/tests/components/Pagination.test.tsx @@ -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( + , + ) + fireEvent.click(getByText(labels.first)) + expect(mock).toBeCalledWith(1) + }) + + it('disabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.first)) + expect(mock).not.toBeCalled() + }) +}) + +describe('last page', () => { + it('enabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.last)) + expect(mock).toBeCalledWith(3) + }) + + it('disabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.last)) + expect(mock).not.toBeCalled() + }) +}) + +describe('previous page', () => { + it('enabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.prev)) + expect(mock).toBeCalledWith(2) + }) + + it('disabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.prev)) + expect(mock).not.toBeCalled() + }) +}) + +describe('next page', () => { + it('enabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + fireEvent.click(getByText(labels.next)) + expect(mock).toBeCalledWith(4) + }) + + it('disabled', () => { + const mock = jest.fn() + const { getByText } = render( + , + ) + 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( + , + ) + + 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( + , + ) + + 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( + , + ) + + 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) + }) + }) +})