diff --git a/docs/framework/angular/guide/migrating.md b/docs/framework/angular/guide/migrating.md index 4a84adc20f..1a0f6cae7a 100644 --- a/docs/framework/angular/guide/migrating.md +++ b/docs/framework/angular/guide/migrating.md @@ -677,6 +677,19 @@ Some row APIs have changed from private to public: |----|-----| | `row._getAllCellsByColumnId()` (private) | `row.getAllCellsByColumnId()` (public) | +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/lit/guide/migrating.md b/docs/framework/lit/guide/migrating.md index af9c4e943e..3125e2ddc7 100644 --- a/docs/framework/lit/guide/migrating.md +++ b/docs/framework/lit/guide/migrating.md @@ -549,6 +549,19 @@ const features = tableFeatures({ Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/preact/guide/migrating.md b/docs/framework/preact/guide/migrating.md index 04576e8cc9..466e927161 100644 --- a/docs/framework/preact/guide/migrating.md +++ b/docs/framework/preact/guide/migrating.md @@ -532,6 +532,19 @@ const features = tableFeatures({ Several underscore-prefixed APIs are now public without the underscore. For example, `row._getAllCellsByColumnId()` becomes `row.getAllCellsByColumnId()`. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/react/guide/migrating.md b/docs/framework/react/guide/migrating.md index e0e540394a..dbcee55f0f 100644 --- a/docs/framework/react/guide/migrating.md +++ b/docs/framework/react/guide/migrating.md @@ -935,6 +935,19 @@ Some row APIs have changed from private to public: If you were accessing this internal API, you can now use it without the underscore prefix. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/solid/guide/migrating.md b/docs/framework/solid/guide/migrating.md index 40c82eedfc..05a7fd0167 100644 --- a/docs/framework/solid/guide/migrating.md +++ b/docs/framework/solid/guide/migrating.md @@ -510,6 +510,19 @@ const features = tableFeatures({ Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/svelte/guide/migrating.md b/docs/framework/svelte/guide/migrating.md index d78c234997..f1fa5ae7f6 100644 --- a/docs/framework/svelte/guide/migrating.md +++ b/docs/framework/svelte/guide/migrating.md @@ -587,6 +587,19 @@ const features = tableFeatures({ Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/docs/framework/vue/guide/migrating.md b/docs/framework/vue/guide/migrating.md index 2fb5842e3e..046191843f 100644 --- a/docs/framework/vue/guide/migrating.md +++ b/docs/framework/vue/guide/migrating.md @@ -533,6 +533,19 @@ const features = tableFeatures({ Underscore-prefixed APIs that are now public should be called without `_`, such as `row.getAllCellsByColumnId()`. +### Row Selection API Changes + +The "some rows selected" checks were simplified to mean "at least one row is selected": + +| API | v8 | v9 | +|-----|-----|-----| +| `table.getIsSomeRowsSelected()` | `true` when some but not all rows are selected | `true` when at least one row is selected | +| `table.getIsSomePageRowsSelected()` | `true` when some but not all page rows are selected | `true` when at least one page row is selected | + +In v8 these returned `false` once every row was selected; in v9 they stay `true`. If you use them to drive an indeterminate "select all" checkbox, gate the indeterminate state on the matching all-selected check so it clears at full selection: + +`getIsSomeRowsSelected() && !getIsAllRowsSelected()` + --- ## TypeScript Changes Summary diff --git a/examples/react/row-selection/src/main.tsx b/examples/react/row-selection/src/main.tsx index 4ff0ba70af..bc2eabb283 100644 --- a/examples/react/row-selection/src/main.tsx +++ b/examples/react/row-selection/src/main.tsx @@ -289,8 +289,10 @@ function App() {
- -
{JSON.stringify(table.state, null, 2)}
+ +
+            {data.length < 1_001 && JSON.stringify(table.state, null, 2)}
+          
diff --git a/packages/table-core/src/features/row-selection/rowSelectionFeature.ts b/packages/table-core/src/features/row-selection/rowSelectionFeature.ts index 691e9dadc3..8e2fc95bf7 100644 --- a/packages/table-core/src/features/row-selection/rowSelectionFeature.ts +++ b/packages/table-core/src/features/row-selection/rowSelectionFeature.ts @@ -20,6 +20,7 @@ import { table_getIsSomePageRowsSelected, table_getIsSomeRowsSelected, table_getPreSelectedRowModel, + table_getSelectedRowIds, table_getSelectedRowModel, table_getToggleAllPageRowsSelectedHandler, table_getToggleAllRowsSelectedHandler, @@ -88,10 +89,11 @@ export const rowSelectionFeature: TableFeature = { fn: (defaultState) => table_resetRowSelection(table, defaultState), }, table_toggleAllRowsSelected: { - fn: (value) => table_toggleAllRowsSelected(table, value), + fn: (value, opts) => table_toggleAllRowsSelected(table, value, opts), }, table_toggleAllPageRowsSelected: { - fn: (value) => table_toggleAllPageRowsSelected(table, value), + fn: (value, opts) => + table_toggleAllPageRowsSelected(table, value, opts), }, table_getPreSelectedRowModel: { fn: () => table_getPreSelectedRowModel(table), @@ -117,17 +119,37 @@ export const rowSelectionFeature: TableFeature = { table.getSortedRowModel(), ], }, + table_getSelectedRowIds: { + fn: () => table_getSelectedRowIds(table), + memoDeps: () => [table.atoms.rowSelection?.get()], + }, table_getIsAllRowsSelected: { fn: () => table_getIsAllRowsSelected(table), + memoDeps: () => [ + table.atoms.rowSelection?.get(), + table.getFilteredRowModel(), + table.options.enableRowSelection, + ], }, table_getIsAllPageRowsSelected: { fn: () => table_getIsAllPageRowsSelected(table), + memoDeps: () => [ + table.atoms.rowSelection?.get(), + table.getPaginatedRowModel(), + table.options.enableRowSelection, + ], }, table_getIsSomeRowsSelected: { fn: () => table_getIsSomeRowsSelected(table), + memoDeps: () => [table.atoms.rowSelection?.get()], }, table_getIsSomePageRowsSelected: { fn: () => table_getIsSomePageRowsSelected(table), + memoDeps: () => [ + table.atoms.rowSelection?.get(), + table.getPaginatedRowModel(), + table.options.enableRowSelection, + ], }, table_getToggleAllRowsSelectedHandler: { fn: () => table_getToggleAllRowsSelectedHandler(table), diff --git a/packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts b/packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts index 8af6d8f280..8d79e4bac9 100644 --- a/packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts +++ b/packages/table-core/src/features/row-selection/rowSelectionFeature.types.ts @@ -3,7 +3,7 @@ import type { TableFeatures } from '../../types/TableFeatures' import type { RowModel } from '../../core/row-models/coreRowModelsFeature.types' import type { Row } from '../../types/Row' -export type RowSelectionState = Record +export type RowSelectionState = Record export interface TableState_RowSelection { rowSelection: RowSelectionState @@ -98,6 +98,10 @@ export interface Table_RowSelection< * Builds a selected-row model from rows after grouping. */ getGroupedSelectedRowModel: () => RowModel + /** + * Returns the ids of all selected rows. + */ + getSelectedRowIds: () => Array /** * Checks whether every selectable row on the current page is selected. */ @@ -143,9 +147,15 @@ export interface Table_RowSelection< /** * Selects/deselects all rows on the current page. */ - toggleAllPageRowsSelected: (value?: boolean) => void + toggleAllPageRowsSelected: ( + value?: boolean, + opts?: { deselectAll?: boolean }, + ) => void /** * Selects/deselects all rows in the table. */ - toggleAllRowsSelected: (value?: boolean) => void + toggleAllRowsSelected: ( + value?: boolean, + opts?: { deselectAll?: boolean }, + ) => void } diff --git a/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts b/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts index 7279af6241..f5ecfd2a79 100644 --- a/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts +++ b/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts @@ -1,4 +1,9 @@ -import { cloneState, hasOwn, makeObjectMap } from '../../utils' +import { + callMemoOrStaticFn, + cloneState, + hasOwn, + makeObjectMap, +} from '../../utils' import type { RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' import type { RowModel } from '../../core/row-models/coreRowModelsFeature.types' @@ -65,7 +70,7 @@ export function table_resetRowSelection< defaultState ? makeObjectMap() : Object.assign( - makeObjectMap(), + makeObjectMap(), cloneState(table.initialState.rowSelection ?? {}), ), ) @@ -87,26 +92,36 @@ export function table_resetRowSelection< export function table_toggleAllRowsSelected< TFeatures extends TableFeatures, TData extends RowData, ->(table: Table_Internal, value?: boolean) { +>( + table: Table_Internal, + value?: boolean, + opts?: { deselectAll?: boolean }, +) { table_setRowSelection(table, (old) => { value = - typeof value !== 'undefined' ? value : !table_getIsAllRowsSelected(table) - - const rowSelection = Object.assign( - makeObjectMap(), - old, - ) + typeof value !== 'undefined' + ? value + : !callMemoOrStaticFn( + table, + 'getIsAllRowsSelected', + table_getIsAllRowsSelected, + ) + + if (opts?.deselectAll && !value) { + // deselectAll opt: clear the whole selection map instead of deleting ids one-by-one + return makeObjectMap() + } + const rowSelection = Object.assign(makeObjectMap(), old) const preGroupedFlatRows = table.getPreGroupedRowModel().flatRows // We don't use `mutateRowIsSelected` here for performance reasons. // All of the rows are flat already, so it wouldn't be worth it if (value) { preGroupedFlatRows.forEach((row) => { - if (!row_getCanSelect(row)) { - return + if (row_getCanSelect(row)) { + rowSelection[row.id] = true } - rowSelection[row.id] = true }) } else { preGroupedFlatRows.forEach((row) => { @@ -132,15 +147,28 @@ export function table_toggleAllRowsSelected< export function table_toggleAllPageRowsSelected< TFeatures extends TableFeatures, TData extends RowData, ->(table: Table_Internal, value?: boolean) { +>( + table: Table_Internal, + value?: boolean, + opts?: { deselectAll?: boolean }, +) { table_setRowSelection(table, (old) => { const resolvedValue = typeof value !== 'undefined' ? value - : !table_getIsAllPageRowsSelected(table) + : !callMemoOrStaticFn( + table, + 'getIsAllPageRowsSelected', + table_getIsAllPageRowsSelected, + ) + + if (opts?.deselectAll && !resolvedValue) { + // deselectAll opt: clear the whole selection map instead of deleting ids one-by-one + return makeObjectMap() + } const rowSelection: RowSelectionState = Object.assign( - makeObjectMap(), + makeObjectMap(), old, ) @@ -187,7 +215,13 @@ export function table_getSelectedRowModel< >(table: Table_Internal) { const rowModel = table.getCoreRowModel() - if (!Object.keys(table.atoms.rowSelection?.get() ?? {}).length) { + if ( + !callMemoOrStaticFn( + table, + 'getIsSomeRowsSelected', + table_getIsSomeRowsSelected, + ) + ) { return { rows: [], flatRows: [], @@ -195,7 +229,7 @@ export function table_getSelectedRowModel< } } - return selectRowsFn(rowModel) + return selectRowsFn(rowModel, table) } /** @@ -215,7 +249,13 @@ export function table_getFilteredSelectedRowModel< >(table: Table_Internal) { const rowModel = table.getCoreRowModel() - if (!Object.keys(table.atoms.rowSelection?.get() ?? {}).length) { + if ( + !callMemoOrStaticFn( + table, + 'getIsSomeRowsSelected', + table_getIsSomeRowsSelected, + ) + ) { return { rows: [], flatRows: [], @@ -223,7 +263,7 @@ export function table_getFilteredSelectedRowModel< } } - return selectRowsFn(rowModel) + return selectRowsFn(rowModel, table) } /** @@ -243,7 +283,13 @@ export function table_getGroupedSelectedRowModel< >(table: Table_Internal) { const rowModel = table.getCoreRowModel() - if (!Object.keys(table.atoms.rowSelection?.get() ?? {}).length) { + if ( + !callMemoOrStaticFn( + table, + 'getIsSomeRowsSelected', + table_getIsSomeRowsSelected, + ) + ) { return { rows: [], flatRows: [], @@ -251,7 +297,22 @@ export function table_getGroupedSelectedRowModel< } } - return selectRowsFn(rowModel) + return selectRowsFn(rowModel, table) +} + +/** + * Returns the ids of all selected rows. + * + * @example + * ```ts + * const selectedRowIds = table_getSelectedRowIds(table) + * ``` + */ +export function table_getSelectedRowIds< + TFeatures extends TableFeatures, + TData extends RowData, +>(table: Table_Internal): Array { + return Object.keys(table.atoms.rowSelection?.get() ?? {}) } /** @@ -270,7 +331,7 @@ export function table_getIsAllRowsSelected< TData extends RowData, >(table: Table_Internal) { const preGroupedFlatRows = table.getFilteredRowModel().flatRows - const rowSelection: RowSelectionState = table.atoms.rowSelection?.get() ?? {} + const rowSelection = table.atoms.rowSelection?.get() ?? {} let isAllRowsSelected = Boolean( preGroupedFlatRows.length && Object.keys(rowSelection).length, @@ -279,8 +340,7 @@ export function table_getIsAllRowsSelected< if (isAllRowsSelected) { if ( preGroupedFlatRows.some( - (row) => - row_getCanSelect(row) && !isRowIdSelected(rowSelection, row.id), + (row) => row_getCanSelect(row) && !isRowSelected(row, rowSelection), ) ) { isAllRowsSelected = false @@ -307,13 +367,13 @@ export function table_getIsAllPageRowsSelected< const paginationFlatRows = table .getPaginatedRowModel() .flatRows.filter((row) => row_getCanSelect(row)) - const rowSelection: RowSelectionState = table.atoms.rowSelection?.get() ?? {} + const rowSelection = table.atoms.rowSelection?.get() ?? {} let isAllPageRowsSelected = !!paginationFlatRows.length if ( isAllPageRowsSelected && - paginationFlatRows.some((row) => !isRowIdSelected(rowSelection, row.id)) + paginationFlatRows.some((row) => !isRowSelected(row, rowSelection)) ) { isAllPageRowsSelected = false } @@ -324,8 +384,7 @@ export function table_getIsAllPageRowsSelected< /** * Checks whether selection is partially applied across filtered rows. * - * The result is true when at least one row id is selected but fewer ids are - * selected than the current filtered flat row count. + * The result is true when at least one row id is selected * * @example * ```ts @@ -336,21 +395,15 @@ export function table_getIsSomeRowsSelected< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal) { - const totalSelected = Object.keys( - table.atoms.rowSelection?.get() ?? {}, - ).length return ( - totalSelected > 0 && - totalSelected < table.getFilteredRowModel().flatRows.length + callMemoOrStaticFn(table, 'getSelectedRowIds', table_getSelectedRowIds) + .length > 0 ) } /** * Checks whether the current page has a partial selection. * - * This is false when all selectable page rows are selected. Otherwise it is true - * if any selectable page row or descendant is selected. - * * @example * ```ts * const somePageRowsSelected = table_getIsSomePageRowsSelected(table) @@ -360,12 +413,10 @@ export function table_getIsSomePageRowsSelected< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal) { - const paginationFlatRows = table.getPaginatedRowModel().flatRows - return table_getIsAllPageRowsSelected(table) - ? false - : paginationFlatRows - .filter((row) => row_getCanSelect(row)) - .some((row) => row_getIsSelected(row) || row_getIsSomeSelected(row)) + return table + .getPaginatedRowModel() + .flatRows.filter((row) => row_getCanSelect(row)) + .some((row) => row_getIsSelected(row) || row_getIsSomeSelected(row)) } /** @@ -425,6 +476,10 @@ export function table_getToggleAllPageRowsSelectedHandler< * @example * ```ts * row_toggleSelected(row) + * row_toggleSelected(row, true) + * row_toggleSelected(row, false) + * row_toggleSelected(row, true, { selectChildren: false }) + * row_toggleSelected(row, false, { selectChildren: false }) * ``` */ export function row_toggleSelected< @@ -446,20 +501,17 @@ export function row_toggleSelected< return old } - const selectedRowIds = Object.assign( - makeObjectMap(), - old, - ) + const rowSelection = Object.assign(makeObjectMap(), old) mutateRowIsSelected( - selectedRowIds, + rowSelection, row.id, value, opts?.selectChildren ?? true, row.table, ) - return selectedRowIds + return rowSelection }) } @@ -477,7 +529,8 @@ export function row_getIsSelected< TFeatures extends TableFeatures, TData extends RowData, >(row: Row) { - return isRowSelected(row) + const rowSelection = row.table.atoms.rowSelection?.get() ?? {} + return isRowSelected(row, rowSelection) } /** @@ -613,28 +666,28 @@ const mutateRowIsSelected = < TFeatures extends TableFeatures, TData extends RowData, >( - selectedRowIds: Record, + rowSelection: RowSelectionState, rowId: string, value: boolean, includeChildren: boolean, table: Table_Internal, -) => { +): void => { const row = table.getRow(rowId, true) if (value) { if (!row_getCanMultiSelect(row)) { - Object.keys(selectedRowIds).forEach((key) => delete selectedRowIds[key]) + Object.keys(rowSelection).forEach((key) => delete rowSelection[key]) } if (row_getCanSelect(row)) { - selectedRowIds[rowId] = true + rowSelection[rowId] = true } } else { - delete selectedRowIds[rowId] + delete rowSelection[rowId] } if (includeChildren && row.subRows.length && row_getCanSelectSubRows(row)) { row.subRows.forEach((r) => - mutateRowIsSelected(selectedRowIds, r.id, value, includeChildren, table), + mutateRowIsSelected(rowSelection, r.id, value, includeChildren, table), ) } } @@ -653,10 +706,13 @@ const mutateRowIsSelected = < export function selectRowsFn< TFeatures extends TableFeatures, TData extends RowData, ->(rowModel: RowModel): RowModel { +>( + rowModel: RowModel, + table: Table_Internal, +): RowModel { const newSelectedFlatRows: Array> = [] const newSelectedRowsById = makeObjectMap>() - + const rowSelection = table.atoms.rowSelection?.get() ?? {} // Filters top level and nested rows. const recurseRows = ( rows: Array>, @@ -665,7 +721,7 @@ export function selectRowsFn< const result: Array> = [] for (let i = 0; i < rows.length; i++) { const row = rows[i]! - const isSelected = isRowSelected(row) + const isSelected = isRowSelected(row, rowSelection) if (isSelected) { newSelectedFlatRows.push(row) @@ -709,15 +765,8 @@ export function selectRowsFn< export function isRowSelected< TFeatures extends TableFeatures, TData extends RowData, ->(row: Row): boolean { - return isRowIdSelected(row.table.atoms.rowSelection?.get(), row.id) -} - -function isRowIdSelected( - rowSelection: RowSelectionState | undefined, - rowId: string, -): boolean { - return !!(rowSelection && hasOwn(rowSelection, rowId) && rowSelection[rowId]) +>(row: Row, rowSelection: RowSelectionState): boolean { + return !!(hasOwn(rowSelection, row.id) && rowSelection[row.id]) } /** @@ -736,6 +785,8 @@ export function isSubRowSelected< >(row: Row): boolean | 'some' | 'all' { if (!row.subRows.length) return false + const rowSelection = row.table.atoms.rowSelection?.get() ?? {} + let allChildrenSelected = true let someSelected = false @@ -746,7 +797,7 @@ export function isSubRowSelected< } if (row_getCanSelect(subRow)) { - if (isRowSelected(subRow)) { + if (isRowSelected(subRow, rowSelection)) { someSelected = true } else { allChildrenSelected = false diff --git a/packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts b/packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts index 722639b6cd..210ff58bc6 100644 --- a/packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts +++ b/packages/table-core/tests/implementation/features/row-selection/rowSelectionFeature.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { constructTable, coreFeatures, @@ -9,7 +9,7 @@ import * as RowSelectionUtils from '../../../../src/features/row-selection/rowSe import { generateTestData } from '../../../fixtures/data/generateTestData' import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' import type { Person } from '../../../fixtures/data/types' -import type { ColumnDef, Row } from '../../../../src' +import type { ColumnDef, Row, Table_Internal } from '../../../../src' // TODO: bring up to new test structure @@ -58,7 +58,10 @@ describe('rowSelectionFeature', () => { }) const rowModel = table.getCoreRowModel() - const result = RowSelectionUtils.selectRowsFn(rowModel) + const result = RowSelectionUtils.selectRowsFn( + rowModel, + table as Table_Internal, + ) expect(result.rows.length).toBe(2) expect(result.flatRows.length).toBe(2) @@ -86,7 +89,10 @@ describe('rowSelectionFeature', () => { }) const rowModel = table.getCoreRowModel() - const result = RowSelectionUtils.selectRowsFn(rowModel) + const result = RowSelectionUtils.selectRowsFn( + rowModel, + table as Table_Internal, + ) expect(result.rows[0]?.subRows?.length).toBe(1) expect(result.flatRows.length).toBe(2) @@ -113,7 +119,10 @@ describe('rowSelectionFeature', () => { }) const rowModel = table.getCoreRowModel() - const result = RowSelectionUtils.selectRowsFn(rowModel) + const result = RowSelectionUtils.selectRowsFn( + rowModel, + table as Table_Internal, + ) expect(result.rows.length).toBe(0) expect(result.flatRows.length).toBe(1) @@ -141,7 +150,10 @@ describe('rowSelectionFeature', () => { }) const rowModel = table.getCoreRowModel() - const result = RowSelectionUtils.selectRowsFn(rowModel) + const result = RowSelectionUtils.selectRowsFn( + rowModel, + table as Table_Internal, + ) // Selected parents with subRows are cloned; the clone must keep the // shared row prototype so APIs like getValue() still work @@ -168,7 +180,10 @@ describe('rowSelectionFeature', () => { }) const rowModel = table.getCoreRowModel() - const result = RowSelectionUtils.selectRowsFn(rowModel) + const result = RowSelectionUtils.selectRowsFn( + rowModel, + table as Table_Internal, + ) expect(result.rows.length).toBe(0) expect(result.flatRows.length).toBe(0) @@ -188,7 +203,7 @@ describe('rowSelectionFeature', () => { initialState: { rowSelection: { '123': true, - '456': false, + // '456': false, }, }, columns, @@ -196,7 +211,10 @@ describe('rowSelectionFeature', () => { const row = { id: '123', data: {}, table } as any - const result = RowSelectionUtils.isRowSelected(row) + const result = RowSelectionUtils.isRowSelected( + row, + table.atoms.rowSelection?.get() ?? {}, + ) expect(result).toEqual(true) }) @@ -212,7 +230,7 @@ describe('rowSelectionFeature', () => { initialState: { rowSelection: { '123': true, - '456': false, + // '456': false, }, }, columns, @@ -220,7 +238,10 @@ describe('rowSelectionFeature', () => { const row = { id: '456', data: {}, table } as any - const result = RowSelectionUtils.isRowSelected(row) + const result = RowSelectionUtils.isRowSelected( + row, + table.atoms.rowSelection?.get() ?? {}, + ) expect(result).toEqual(false) }) @@ -236,7 +257,7 @@ describe('rowSelectionFeature', () => { initialState: { rowSelection: { '123': true, - '456': false, + // '456': false, }, }, columns, @@ -244,7 +265,10 @@ describe('rowSelectionFeature', () => { const row = { id: '789', data: {}, table } as any - const result = RowSelectionUtils.isRowSelected(row) + const result = RowSelectionUtils.isRowSelected( + row, + table.atoms.rowSelection?.get() ?? {}, + ) expect(result).toEqual(false) }) @@ -263,7 +287,10 @@ describe('rowSelectionFeature', () => { const row = { id: '789', data: {}, table } as any - const result = RowSelectionUtils.isRowSelected(row) + const result = RowSelectionUtils.isRowSelected( + row, + table.atoms.rowSelection?.get() ?? {}, + ) expect(result).toEqual(false) }) }) @@ -411,4 +438,110 @@ describe('rowSelectionFeature', () => { expect(result).toEqual('some') }) }) + describe('toggleAllRowsSelected', () => { + // `99` is selected but absent from the row model, standing in for a row that + // is filtered out or otherwise not present in the current pre-grouped model. + const makeTable = () => { + const data = generateTestData(5) + const columns = generateColumnDefs(data) + return constructTable({ + features, + enableRowSelection: true, + renderFallbackValue: '', + data, + initialState: { + rowSelection: { '0': true, '2': true, '99': true }, + }, + columns, + }) + } + + it('deselects only in-model ids by default, preserving out-of-model ids', () => { + const table = makeTable() + table.toggleAllRowsSelected(false) + expect(Object.keys(table.atoms.rowSelection.get())).toEqual(['99']) + }) + + it('clears the entire selection when opts.deselectAll is true', () => { + const table = makeTable() + table.toggleAllRowsSelected(false, { deselectAll: true }) + expect(Object.keys(table.atoms.rowSelection.get())).toEqual([]) + }) + + it('does not clear selection when deselectAll is paired with a select', () => { + const table = makeTable() + // deselectAll only applies when the resolved value is a deselect + table.toggleAllRowsSelected(true, { deselectAll: true }) + expect(table.atoms.rowSelection.get()['99']).toBe(true) + }) + }) + describe('memoization', () => { + // The `enableRowSelection` predicate is invoked once per row by + // `row_getCanSelect`, so its call count is a proxy for how many times the + // selection getters actually walk the row model. + it('memoizes getIsAllRowsSelected until selection or row model changes', () => { + const data = generateTestData(10) + const columns = generateColumnDefs(data) + const enableRowSelection = vi.fn(() => true) + + const table = constructTable({ + features, + enableRowSelection, + renderFallbackValue: '', + data, + initialState: { + rowSelection: Object.fromEntries( + data.map((_, index) => [String(index), true]), + ), + }, + columns, + }) + + expect(table.getIsAllRowsSelected()).toBe(true) + const callsAfterFirst = enableRowSelection.mock.calls.length + expect(callsAfterFirst).toBeGreaterThan(0) + + // Repeated calls with unchanged deps must not re-walk the row model + table.getIsAllRowsSelected() + table.getIsAllRowsSelected() + expect(enableRowSelection.mock.calls.length).toBe(callsAfterFirst) + + // Changing the selection invalidates the memo and recomputes + table.setRowSelection({ '0': true }) + expect(table.getIsAllRowsSelected()).toBe(false) + expect(enableRowSelection.mock.calls.length).toBeGreaterThan( + callsAfterFirst, + ) + }) + + it('memoizes getIsSomePageRowsSelected until selection changes', () => { + const data = generateTestData(10) + const columns = generateColumnDefs(data) + const enableRowSelection = vi.fn(() => true) + + const table = constructTable({ + features, + enableRowSelection, + renderFallbackValue: '', + data, + initialState: { + rowSelection: { '0': true }, + }, + columns, + }) + + expect(table.getIsSomePageRowsSelected()).toBe(true) + const callsAfterFirst = enableRowSelection.mock.calls.length + + table.getIsSomePageRowsSelected() + table.getIsSomePageRowsSelected() + expect(enableRowSelection.mock.calls.length).toBe(callsAfterFirst) + + table.setRowSelection({}) + expect(table.getIsSomePageRowsSelected()).toBe(false) + expect(enableRowSelection.mock.calls.length).toBeGreaterThan( + callsAfterFirst, + ) + }) + }) })