Skip to content

Commit 81616e9

Browse files
Add support for numbered pagination
1 parent 42b8270 commit 81616e9

File tree

7 files changed

+1002
-29
lines changed

7 files changed

+1002
-29
lines changed
Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,54 @@
11
import classNames from 'classnames';
2-
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
3-
import { PaginationItem } from './components/index.js';
2+
import { Children, forwardRef, type ComponentPropsWithoutRef } from 'react';
3+
import { PaginationItem, PaginationLink } from './components/index.js';
4+
import { childIsOfComponentType } from '#util/types/TypeGuards.js';
45

56
export type PaginationProps = ComponentPropsWithoutRef<'nav'>;
67

78
const PaginationComponent = forwardRef<HTMLElement, PaginationProps>(
8-
({ className, children, 'aria-label': ariaLabel = 'Pagination', ...rest }, forwardedRef) => (
9-
<nav
10-
className={classNames('nhsuk-pagination', className)}
11-
role="navigation"
12-
aria-label={ariaLabel}
13-
ref={forwardedRef}
14-
{...rest}
15-
>
16-
<ul className="nhsuk-list nhsuk-pagination__list">{children}</ul>
17-
</nav>
18-
),
9+
({ className, children, 'aria-label': ariaLabel = 'Pagination', ...rest }, forwardedRef) => {
10+
const items = Children.toArray(children);
11+
12+
// Filter previous and next links
13+
const links = items.filter((child) => childIsOfComponentType(child, PaginationLink));
14+
const linkPrevious = links.find(({ props }) => props.previous);
15+
const linkNext = links.find(({ props }) => props.next);
16+
17+
// Filter numbered list items
18+
const listItems = items.filter((child) => childIsOfComponentType(child, PaginationItem));
19+
const listItemsNumbered = listItems.filter(({ props }) => props.number || props.ellipsis);
20+
21+
return (
22+
<nav
23+
className={classNames(
24+
'nhsuk-pagination',
25+
{ 'nhsuk-pagination--numbered': listItemsNumbered.length },
26+
className,
27+
)}
28+
role="navigation"
29+
aria-label={ariaLabel}
30+
ref={forwardedRef}
31+
{...rest}
32+
>
33+
{linkPrevious}
34+
<ul
35+
className={
36+
listItemsNumbered.length
37+
? 'nhsuk-pagination__list' // Standard pagination list class
38+
: 'nhsuk-list nhsuk-pagination__list' // Legacy pagination list class
39+
}
40+
>
41+
{listItems}
42+
</ul>
43+
{linkNext}
44+
</nav>
45+
);
46+
},
1947
);
2048

2149
PaginationComponent.displayName = 'Pagination';
2250

2351
export const Pagination = Object.assign(PaginationComponent, {
2452
Item: PaginationItem,
53+
Link: PaginationLink,
2554
});

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

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { render } from '@testing-library/react';
22
import { Pagination } from '..';
3+
import { createRef } from 'react';
34

45
describe('Pagination', () => {
56
it('matches snapshot', () => {
@@ -8,6 +9,119 @@ describe('Pagination', () => {
89
expect(container).toMatchSnapshot('Pagination');
910
});
1011

12+
it('matches snapshot with next item', () => {
13+
const { container } = render(
14+
<Pagination>
15+
<Pagination.Item href="#" labelText="Page name" next />
16+
</Pagination>,
17+
);
18+
19+
expect(container).toMatchSnapshot();
20+
});
21+
22+
it('matches snapshot with previous item', () => {
23+
const { container } = render(
24+
<Pagination>
25+
<Pagination.Item href="#" labelText="Page name" previous />
26+
</Pagination>,
27+
);
28+
29+
expect(container).toMatchSnapshot();
30+
});
31+
32+
it('matches snapshot with previous and next items', () => {
33+
const { container } = render(
34+
<Pagination>
35+
<Pagination.Item href="#" labelText="Treatments" previous />
36+
<Pagination.Item href="#" labelText="Symptoms" next />
37+
</Pagination>,
38+
);
39+
40+
expect(container).toMatchSnapshot();
41+
});
42+
43+
it('matches snapshot with previous and next items (translated)', () => {
44+
const { container } = render(
45+
<Pagination>
46+
<Pagination.Item href="#" labelText="Driniaethau" previous>
47+
Blaenorol
48+
</Pagination.Item>
49+
<Pagination.Item href="#" labelText="Symptomau" next>
50+
Nesaf
51+
</Pagination.Item>
52+
</Pagination>,
53+
);
54+
55+
expect(container).toMatchSnapshot();
56+
});
57+
58+
it('matches snapshot with numbered items', () => {
59+
const { container } = render(
60+
<Pagination>
61+
<Pagination.Link href="/results?page=1" previous />
62+
<Pagination.Item href="/results?page=1" number={1} />
63+
<Pagination.Item href="/results?page=2" number={2} current />
64+
<Pagination.Item href="/results?page=3" number={3} />
65+
<Pagination.Link href="/results?page=3" next />
66+
</Pagination>,
67+
);
68+
69+
expect(container).toMatchSnapshot();
70+
});
71+
72+
it('matches snapshot with numbered items (translated)', () => {
73+
const { container } = render(
74+
<Pagination>
75+
<Pagination.Link href="/results?page=1" previous>
76+
Blaenorol
77+
</Pagination.Link>
78+
<Pagination.Item href="/results?page=1" number={1} />
79+
<Pagination.Item href="/results?page=2" number={2} current />
80+
<Pagination.Item href="/results?page=3" number={3} />
81+
<Pagination.Link href="/results?page=3" next>
82+
Nesaf
83+
</Pagination.Link>
84+
</Pagination>,
85+
);
86+
87+
expect(container).toMatchSnapshot();
88+
});
89+
90+
it('forwards refs', () => {
91+
const ref = createRef<HTMLElement>();
92+
93+
const { container } = render(
94+
<Pagination ref={ref}>
95+
<Pagination.Item href="#" labelText="Treatments" previous />
96+
<Pagination.Item href="#" labelText="Symptoms" next />
97+
</Pagination>,
98+
);
99+
100+
const paginationEl = container.querySelector('nav');
101+
102+
expect(ref.current).toBe(paginationEl);
103+
expect(ref.current).toHaveClass('nhsuk-pagination');
104+
});
105+
106+
it('forwards refs with numbered items', () => {
107+
const ref = createRef<HTMLElement>();
108+
109+
const { container } = render(
110+
<Pagination ref={ref}>
111+
<Pagination.Link href="/results?page=1" previous />
112+
<Pagination.Item href="/results?page=1" number={1} />
113+
<Pagination.Item href="/results?page=2" number={2} current />
114+
<Pagination.Item href="/results?page=3" number={3} />
115+
<Pagination.Link href="/results?page=3" next />
116+
</Pagination>,
117+
);
118+
119+
const paginationEl = container.querySelector('nav');
120+
121+
expect(ref.current).toBe(paginationEl);
122+
expect(ref.current).toHaveClass('nhsuk-pagination', 'nhsuk-pagination--numbered');
123+
});
124+
11125
describe('Pagination.Item', () => {
12126
it('matches snapshot with previous item', () => {
13127
const { container } = render(<Pagination.Item href="#" labelText="Page name" previous />);
@@ -33,6 +147,18 @@ describe('Pagination', () => {
33147
expect(container).toMatchSnapshot();
34148
});
35149

150+
it('matches snapshot with number', () => {
151+
const { container } = render(<Pagination.Item href="#" number={10} />);
152+
153+
expect(container).toMatchSnapshot();
154+
});
155+
156+
it('matches snapshot with ellipsis', () => {
157+
const { container } = render(<Pagination.Item ellipsis />);
158+
159+
expect(container).toMatchSnapshot();
160+
});
161+
36162
it('renders previous elements', () => {
37163
const { container } = render(<Pagination.Item href="#" labelText="Page name" previous />);
38164

@@ -76,5 +202,74 @@ describe('Pagination', () => {
76202
expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-left');
77203
expect(iconEl).toHaveClass('nhsuk-icon--arrow-right');
78204
});
205+
206+
it('renders number elements', () => {
207+
const { container } = render(<Pagination.Item href="#" number={10} />);
208+
209+
const itemEl = container.querySelector('li');
210+
const linkEl = container.querySelector('a');
211+
212+
expect(itemEl).toHaveClass('nhsuk-pagination__item');
213+
expect(linkEl).toHaveClass('nhsuk-pagination__link');
214+
expect(linkEl).toHaveTextContent('10');
215+
expect(linkEl).toHaveAccessibleName('Page 10');
216+
});
217+
218+
it('renders ellipsis elements', () => {
219+
const { container } = render(<Pagination.Item ellipsis />);
220+
221+
const itemEl = container.querySelector('li');
222+
const linkEl = container.querySelector('a');
223+
224+
expect(itemEl).toHaveClass('nhsuk-pagination__item', 'nhsuk-pagination__item--ellipsis');
225+
expect(itemEl).toHaveTextContent('⋯');
226+
expect(linkEl).toBeNull();
227+
});
228+
});
229+
230+
describe('Pagination.Link', () => {
231+
it('matches snapshot with previous link', () => {
232+
const { container } = render(<Pagination.Link href="#" previous />);
233+
234+
expect(container).toMatchSnapshot();
235+
});
236+
237+
it('matches snapshot with next link', () => {
238+
const { container } = render(<Pagination.Link href="#" next />);
239+
240+
expect(container).toMatchSnapshot();
241+
});
242+
243+
it('renders previous elements', () => {
244+
const { container } = render(<Pagination.Link href="#" previous />);
245+
246+
const linkEl = container.querySelector('a');
247+
const titleEl = container.querySelector('.nhsuk-pagination__title');
248+
const iconEl = container.querySelector('.nhsuk-icon');
249+
250+
expect(linkEl).toHaveClass('nhsuk-pagination__previous');
251+
expect(linkEl).not.toHaveClass('nhsuk-pagination__next');
252+
253+
expect(titleEl).toHaveTextContent('Previous');
254+
255+
expect(iconEl).toHaveClass('nhsuk-icon--arrow-left');
256+
expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-right');
257+
});
258+
259+
it('renders next elements', () => {
260+
const { container } = render(<Pagination.Link href="#" next />);
261+
262+
const linkEl = container.querySelector('a');
263+
const titleEl = container.querySelector('.nhsuk-pagination__title');
264+
const iconEl = container.querySelector('.nhsuk-icon');
265+
266+
expect(linkEl).not.toHaveClass('nhsuk-pagination__previous');
267+
expect(linkEl).toHaveClass('nhsuk-pagination__next');
268+
269+
expect(titleEl).toHaveTextContent('Next');
270+
271+
expect(iconEl).not.toHaveClass('nhsuk-icon--arrow-left');
272+
expect(iconEl).toHaveClass('nhsuk-icon--arrow-right');
273+
});
79274
});
80275
});

0 commit comments

Comments
 (0)