From ba5296a22865a279ab7eab8b5e40e9df7feb4ccb Mon Sep 17 00:00:00 2001 From: Kevin Weng Date: Fri, 20 Feb 2026 23:23:51 -0800 Subject: [PATCH 1/4] fix: table-core's getIsAllSubRowsSelected returns false when subrows are unselectable --- .../table-core/src/features/RowSelection.ts | 88 ++-- .../table-core/tests/RowSelection.test.ts | 401 +++++++++++++++--- 2 files changed, 383 insertions(+), 106 deletions(-) diff --git a/packages/table-core/src/features/RowSelection.ts b/packages/table-core/src/features/RowSelection.ts index e5fddaba9d..38c90baa31 100644 --- a/packages/table-core/src/features/RowSelection.ts +++ b/packages/table-core/src/features/RowSelection.ts @@ -76,7 +76,7 @@ export interface RowSelectionRow { */ getCanSelectSubRows: () => boolean /** - * Returns whether or not all of the row's sub rows are selected. + * Returns whether or not all of the row's selectable sub rows are selected. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getisallsubrowsselected) * @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection) */ @@ -88,7 +88,7 @@ export interface RowSelectionRow { */ getIsSelected: () => boolean /** - * Returns whether or not some of the row's sub rows are selected. + * Returns whether or not some of the row's selectable sub rows are selected. * @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#getissomeselected) * @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection) */ @@ -503,13 +503,15 @@ export const RowSelection: TableFeature = { } row.getIsSomeSelected = () => { - const { rowSelection } = table.getState() - return isSubRowSelected(row, rowSelection, table) === 'some' + const selectable = getSelectableSubRowCount(row) + const selected = getSelectedSubRowCount(row) + return selected > 0 && selected < selectable; } row.getIsAllSubRowsSelected = () => { - const { rowSelection } = table.getState() - return isSubRowSelected(row, rowSelection, table) === 'all' + const selectable = getSelectableSubRowCount(row) + const selected = getSelectedSubRowCount(row) + return selected > 0 && selected === selectable; } row.getCanSelect = () => { @@ -636,43 +638,43 @@ export function isRowSelected( return selection[row.id] ?? false } -export function isSubRowSelected( - row: Row, - selection: Record, - table: Table, -): boolean | 'some' | 'all' { - if (!row.subRows?.length) return false - - let allChildrenSelected = true - let someSelected = false - - row.subRows.forEach((subRow) => { - // Bail out early if we know both of these - if (someSelected && !allChildrenSelected) { - return +/** + * Determines the number of selectable sub-rows and nested sub-rows using BFS traversal + * + * @param {Row} row - The table row to evaluate. + * @returns {number} The number of selectable sub rows. + */ +export function getSelectableSubRowCount(row: Row): number { + return countSubRows(row, (node) => node.getCanSelect()); +}; + +/** + * Determines the number of selectable and selected sub-rows and nested sub-rows using BFS traversal + * + * @param {Row} row - The table row to evaluate. + * @returns {number} The number of selected sub rows. + */ +export function getSelectedSubRowCount(row: Row): number { + return countSubRows(row, (node) => node.getCanSelect() && node.getIsSelected()); +}; + +/** + * Count the number of sub-rows that satisfy a given condition (checked via BFS across all nested sub-rows). + * + * @param row The table row to evaluate. + * @param eligibility A function that takes a row and returns a boolean indicating whether the row should be counted. + * @returns The number of sub-rows that satisfy the condition. + */ +function countSubRows(row: Row, eligibility: (row: Row) => boolean): number { + const q = [...row.subRows]; + let count = 0; + while (q.length) { + const node = q.shift()!; + if (eligibility(node)) { + count += 1; } + q.push(...node.subRows); + } - if (subRow.getCanSelect()) { - if (isRowSelected(subRow, selection)) { - someSelected = true - } else { - allChildrenSelected = false - } - } - - // Check row selection of nested subrows - if (subRow.subRows && subRow.subRows.length) { - const subRowChildrenSelected = isSubRowSelected(subRow, selection, table) - if (subRowChildrenSelected === 'all') { - someSelected = true - } else if (subRowChildrenSelected === 'some') { - someSelected = true - allChildrenSelected = false - } else { - allChildrenSelected = false - } - } - }) - - return allChildrenSelected ? 'all' : someSelected ? 'some' : false + return count; } diff --git a/packages/table-core/tests/RowSelection.test.ts b/packages/table-core/tests/RowSelection.test.ts index 266bf27806..d07ba7d48e 100644 --- a/packages/table-core/tests/RowSelection.test.ts +++ b/packages/table-core/tests/RowSelection.test.ts @@ -147,9 +147,12 @@ describe('RowSelection', () => { expect(result).toEqual(false) }) }) - describe('isSubRowSelected', () => { - it('should return false if there are no sub-rows', () => { - const data = makeData(3) + + // IMO the following 2 tests suites are not useful as they are not exposed to the public interface + // Instead the 2 RowSelectionRow.* test suites are more useful. + describe('getSelectableSubRowCount', () => { + it('should return 0 when there are no sub-rows', () => { + const data = makeData(1) const columns = generateColumns(data) const table = createTable({ @@ -157,24 +160,38 @@ describe('RowSelection', () => { onStateChange() {}, renderFallbackValue: '', data, - state: {}, + state: { rowSelection: {} }, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectableSubRowCount(firstRow) + expect(result).toBe(0) + }) - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) + it('should return 0 when all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) - expect(result).toEqual(false) + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows are selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectableSubRowCount(firstRow) + expect(result).toBe(0) }) - it('should return false if no sub-rows are selected', () => { - const data = makeData(3, 2) + it('should return number of selectable sub-rows', () => { + const data = makeData(1, 2) const columns = generateColumns(data) const table = createTable({ @@ -183,26 +200,40 @@ describe('RowSelection', () => { renderFallbackValue: '', data, getSubRows: (row) => row.subRows, - state: { - rowSelection: {}, - }, + state: { rowSelection: {} }, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectableSubRowCount(firstRow) + expect(result).toBe(2) + }) - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) + it('should count all nested selectable sub-rows', () => { + const data = makeData(1, 2, 2) + const columns = generateColumns(data) - expect(result).toEqual(false) + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectableSubRowCount(firstRow) + expect(result).toBe(5) // 2 direct sub-rows + 3 nested sub-rows }) + }) - it('should return some if some sub-rows are selected', () => { - const data = makeData(3, 2) + describe('getSelectedSubRowCount', () => { + it('should return 0 when there are no sub-rows', () => { + const data = makeData(3) const columns = generateColumns(data) const table = createTable({ @@ -210,10 +241,70 @@ describe('RowSelection', () => { onStateChange() {}, renderFallbackValue: '', data, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectedSubRowCount(firstRow) + expect(result).toBe(0) + }) + + it('should return 0 when all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows are selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectedSubRowCount(firstRow) + expect(result).toBe(0) + }) + + it('should return 0 when no sub-rows are selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectedSubRowCount(firstRow) + expect(result).toBe(0) + }) + + it('should return number of selectable and selected sub-rows', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.1', // second sub-row is selectable + onStateChange() {}, + renderFallbackValue: '', + data, getSubRows: (row) => row.subRows, state: { rowSelection: { '0.0': true, + '0.1': true, }, }, columns, @@ -221,22 +312,16 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] - - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) - - expect(result).toEqual('some') + const result = RowSelection.getSelectedSubRowCount(firstRow) + expect(result).toBe(1) }) - it('should return all if all sub-rows are selected', () => { - const data = makeData(3, 2) + it('should count all nested selectable and selected sub-rows', () => { + const data = makeData(1, 2, 2) const columns = generateColumns(data) const table = createTable({ - enableRowSelection: true, + enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable onStateChange() {}, renderFallbackValue: '', data, @@ -244,7 +329,9 @@ describe('RowSelection', () => { state: { rowSelection: { '0.0': true, - '0.1': true, + '0.0.0': true, + '0.0.1': true, + '0.1.1': true, }, }, columns, @@ -252,28 +339,205 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] + const result = RowSelection.getSelectedSubRowCount(firstRow) + expect(result).toBe(3) + }) + }); + + describe('RowSelectionRow.getIsSomeSelected', () => { + it('should return false if there are no sub-rows', () => { + const data = makeData(2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) + const { rows } = table.getCoreRowModel(); + rows.forEach(row => { + expect(row.getIsSomeSelected()).toBe(false) + }); + }) - expect(result).toEqual('all') + it('should return false if all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true, '0.1': true } }, // even if all sub-rows are selected, they are not selectable + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsSomeSelected()).toBe(false) }) - it('should return all if all selectable sub-rows are selected', () => { - const data = makeData(3, 2) + + it('should return false if all selectable sub-rows are not selected', () => { + const data = makeData(1, 2) const columns = generateColumns(data) const table = createTable({ - enableRowSelection: (row) => row.index === 0, // only first row is selectable (of 2 sub-rows) + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0': true } }, // parent row's own selection does not impact outcome + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsSomeSelected()).toBe(false) + }) + + it('should return false if all selectable sub-rows are selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true, '0.1': true } }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsSomeSelected()).toBe(false) + }) + + it('should return true if some selectable sub-rows are selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true } }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsSomeSelected()).toBe(true) + }) + + it('should return true if one nested selectable sub-row is selected', () => { + const data = makeData(1, 2, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0.0': true } }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsSomeSelected()).toBe(true) + }) + }); + + describe('RowSelectionRow.getIsAllSubRowsSelected', () => { + it('should return false if there are no sub-rows', () => { + const data = makeData(2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const { rows } = table.getCoreRowModel(); + rows.forEach(row => { + expect(row.getIsAllSubRowsSelected()).toBe(false) + }); + }) + + it('should return false if all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsAllSubRowsSelected()).toBe(false) + }) + + it('should return false if any selectable sub-row is not selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true } }, // only one of two selected + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsAllSubRowsSelected()).toBe(false) + }) + + it('should return false if any nested selectable sub-row is not selected', () => { + const data = makeData(1, 2, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, onStateChange() {}, renderFallbackValue: '', data, getSubRows: (row) => row.subRows, state: { rowSelection: { - '0.0': true, // first sub-row + '0.0': true, + '0.0.0': true, + '0.0.1': true, + '0.1': true, + '0.1.0': true, + // '0.1.1' not selected }, }, columns, @@ -281,17 +545,30 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsAllSubRowsSelected()).toBe(false) + }) - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) + it('should return true if all selectable sub-rows are selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) - expect(result).toEqual('all') + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true, '0.1': true } }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsAllSubRowsSelected()).toBe(true) }) - it('should return some when some nested sub-rows are selected', () => { - const data = makeData(3, 2, 2) + + it('should return true if all selectable sub-rows including nested ones are selected', () => { + const data = makeData(1, 2, 2) const columns = generateColumns(data) const table = createTable({ @@ -302,7 +579,12 @@ describe('RowSelection', () => { getSubRows: (row) => row.subRows, state: { rowSelection: { - '0.0.0': true, // first nested sub-row + '0.0': true, + '0.0.0': true, + '0.0.1': true, + '0.1': true, + '0.1.0': true, + '0.1.1': true, }, }, columns, @@ -310,14 +592,7 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] - - const result = RowSelection.isSubRowSelected( - firstRow, - table.getState().rowSelection, - table, - ) - - expect(result).toEqual('some') + expect(firstRow.getIsAllSubRowsSelected()).toBe(true) }) - }) + }); }) From 519e40d597729726e9ce601bd7e1e972a9ca97da Mon Sep 17 00:00:00 2001 From: Kevin Weng Date: Fri, 20 Feb 2026 23:54:08 -0800 Subject: [PATCH 2/4] chore: add changeset --- .changeset/cute-walls-judge.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cute-walls-judge.md diff --git a/.changeset/cute-walls-judge.md b/.changeset/cute-walls-judge.md new file mode 100644 index 0000000000..455165e9b4 --- /dev/null +++ b/.changeset/cute-walls-judge.md @@ -0,0 +1,5 @@ +--- +'@tanstack/table-core': patch +--- + +Fixed table-core `row.getIsAllSubRowsSelected` now returns false when subrows are unselectable () From 28e37296aab6a27d054566b3af9019e539d58ac2 Mon Sep 17 00:00:00 2001 From: Kevin Weng Date: Sat, 21 Feb 2026 10:33:02 -0800 Subject: [PATCH 3/4] chore: refactor double traversal and retain isSubRowSelected --- .../table-core/src/features/RowSelection.ts | 67 ++-- .../table-core/tests/RowSelection.test.ts | 369 +++++++++++++----- 2 files changed, 303 insertions(+), 133 deletions(-) diff --git a/packages/table-core/src/features/RowSelection.ts b/packages/table-core/src/features/RowSelection.ts index 38c90baa31..b394e33783 100644 --- a/packages/table-core/src/features/RowSelection.ts +++ b/packages/table-core/src/features/RowSelection.ts @@ -503,15 +503,14 @@ export const RowSelection: TableFeature = { } row.getIsSomeSelected = () => { - const selectable = getSelectableSubRowCount(row) - const selected = getSelectedSubRowCount(row) - return selected > 0 && selected < selectable; + const { selectableCount, selectedCount } = getSubRowSelectionsCount(row); + return selectedCount > 0 && selectedCount < selectableCount; } row.getIsAllSubRowsSelected = () => { - const selectable = getSelectableSubRowCount(row) - const selected = getSelectedSubRowCount(row) - return selected > 0 && selected === selectable; + const { selectableCount, selectedCount } = getSubRowSelectionsCount(row); + + return selectedCount > 0 && selectedCount === selectableCount; } row.getCanSelect = () => { @@ -639,42 +638,40 @@ export function isRowSelected( } /** - * Determines the number of selectable sub-rows and nested sub-rows using BFS traversal - * - * @param {Row} row - The table row to evaluate. - * @returns {number} The number of selectable sub rows. - */ -export function getSelectableSubRowCount(row: Row): number { - return countSubRows(row, (node) => node.getCanSelect()); -}; - -/** - * Determines the number of selectable and selected sub-rows and nested sub-rows using BFS traversal - * - * @param {Row} row - The table row to evaluate. - * @returns {number} The number of selected sub rows. - */ -export function getSelectedSubRowCount(row: Row): number { - return countSubRows(row, (node) => node.getCanSelect() && node.getIsSelected()); -}; - -/** - * Count the number of sub-rows that satisfy a given condition (checked via BFS across all nested sub-rows). + * Determines the number of selectable sub-rows and selected sub-rows (checked via BFS across all nested sub-rows). * * @param row The table row to evaluate. - * @param eligibility A function that takes a row and returns a boolean indicating whether the row should be counted. * @returns The number of sub-rows that satisfy the condition. */ -function countSubRows(row: Row, eligibility: (row: Row) => boolean): number { +export function getSubRowSelectionsCount(row: Row): { selectableCount: number, selectedCount: number } { const q = [...row.subRows]; - let count = 0; - while (q.length) { - const node = q.shift()!; - if (eligibility(node)) { - count += 1; + let selectableCount = 0; + let selectedCount = 0; + let index = 0; + while (index < q.length) { + const node = q[index]!; + if (node.getCanSelect()) { + selectableCount += 1; + if (node.getIsSelected()) { + selectedCount += 1; + } } q.push(...node.subRows); + index += 1; } - return count; + return { selectableCount, selectedCount }; +} + +export function isSubRowSelected( + row: Row, + _selection: Record, + _table: Table, +): boolean | 'some' | 'all' { + if (row.getIsSomeSelected()) { + return 'some'; + } else if (row.getIsAllSubRowsSelected()) { + return 'all'; + } + return false; } diff --git a/packages/table-core/tests/RowSelection.test.ts b/packages/table-core/tests/RowSelection.test.ts index d07ba7d48e..01d78f9d44 100644 --- a/packages/table-core/tests/RowSelection.test.ts +++ b/packages/table-core/tests/RowSelection.test.ts @@ -147,12 +147,9 @@ describe('RowSelection', () => { expect(result).toEqual(false) }) }) - - // IMO the following 2 tests suites are not useful as they are not exposed to the public interface - // Instead the 2 RowSelectionRow.* test suites are more useful. - describe('getSelectableSubRowCount', () => { - it('should return 0 when there are no sub-rows', () => { - const data = makeData(1) + describe('isSubRowSelected', () => { + it('should return false if there are no sub-rows', () => { + const data = makeData(3) const columns = generateColumns(data) const table = createTable({ @@ -160,38 +157,24 @@ describe('RowSelection', () => { onStateChange() {}, renderFallbackValue: '', data, - state: { rowSelection: {} }, + state: {}, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectableSubRowCount(firstRow) - expect(result).toBe(0) - }) - - it('should return 0 when all sub-rows are not selectable', () => { - const data = makeData(1, 2) - const columns = generateColumns(data) - const table = createTable({ - enableRowSelection: (row) => row.depth === 0, // only root rows are selectable - onStateChange() {}, - renderFallbackValue: '', - data, - getSubRows: (row) => row.subRows, - state: { rowSelection: {} }, - columns, - getCoreRowModel: getCoreRowModel(), - }) + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) - const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectableSubRowCount(firstRow) - expect(result).toBe(0) + expect(result).toEqual(false) }) - it('should return number of selectable sub-rows', () => { - const data = makeData(1, 2) + it('should return false if no sub-rows are selected', () => { + const data = makeData(3, 2) const columns = generateColumns(data) const table = createTable({ @@ -200,40 +183,26 @@ describe('RowSelection', () => { renderFallbackValue: '', data, getSubRows: (row) => row.subRows, - state: { rowSelection: {} }, + state: { + rowSelection: {}, + }, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectableSubRowCount(firstRow) - expect(result).toBe(2) - }) - - it('should count all nested selectable sub-rows', () => { - const data = makeData(1, 2, 2) - const columns = generateColumns(data) - const table = createTable({ - enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable - onStateChange() {}, - renderFallbackValue: '', - data, - getSubRows: (row) => row.subRows, - state: { rowSelection: {} }, - columns, - getCoreRowModel: getCoreRowModel(), - }) + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) - const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectableSubRowCount(firstRow) - expect(result).toBe(5) // 2 direct sub-rows + 3 nested sub-rows + expect(result).toEqual(false) }) - }) - describe('getSelectedSubRowCount', () => { - it('should return 0 when there are no sub-rows', () => { - const data = makeData(3) + it('should return some if some sub-rows are selected', () => { + const data = makeData(3, 2) const columns = generateColumns(data) const table = createTable({ @@ -241,38 +210,29 @@ describe('RowSelection', () => { onStateChange() {}, renderFallbackValue: '', data, - state: { rowSelection: {} }, + getSubRows: (row) => row.subRows, + state: { + rowSelection: { + '0.0': true, + }, + }, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectedSubRowCount(firstRow) - expect(result).toBe(0) - }) - it('should return 0 when all sub-rows are not selectable', () => { - const data = makeData(1, 2) - const columns = generateColumns(data) + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) - const table = createTable({ - enableRowSelection: (row) => row.depth === 0, // only root rows are selectable - onStateChange() {}, - renderFallbackValue: '', - data, - getSubRows: (row) => row.subRows, - state: { rowSelection: {} }, - columns, - getCoreRowModel: getCoreRowModel(), - }) - - const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectedSubRowCount(firstRow) - expect(result).toBe(0) + expect(result).toEqual('some') }) - it('should return 0 when no sub-rows are selected', () => { - const data = makeData(1, 2) + it('should return all if all sub-rows are selected', () => { + const data = makeData(3, 2) const columns = generateColumns(data) const table = createTable({ @@ -281,30 +241,39 @@ describe('RowSelection', () => { renderFallbackValue: '', data, getSubRows: (row) => row.subRows, - state: { rowSelection: {} }, + state: { + rowSelection: { + '0.0': true, + '0.1': true, + }, + }, columns, getCoreRowModel: getCoreRowModel(), }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectedSubRowCount(firstRow) - expect(result).toBe(0) - }) - it('should return number of selectable and selected sub-rows', () => { - const data = makeData(1, 2) + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) + + expect(result).toEqual('all') + }) + it('should return all if all selectable sub-rows are selected', () => { + const data = makeData(3, 2) const columns = generateColumns(data) const table = createTable({ - enableRowSelection: (row) => row.id !== '0.1', // second sub-row is selectable + enableRowSelection: (row) => row.index === 0, // only first row is selectable (of 2 sub-rows) onStateChange() {}, renderFallbackValue: '', data, getSubRows: (row) => row.subRows, state: { rowSelection: { - '0.0': true, - '0.1': true, + '0.0': true, // first sub-row }, }, columns, @@ -312,26 +281,28 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectedSubRowCount(firstRow) - expect(result).toBe(1) - }) - it('should count all nested selectable and selected sub-rows', () => { - const data = makeData(1, 2, 2) + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) + + expect(result).toEqual('all') + }) + it('should return some when some nested sub-rows are selected', () => { + const data = makeData(3, 2, 2) const columns = generateColumns(data) const table = createTable({ - enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable + enableRowSelection: true, onStateChange() {}, renderFallbackValue: '', data, getSubRows: (row) => row.subRows, state: { rowSelection: { - '0.0': true, - '0.0.0': true, - '0.0.1': true, - '0.1.1': true, + '0.0.0': true, // first nested sub-row }, }, columns, @@ -339,9 +310,211 @@ describe('RowSelection', () => { }) const firstRow = table.getCoreRowModel().rows[0] - const result = RowSelection.getSelectedSubRowCount(firstRow) - expect(result).toBe(3) + + const result = RowSelection.isSubRowSelected( + firstRow, + table.getState().rowSelection, + table, + ) + + expect(result).toEqual('some') + }) + }) + + describe('getSubRowSelectionsCount', () => { + describe('selectableCount', () => { + it('should be 0 when there are no sub-rows', () => { + const data = makeData(1) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectableCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectableCount).toBe(0) + }) + + it('should be 0 when all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows are selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectableCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectableCount).toBe(0) + }) + + it('should be number of selectable sub-rows', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectableCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectableCount).toBe(2) + }) + + it('should include all nested selectable sub-rows', () => { + const data = makeData(1, 2, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectableCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectableCount).toBe(5) // 2 direct sub-rows + 3 nested sub-rows + }) }) + + describe('selectedCount', () => { + it('should be 0 when there are no sub-rows', () => { + const data = makeData(3) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectedCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectedCount).toBe(0) + }) + + it('should be 0 when all sub-rows are not selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.depth === 0, // only root rows are selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectedCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectedCount).toBe(0) + }) + + it('should be 0 when no sub-rows are selected', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: true, + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: {} }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectedCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectedCount).toBe(0) + }) + + it('should be number of selectable and selected sub-rows', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.1', // second sub-row is not selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { + rowSelection: { + '0.0': true, + '0.1': true, + }, + }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectedCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectedCount).toBe(1) + }) + + it('should include all nested selectable and selected sub-rows', () => { + const data = makeData(1, 2, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.0.1', // make one of the sub-rows non-selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { + rowSelection: { + '0.0': true, + '0.0.0': true, + '0.0.1': true, + '0.1.1': true, + }, + }, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + const { selectedCount } = RowSelection.getSubRowSelectionsCount(firstRow) + expect(selectedCount).toBe(3) + }) + }); }); describe('RowSelectionRow.getIsSomeSelected', () => { From c4bd664660d5ae0641f89bdf4972aa7a109255f5 Mon Sep 17 00:00:00 2001 From: Kevin Weng Date: Sat, 21 Feb 2026 10:50:25 -0800 Subject: [PATCH 4/4] test: add suggested test --- .../table-core/tests/RowSelection.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/table-core/tests/RowSelection.test.ts b/packages/table-core/tests/RowSelection.test.ts index 01d78f9d44..759f51b6f6 100644 --- a/packages/table-core/tests/RowSelection.test.ts +++ b/packages/table-core/tests/RowSelection.test.ts @@ -767,5 +767,24 @@ describe('RowSelection', () => { const firstRow = table.getCoreRowModel().rows[0] expect(firstRow.getIsAllSubRowsSelected()).toBe(true) }) + + it('should return true when all selectable sub-rows are selected and some are non-selectable', () => { + const data = makeData(1, 2) + const columns = generateColumns(data) + + const table = createTable({ + enableRowSelection: (row) => row.id !== '0.1', // second sub-row is NOT selectable + onStateChange() {}, + renderFallbackValue: '', + data, + getSubRows: (row) => row.subRows, + state: { rowSelection: { '0.0': true } }, // only selectable sub-row selected + columns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstRow = table.getCoreRowModel().rows[0] + expect(firstRow.getIsAllSubRowsSelected()).toBe(true) + }) }); })