From d6ef41de56ae6a42c973ca7ba8c08f61f410df1a Mon Sep 17 00:00:00 2001 From: agustin Date: Wed, 14 Oct 2026 21:09:19 -0300 Subject: [PATCH] perf(table): keep a row's cell order in a plain array --- .changeset/table-row-cell-order.md | 5 +++ .../table/root/table-drop-column-test.svelte | 40 +++++++++++++++++++ .../ui/src/lib/table/root/table-root.test.ts | 18 +++++++++ .../ui/src/lib/table/row/table-row.svelte | 24 +++++------ 4 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 .changeset/table-row-cell-order.md create mode 100644 packages/ui/src/lib/table/root/table-drop-column-test.svelte diff --git a/.changeset/table-row-cell-order.md b/.changeset/table-row-cell-order.md new file mode 100644 index 00000000..c21c8227 --- /dev/null +++ b/.changeset/table-row-cell-order.md @@ -0,0 +1,5 @@ +--- +'@human-kit/ui': patch +--- + +Scrolling a wide virtualized table no longer stalls. A row kept the order of its cells in a `SvelteSet` and a `SvelteMap`, and rewrote the index of every cell after one that left, so a row of fourteen columns leaving the viewport made around a hundred reactive writes. The order now lives in a plain array, read with `indexOf`, and the row's existing epoch announces when a removal moves the cells after it. diff --git a/packages/ui/src/lib/table/root/table-drop-column-test.svelte b/packages/ui/src/lib/table/root/table-drop-column-test.svelte new file mode 100644 index 00000000..3c8ad04f --- /dev/null +++ b/packages/ui/src/lib/table/root/table-drop-column-test.svelte @@ -0,0 +1,40 @@ + + + + + + {#each columns as column (column)} + + {column} + + {/each} + + + + + {#each rows as row (row.id)} + + {#each columns as column (column)} + {row[column]} + {/each} + + {/each} + + + + diff --git a/packages/ui/src/lib/table/root/table-root.test.ts b/packages/ui/src/lib/table/root/table-root.test.ts index c31950e0..99fcc0c1 100644 --- a/packages/ui/src/lib/table/root/table-root.test.ts +++ b/packages/ui/src/lib/table/root/table-root.test.ts @@ -3,6 +3,7 @@ import { render } from 'vitest-browser-svelte'; import { userEvent } from 'vitest/browser'; import TableTest from './table-test.svelte'; import TableReorderTest from './table-reorder-test.svelte'; +import TableDropColumnTest from './table-drop-column-test.svelte'; import { expectNoFalseFocusAttributes } from '../../test-utils/focus-contract'; function getHeaderCells(container: HTMLElement) { @@ -1266,4 +1267,21 @@ describe('Table.Root', () => { getBodySecondColumnCells(document.body).map((cell) => cell.textContent?.trim()) ).toContain('Admin'); }); + + // A row keeps the order of its cells in plain arrays, announced by an epoch: a cell that + // leaves from the middle has to move the ones after it down, in the header and the body. + it('moves the cells after a removed one down a column', async () => { + render(TableDropColumnTest); + const index = (testId: string) => + document.querySelector(`[data-testid="${testId}"]`)?.getAttribute('data-column-index'); + + expect(index('role-header')).toBe('2'); + expect(index('role-cell-danilo')).toBe('2'); + + document.querySelector('[data-testid="drop-group"]')?.click(); + + await expect.poll(() => index('role-header')).toBe('1'); + await expect.poll(() => index('role-cell-danilo')).toBe('1'); + await expect.poll(() => index('role-cell-zahra')).toBe('1'); + }); }); diff --git a/packages/ui/src/lib/table/row/table-row.svelte b/packages/ui/src/lib/table/row/table-row.svelte index 3bf88406..661ebfd6 100644 --- a/packages/ui/src/lib/table/row/table-row.svelte +++ b/packages/ui/src/lib/table/row/table-row.svelte @@ -1,6 +1,5 @@