Skip to content

Commit bd9225d

Browse files
authored
feat(Pagination): add renderPage prop to Pagination (#5820)
Co-authored-by: hussam-i-am <[email protected]>
1 parent 5f40e43 commit bd9225d

23 files changed

+106
-63
lines changed

.changeset/cyan-lights-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
feat(Pagination): add renderPage prop to Pagination

e2e/components/Pagination.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const stories = [
1515
title: 'Hide Page Numbers',
1616
id: 'components-pagination-features--hide-page-numbers',
1717
},
18+
{
19+
title: 'Render Links',
20+
id: 'components-pagination-features--render-links',
21+
},
1822
] as const
1923

2024
test.describe('Pagination', () => {

packages/react/src/ActionList/ActionList.examples.stories.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Text from '../Text'
1919
import FormControl from '../FormControl'
2020
import {AriaStatus} from '../live-region'
2121
import {VisuallyHidden} from '../VisuallyHidden'
22+
import {ReactRouterLikeLink} from '../__tests__/mocks/ReactRouterLink'
2223

2324
const meta: Meta = {
2425
title: 'Components/ActionList/Examples',
@@ -31,17 +32,6 @@ const meta: Meta = {
3132
}
3233
export default meta
3334

34-
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
35-
const ReactRouterLikeLink = forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
36-
({to, children, ...props}: {to: string; children: React.ReactNode}, ref) => {
37-
return (
38-
<a ref={ref} href={to} {...props}>
39-
{children}
40-
</a>
41-
)
42-
},
43-
)
44-
4535
const NextJSLikeLink = forwardRef(
4636
({href, children}: {href: string; children: React.ReactNode}, ref): React.ReactElement => {
4737
const child = React.Children.only(children)

packages/react/src/Button/LinkButton.features.stories.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import React from 'react'
12
import {EyeIcon, ChevronRightIcon, HeartIcon, DownloadIcon} from '@primer/octicons-react'
2-
import React, {forwardRef} from 'react'
33
import {LinkButton} from '.'
4+
import {ReactRouterLikeLink} from '../__tests__/mocks/ReactRouterLink'
45

56
export default {
67
title: 'Components/LinkButton/Features',
@@ -66,17 +67,6 @@ export const Large = () => (
6667
</LinkButton>
6768
)
6869

69-
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
70-
const ReactRouterLikeLink = forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
71-
({to, children, ...props}: {to: string; children: React.ReactNode}, ref) => {
72-
return (
73-
<a ref={ref} href={to} {...props}>
74-
{children}
75-
</a>
76-
)
77-
},
78-
)
79-
8070
export const WithReactRouter = () => (
8171
<LinkButton to="/dummy" as={ReactRouterLikeLink}>
8272
Default

packages/react/src/NavList/NavList.stories.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '@primer/octicons-react'
2222
import Octicon from '../Octicon'
2323
import VisuallyHidden from '../_VisuallyHidden'
24+
import {ReactRouterLikeLink} from '../__tests__/mocks/ReactRouterLink'
2425

2526
const meta: Meta = {
2627
title: 'Components/NavList',
@@ -110,17 +111,6 @@ export const WithNestedSubItems: StoryFn = () => (
110111
</PageLayout>
111112
)
112113

113-
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
114-
const ReactRouterLikeLink = React.forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
115-
({to, children, ...props}, ref) => {
116-
return (
117-
<a ref={ref} href={to} {...props}>
118-
{children}
119-
</a>
120-
)
121-
},
122-
)
123-
124114
export const WithReactRouterLink = () => (
125115
<PageLayout>
126116
<PageLayout.Pane position="start">

packages/react/src/NavList/NavList.test.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ import React from 'react'
33
import {ThemeProvider} from '..'
44
import {NavList} from './NavList'
55
import {FeatureFlags} from '../FeatureFlags'
6-
7-
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
8-
9-
const ReactRouterLikeLink = React.forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
10-
({to, children, ...props}, ref) => {
11-
return (
12-
<a ref={ref} href={to} {...props}>
13-
{children}
14-
</a>
15-
)
16-
},
17-
)
6+
import {ReactRouterLikeLink} from '../__tests__/mocks/ReactRouterLink'
187

198
type NextJSLinkProps = {href: string; children: React.ReactNode}
209

@@ -97,7 +86,7 @@ describe('NavList.Item', () => {
9786
expect(aboutLink).not.toHaveAttribute('aria-current')
9887
})
9988

100-
it('is compatiable with React-Router-like link components', () => {
89+
it('is compatible with React-Router-like link components', () => {
10190
const {getByRole} = render(
10291
<NavList>
10392
<NavList.Item as={ReactRouterLikeLink} to={'/'} aria-current="page">

packages/react/src/Pagination/Pagination.docs.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
"defaultValue": "2",
6767
"description": "How many pages to display on each side of the currently selected page."
6868
},
69+
{
70+
"name": "renderPage",
71+
"type": "function",
72+
"defaultValue": "(props: PageProps) => ReactNode",
73+
"description": "Provide a custom component or render prop to render each page link within the component."
74+
},
6975
{
7076
"name": "sx",
7177
"type": "SystemStyleObject"

packages/react/src/Pagination/Pagination.features.stories.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from 'react'
1+
import React, {useState} from 'react'
22
import type {Meta} from '@storybook/react'
33
import type {ComponentProps} from '../utils/types'
44
import Pagination from './Pagination'
5+
import {ReactRouterLikeLink} from '../__tests__/mocks/ReactRouterLink'
56

67
export default {
78
title: 'Components/Pagination/Features',
@@ -32,3 +33,19 @@ HidePageNumbersByViewport.parameters = {
3233
export const HigherSurroundingPageCount = () => (
3334
<Pagination pageCount={15} currentPage={5} surroundingPageCount={4} onPageChange={e => e.preventDefault()} />
3435
)
36+
37+
export const RenderLinks = () => {
38+
const [page, setPage] = useState(2)
39+
40+
return (
41+
<Pagination
42+
pageCount={15}
43+
currentPage={page}
44+
onPageChange={(e, n) => {
45+
e.preventDefault()
46+
setPage(n)
47+
}}
48+
renderPage={({number, ...props}) => <ReactRouterLikeLink to={`#${number}`} {...props} />}
49+
/>
50+
)
51+
}

packages/react/src/Pagination/Pagination.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Box from '../Box'
44
import {get} from '../constants'
55
import type {SxProp} from '../sx'
66
import sx from '../sx'
7-
import {buildComponentData, buildPaginationModel} from './model'
7+
import {buildComponentData, buildPaginationModel, type PageDataProps} from './model'
88
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
99
import {viewportRanges} from '../hooks/useResponsiveValue'
1010
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
@@ -146,6 +146,17 @@ const Page = toggleStyledComponent(
146146
`,
147147
)
148148

149+
export type PageProps = {
150+
/* Unique key for the page number */
151+
key: string
152+
/* Children to render, typically the page number, 'Prev', or 'Next' */
153+
children: React.ReactNode
154+
/* Page number */
155+
number: number
156+
/* Default styles for the page number */
157+
className: string
158+
} & Omit<PageDataProps['props'], 'as' | 'role'>
159+
149160
type UsePaginationPagesParameters = {
150161
theme?: Record<string, unknown> // set to theme type once /src/theme.js is converted
151162
pageCount: number
@@ -155,6 +166,7 @@ type UsePaginationPagesParameters = {
155166
marginPageCount: number
156167
showPages?: PaginationProps['showPages']
157168
surroundingPageCount: number
169+
renderPage?: (props: PageProps) => React.ReactNode
158170
}
159171

160172
function usePaginationPages({
@@ -166,6 +178,7 @@ function usePaginationPages({
166178
marginPageCount,
167179
showPages,
168180
surroundingPageCount,
181+
renderPage,
169182
}: UsePaginationPagesParameters) {
170183
const pageChange = React.useCallback((n: number) => (e: React.MouseEvent) => onPageChange(e, n), [onPageChange])
171184

@@ -178,13 +191,17 @@ function usePaginationPages({
178191
const children = React.useMemo(() => {
179192
return model.map(page => {
180193
const {props, key, content} = buildComponentData(page, hrefBuilder, pageChange(page.num))
194+
if (renderPage && props.as !== 'span') {
195+
return renderPage({key, children: content, number: page.num, className: classes.Page, ...props})
196+
}
197+
181198
return (
182199
<Page {...props} key={key} theme={theme} className={clsx(enabled && classes.Page)}>
183200
{content}
184201
</Page>
185202
)
186203
})
187-
}, [model, hrefBuilder, pageChange, theme, enabled])
204+
}, [model, hrefBuilder, pageChange, renderPage, theme, enabled])
188205

189206
return children
190207
}
@@ -233,6 +250,7 @@ export type PaginationProps = {
233250
marginPageCount?: number
234251
showPages?: boolean | ResponsiveValue<boolean>
235252
surroundingPageCount?: number
253+
renderPage?: (props: PageProps) => React.ReactNode
236254
}
237255

238256
function Pagination({
@@ -244,6 +262,7 @@ function Pagination({
244262
marginPageCount = 1,
245263
showPages = true,
246264
surroundingPageCount = 2,
265+
renderPage,
247266
...rest
248267
}: PaginationProps) {
249268
const pageElements = usePaginationPages({
@@ -255,6 +274,7 @@ function Pagination({
255274
marginPageCount,
256275
showPages,
257276
surroundingPageCount,
277+
renderPage,
258278
})
259279

260280
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)

packages/react/src/Pagination/model.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,27 @@ type PageType = {
135135
precedesBreak?: boolean
136136
}
137137

138+
export type PageDataProps = {
139+
props: {
140+
href?: string
141+
rel?: string
142+
'aria-label'?: string
143+
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean
144+
'aria-hidden'?: boolean
145+
'aria-disabled'?: boolean
146+
onClick?: (e: React.MouseEvent) => void
147+
as?: string
148+
role?: string
149+
}
150+
key: string
151+
content: string
152+
}
153+
138154
export function buildComponentData(
139155
page: PageType,
140156
hrefBuilder: (n: number) => string,
141157
onClick: (e: React.MouseEvent) => void,
142-
) {
158+
): PageDataProps {
143159
const props = {}
144160
let content = ''
145161
let key = ''

packages/react/src/__tests__/Pagination/Pagination.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Pagination from '../../Pagination'
33
import {behavesAsComponent} from '../../utils/testing'
44
import {render as HTMLRender} from '@testing-library/react'
55
import axe from 'axe-core'
6+
import {ReactRouterLikeLink} from '../mocks/ReactRouterLink'
67

78
const reqProps = {pageCount: 10, currentPage: 1}
89
const comp = <Pagination {...reqProps} />
@@ -24,4 +25,16 @@ describe('Pagination', () => {
2425
})
2526
expect(results).toHaveNoViolations()
2627
})
28+
29+
it('should render links instead of anchor tags with the renderPage prop', () => {
30+
const {container} = HTMLRender(
31+
<Pagination
32+
pageCount={10}
33+
currentPage={1}
34+
renderPage={({number, ...props}) => <ReactRouterLikeLink to={`#${number}`} {...props} />}
35+
/>,
36+
)
37+
38+
expect(container.querySelectorAll('a').length).toEqual(10)
39+
})
2740
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
export type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode; className?: string}
4+
5+
export const ReactRouterLikeLink = React.forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
6+
({to, children, ...props}, ref) => {
7+
return (
8+
<a ref={ref} href={to} {...props}>
9+
{children}
10+
</a>
11+
)
12+
},
13+
)

packages/react/src/stories/deprecated/ActionList.stories.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {ActionList as _ActionList} from '../../deprecated/ActionList'
1919
import {Header} from '../../deprecated/ActionList/Header'
2020
import BaseStyles from '../../BaseStyles'
2121
import sx from '../../sx'
22+
import {ReactRouterLikeLink} from '../../__tests__/mocks/ReactRouterLink'
2223

2324
const ActionList = Object.assign(_ActionList, {
2425
Header,
@@ -365,17 +366,6 @@ export function SizeStressTestingStory(): JSX.Element {
365366
}
366367
SizeStressTestingStory.storyName = 'Size Stress Testing'
367368

368-
type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
369-
const ReactRouterLikeLink = forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(
370-
({to, children, ...props}: {to: string; children: React.ReactNode}, ref) => {
371-
return (
372-
<a ref={ref} href={to} {...props}>
373-
{children}
374-
</a>
375-
)
376-
},
377-
)
378-
379369
const NextJSLikeLink = forwardRef(
380370
({href, children}: {href: string; children: React.ReactNode}, ref): React.ReactElement => {
381371
const child = React.Children.only(children)

packages/react/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"src/**/*.js",
1111
"src/**/*.ts",
1212
"src/**/*.tsx",
13-
"script/**/*.ts"
13+
"script/**/*.ts",
1414
]
1515
}

0 commit comments

Comments
 (0)