Skip to content

Commit 81d1aa4

Browse files
committed
Create pagination component
1 parent d2378b0 commit 81d1aa4

File tree

5 files changed

+444
-10
lines changed

5 files changed

+444
-10
lines changed

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"lint": "yarn lint:scripts",
2525
"lint:scripts": "eslint app/",
2626
"ts-check": "yarn tsc --noEmit --skipLibCheck",
27-
"test": "echo \"Error: no test specified\" && exit 0"
27+
"test": "jest"
2828
},
2929
"engines": {
3030
"node": "20.x"
@@ -64,7 +64,7 @@
6464
"@stac-manager/data-plugins": "*",
6565
"@stac-manager/data-widgets": "*",
6666
"@testing-library/jest-dom": "^6.6.2",
67-
"@testing-library/react": "^16.0.1",
67+
"@testing-library/react": "^16.2.0",
6868
"@testing-library/user-event": "^14.5.2",
6969
"@turf/bbox": "^7.1.0",
7070
"@turf/bbox-polygon": "^7.1.0",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { Dispatch, SetStateAction } from 'react';
2+
import { Button, ButtonGroup } from '@chakra-ui/react';
3+
4+
import { usePaginate } from '$utils/usePaginateHook';
5+
import { zeroPad } from '$utils/format';
6+
7+
interface PaginationProps {
8+
page: number;
9+
numPages: number;
10+
onPageChange: Dispatch<SetStateAction<number>>;
11+
}
12+
13+
export function Pagination(props: PaginationProps) {
14+
const { numPages, page, onPageChange } = props;
15+
const paginate = usePaginate({
16+
numPages,
17+
currentPage: page,
18+
onPageChange
19+
});
20+
21+
return (
22+
<ButtonGroup size='sm' variant='outline' isAttached>
23+
<Button onClick={paginate.goFirst} disabled={!paginate.hasPrevious}>
24+
First
25+
</Button>
26+
<Button onClick={paginate.goPrevious} disabled={!paginate.hasPrevious}>
27+
Previous
28+
</Button>
29+
{paginate.left.map((p) => (
30+
<Button key={p} onClick={() => paginate.goToPage(p)}>
31+
{zeroPad(p)}
32+
</Button>
33+
))}
34+
{paginate.hasLeftBreak && <Button disabled>...</Button>}
35+
{paginate.pages.map((p) => (
36+
<Button
37+
key={p}
38+
onClick={() => paginate.goToPage(p)}
39+
variant={p === page ? 'solid' : 'outline'}
40+
colorScheme={p === page ? 'primary' : undefined}
41+
>
42+
{zeroPad(p)}
43+
</Button>
44+
))}
45+
{paginate.hasRightBreak && <Button disabled>...</Button>}
46+
{paginate.right.map((p) => (
47+
<Button key={p} onClick={() => paginate.goToPage(p)}>
48+
{zeroPad(p)}
49+
</Button>
50+
))}
51+
<Button onClick={paginate.goNext} disabled={!paginate.hasNext}>
52+
Next
53+
</Button>
54+
<Button onClick={paginate.goLast} disabled={!paginate.hasNext}>
55+
Last
56+
</Button>
57+
</ButtonGroup>
58+
);
59+
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import { usePaginate } from './usePaginateHook';
3+
4+
describe('usePaginate', () => {
5+
it('should initialize pagination correctly', () => {
6+
const { result } = renderHook(() =>
7+
usePaginate({ numPages: 20, currentPage: 10, onPageChange: jest.fn() })
8+
);
9+
10+
expect(result.current.pages).toEqual([9, 10, 11]);
11+
expect(result.current.left).toEqual([1, 2]);
12+
expect(result.current.right).toEqual([19, 20]);
13+
expect(result.current.hasLeftBreak).toBe(true);
14+
expect(result.current.hasRightBreak).toBe(true);
15+
});
16+
17+
it('should handle next page correctly', () => {
18+
const onPageChange = jest.fn();
19+
const { result } = renderHook(() =>
20+
usePaginate({ numPages: 10, currentPage: 1, onPageChange })
21+
);
22+
23+
act(() => {
24+
result.current.goNext();
25+
});
26+
27+
expect(onPageChange).toHaveBeenCalled();
28+
expect(onPageChange).toHaveBeenCalledWith(expect.any(Function));
29+
// The fn should increase by one.
30+
expect(onPageChange.mock.lastCall[0](10)).toEqual(11);
31+
});
32+
33+
it('should handle previous page correctly', () => {
34+
const onPageChange = jest.fn();
35+
const { result } = renderHook(() =>
36+
usePaginate({ numPages: 10, currentPage: 2, onPageChange })
37+
);
38+
39+
act(() => {
40+
result.current.goPrevious();
41+
});
42+
43+
expect(onPageChange).toHaveBeenCalled();
44+
expect(onPageChange).toHaveBeenCalledWith(expect.any(Function));
45+
// The fn should decrease by one.
46+
expect(onPageChange.mock.lastCall[0](10)).toEqual(9);
47+
});
48+
49+
it('should handle first page correctly', () => {
50+
const onPageChange = jest.fn();
51+
const { result } = renderHook(() =>
52+
usePaginate({ numPages: 10, currentPage: 5, onPageChange })
53+
);
54+
55+
act(() => {
56+
result.current.goFirst();
57+
});
58+
59+
expect(onPageChange).toHaveBeenCalledWith(1);
60+
});
61+
62+
it('should handle last page correctly', () => {
63+
const onPageChange = jest.fn();
64+
const { result } = renderHook(() =>
65+
usePaginate({ numPages: 10, currentPage: 5, onPageChange })
66+
);
67+
68+
act(() => {
69+
result.current.goLast();
70+
});
71+
72+
expect(onPageChange).toHaveBeenCalledWith(10);
73+
});
74+
75+
it('should handle go to specific page correctly', () => {
76+
const onPageChange = jest.fn();
77+
const { result } = renderHook(() =>
78+
usePaginate({ numPages: 10, currentPage: 5, onPageChange })
79+
);
80+
81+
act(() => {
82+
result.current.goToPage(3);
83+
});
84+
85+
expect(onPageChange).toHaveBeenCalledWith(3);
86+
});
87+
88+
it('should not go to an invalid page', () => {
89+
const onPageChange = jest.fn();
90+
const { result } = renderHook(() =>
91+
usePaginate({ numPages: 10, currentPage: 5, onPageChange })
92+
);
93+
94+
act(() => {
95+
result.current.goToPage(11);
96+
});
97+
98+
expect(onPageChange).not.toHaveBeenCalled();
99+
});
100+
101+
it('should not have margins when fewer than needed pages.', () => {
102+
// Needed pages are pageRange + 2 * (marginsRange + 1)
103+
// 2 * (marginsRange + 1) to have space for the ellipsis on each side
104+
// Default values are 2 for marginsRange and 5 for pageRange
105+
const { result } = renderHook(() =>
106+
usePaginate({
107+
numPages: 10,
108+
currentPage: 1,
109+
onPageChange: jest.fn(),
110+
pageRange: 5,
111+
marginsRange: 2
112+
})
113+
);
114+
115+
expect(result.current.pages).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
116+
expect(result.current.left).toEqual([]);
117+
expect(result.current.right).toEqual([]);
118+
expect(result.current.hasLeftBreak).toBe(false);
119+
expect(result.current.hasRightBreak).toBe(false);
120+
});
121+
122+
it('should render correctly when pageRange and marginsRange change', () => {
123+
const { result } = renderHook(() =>
124+
usePaginate({
125+
numPages: 11,
126+
currentPage: 6,
127+
onPageChange: jest.fn(),
128+
pageRange: 5,
129+
marginsRange: 1
130+
})
131+
);
132+
// Min blocks (between pages and ellipsis): 5 + 2 * (1 + 1) = 9
133+
134+
expect(result.current.pages).toEqual([4, 5, 6, 7, 8]);
135+
expect(result.current.left).toEqual([1]);
136+
expect(result.current.right).toEqual([11]);
137+
expect(result.current.hasLeftBreak).toBe(true);
138+
expect(result.current.hasRightBreak).toBe(true);
139+
});
140+
141+
it('should render correctly as pages change', () => {
142+
const { result, rerender } = renderHook((page: number = 1) =>
143+
usePaginate({
144+
numPages: 20,
145+
onPageChange: jest.fn(),
146+
currentPage: page
147+
})
148+
);
149+
150+
// Min blocks (between pages and ellipsis): 3 + 2 * (2 + 1) = 9
151+
152+
// Until page 5 it won't change anything because for the left side to have
153+
// pages we need a minimum of pageMargin (2) + ellipsis (1) + page to be
154+
// hidden (1) + half the page range (1)
155+
156+
for (let index = 1; index < 6; index++) {
157+
rerender(index);
158+
159+
expect(result.current.pages).toEqual([1, 2, 3, 4, 5, 6]);
160+
expect(result.current.left).toEqual([]);
161+
expect(result.current.right).toEqual([19, 20]);
162+
expect(result.current.hasLeftBreak).toBe(false);
163+
expect(result.current.hasRightBreak).toBe(true);
164+
}
165+
166+
// from 6 to we have a left and a right margin.
167+
for (let index = 6; index < 16; index++) {
168+
rerender(index);
169+
170+
expect(result.current.pages).toEqual([index - 1, index, index + 1]);
171+
expect(result.current.left).toEqual([1, 2]);
172+
expect(result.current.right).toEqual([19, 20]);
173+
expect(result.current.hasLeftBreak).toBe(true);
174+
expect(result.current.hasRightBreak).toBe(true);
175+
}
176+
177+
// from 16 to 20 we don't have a right margin.
178+
for (let index = 16; index <= 20; index++) {
179+
rerender(index);
180+
181+
expect(result.current.pages).toEqual([15, 16, 17, 18, 19, 20]);
182+
expect(result.current.left).toEqual([1, 2]);
183+
expect(result.current.right).toEqual([]);
184+
expect(result.current.hasLeftBreak).toBe(true);
185+
expect(result.current.hasRightBreak).toBe(false);
186+
}
187+
});
188+
189+
it('should render correctly as pages change for margin 0', () => {
190+
const { result, rerender } = renderHook((page: number = 1) =>
191+
usePaginate({
192+
numPages: 10,
193+
onPageChange: jest.fn(),
194+
currentPage: page,
195+
marginsRange: 0
196+
})
197+
);
198+
199+
// Since there are no margins there should always be the pageRange pages
200+
expect(result.current.pages).toEqual([1, 2, 3]);
201+
expect(result.current.left).toEqual([]);
202+
expect(result.current.right).toEqual([]);
203+
expect(result.current.hasLeftBreak).toBe(false);
204+
expect(result.current.hasRightBreak).toBe(false);
205+
206+
for (let index = 2; index < 10; index++) {
207+
rerender(index);
208+
209+
expect(result.current.pages).toEqual([index - 1, index, index + 1]);
210+
expect(result.current.left).toEqual([]);
211+
expect(result.current.right).toEqual([]);
212+
expect(result.current.hasLeftBreak).toBe(false);
213+
expect(result.current.hasRightBreak).toBe(false);
214+
}
215+
216+
rerender(10);
217+
218+
expect(result.current.pages).toEqual([8, 9, 10]);
219+
expect(result.current.left).toEqual([]);
220+
expect(result.current.right).toEqual([]);
221+
expect(result.current.hasLeftBreak).toBe(false);
222+
expect(result.current.hasRightBreak).toBe(false);
223+
});
224+
225+
it('should throw an error when currentPage is out of bounds', () => {
226+
expect.assertions(1);
227+
try {
228+
usePaginate({ numPages: 10, currentPage: 11, onPageChange: jest.fn() });
229+
} catch (error) {
230+
expect((error as Error).message).toEqual(
231+
'current page is out of bounds. [1, numPages]'
232+
);
233+
}
234+
});
235+
236+
it('should throw an error when page range is less than 1', () => {
237+
expect.assertions(1);
238+
try {
239+
usePaginate({
240+
numPages: 10,
241+
currentPage: 1,
242+
onPageChange: jest.fn(),
243+
pageRange: 0
244+
});
245+
} catch (error) {
246+
expect((error as Error).message).toEqual('pageRange must be at least 1');
247+
}
248+
});
249+
250+
it('should throw an error when marginRange is negative', () => {
251+
expect.assertions(1);
252+
try {
253+
usePaginate({
254+
numPages: 10,
255+
currentPage: 1,
256+
onPageChange: jest.fn(),
257+
marginsRange: -1
258+
});
259+
} catch (error) {
260+
expect((error as Error).message).toEqual(
261+
'marginsRange cannot be negative'
262+
);
263+
}
264+
});
265+
});

0 commit comments

Comments
 (0)