Skip to content

Commit 58c9161

Browse files
committed
Move rendering logic into root component
1 parent 9fb2455 commit 58c9161

File tree

2 files changed

+197
-49
lines changed

2 files changed

+197
-49
lines changed

src/components/ui/pagination.tsx

Lines changed: 177 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,184 @@ import * as React from 'react'
22
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react'
33

44
import { cn } from '../../lib/utils'
5-
import { Button, buttonVariants } from './button'
5+
import { buttonVariants } from './button'
6+
7+
type PaginationProps = Omit<React.ComponentProps<'nav'>, 'onChange'> & {
8+
totalCount: number
9+
selectedPage: number
10+
showFirstPageButton?: boolean
11+
showLastPageButton?: boolean
12+
onPageChange?: (page: number) => void
13+
isTotalCountClipped?: boolean
14+
rowsPerPage: number
15+
}
16+
17+
function Pagination({
18+
className,
19+
totalCount,
20+
selectedPage,
21+
showFirstPageButton = false,
22+
showLastPageButton = false,
23+
onPageChange,
24+
isTotalCountClipped = false,
25+
rowsPerPage,
26+
...props
27+
}: PaginationProps) {
28+
if (totalCount <= 0 || selectedPage <= 0 || rowsPerPage <= 0) {
29+
return null
30+
}
31+
const totalCountBoundary = totalCount + (selectedPage - 1) * rowsPerPage
32+
const count = Math.ceil(totalCountBoundary / rowsPerPage)
33+
34+
if (count <= 0) {
35+
return null
36+
}
37+
38+
const handlePageChange = (newPage: number) => {
39+
if (onPageChange) {
40+
onPageChange(newPage)
41+
}
42+
}
43+
44+
const renderPaginationItems = () => {
45+
const items = []
46+
const maxVisiblePages = 7
47+
48+
items.push(
49+
<PaginationItem key="prev">
50+
<PaginationPrevious onClick={() => handlePageChange(selectedPage - 1)} disabled={selectedPage <= 1} />
51+
</PaginationItem>
52+
)
53+
54+
if (showFirstPageButton) {
55+
items.push(
56+
<PaginationItem key="first">
57+
<PaginationLink
58+
onClick={() => handlePageChange(1)}
59+
disabled={selectedPage <= 1}
60+
aria-label="Go to first page"
61+
>
62+
First
63+
</PaginationLink>
64+
</PaginationItem>
65+
)
66+
}
67+
68+
if (count <= maxVisiblePages) {
69+
for (let i = 1; i <= count; i++) {
70+
items.push(
71+
<PaginationItem key={i}>
72+
<PaginationLink selected={selectedPage === i} onClick={() => handlePageChange(i)}>
73+
{i}
74+
</PaginationLink>
75+
</PaginationItem>
76+
)
77+
}
78+
} else {
79+
const leftSiblingIndex = Math.max(selectedPage - 1, 1)
80+
const rightSiblingIndex = Math.min(selectedPage + 1, count)
81+
const shouldShowLeftDots = leftSiblingIndex > 2
82+
const shouldShowRightDots = rightSiblingIndex < count - 1
83+
if (!shouldShowLeftDots && shouldShowRightDots) {
84+
const leftItemCount = 5
85+
for (let i = 1; i <= leftItemCount; i++) {
86+
items.push(
87+
<PaginationItem key={i}>
88+
<PaginationLink selected={selectedPage === i} onClick={() => handlePageChange(i)}>
89+
{i}
90+
</PaginationLink>
91+
</PaginationItem>
92+
)
93+
}
94+
95+
items.push(<PaginationEllipsis key="ellipsis-right" />)
96+
97+
items.push(
98+
<PaginationItem key={count}>
99+
<PaginationLink onClick={() => handlePageChange(count)}>{count}</PaginationLink>
100+
</PaginationItem>
101+
)
102+
} else if (shouldShowLeftDots && !shouldShowRightDots) {
103+
items.push(
104+
<PaginationItem key={1}>
105+
<PaginationLink onClick={() => handlePageChange(1)}>1</PaginationLink>
106+
</PaginationItem>
107+
)
108+
109+
items.push(<PaginationEllipsis key="ellipsis-left" />)
110+
111+
const rightItemCount = 5
112+
for (let i = count - rightItemCount + 1; i <= count; i++) {
113+
items.push(
114+
<PaginationItem key={i}>
115+
<PaginationLink selected={selectedPage === i} onClick={() => handlePageChange(i)}>
116+
{i}
117+
</PaginationLink>
118+
</PaginationItem>
119+
)
120+
}
121+
} else if (shouldShowLeftDots && shouldShowRightDots) {
122+
items.push(
123+
<PaginationItem key={1}>
124+
<PaginationLink onClick={() => handlePageChange(1)}>1</PaginationLink>
125+
</PaginationItem>
126+
)
127+
128+
items.push(<PaginationEllipsis key="ellipsis-left" />)
129+
130+
for (let i = leftSiblingIndex; i <= rightSiblingIndex; i++) {
131+
items.push(
132+
<PaginationItem key={i}>
133+
<PaginationLink selected={selectedPage === i} onClick={() => handlePageChange(i)}>
134+
{i}
135+
</PaginationLink>
136+
</PaginationItem>
137+
)
138+
}
139+
140+
items.push(<PaginationEllipsis key="ellipsis-right" />)
141+
142+
items.push(
143+
<PaginationItem key={count}>
144+
<PaginationLink onClick={() => handlePageChange(count)}>{count}</PaginationLink>
145+
</PaginationItem>
146+
)
147+
}
148+
}
149+
150+
if (showLastPageButton && !isTotalCountClipped) {
151+
items.push(
152+
<PaginationItem key="last">
153+
<PaginationLink
154+
onClick={() => handlePageChange(count)}
155+
disabled={selectedPage >= count}
156+
aria-label="Go to last page"
157+
>
158+
Last
159+
</PaginationLink>
160+
</PaginationItem>
161+
)
162+
}
163+
164+
items.push(
165+
<PaginationItem key="next">
166+
<PaginationNext onClick={() => handlePageChange(selectedPage + 1)} disabled={selectedPage >= count} />
167+
</PaginationItem>
168+
)
169+
170+
return items
171+
}
6172

7-
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
8173
return (
9174
<nav
10175
role="navigation"
11176
aria-label="pagination"
12177
data-slot="pagination"
13178
className={cn('mx-auto flex w-full justify-center', className)}
14179
{...props}
15-
/>
180+
>
181+
<PaginationContent>{renderPaginationItems()}</PaginationContent>
182+
</nav>
16183
)
17184
}
18185

@@ -31,34 +198,32 @@ function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
31198
}
32199

33200
type PaginationLinkProps<C extends React.ElementType = 'a'> = {
34-
isActive?: boolean
201+
selected?: boolean
35202
disabled?: boolean
36203
linkComponent?: C
37-
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
38-
Omit<React.ComponentProps<C>, 'ref'>
204+
} & Omit<React.ComponentProps<C>, 'ref'>
39205

40206
function PaginationLink<C extends React.ElementType = 'a'>({
41207
className,
42-
isActive,
208+
selected,
43209
disabled,
44-
size = 'icon',
45210
linkComponent,
46211
...props
47212
}: PaginationLinkProps<C>) {
48213
const Component = linkComponent || 'a'
49214

50215
return (
51216
<Component
52-
aria-current={isActive ? 'page' : undefined}
217+
aria-current={selected ? 'page' : undefined}
53218
aria-disabled={disabled}
54219
tabIndex={disabled ? -1 : undefined}
55220
data-slot="pagination-link"
56-
data-active={isActive}
221+
data-active={selected}
57222
data-disabled={disabled}
58223
className={cn(
59224
buttonVariants({
60-
variant: isActive ? 'outline' : 'ghost',
61-
size,
225+
variant: selected ? 'outline' : 'ghost',
226+
size: 'default',
62227
}),
63228
disabled && 'pointer-events-none opacity-50',
64229
className
@@ -77,7 +242,6 @@ function PaginationPrevious({
77242
return (
78243
<PaginationLink
79244
aria-label="Go to previous page"
80-
size="default"
81245
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
82246
linkComponent={linkComponent}
83247
{...props}
@@ -97,7 +261,6 @@ function PaginationNext({
97261
return (
98262
<PaginationLink
99263
aria-label="Go to next page"
100-
size="default"
101264
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
102265
linkComponent={linkComponent}
103266
{...props}

src/stories/Pagination/Pagination.stories.tsx

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import type { StoryObj } from '@storybook/react-vite'
2-
import {
3-
Pagination,
4-
PaginationContent,
5-
PaginationItem,
6-
PaginationLink,
7-
PaginationNext,
8-
PaginationPrevious,
9-
PaginationEllipsis,
10-
} from '../../components/ui/pagination.tsx'
2+
import { Pagination } from '../../components/ui/pagination.tsx'
113
import { expect, within } from 'storybook/test'
124

135
const meta = {
@@ -25,39 +17,32 @@ const meta = {
2517
url: 'https://www.figma.com/design/dSsI9L6NSpNCorbSdiYd1k/Oasis-Design-System---shadcn-ui---Default---December-2024?node-id=65-516&p=f&t=LMIwZIurfLRROj6v-0',
2618
},
2719
},
20+
argTypes: {
21+
totalCount: {
22+
description:
23+
'The total number of records that match the query, i.e. the number of records the query would return with limit=infinity.',
24+
control: 'number',
25+
},
26+
isTotalCountClipped: {
27+
description: 'Whether total_count is clipped for performance reasons.',
28+
control: 'number',
29+
},
30+
},
2831
tags: ['autodocs'],
2932
}
3033

3134
export default meta
3235
type Story = StoryObj<typeof Pagination>
3336

3437
export const Default: Story = {
35-
render: () => (
36-
<Pagination>
37-
<PaginationContent>
38-
<PaginationItem>
39-
<PaginationPrevious href="#" />
40-
</PaginationItem>
41-
<PaginationItem>
42-
<PaginationLink href="#">1</PaginationLink>
43-
</PaginationItem>
44-
<PaginationItem>
45-
<PaginationLink href="#" isActive>
46-
2
47-
</PaginationLink>
48-
</PaginationItem>
49-
<PaginationItem>
50-
<PaginationEllipsis />
51-
</PaginationItem>
52-
<PaginationItem>
53-
<PaginationLink href="#">15</PaginationLink>
54-
</PaginationItem>
55-
<PaginationItem>
56-
<PaginationNext href="#" />
57-
</PaginationItem>
58-
</PaginationContent>
59-
</Pagination>
60-
),
38+
args: {
39+
totalCount: 99,
40+
selectedPage: 2,
41+
rowsPerPage: 10,
42+
showFirstPageButton: true,
43+
showLastPageButton: true,
44+
isTotalCountClipped: false,
45+
},
6146
play: async ({ canvasElement }) => {
6247
const canvas = within(canvasElement)
6348
const pagination = canvas.getByRole('navigation')

0 commit comments

Comments
 (0)