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
122 changes: 121 additions & 1 deletion packages/table-core/src/features/RowSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface RowSelectionOptions<TData extends RowData> {
enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
/**
* Enables/disables automatic sub-row selection when a parent row is selected, or a function that enables/disables automatic sub-row selection for each row.
* When enabled, also provides enhanced parent-child selection behavior:
* - Parent rows show as indeterminate when partially selected
* - Parent rows auto-select when all children are selected
* - Parent rows auto-unselect when no children are selected
* (Use in combination with expanding or grouping features)
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#enablesubrowselection)
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
Expand Down Expand Up @@ -495,11 +499,77 @@ export const RowSelection: TableFeature = {
}
row.getIsSelected = () => {
const { rowSelection } = table.getState()
return isRowSelected(row, rowSelection)
const directlySelected = isRowSelected(row, rowSelection)

// For leaf rows, return direct selection state
if (!row.subRows?.length) {
return directlySelected
}

// Enhanced parent-child logic only applies when enableSubRowSelection is enabled
if (!row.getCanSelectSubRows()) {
return directlySelected
}

// For parent rows, implement enhanced selection logic
const allChildrenSelected = row.subRows.every(child => child.getIsSelected())
const someChildrenSelected = row.subRows.some(child =>
child.getIsSelected() || child.getIsSomeSelected())

// Parent shows as indeterminate (false) when:
// - Parent is explicitly selected AND
// - Not all children are selected AND
// - Some children are selected
if (directlySelected && !allChildrenSelected && someChildrenSelected) {
return false // Indeterminate state
}

// Parent is selected when:
// - Explicitly selected (with no children selected), OR
// - All children are selected (implicit selection)
return directlySelected || allChildrenSelected
}

row.getIsSomeSelected = () => {
const { rowSelection } = table.getState()

// For rows with children, implement the enhanced indeterminate logic
if (row.subRows?.length) {

// Enhanced indeterminate logic only applies when enableSubRowSelection is enabled
if (!row.getCanSelectSubRows()) {
return isSubRowSelected(row, rowSelection, table) === 'some'
}

// Count children that are selected OR indeterminate (recursive check)
let fullySelectedCount = 0
let partiallySelectedCount = 0

row.subRows.forEach(subRow => {
const isDirectlySelected = rowSelection[subRow.id]
const isChildIndeterminate = subRow.getIsSomeSelected()

if (isDirectlySelected && !isChildIndeterminate) {
// Child is fully selected (and not indeterminate)
fullySelectedCount++;
} else if (isDirectlySelected || isChildIndeterminate) {
// Child is partially selected (indeterminate) or selected but has indeterminate descendants
partiallySelectedCount++;
}
})

const totalChildren = row.subRows.length
const hasAnySelection = fullySelectedCount > 0 || partiallySelectedCount > 0
const hasPartialSelection = partiallySelectedCount > 0 || fullySelectedCount < totalChildren

// Parent is indeterminate if:
// 1. Has some selection but not all children are fully selected, OR
// 2. Has any children that are themselves indeterminate
const result = hasAnySelection && hasPartialSelection

return result
}

return isSubRowSelected(row, rowSelection, table) === 'some'
}

Expand Down Expand Up @@ -576,6 +646,56 @@ const mutateRowIsSelected = <TData extends RowData>(
mutateRowIsSelected(selectedRowIds, row.id, value, includeChildren, table)
)
}

updateParentSelectionState(selectedRowIds, row, table)
}

// Helper function to manage parent selection based on children state
const updateParentSelectionState = <TData extends RowData>(
selectedRowIds: Record<string, boolean>,
row: Row<TData>,
table: Table<TData>
) => {
// Find the parent row by checking all rows in the table
const allRows = table.getCoreRowModel().flatRows
const parentRow = allRows.find(r =>
r.subRows?.some(subRow => subRow.id === row.id)
)

if (!parentRow || !parentRow.subRows?.length) {
return // No parent found or parent has no children
}

// Enhanced parent state logic only applies when enableSubRowSelection is enabled
if (!parentRow.getCanSelectSubRows()) {
return // Skip enhanced logic if sub-row selection is disabled
}

// Count selected children - use direct selection state to avoid recursion issues
const selectedChildren = parentRow.subRows.filter(child =>
selectedRowIds[child.id]
)
const totalChildren = parentRow.subRows.length

if (selectedChildren.length === 0) {
// No children selected at all, unselect parent
delete selectedRowIds[parentRow.id]
} else if (selectedChildren.length === totalChildren) {
// All children are directly selected, select parent fully
if (parentRow.getCanSelect()) {
selectedRowIds[parentRow.id] = true
}
} else {
// Some children selected, parent should remain selected but show indeterminate
// Keep parent as true so getIsSelected() returns true for checkbox state
// getIsSomeSelected() will handle the indeterminate visual recursively
if (parentRow.getCanSelect()) {
selectedRowIds[parentRow.id] = true
}
}

// Recursively check the parent's parent
updateParentSelectionState(selectedRowIds, parentRow, table)
}

export function selectRowsFn<TData extends RowData>(
Expand Down
Loading