Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/datatable-integrated-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@primer/react': minor
---

DataTable: Add integrated pagination via the `pagination` prop. Pass
`pagination={true}` (or an options object) and `DataTable` renders the
existing `<Table.Pagination>` for you and slices the rows automatically —
consumers no longer need to wire `<Table.Pagination>` and manual row
slicing themselves. Supports controlled (`pageIndex` / `onPageChange`) and
uncontrolled modes, plus an `externalPagination` escape hatch for
server-driven pagination. The existing manual `<Table.Pagination>`
composition pattern continues to work unchanged for callers that want
finer control.
37 changes: 37 additions & 0 deletions packages/react/src/DataTable/DataTable.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
},
{
"id": "experimental-components-datatable-features--with-pagination"
},
{
"id": "experimental-components-datatable-features--with-integrated-pagination"
},
{
"id": "experimental-components-datatable-features--with-integrated-pagination-controlled"
},
{
"id": "experimental-components-datatable-features--with-integrated-pagination-external"
}
],
"importPath": "@primer/react/experimental",
Expand Down Expand Up @@ -112,6 +121,34 @@
"required": false,
"description": "Fires every time the user clicks a sortable column header. It reports the column id that is now sorted and the direction after the toggle (never 'NONE').",
"defaultValue": ""
},
{
"name": "pagination",
"type": "false | true | { pageSize?: number; defaultPageIndex?: number; 'aria-label'?: string; showPages?: boolean | ResponsiveValue<boolean> }",
"required": false,
"description": "Render an integrated pagination control beneath the table. Pass `true` for defaults, an options object to customize, or omit to opt out and continue composing `<Table.Pagination>` manually.",
"defaultValue": "false"
},
{
"name": "pageIndex",
"type": "number",
"required": false,
"description": "Controlled page index. When provided, the parent owns the page state and `pagination.defaultPageIndex` is ignored. Pair with `onPageChange`.",
"defaultValue": ""
},
{
"name": "onPageChange",
"type": "(pageIndex: number) => void",
"required": false,
"description": "Called whenever the page index changes (controlled or uncontrolled).",
"defaultValue": ""
},
{
"name": "externalPagination",
"type": "boolean",
"required": false,
"description": "When `true`, disables client-side row slicing. The pagination control still renders and `onPageChange` still fires, but `data` is rendered as-is. Use for server-driven pagination.",
"defaultValue": "false"
}
],
"subcomponents": [
Expand Down
95 changes: 95 additions & 0 deletions packages/react/src/DataTable/DataTable.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1715,3 +1715,98 @@ export const WithNetworkError = () => {
</Table.Container>
)
}

export const WithIntegratedPagination = () => (
<Table.Container>
<Table.Title as="h2" id="repositories">
Repositories
</Table.Title>
<Table.Subtitle as="p" id="repositories-subtitle">
The `pagination` prop renders an integrated pager and slices the rows for you. No manual `Table.Pagination` wiring
needed.
</Table.Subtitle>
<DataTable
aria-labelledby="repositories"
aria-describedby="repositories-subtitle"
data={repos}
columns={[
{header: 'Repository', field: 'name', rowHeader: true, sortBy: 'alphanumeric'},
{
header: 'Type',
field: 'type',
renderCell: row => <Label>{uppercase(row.type)}</Label>,
},
{
header: 'Updated',
field: 'updatedAt',
renderCell: row => <RelativeTime date={new Date(row.updatedAt)} />,
},
]}
pagination={{pageSize: 10, 'aria-label': 'Pagination for Repositories'}}
/>
</Table.Container>
)

export const WithIntegratedPaginationControlled = () => {
const [pageIndex, setPageIndex] = React.useState(0)
return (
<Table.Container>
<Table.Title as="h2" id="repositories">
Repositories
</Table.Title>
<Table.Subtitle as="p" id="repositories-subtitle">
The parent owns `pageIndex`. Combine with custom buttons or a URL query parameter for deep-linkable pagination.
</Table.Subtitle>
<DataTable
aria-labelledby="repositories"
aria-describedby="repositories-subtitle"
data={repos}
columns={[
{header: 'Repository', field: 'name', rowHeader: true},
{header: 'Type', field: 'type'},
]}
pagination={{pageSize: 10}}
pageIndex={pageIndex}
onPageChange={next => {
action('onPageChange')(next)
setPageIndex(next)
}}
/>
</Table.Container>
)
}

export const WithIntegratedPaginationExternal = () => {
const pageSize = 10
const [pageIndex, setPageIndex] = React.useState(0)
// Simulate server-side pagination: only this page's slice is in scope.
const start = pageIndex * pageSize
const visible = repos.slice(start, start + pageSize)
return (
<Table.Container>
<Table.Title as="h2" id="repositories">
Repositories
</Table.Title>
<Table.Subtitle as="p" id="repositories-subtitle">
`externalPagination` lets the consumer fetch one page of data at a time. The component renders whatever rows are
in `data`.
</Table.Subtitle>
<DataTable
aria-labelledby="repositories"
aria-describedby="repositories-subtitle"
data={visible}
columns={[
{header: 'Repository', field: 'name', rowHeader: true},
{header: 'Type', field: 'type'},
]}
pagination={{pageSize}}
pageIndex={pageIndex}
externalPagination
onPageChange={next => {
action('onPageChange')(next)
setPageIndex(next)
}}
/>
</Table.Container>
)
}
Loading
Loading