diff --git a/examples/react/column-resizing-performant/src/main.tsx b/examples/react/column-resizing-performant/src/main.tsx index 8d3335ac1a..3f9692ab4e 100644 --- a/examples/react/column-resizing-performant/src/main.tsx +++ b/examples/react/column-resizing-performant/src/main.tsx @@ -119,7 +119,7 @@ function App() { Regenerate Data
diff --git a/examples/vue/virtualized-infinite-scrolling/package.json b/examples/vue/virtualized-infinite-scrolling/package.json index 0da266d956..f096657f13 100644 --- a/examples/vue/virtualized-infinite-scrolling/package.json +++ b/examples/vue/virtualized-infinite-scrolling/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@faker-js/faker": "^10.4.0", + "@tanstack/match-sorter-utils": "^9.0.0-alpha.4", "@tanstack/vue-query": "^5.100.10", "@tanstack/vue-store": "^0.11.0", "@tanstack/vue-table": "^9.0.0-alpha.45", diff --git a/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts b/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts index e6cde63817..5014f5a1de 100644 --- a/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts +++ b/packages/table-core/src/features/column-ordering/columnOrderingFeature.ts @@ -63,6 +63,7 @@ export function constructColumnOrderingFeature< column.table.atoms.columnOrder?.get(), column.table.atoms.columnPinning?.get(), column.table.atoms.grouping?.get(), + column.table.atoms.columnVisibility?.get(), ], }, column_getIsFirstColumn: { diff --git a/packages/table-core/src/features/column-pinning/columnPinningFeature.ts b/packages/table-core/src/features/column-pinning/columnPinningFeature.ts index b399295894..5ef3961929 100644 --- a/packages/table-core/src/features/column-pinning/columnPinningFeature.ts +++ b/packages/table-core/src/features/column-pinning/columnPinningFeature.ts @@ -301,11 +301,7 @@ export function constructColumnPinningFeature< }, table_getPinnedLeafColumns: { fn: (position) => table_getPinnedLeafColumns(table, position), - memoDeps: (position) => [ - position, - table.options.columns, - table.atoms.columnPinning?.get(), - ], + // must not memo here as it's just a shortcut function }, // visible leaf columns table_getLeftVisibleLeafColumns: { @@ -334,12 +330,7 @@ export function constructColumnPinningFeature< }, table_getPinnedVisibleLeafColumns: { fn: (position) => table_getPinnedVisibleLeafColumns(table, position), - memoDeps: (position) => [ - position, - table.options.columns, - table.atoms.columnPinning?.get(), - table.atoms.columnVisibility?.get(), - ], + // must not memo here as it's just a shortcut function }, }) }, diff --git a/packages/table-core/src/features/column-sizing/columnSizingFeature.ts b/packages/table-core/src/features/column-sizing/columnSizingFeature.ts index 964c1c011b..e054520bec 100644 --- a/packages/table-core/src/features/column-sizing/columnSizingFeature.ts +++ b/packages/table-core/src/features/column-sizing/columnSizingFeature.ts @@ -1,10 +1,8 @@ import { assignPrototypeAPIs, assignTableAPIs, - callMemoOrStaticFn, makeStateUpdater, } from '../../utils' -import { table_getPinnedVisibleLeafColumns } from '../column-pinning/columnPinningFeature.utils' import { column_getAfter, column_getSize, @@ -75,31 +73,31 @@ export function constructColumnSizingFeature< assignPrototypeAPIs('columnSizingFeature', prototype, table, { column_getSize: { fn: (column) => column_getSize(column), + memoDeps: (column) => [ + table.options.columns, + table.atoms.columnSizing?.get()?.[column.id], // just this column's size state + ], }, column_getStart: { fn: (column, position) => column_getStart(column, position), memoDeps: (column, position) => [ position, - callMemoOrStaticFn( - column.table, - 'getPinnedVisibleLeafColumns', - table_getPinnedVisibleLeafColumns, - position, - ), - column.table.atoms.columnSizing?.get(), + table.options.columns, + table.atoms.columnSizing?.get(), + table.atoms.columnOrder?.get(), + table.atoms.columnPinning?.get(), + table.atoms.columnVisibility?.get(), ], }, column_getAfter: { fn: (column, position) => column_getAfter(column, position), memoDeps: (column, position) => [ position, - callMemoOrStaticFn( - column.table, - 'getPinnedVisibleLeafColumns', - table_getPinnedVisibleLeafColumns, - position, - ), - column.table.atoms.columnSizing?.get(), + table.options.columns, + table.atoms.columnSizing?.get(), + table.atoms.columnOrder?.get(), + table.atoms.columnPinning?.get(), + table.atoms.columnVisibility?.get(), ], }, column_resetSize: { @@ -112,9 +110,23 @@ export function constructColumnSizingFeature< assignPrototypeAPIs('columnSizingFeature', prototype, table, { header_getSize: { fn: (header) => header_getSize(header), + memoDeps: (header) => [ + table.options.columns, + header.column.columns.length > 0 + ? table.atoms.columnSizing?.get() // must be all columns (sum child columns) + : table.atoms.columnSizing?.get()?.[header.column.id], // can just check it's associated column size state + ], }, header_getStart: { fn: (header) => header_getStart(header), + memoDeps: (header, position) => [ + position, + table.options.columns, + table.atoms.columnSizing?.get(), + table.atoms.columnOrder?.get(), + table.atoms.columnPinning?.get(), + table.atoms.columnVisibility?.get(), + ], }, }) }, diff --git a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts index ba3384af33..4ac65058eb 100644 --- a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts +++ b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts @@ -91,6 +91,14 @@ export function column_getStart< column: Column_Internal, position: ColumnPinningPosition | 'center', ): number { + const index = callMemoOrStaticFn( + column, + 'getIndex', + column_getIndex, + position, + ) + if (index <= 0) return 0 + const visibleLeafColumns = callMemoOrStaticFn( column.table, 'getPinnedVisibleLeafColumns', @@ -98,9 +106,11 @@ export function column_getStart< position, ) - return visibleLeafColumns - .slice(0, callMemoOrStaticFn(column, 'getIndex', column_getIndex, position)) - .reduce((sum: number, c) => sum + column_getSize(c), 0) + const prevColumn = visibleLeafColumns[index - 1]! + return ( + callMemoOrStaticFn(prevColumn, 'getStart', column_getStart, position) + + callMemoOrStaticFn(prevColumn, 'getSize', column_getSize) + ) } /** @@ -127,12 +137,19 @@ export function column_getAfter< table_getPinnedVisibleLeafColumns, position, ) + const index = callMemoOrStaticFn( + column, + 'getIndex', + column_getIndex, + position, + ) + if (index < 0 || index >= visibleLeafColumns.length - 1) return 0 - return visibleLeafColumns - .slice( - callMemoOrStaticFn(column, 'getIndex', column_getIndex, position) + 1, - ) - .reduce((sum: number, c) => sum + column_getSize(c), 0) + const nextColumn = visibleLeafColumns[index + 1]! + return ( + callMemoOrStaticFn(nextColumn, 'getSize', column_getSize) + + callMemoOrStaticFn(nextColumn, 'getAfter', column_getAfter, position) + ) } /** @@ -204,7 +221,8 @@ export function header_getStart< const prevSiblingHeader = header.headerGroup?.headers[header.index - 1] if (prevSiblingHeader) { return ( - header_getStart(prevSiblingHeader) + header_getSize(prevSiblingHeader) + callMemoOrStaticFn(prevSiblingHeader, 'getStart', header_getStart) + + callMemoOrStaticFn(prevSiblingHeader, 'getSize', header_getSize) ) } } diff --git a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts index 40de15b8ee..b7da03f59c 100644 --- a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts +++ b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.ts @@ -70,8 +70,8 @@ export function constructColumnVisibilityFeature< column_getIsVisible: { fn: (column) => column_getIsVisible(column), memoDeps: (column) => [ - column.table.options.columns, - column.table.atoms.columnVisibility?.get(), + table.options.columns, + table.atoms.columnVisibility?.get(), column.columns, ], }, @@ -93,8 +93,8 @@ export function constructColumnVisibilityFeature< fn: (row) => row_getVisibleCells(row), memoDeps: (row) => [ row.getAllCells(), - row.table.atoms.columnPinning?.get(), - row.table.atoms.columnVisibility?.get(), + table.atoms.columnPinning?.get(), + table.atoms.columnVisibility?.get(), ], }, }) diff --git a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts index f550db70b7..f9c2e9a03a 100644 --- a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts +++ b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts @@ -139,19 +139,30 @@ export function row_getVisibleCells< row.table.atoms.columnPinning?.get() ?? getDefaultColumnPinningState() if (!left.length && !right.length) return cells // no pinning, return early - // re-order cells for column pinning - const leftCells = [] - const rightCells = [] - const centerCells = [] + const cellsByColumnId = new Map>() + for (const cell of cells) cellsByColumnId.set(cell.column.id, cell) + + const leftCells: Array> = [] + for (const columnId of left) { + const cell = cellsByColumnId.get(columnId) + if (cell) leftCells.push(cell) + } + + const rightCells: Array> = [] + for (const columnId of right) { + const cell = cellsByColumnId.get(columnId) + if (cell) rightCells.push(cell) + } + + // Center cells: visible cells in natural column order, minus pinned ones. + const leftSet = new Set(left) + const rightSet = new Set(right) + const centerCells: Array> = [] for (const cell of cells) { - if (left.includes(cell.column.id)) { - leftCells.push(cell) - } else if (right.includes(cell.column.id)) { - rightCells.push(cell) - } else { - centerCells.push(cell) - } + const id = cell.column.id + if (!leftSet.has(id) && !rightSet.has(id)) centerCells.push(cell) } + return [...leftCells, ...centerCells, ...rightCells] } diff --git a/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts b/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts new file mode 100644 index 0000000000..eecd6e7bb5 --- /dev/null +++ b/packages/table-core/tests/unit/features/column-sizing/columnSizingFeature.utils.test.ts @@ -0,0 +1,324 @@ +import { describe, expect, it } from 'vitest' +import { + columnSizingFeature, + constructTable, + coreFeatures, + createCoreRowModel, +} from '../../../../src' +import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' + +const _features = { + ...coreFeatures, + columnSizingFeature, + coreReativityFeature: storeReactivityBindings(), +} + +type Item = { id: string; a: string; b: string; c: string; d: string } + +const data: Array = [ + { id: '1', a: 'a1', b: 'b1', c: 'c1', d: 'd1' }, + { id: '2', a: 'a2', b: 'b2', c: 'c2', d: 'd2' }, +] + +function makeTable(opts: { + columns: Array + columnSizing?: Record +}): any { + return constructTable({ + _features, + _rowModels: { coreRowModel: createCoreRowModel() }, + columns: opts.columns, + data, + state: opts.columnSizing ? { columnSizing: opts.columnSizing } : undefined, + } as any) +} + +describe('header_getSize', () => { + it('returns default size for a leaf header', () => { + const table = makeTable({ + columns: [{ id: 'a', accessorKey: 'a' }], + }) + const header = table.getHeaderGroups()[0].headers[0] + expect(header.getSize()).toBe(150) + }) + + it('returns columnDef.size for a leaf header', () => { + const table = makeTable({ + columns: [{ id: 'a', accessorKey: 'a', size: 200 }], + }) + const header = table.getHeaderGroups()[0].headers[0] + expect(header.getSize()).toBe(200) + }) + + it('returns sum of subHeader sizes for a parent header', () => { + const table = makeTable({ + columns: [ + { + id: 'group', + header: 'Group', + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }, + ], + }) + const groupRow = table.getHeaderGroups()[0] + const groupHeader = groupRow.headers[0] + expect(groupHeader.getSize()).toBe(300) + }) + + it('respects columnSizing state', () => { + const table = makeTable({ + columns: [{ id: 'a', accessorKey: 'a', size: 100 }], + columnSizing: { a: 250 }, + }) + const header = table.getHeaderGroups()[0].headers[0] + expect(header.getSize()).toBe(250) + }) +}) + +describe('header_getStart', () => { + it('returns 0 for the first header', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const headers = table.getHeaderGroups()[0].headers + expect(headers[0].getStart()).toBe(0) + }) + + it('returns size of preceding header for the second header', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const headers = table.getHeaderGroups()[0].headers + expect(headers[1].getStart()).toBe(100) + }) + + it('returns running sum of preceding sizes', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + { id: 'c', accessorKey: 'c', size: 50 }, + { id: 'd', accessorKey: 'd', size: 75 }, + ], + }) + const headers = table.getHeaderGroups()[0].headers + expect(headers[0].getStart()).toBe(0) + expect(headers[1].getStart()).toBe(100) + expect(headers[2].getStart()).toBe(300) + expect(headers[3].getStart()).toBe(350) + }) + + it('respects columnSizing state', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + columnSizing: { a: 75 }, + }) + const headers = table.getHeaderGroups()[0].headers + expect(headers[1].getStart()).toBe(75) + }) + + it('updates getStart when columnSizing changes (memo invalidation)', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + let headers = table.getHeaderGroups()[0].headers + expect(headers[1].getStart()).toBe(100) + + table.setColumnSizing({ a: 500 }) + headers = table.getHeaderGroups()[0].headers + expect(headers[1].getStart()).toBe(500) + }) + + it('returns running sum across nested header groups (parent row)', () => { + const table = makeTable({ + columns: [ + { + id: 'g1', + header: 'g1', + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }, + { + id: 'g2', + header: 'g2', + columns: [ + { id: 'c', accessorKey: 'c', size: 50 }, + { id: 'd', accessorKey: 'd', size: 75 }, + ], + }, + ], + }) + const groups = table.getHeaderGroups() + const parentRow = groups[0].headers + expect(parentRow[0].getStart()).toBe(0) + // group 2 starts after group 1 (100 + 200) + expect(parentRow[1].getStart()).toBe(300) + + const leafRow = groups[1].headers + expect(leafRow[0].getStart()).toBe(0) + expect(leafRow[1].getStart()).toBe(100) + expect(leafRow[2].getStart()).toBe(300) + expect(leafRow[3].getStart()).toBe(350) + }) + + it('returns 0 for a single-header group', () => { + const table = makeTable({ + columns: [{ id: 'a', accessorKey: 'a', size: 100 }], + }) + const headers = table.getHeaderGroups()[0].headers + expect(headers[0].getStart()).toBe(0) + }) +}) + +describe('column_getStart', () => { + it('returns 0 for the first column', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[0].getStart()).toBe(0) + }) + + it('returns size of preceding column for the second column', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[1].getStart()).toBe(100) + }) + + it('returns running sum of preceding column sizes', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + { id: 'c', accessorKey: 'c', size: 50 }, + { id: 'd', accessorKey: 'd', size: 75 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[0].getStart()).toBe(0) + expect(cols[1].getStart()).toBe(100) + expect(cols[2].getStart()).toBe(300) + expect(cols[3].getStart()).toBe(350) + }) + + it('respects columnSizing state', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + { id: 'c', accessorKey: 'c', size: 50 }, + ], + columnSizing: { a: 75, b: 30 }, + }) + const cols = table.getAllLeafColumns() + expect(cols[2].getStart()).toBe(105) + }) + + it('updates getStart when columnSizing changes (memo invalidation)', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + let cols = table.getAllLeafColumns() + expect(cols[1].getStart()).toBe(100) + + table.setColumnSizing({ a: 500 }) + cols = table.getAllLeafColumns() + expect(cols[1].getStart()).toBe(500) + }) +}) + +describe('column_getAfter', () => { + it('returns 0 for the last column', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[1].getAfter()).toBe(0) + }) + + it('returns size of following column for the second-to-last column', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[0].getAfter()).toBe(200) + }) + + it('returns running sum of following column sizes', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + { id: 'c', accessorKey: 'c', size: 50 }, + { id: 'd', accessorKey: 'd', size: 75 }, + ], + }) + const cols = table.getAllLeafColumns() + expect(cols[0].getAfter()).toBe(325) + expect(cols[1].getAfter()).toBe(125) + expect(cols[2].getAfter()).toBe(75) + expect(cols[3].getAfter()).toBe(0) + }) + + it('respects columnSizing state', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + { id: 'c', accessorKey: 'c', size: 50 }, + ], + columnSizing: { b: 30, c: 25 }, + }) + const cols = table.getAllLeafColumns() + expect(cols[0].getAfter()).toBe(55) + }) + + it('updates getAfter when columnSizing changes (memo invalidation)', () => { + const table = makeTable({ + columns: [ + { id: 'a', accessorKey: 'a', size: 100 }, + { id: 'b', accessorKey: 'b', size: 200 }, + ], + }) + let cols = table.getAllLeafColumns() + expect(cols[0].getAfter()).toBe(200) + + table.setColumnSizing({ b: 500 }) + cols = table.getAllLeafColumns() + expect(cols[0].getAfter()).toBe(500) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc5e25908b..787cd89e4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7353,6 +7353,9 @@ importers: '@faker-js/faker': specifier: ^10.4.0 version: 10.4.0 + '@tanstack/match-sorter-utils': + specifier: workspace:* + version: link:../../../packages/match-sorter-utils '@tanstack/vue-query': specifier: ^5.100.10 version: 5.100.10(vue@3.5.34(typescript@6.0.3))