Skip to content

Commit 42288af

Browse files
Align pagination with NHS.UK frontend
1 parent 97f19db commit 42288af

File tree

6 files changed

+356
-81
lines changed

6 files changed

+356
-81
lines changed

docs/upgrade-to-6.0.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,14 @@ The [notification banner](https://service-manual.nhs.uk/design-system/components
5151
All components have been tested as React Server Components (RSC) but due to [multipart namespace component limitations](https://ivicabatinic.from.hr/posts/multipart-namespace-components-addressing-rsc-and-dot-notation-issues) an alternative syntax (without dot notation) can be used as a workaround:
5252

5353
```patch
54-
<Pagination>
55-
- <Pagination.Link href="/section/treatments" previous>
56-
+ <PaginationLink href="/section/treatments" previous>
57-
Treatments
58-
- </Pagination.Link>
59-
+ </PaginationLink>
60-
- <Pagination.Link href="/section/symptoms" next>
61-
+ <PaginationLink href="/section/symptoms" next>
62-
Symptoms
63-
- </Pagination.Link>
64-
+ </PaginationLink>
65-
</Pagination>
54+
<Breadcrumb>
55+
- <Breadcrumb.Item href="#">Home</Breadcrumb.Item>
56+
- <Breadcrumb.Item href="#">NHS services</Breadcrumb.Item>
57+
- <Breadcrumb.Item href="#">Hospitals</Breadcrumb.Item>
58+
+ <BreadcrumbItem href="#">Home</BreadcrumbItem>
59+
+ <BreadcrumbItem href="#">NHS services</BreadcrumbItem>
60+
+ <BreadcrumbItem href="#">Hospitals</BreadcrumbItem>
61+
</Breadcrumb>
6662
```
6763

6864
## Breaking changes
@@ -441,6 +437,26 @@ To align with NHS.UK frontend, the error summary component is automatically aler
441437
</ErrorSummary>
442438
```
443439

440+
### Pagination
441+
442+
To align with NHS.UK frontend, the pagination link component automatically renders its own "Previous page" or "Next page" text, with "page" being visually hidden. You will need to make the following changes:
443+
444+
- rename the `Pagination.Link` component to `Pagination.Item`
445+
- move text content (or the `children` prop) to the `labelText` prop
446+
447+
```patch
448+
<Pagination>
449+
- <Pagination.Link href="/section/treatments" previous>
450+
- Treatments
451+
- </Pagination.Link>
452+
- <Pagination.Link href="/section/symptoms" next>
453+
- Symptoms
454+
- </Pagination.Link>
455+
+ <Pagination.Item labelText="Treatments" href="/section/treatments" previous />
456+
+ <Pagination.Item labelText="Symptoms" href="/section/symptoms" next />
457+
</Pagination>
458+
```
459+
444460
### Select
445461

446462
You must rename the `Select` prop `selectRef` to `ref` for consistency with other components:

src/__tests__/index.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ describe('Index', () => {
9696
'NotificationBannerLink',
9797
'NotificationBannerTitle',
9898
'Pagination',
99+
'PaginationItem',
99100
'PaginationLink',
101+
'PaginationLinkText',
100102
'Panel',
101103
'PanelTitle',
102104
'Radios',
Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,98 @@
11
import classNames from 'classnames';
2-
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
2+
import { forwardRef, type ComponentPropsWithoutRef, type FC, type PropsWithChildren } from 'react';
33
import { ArrowLeftIcon, ArrowRightIcon } from '#components/content-presentation/index.js';
44
import { type AsElementLink } from '#util/types/LinkTypes.js';
55

6-
export interface PaginationLinkProps extends AsElementLink<HTMLAnchorElement> {
7-
previous?: boolean;
8-
next?: boolean;
9-
}
6+
export type PaginationItemProps = PaginationLinkProps;
7+
8+
export const PaginationItem = forwardRef<HTMLAnchorElement, PaginationItemProps>(
9+
(props, forwardedRef) => {
10+
return (
11+
<li
12+
className={classNames(
13+
{ 'nhsuk-pagination-item--previous': props.previous },
14+
{ 'nhsuk-pagination-item--next': props.next },
15+
)}
16+
>
17+
<PaginationLink ref={forwardedRef} {...props} />
18+
</li>
19+
);
20+
},
21+
);
22+
23+
export type PaginationLinkProps = PaginationLinkTextProps & AsElementLink<HTMLAnchorElement>;
1024

1125
export const PaginationLink = forwardRef<HTMLAnchorElement, PaginationLinkProps>(
12-
({ className, children, asElement: Element = 'a', previous, next, ...rest }, forwardedRef) => (
13-
<li
14-
className={classNames(
15-
{ 'nhsuk-pagination-item--previous': previous },
16-
{ 'nhsuk-pagination-item--next': next },
17-
)}
18-
>
26+
({ className, asElement: Element = 'a', ...rest }, forwardedRef) => {
27+
const { children, labelText, previous, next, ...elementRest } = rest;
28+
29+
const isPrevious = !!previous && !next;
30+
const isNext = !!next && !previous;
31+
32+
return (
1933
<Element
2034
className={classNames(
2135
'nhsuk-pagination__link',
22-
{ 'nhsuk-pagination__link--prev': previous },
23-
{ 'nhsuk-pagination__link--next': next },
36+
{ 'nhsuk-pagination__link--prev': isPrevious },
37+
{ 'nhsuk-pagination__link--next': isNext },
2438
className,
2539
)}
40+
rel={isPrevious || isNext ? (isPrevious ? 'prev' : 'next') : undefined}
2641
ref={forwardedRef}
27-
{...rest}
42+
{...elementRest}
2843
>
29-
<span className="nhsuk-pagination__title">
30-
{previous ? 'Previous' : null}
31-
{next ? 'Next' : null}
32-
</span>
33-
<span className="nhsuk-u-visually-hidden">:</span>
34-
<span className="nhsuk-pagination__page">{children}</span>
35-
{previous ? <ArrowLeftIcon /> : null}
36-
{next ? <ArrowRightIcon /> : null}
44+
<PaginationLinkText {...rest} />
45+
{isPrevious ? <ArrowLeftIcon /> : null}
46+
{isNext ? <ArrowRightIcon /> : null}
3747
</Element>
38-
</li>
39-
),
48+
);
49+
},
4050
);
4151

52+
export type PaginationLinkTextProps = PropsWithChildren &
53+
(
54+
| WithLabelText<{
55+
previous: true;
56+
next?: never;
57+
}>
58+
| WithLabelText<{
59+
previous?: never;
60+
next: true;
61+
}>
62+
);
63+
64+
type WithLabelText<T> = T & {
65+
labelText?: string;
66+
};
67+
68+
export const PaginationLinkText: FC<PaginationLinkTextProps> = ({
69+
children,
70+
previous,
71+
next,
72+
labelText,
73+
}) => {
74+
return (
75+
<>
76+
{children || previous || next ? (
77+
<span className="nhsuk-pagination__title">
78+
{children || (
79+
<>
80+
{previous ? 'Previous' : 'Next'}
81+
<span className="nhsuk-u-visually-hidden"> page</span>
82+
</>
83+
)}
84+
</span>
85+
) : null}
86+
{labelText ? (
87+
<>
88+
<span className="nhsuk-u-visually-hidden">:</span>
89+
<span className="nhsuk-pagination__page">{labelText}</span>
90+
</>
91+
) : null}
92+
</>
93+
);
94+
};
95+
4296
export type PaginationProps = ComponentPropsWithoutRef<'nav'>;
4397

4498
const PaginationComponent = forwardRef<HTMLElement, PaginationProps>(
@@ -56,8 +110,10 @@ const PaginationComponent = forwardRef<HTMLElement, PaginationProps>(
56110
);
57111

58112
PaginationComponent.displayName = 'Pagination';
113+
PaginationItem.displayName = 'Pagination.Item';
59114
PaginationLink.displayName = 'Pagination.Link';
115+
PaginationLinkText.displayName = 'Pagination.LinkText';
60116

61117
export const Pagination = Object.assign(PaginationComponent, {
62-
Link: PaginationLink,
118+
Item: PaginationItem,
63119
});

src/components/navigation/pagination/__tests__/Pagination.test.tsx

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,73 @@ describe('Pagination', () => {
88
expect(container).toMatchSnapshot('Pagination');
99
});
1010

11-
describe('Pagination.Link', () => {
12-
it('matches snapshot', () => {
13-
const { container } = render(<Pagination.Link />);
11+
describe('Pagination.Item', () => {
12+
it('matches snapshot with previous item', () => {
13+
const { container } = render(<Pagination.Item href="#" labelText="Page name" previous />);
1414

15-
expect(container).toMatchSnapshot('Pagination.Link');
15+
expect(container).toMatchSnapshot();
16+
});
17+
18+
it('matches snapshot with previous item (no label text)', () => {
19+
const { container } = render(<Pagination.Item href="#" previous />);
20+
21+
expect(container).toMatchSnapshot();
22+
});
23+
24+
it('matches snapshot with next item', () => {
25+
const { container } = render(<Pagination.Item href="#" labelText="Page name" next />);
26+
27+
expect(container).toMatchSnapshot();
28+
});
29+
30+
it('matches snapshot with next item (no label text)', () => {
31+
const { container } = render(<Pagination.Item href="#" next />);
32+
33+
expect(container).toMatchSnapshot();
1634
});
1735

1836
it('renders previous elements', () => {
19-
const { container } = render(<Pagination.Link previous>PreviousText</Pagination.Link>);
20-
21-
expect(container.querySelector('.nhsuk-pagination-item--previous')).toBeTruthy();
22-
expect(container.querySelector('.nhsuk-pagination-item--next')).toBeFalsy();
23-
expect(container.querySelector('.nhsuk-pagination__title')?.textContent).toBe('Previous');
24-
expect(container.querySelector('.nhsuk-icon--arrow-left')).toBeTruthy();
25-
expect(container.querySelector('.nhsuk-icon--arrow-right')).toBeFalsy();
26-
expect(container.querySelector('.nhsuk-pagination__page')?.textContent).toBe('PreviousText');
27-
expect(
28-
container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--prev'),
29-
).toBeTruthy();
30-
expect(
31-
container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--next'),
32-
).toBeFalsy();
37+
const { container } = render(<Pagination.Item href="#" labelText="Page name" previous />);
38+
39+
const itemEl = container.querySelector('li');
40+
const linkEl = container.querySelector('a');
41+
const titleEl = container.querySelector('.nhsuk-pagination__title');
42+
const pageEl = container.querySelector('.nhsuk-pagination__page');
43+
const iconEl = container.querySelector('.nhsuk-icon');
44+
45+
expect(itemEl).toHaveClass('nhsuk-pagination-item--previous');
46+
expect(itemEl).not.toHaveClass('nhsuk-pagination-item--next');
47+
48+
expect(linkEl).toHaveClass('nhsuk-pagination__link', 'nhsuk-pagination__link--prev');
49+
expect(linkEl).not.toHaveClass('nhsuk-pagination__link--next');
50+
51+
expect(titleEl).toHaveTextContent('Previous');
52+
expect(pageEl).toHaveTextContent('Page name');
53+
54+
expect(iconEl).toHaveClass('nhsuk-icon--arrow-left');
55+
expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-right');
3356
});
3457

3558
it('renders next elements', () => {
36-
const { container } = render(<Pagination.Link next>NextText</Pagination.Link>);
37-
38-
expect(container.querySelector('.nhsuk-pagination-item--previous')).toBeFalsy();
39-
expect(container.querySelector('.nhsuk-pagination-item--next')).toBeTruthy();
40-
expect(container.querySelector('.nhsuk-pagination__title')?.textContent).toBe('Next');
41-
expect(container.querySelector('.nhsuk-icon--arrow-left')).toBeFalsy();
42-
expect(container.querySelector('.nhsuk-icon--arrow-right')).toBeTruthy();
43-
expect(container.querySelector('.nhsuk-pagination__page')?.textContent).toBe('NextText');
44-
expect(
45-
container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--prev'),
46-
).toBeFalsy();
47-
expect(
48-
container.querySelector('.nhsuk-pagination__link.nhsuk-pagination__link--next'),
49-
).toBeTruthy();
59+
const { container } = render(<Pagination.Item href="#" labelText="Page name" next />);
60+
61+
const itemEl = container.querySelector('li');
62+
const linkEl = container.querySelector('a');
63+
const titleEl = container.querySelector('.nhsuk-pagination__title');
64+
const pageEl = container.querySelector('.nhsuk-pagination__page');
65+
const iconEl = container.querySelector('.nhsuk-icon');
66+
67+
expect(itemEl).not.toHaveClass('nhsuk-pagination-item--previous');
68+
expect(itemEl).toHaveClass('nhsuk-pagination-item--next');
69+
70+
expect(linkEl).not.toHaveClass('nhsuk-pagination__link--prev');
71+
expect(linkEl).toHaveClass('nhsuk-pagination__link', 'nhsuk-pagination__link--next');
72+
73+
expect(titleEl).toHaveTextContent('Next');
74+
expect(pageEl).toHaveTextContent('Page name');
75+
76+
expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-left');
77+
expect(iconEl).toHaveClass('nhsuk-icon--arrow-right');
5078
});
5179
});
5280
});

0 commit comments

Comments
 (0)