Last active
December 7, 2023 10:05
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
)} | |
</> | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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