From 65cd6c72c4312b1da1b32c43d74116f15e131935 Mon Sep 17 00:00:00 2001 From: Ryan Tablada Date: Mon, 6 Jul 2026 09:19:32 -0500 Subject: [PATCH] feat: allow overriding canNextPage/canPreviousPage in pagination state --- docs/framework/react/guide/pagination.md | 6 +- .../rowPaginationFeature.types.ts | 8 ++ .../rowPaginationFeature.utils.ts | 19 ++- .../rowPaginationFeature.utils.test.ts | 123 ++++++++++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 packages/table-core/tests/unit/features/row-pagination/rowPaginationFeature.utils.test.ts diff --git a/docs/framework/react/guide/pagination.md b/docs/framework/react/guide/pagination.md index b92ef2020a..1792026019 100644 --- a/docs/framework/react/guide/pagination.md +++ b/docs/framework/react/guide/pagination.md @@ -90,7 +90,7 @@ No pagination row model is needed for server-side pagination, but if you have pr #### Page Count and Row Count -The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. +The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. When you do know whether more pages are available (for example, a server response that says "has more"), you can override this by setting the optional `canNextPage`/`canPreviousPage` values on the [pagination state](#pagination-state) directly. ```tsx import { @@ -121,6 +121,10 @@ The `pagination` state is an object that contains the following properties: - `pageIndex`: The current page index (zero-based). - `pageSize`: The current page size. +- `canNextPage` _(optional)_: Explicitly overrides the value returned by `getCanNextPage()`. Useful for manual/server-side pagination (e.g. news feeds) where the total `pageCount`/`rowCount` is unknown. +- `canPreviousPage` _(optional)_: Explicitly overrides the value returned by `getCanPreviousPage()`. Useful for manual/server-side pagination where the total `pageCount`/`rowCount` is unknown. + +> **Note**: The pagination navigation APIs (`setPageIndex`, `setPageSize`, `nextPage`, etc.) preserve `canNextPage`/`canPreviousPage` when they update the state. However, if you replace the whole `pagination` object yourself (for example inside `onPaginationChange`), you must re-supply these flags to keep the overrides in effect. For reactive reads that should re-render your UI, use `table.state.pagination`. In event handlers, you can read the current snapshot with `table.atoms.pagination.get()`, but this read does not subscribe the component to future changes. diff --git a/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts b/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts index 33862aad6f..d8091b7649 100644 --- a/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts +++ b/packages/table-core/src/features/row-pagination/rowPaginationFeature.types.ts @@ -6,6 +6,14 @@ import type { TableFeatures } from '../../types/TableFeatures' export interface PaginationState { pageIndex: number pageSize: number + /** + * When manually controlling pagination, supply `canNextPage` explicitly to override the derived value. This is useful when working with news feeds or other server paginated data where `pageCount` or `rowCount` is unknown. + */ + canNextPage?: boolean + /** + * When manually controlling pagination, supply `canPreviousPage` explicitly to override the derived value. This is useful when working with news feeds or other server paginated data where `pageCount` or `rowCount` is unknown. + */ + canPreviousPage?: boolean } export interface TableState_RowPagination { diff --git a/packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts b/packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts index 359a944620..fa1b058305 100644 --- a/packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts +++ b/packages/table-core/src/features/row-pagination/rowPaginationFeature.utils.ts @@ -251,7 +251,14 @@ export function table_getCanPreviousPage< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal) { - return (table.atoms.pagination?.get()?.pageIndex ?? 0) > 0 + const pagination = table.atoms.pagination?.get() + const { canPreviousPage, pageIndex } = pagination ?? {} + + if (canPreviousPage !== undefined) { + return canPreviousPage + } + + return (pageIndex ?? defaultPageIndex) > 0 } /** @@ -269,10 +276,16 @@ export function table_getCanNextPage< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal) { - const pageIndex = table.atoms.pagination?.get()?.pageIndex ?? defaultPageIndex - + const pagination = table.atoms.pagination?.get() + const pageIndex = pagination?.pageIndex ?? defaultPageIndex + // Read `pageCount` before any early return so the reactivity system always + // subscribes to its dependencies (pageSize atom + pre-paginated row model). const pageCount = table_getPageCount(table) + if (pagination?.canNextPage !== undefined) { + return pagination.canNextPage + } + if (pageCount === -1) { return true } diff --git a/packages/table-core/tests/unit/features/row-pagination/rowPaginationFeature.utils.test.ts b/packages/table-core/tests/unit/features/row-pagination/rowPaginationFeature.utils.test.ts new file mode 100644 index 0000000000..abffbd1ba0 --- /dev/null +++ b/packages/table-core/tests/unit/features/row-pagination/rowPaginationFeature.utils.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest' +import { + constructTable, + coreFeatures, + createPaginatedRowModel, + rowPaginationFeature, + tableFeatures, +} from '../../../../src' +import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' +import { generateTestColumnDefs } from '../../../fixtures/data/generateTestColumnDefs' +import { generateTestData } from '../../../fixtures/data/generateTestData' +import type { PaginationState } from '../../../../src' +import type { Person } from '../../../fixtures/data/types' + +const features = tableFeatures({ + ...coreFeatures, + rowPaginationFeature, + paginatedRowModel: createPaginatedRowModel(), +}) + +function createPaginationTable(options?: { + rowLength?: number + pagination?: Partial + pageCount?: number + rowCount?: number + manualPagination?: boolean +}) { + const data = generateTestData(options?.rowLength ?? 30) + const columns = generateTestColumnDefs(data) + + return constructTable({ + data, + columns, + getSubRows: (row) => row.subRows, + manualPagination: options?.manualPagination, + pageCount: options?.pageCount, + rowCount: options?.rowCount, + initialState: { + pagination: { + pageIndex: 0, + pageSize: 10, + ...options?.pagination, + }, + }, + features: { + ...features, + coreReactivityFeature: storeReactivityBindings(), + }, + }) +} + +describe('table_getCanPreviousPage', () => { + it('returns false on the first page (pageIndex 0)', () => { + const table = createPaginationTable({ pagination: { pageIndex: 0 } }) + expect(table.getCanPreviousPage()).toBe(false) + }) + + it('returns true when past the first page', () => { + const table = createPaginationTable({ pagination: { pageIndex: 1 } }) + expect(table.getCanPreviousPage()).toBe(true) + }) + + it('honors an explicit canPreviousPage override on the first page', () => { + const table = createPaginationTable({ + pagination: { pageIndex: 0, canPreviousPage: true }, + }) + expect(table.getCanPreviousPage()).toBe(true) + }) + + it('honors an explicit canPreviousPage=false override past the first page', () => { + const table = createPaginationTable({ + pagination: { pageIndex: 1, canPreviousPage: false }, + }) + expect(table.getCanPreviousPage()).toBe(false) + }) +}) + +describe('table_getCanNextPage', () => { + it('returns true when there are more pages', () => { + // 30 rows / pageSize 10 => 3 pages, currently on page 0 + const table = createPaginationTable({ pagination: { pageIndex: 0 } }) + expect(table.getCanNextPage()).toBe(true) + }) + + it('returns false on the last page', () => { + const table = createPaginationTable({ pagination: { pageIndex: 2 } }) + expect(table.getCanNextPage()).toBe(false) + }) + + it('returns true for an unknown page count (pageCount -1)', () => { + const table = createPaginationTable({ + manualPagination: true, + pageCount: -1, + }) + expect(table.getCanNextPage()).toBe(true) + }) + + it('returns false for an empty page count (pageCount 0)', () => { + const table = createPaginationTable({ + manualPagination: true, + pageCount: 0, + }) + expect(table.getCanNextPage()).toBe(false) + }) + + it('honors canNextPage=false override even when pageCount is unknown (-1)', () => { + const table = createPaginationTable({ + manualPagination: true, + pageCount: -1, + pagination: { canNextPage: false }, + }) + expect(table.getCanNextPage()).toBe(false) + }) + + it('honors canNextPage=true override even when pageCount is 0', () => { + const table = createPaginationTable({ + manualPagination: true, + pageCount: 0, + pagination: { canNextPage: true }, + }) + expect(table.getCanNextPage()).toBe(true) + }) +})