Skip to content

Instantly share code, notes, and snippets.

@rowellx68
Last active December 7, 2023 10:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rowellx68/b7570b4f3c7e2b93c6ce2ff0573c0ade to your computer and use it in GitHub Desktop.
Save rowellx68/b7570b4f3c7e2b93c6ce2ff0573c0ade to your computer and use it in GitHub Desktop.
A re-implementation of the GOV.UK pagination component in React with NHS.UK colours.
import { NumberedPagination } from '@/components/NumberedPagination'
type ItemsPaginationProps = {
totalItems: number
itemsPerPage: number
currentPage: number
}
export const ItemsPagination: React.FC<ItemsPaginationProps> = ({
totalItems,
itemsPerPage,
currentPage,
}): React.JSX.Element => {
const totalPages = Math.ceil(totalItems / itemsPerPage)
const allPages = Array.from({ length: totalPages }, (_, i) => i + 1)
const items: (number | undefined)[] = []
// If there are less than 5 pages, show all pages
// If the current page is less than 4, show the first 5 pages
// If the current page is more than 3 from the end, show the last 5 pages
// Otherwise, show the current page, the 2 pages before it, the 2 pages after it and the first and last pages
if (totalPages <= 5) {
items.push(...allPages)
} else {
if (currentPage <= 4) {
items.push(...allPages.slice(0, 5))
items.push(undefined)
items.push(totalPages)
} else if (currentPage >= totalPages - 3) {
items.push(1)
items.push(undefined)
items.push(...allPages.slice(totalPages - 5, totalPages + 1))
} else {
items.push(1)
items.push(undefined)
items.push(...allPages.slice(currentPage - 2, currentPage + 1))
items.push(undefined)
items.push(totalPages)
}
}
return (
<>
{totalPages > 1 && (
<NumberedPagination className="nhsuk-u-margin-top-5">
{currentPage > 1 && (
<NumberedPagination.PreviousPage to={`?page=${currentPage - 1}`}>
Previous
</NumberedPagination.PreviousPage>
)}
<NumberedPagination.List>
{items.map((item, index) => (
<NumberedPagination.Item
key={index}
to={`?page=${item}`}
current={item === currentPage}
ellipses={item === undefined}
>
{item !== undefined &&
<>{item}</>
}
</NumberedPagination.Item>
))}
</NumberedPagination.List>
{currentPage < totalPages && (
<NumberedPagination.NextPage to={`?page=${currentPage + 1}`}>
Next
</NumberedPagination.NextPage>
)}
</NumberedPagination>
)}
</>
)
}
@use 'sass:map';
@import 'nhsuk-frontend/packages/core/settings/_all';
@import 'nhsuk-frontend/packages/core/tools/_all';
$breakpoint-tablet: map-get(
$map: $mq-breakpoints,
$key: 'tablet',
);
.nhsuk {
&-numbered-pagination {
margin-bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
@media screen and (min-width: $breakpoint-tablet) {
flex-direction: row;
align-items: flex-start;
}
&__list {
margin: 0;
padding: 0;
list-style: none;
}
&__item,
&__prev,
&__next {
@include nhsuk-font(19);
box-sizing: border-box;
position: relative;
min-width: 45px;
min-height: 45px;
padding: nhsuk-spacing(2) nhsuk-spacing(3);
float: left;
&:hover {
background-color: $color_nhsuk-grey-4;
}
}
&__item {
display: none;
text-align: center;
@media screen and (min-width: $breakpoint-tablet) {
display: block;
}
}
&__prev,
&__next {
@include nhsuk-typography-weight-bold;
.nhsuk-numbered-pagination__link {
display: flex;
align-items: center;
}
}
&__prev {
padding-left: 0;
}
&__next {
padding-right: 0;
}
&__item--current,
&__item--ellipses,
&__item:first-child,
&__item:last-child {
display: block;
}
&__item--current {
@include nhsuk-typography-weight-bold;
outline: 1px solid transparent;
background-color: $nhsuk-link-color;
&:hover {
background-color: $nhsuk-link-color;
}
.nhsuk-numbered-pagination__link {
color: $color_nhsuk-white;
}
}
&__item--ellipses {
@include nhsuk-typography-weight-bold;
&:hover {
background-color: transparent;
}
}
&__link {
display: block;
min-width: nhsuk-spacing(3);
@media screen {
&:after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
&:focus {
.nhsuk-numbered-pagination__icon {
color: $nhsuk-focus-text-color;
}
.nhsuk-numbered-pagination__link-label {
text-decoration: none;
}
.nhsuk-numbered-pagination__link-title--decorated {
text-decoration: none;
}
}
}
&__link-label {
@include nhsuk-font($size: 19, $weight: 'regular');
@include nhsuk-link-style-default;
display: inline-block;
padding-left: nhsuk-spacing(6);
}
&__icon {
width: nhsuk-px-to-rem(15px);
height: nhsuk-px-to-rem(13px);
color: $nhsuk-secondary-text-color;
fill: currentcolor;
forced-color-adjust: auto;
}
&__icon--prev {
margin-right: nhsuk-spacing(3);
}
&__icon--next {
margin-left: nhsuk-spacing(3);
}
}
}
import clsx from 'clsx'
import { HTMLProps } from 'react'
import { Link, LinkProps } from 'react-router-dom'
import './NumberedPagination.scss'
type NumberPagination = {
List: typeof List
Item: typeof Item
PreviousPage: typeof PreviousPageLink
NextPage: typeof NextPageLink
} & React.FC<HTMLProps<HTMLDivElement>>
type ItemProps = {
ellipses?: boolean
current?: boolean
} & LinkProps
const PreviousPageLink: React.FC<LinkProps> = ({
className,
children,
...rest
}): JSX.Element => {
return (
<div className="nhsuk-numbered-pagination__prev">
<Link
className={clsx(
'nhsuk-link nhsuk-numbered-pagination__link',
className,
)}
{...rest}
>
<svg
className="nhsuk-numbered-pagination__icon nhsuk-numbered-pagination__icon--prev"
xmlns="http://www.w3.org/2000/svg"
height="13"
width="15"
aria-hidden="true"
focusable="false"
viewBox="0 0 15 13"
>
<path d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"></path>
</svg>
<span className="nhsuk-numbered-pagination__link-title">
{children}
</span>
</Link>
</div>
)
}
const NextPageLink: React.FC<LinkProps> = ({
className,
children,
...rest
}): JSX.Element => {
return (
<div className="nhsuk-numbered-pagination__next">
<Link
className={clsx(
'nhsuk-link nhsuk-numbered-pagination__link',
className,
)}
{...rest}
>
<span className="nhsuk-numbered-pagination__link-title">
{children}
</span>
<svg
className="nhsuk-numbered-pagination__icon nhsuk-numbered-pagination__icon--next"
xmlns="http://www.w3.org/2000/svg"
height="13"
width="15"
aria-hidden="true"
focusable="false"
viewBox="0 0 15 13"
>
<path d="m8.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"></path>
</svg>
</Link>
</div>
)
}
const List: React.FC<HTMLProps<HTMLUListElement>> = ({
className,
children,
}): JSX.Element => {
return (
<ul className={clsx('nhsuk-numbered-pagination__list', className)}>
{children}
</ul>
)
}
const Item: React.FC<ItemProps> = ({
className,
children,
ellipses,
current,
...rest
}): JSX.Element => {
return (
<li
className={clsx('nhsuk-numbered-pagination__item', {
'nhsuk-numbered-pagination__item--ellipses': ellipses,
'nhsuk-numbered-pagination__item--current': current && !ellipses,
})}
>
{ellipses ? (
'⋯'
) : (
<Link
className={clsx(
'nhsuk-link nhsuk-numbered-pagination__link',
className,
)}
{...rest}
aria-label={`Page ${children}`}
aria-current={current ? 'page' : undefined}
>
{children}
</Link>
)}
</li>
)
}
const NumberedPagination: NumberPagination = ({
className,
children,
role = 'navigation',
'aria-label': ariaLabel = 'results',
...rest
}): JSX.Element => {
return (
<nav
className={clsx('nhsuk-numbered-pagination', className)}
role={role}
aria-label={ariaLabel}
{...rest}
>
{children}
</nav>
)
}
NumberedPagination.List = List
NumberedPagination.Item = Item
NumberedPagination.NextPage = NextPageLink
NumberedPagination.PreviousPage = PreviousPageLink
NumberedPagination.displayName = 'NumberedPagination'
List.displayName = 'NumberedPagination.List'
Item.displayName = 'NumberedPagination.Item'
PreviousPageLink.displayName = 'NumberedPagination.PreviousPage'
NextPageLink.displayName = 'NumberedPagination.NextPage'
export { NumberedPagination }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment