diff --git a/src/Pagination/Pagination.stories.tsx b/src/Pagination/Pagination.stories.tsx index ff73e336..c884a34e 100644 --- a/src/Pagination/Pagination.stories.tsx +++ b/src/Pagination/Pagination.stories.tsx @@ -11,164 +11,54 @@ export default { export const Default: Story = (args) => { return ( - - - - - - + console.log(currentPage)} /> ) } -Default.args = {} +Default.args = { totalCount: 101 } export const Sizes: Story = (args) => { return ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ + + +
) } -Sizes.args = {} +Sizes.args = { totalCount: 101 } -export const DisabledButton: Story = (args) => { +export const ShowTotal: Story = (args) => { return ( - - - - - - - +
+ `Total ${total} items`} /> + `${range[0]}-${range[1]} of ${total} items`} /> +
) } -DisabledButton.args = {} +ShowTotal.args = { totalCount: 101 } + export const ExtraSmallButtons: Story = (args) => { return ( - - - - - +
+ + `page ${currentPage}`} /> +
) } -ExtraSmallButtons.args = {} +ExtraSmallButtons.args = { totalCount: 101, previousLabel: "«", nextLabel: "»" } export const NextPrevOutlineButtonsWithEqualWidth: Story = ( args ) => { return ( - - - - - ) -} - -NextPrevOutlineButtonsWithEqualWidth.args = { - className: 'grid grid-cols-2', -} - -export const UsingRadioInputs: Story = (args) => { - return ( - - - - - - + ) } -UsingRadioInputs.args = {} +NextPrevOutlineButtonsWithEqualWidth.args = { totalCount: 101, previousLabel: "previous", nextLabel: "next", simple: true, variant: 'outline', className: 'grid grid-cols-2' } diff --git a/src/Pagination/Pagination.test.tsx b/src/Pagination/Pagination.test.tsx index bb197d58..a2a0209e 100644 --- a/src/Pagination/Pagination.test.tsx +++ b/src/Pagination/Pagination.test.tsx @@ -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 = [ - , - , - , -] - describe('Pagination', () => { - it('Should render a group of buttons', () => { - render({buttons}) - 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() }) - it('Should apply additional class namess', () => { - render( - - {buttons} - - ) - expect(screen.getByTestId(testid)).toHaveClass('join', 'custom-class') + it('Should apply additional root class names', () => { + const { container } = render() + expect(container.firstChild).toHaveClass('custom-class') }) - - it('Should apply vertical style when vertical prop is true', () => { - render( - - {buttons} - - ) - expect(screen.getByTestId(testid)).toHaveClass('join-vertical') + it('Should apply additional class names', () => { + const { container } = render() + expect(container.firstChild?.lastChild).toHaveClass('custom-class') }) - it('Should allow passing extra props', () => { - render({buttons}) - expect(screen.getByTestId(testid)).toBeInTheDocument() + it('Should show total correctly', () => { + const { container } = render( `Total ${total} items`} />) + expect(container.firstChild?.firstChild).toHaveTextContent("Total 101 items") }) - - it('Should forward the ref to the root element', () => { - const ref = React.createRef() - render({buttons}) - expect(ref.current).toBeInTheDocument() + it('Should show total correctly 2', () => { + const { container } = render( `${range[0]}-${range[1]} of ${total} items`} />) + expect(container.firstChild?.firstChild).toHaveTextContent("1-10 of 101 items") + }) + it('Should simple correctly', () => { + const { container } = render() + expect(container.firstChild?.lastChild?.childNodes).toHaveLength(2) + }) + it('Should simple correctly 2', () => { + const { container } = render( `page ${currentPage}`} />) + expect(container.firstChild?.lastChild?.childNodes).toHaveLength(3) + }) + it('Should apply size class correctly', () => { + const { container } = render() + expect(container.firstChild?.lastChild?.firstChild).toHaveClass('btn-sm') + }) + it('Should apply variant class correctly', () => { + const { container } = render() + expect(container.firstChild?.lastChild?.firstChild).toHaveClass('btn-outline') + }) + it('Should apply previous label correctly', () => { + const { container } = render() + expect(container.firstChild?.lastChild?.firstChild).toHaveTextContent("previous") + }) + it('Should apply next label correctly', () => { + const { container } = render() + expect(container.firstChild?.lastChild?.lastChild).toHaveTextContent("next") }) }) diff --git a/src/Pagination/Pagination.tsx b/src/Pagination/Pagination.tsx index b1c646e5..8d04fb4e 100644 --- a/src/Pagination/Pagination.tsx +++ b/src/Pagination/Pagination.tsx @@ -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 ( + + ) + } + return null + } + return paginationRange.map((pageNumber, idx) => { + return ( + + ) + }) + + } + + const lastPage = paginationRange[paginationRange.length - 1]; + return ( +
+ {showTotal?.(totalCount, [(currentPage - 1) * pageSize + 1, Math.min(currentPage * pageSize, totalCount)])} + + + {renderButtons()} + + +
+ ) +} export default Pagination diff --git a/src/Pagination/usePagination.ts b/src/Pagination/usePagination.ts new file mode 100644 index 00000000..d9ef3dfe --- /dev/null +++ b/src/Pagination/usePagination.ts @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; + +export const DOTS = '...'; + +const range = (start: number, end: number) => { + let length = end - start + 1; + return Array.from({ length }, (_, idx) => idx + start); +}; + +export const usePagination = ({ + totalCount, + pageSize, + siblingCount = 1, + currentPage +}: { + totalCount: number; + pageSize: number; + siblingCount?: number; + currentPage: number; +}) => { + const paginationRange = useMemo(() => { + const totalPageCount = Math.ceil(totalCount / pageSize); + + // Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS + const totalPageNumbers = siblingCount + 5; + + /* + If the number of pages is less than the page numbers we want to show in our + paginationComponent, we return the range [1..totalPageCount] + */ + if (totalPageNumbers >= totalPageCount) { + return range(1, totalPageCount); + } + + const leftSiblingIndex = Math.max(currentPage - siblingCount, 1); + const rightSiblingIndex = Math.min( + currentPage + siblingCount, + totalPageCount + ); + + /* + We do not want to show dots if there is only one position left + after/before the left/right page count as that would lead to a change if our Pagination + component size which we do not want + */ + const shouldShowLeftDots = leftSiblingIndex > 2; + const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2; + + const firstPageIndex = 1; + const lastPageIndex = totalPageCount; + + if (!shouldShowLeftDots && shouldShowRightDots) { + let leftItemCount = 3 + 2 * siblingCount; + let leftRange = range(1, leftItemCount); + + return [...leftRange, DOTS, totalPageCount]; + } + + if (shouldShowLeftDots && !shouldShowRightDots) { + let rightItemCount = 3 + 2 * siblingCount; + let rightRange = range( + totalPageCount - rightItemCount + 1, + totalPageCount + ); + return [firstPageIndex, DOTS, ...rightRange]; + } + + if (shouldShowLeftDots && shouldShowRightDots) { + let middleRange = range(leftSiblingIndex, rightSiblingIndex); + return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex]; + } + return [] + }, [totalCount, pageSize, siblingCount, currentPage]); + + return paginationRange; +};