1
1
'use client' ;
2
2
3
3
import { AiFillCaretLeft , AiFillCaretRight } from 'react-icons/ai' ;
4
- import { MouseEventHandler } from 'react' ;
4
+ import { HTMLAttributes } from 'react' ;
5
5
6
- interface PaginationProps extends React . HTMLAttributes < HTMLElement > {
6
+ interface PaginationProps extends HTMLAttributes < HTMLElement > {
7
7
currentPage : number ;
8
8
totalPages : number ;
9
9
onPageChange : ( page : number ) => void ;
10
+ search ?: string ;
10
11
readonly ?: boolean ;
11
12
}
12
13
@@ -16,26 +17,42 @@ const getPageRange = (currentPage: number, totalPages: number): number[] => {
16
17
) ;
17
18
} ;
18
19
20
+ function buildHref ( page : number , search ?: string ) {
21
+ const params = new URLSearchParams ( ) ;
22
+ if ( search ) params . set ( 'search' , search ) ;
23
+ if ( page > 1 ) params . set ( 'page' , String ( page ) ) ;
24
+ return `?${ params . toString ( ) } ` ;
25
+ }
26
+
19
27
export function Pagination ( {
20
28
currentPage,
21
29
totalPages,
22
30
onPageChange,
31
+ search,
23
32
readonly = false ,
24
33
...rest
25
34
} : PaginationProps ) {
26
- const buttonBase = 'px-2 py-1 rounded transition text-purple-800 hover:underline focus:underline' ;
27
- const buttonCursor = readonly ? 'cursor-not-allowed' : 'cursor-pointer' ;
35
+ const linkBase = 'px-2 py-1 rounded transition text-purple-800 hover:underline focus:underline' ;
36
+ const linkCursor = readonly ? 'cursor-not-allowed' : 'cursor-pointer' ;
28
37
29
38
const readonlyProps = {
30
- disabled : readonly || undefined ,
31
39
tabIndex : readonly ? - 1 : undefined ,
40
+ 'aria-disabled' : readonly ? true : undefined ,
32
41
} ;
33
42
34
- const createPageHandler : ( page : number ) => MouseEventHandler < HTMLButtonElement > =
35
- ( page ) => ( e ) => {
43
+ // Handles SPA soft navigation and fallback to native on middle-click/new tab
44
+ const handleLinkClick = ( page : number ) => ( e : React . MouseEvent < HTMLAnchorElement > ) => {
45
+ if ( readonly ) {
36
46
e . preventDefault ( ) ;
37
- if ( ! readonly ) onPageChange ( page ) ;
38
- } ;
47
+ return ;
48
+ }
49
+ // Allow ctrl/cmd+click, shift+click (open in new tab/window)
50
+ if ( e . defaultPrevented || e . metaKey || e . altKey || e . ctrlKey || e . shiftKey || e . button !== 0 ) {
51
+ return ;
52
+ }
53
+ e . preventDefault ( ) ;
54
+ onPageChange ( page ) ;
55
+ } ;
39
56
40
57
const pageRange = getPageRange ( currentPage , totalPages ) ;
41
58
@@ -49,67 +66,71 @@ export function Pagination({
49
66
>
50
67
< div className = "w-4 flex justify-start" >
51
68
{ currentPage > 1 && (
52
- < button
53
- onClick = { createPageHandler ( currentPage - 1 ) }
54
- className = { `${ buttonBase } ${ buttonCursor } ` }
69
+ < a
70
+ href = { buildHref ( currentPage - 1 , search ) }
55
71
aria-label = "Previous page"
72
+ className = { `${ linkBase } ${ linkCursor } ` }
73
+ onClick = { handleLinkClick ( currentPage - 1 ) }
56
74
{ ...readonlyProps }
57
75
>
58
76
< AiFillCaretLeft aria-hidden = "true" />
59
- </ button >
77
+ </ a >
60
78
) }
61
79
</ div >
62
80
63
81
< div className = "flex gap-1 items-center" >
64
82
{ pageRange . map ( ( p ) =>
65
83
p === currentPage ? (
66
- < button
84
+ < a
67
85
key = { `${ currentPage } -${ p } ` }
68
86
aria-current = "page"
69
- disabled
70
87
tabIndex = { - 1 }
71
88
className = "px-2 py-1 rounded bg-purple-950 text-white pointer-events-none cursor-not-allowed"
89
+ href = { buildHref ( p , search ) }
72
90
>
73
91
{ p }
74
- </ button >
92
+ </ a >
75
93
) : (
76
- < button
94
+ < a
77
95
key = { `${ currentPage } -${ p } ` }
78
- onClick = { createPageHandler ( p ) }
96
+ href = { buildHref ( p , search ) }
79
97
aria-label = { `Page ${ p } ` }
80
- className = { `${ buttonBase } ${ buttonCursor } ` }
98
+ className = { `${ linkBase } ${ linkCursor } ` }
99
+ onClick = { handleLinkClick ( p ) }
81
100
{ ...readonlyProps }
82
101
>
83
102
{ p }
84
- </ button >
103
+ </ a >
85
104
)
86
105
) }
87
106
88
107
{ ! pageRange . includes ( totalPages ) && (
89
108
< span className = "flex gap-2" >
90
109
< span aria-hidden = "true" > …</ span >
91
- < button
92
- onClick = { createPageHandler ( totalPages ) }
93
- className = { `${ buttonBase } ${ buttonCursor } ` }
110
+ < a
111
+ href = { buildHref ( totalPages , search ) }
112
+ className = { `${ linkBase } ${ linkCursor } ` }
94
113
aria-label = { `Page ${ totalPages } ` }
114
+ onClick = { handleLinkClick ( totalPages ) }
95
115
{ ...readonlyProps }
96
116
>
97
117
{ totalPages }
98
- </ button >
118
+ </ a >
99
119
</ span >
100
120
) }
101
121
</ div >
102
122
103
123
< div className = "w-4 flex justify-end" >
104
124
{ currentPage < totalPages && (
105
- < button
106
- onClick = { createPageHandler ( currentPage + 1 ) }
107
- className = { `${ buttonBase } ${ buttonCursor } ` }
125
+ < a
126
+ href = { buildHref ( currentPage + 1 , search ) }
108
127
aria-label = "Next page"
128
+ className = { `${ linkBase } ${ linkCursor } ` }
129
+ onClick = { handleLinkClick ( currentPage + 1 ) }
109
130
{ ...readonlyProps }
110
131
>
111
132
< AiFillCaretRight aria-hidden = "true" />
112
- </ button >
133
+ </ a >
113
134
) }
114
135
</ div >
115
136
</ nav >
0 commit comments