export const usePagination = ({
totalCount,
pageSize,
siblingCount = 1,
currentPage
}) => {
const paginationRange = useMemo(() => {
const totalPageCount = Math.ceil(totalCount / pageSize);
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
const totalPageNumbers = siblingCount + 5;
/*
Case 1:
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);
}
/*
Calculate left and right sibling index and make sure they are within range 1 and totalPageCount
*/
const leftSiblingIndex = Math.max(currentPage - siblingCount, 1);
const rightSiblingIndex = Math.min(
currentPage + siblingCount,
totalPageCount
);
/*
We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and totalPageCount. Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < totalPageCount - 2
*/
const shouldShowLeftDots = leftSiblingIndex > 2;
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = totalPageCount;
/*
Case 2: No left dots to show, but rights dots to be shown
*/
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [...leftRange, DOTS, totalPageCount];
}
/*
Case 3: No right dots to show, but left dots to be shown
*/
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(
totalPageCount - rightItemCount + 1,
totalPageCount
);
return [firstPageIndex, DOTS, ...rightRange];
}
/*
Case 4: Both left and right dots to be shown
*/
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex];
}
}, [totalCount, pageSize, siblingCount, currentPage]);
return paginationRange;
};
import React from 'react';
import classnames from 'classnames';
import { usePagination, DOTS } from './usePagination';
import './pagination.scss';
const Pagination = props => {
const {
onPageChange,
totalCount,
siblingCount = 1,
currentPage,
pageSize,
className
} = props;
const paginationRange = usePagination({
currentPage,
totalCount,
siblingCount,
pageSize
});
// If there are less than 2 times in pagination range we shall not render the component
if (currentPage === 0 || paginationRange.length < 2) {
return null;
}
const onNext = () => {
onPageChange(currentPage + 1);
};
const onPrevious = () => {
onPageChange(currentPage - 1);
};
let lastPage = paginationRange[paginationRange.length - 1];
return (
<ul
className={classnames('pagination-container', { [className]: className })}
>
{/* Left navigation arrow */}
<li
className={classnames('pagination-item', {
disabled: currentPage === 1
})}
onClick={onPrevious}
>
<div className="arrow left" />
</li>
{paginationRange.map(pageNumber => {
// If the pageItem is a DOT, render the DOTS unicode character
if (pageNumber === DOTS) {
return <li className="pagination-item dots">…</li>;
}
// Render our Page Pills
return (
<li
className={classnames('pagination-item', {
selected: pageNumber === currentPage
})}
onClick={() => onPageChange(pageNumber)}
>
{pageNumber}
</li>
);
})}
{/* Right Navigation arrow */}
<li
className={classnames('pagination-item', {
disabled: currentPage === lastPage
})}
onClick={onNext}
>
<div className="arrow right" />
</li>
</ul>
);
};
export default Pagination;
.pagination-container {
display: flex;
list-style-type: none;
.pagination-item {
padding: 0 12px;
height: 32px;
text-align: center;
margin: auto 4px;
color: rgba(0, 0, 0, 0.87);
display: flex;
box-sizing: border-box;
align-items: center;
letter-spacing: 0.01071em;
border-radius: 16px;
line-height: 1.43;
font-size: 13px;
min-width: 32px;
&.dots:hover {
background-color: transparent;
cursor: default;
}
&:hover {
background-color: rgba(0, 0, 0, 0.04);
cursor: pointer;
}
&.selected {
background-color: rgba(0, 0, 0, 0.08);
}
.arrow {
&::before {
position: relative;
/* top: 3pt; Uncomment this to lower the icons as requested in comments*/
content: '';
/* By using an em scale, the arrows will size with the font */
display: inline-block;
width: 0.4em;
height: 0.4em;
border-right: 0.12em solid rgba(0, 0, 0, 0.87);
border-top: 0.12em solid rgba(0, 0, 0, 0.87);
}
&.left {
transform: rotate(-135deg) translate(-50%);
}
&.right {
transform: rotate(45deg);
}
}
&.disabled {
pointer-events: none;
.arrow::before {
border-right: 0.12em solid rgba(0, 0, 0, 0.43);
border-top: 0.12em solid rgba(0, 0, 0, 0.43);
}
&:hover {
background-color: transparent;
cursor: default;
}
}
}
}
App.js
import React, { useState, useMemo } from 'react';
import Pagination from '../Pagination';
import data from './data/mock-data.json';
import './style.scss';
let PageSize = 10;
export default function App() {
const [currentPage, setCurrentPage] = useState(1);
const currentTableData = useMemo(() => {
const firstPageIndex = (currentPage - 1) * PageSize;
const lastPageIndex = firstPageIndex + PageSize;
return data.slice(firstPageIndex, lastPageIndex);
}, [currentPage]);
return (
<>
<table>
<thead>
<tr>
<th>ID</th>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>EMAIL</th>
<th>PHONE</th>
</tr>
</thead>
<tbody>
{currentTableData.map(item => {
return (
<tr>
<td>{item.id}</td>
<td>{item.first_name}</td>
<td>{item.last_name}</td>
<td>{item.email}</td>
<td>{item.phone}</td>
</tr>
);
})}
</tbody>
</table>
<Pagination
className="pagination-bar"
currentPage={currentPage}
totalCount={data.length}
pageSize={PageSize}
onPageChange={page => setCurrentPage(page)}
/>
</>
);
}