From 990cd38a359fc9fc2a2617a90d4ccd0e1093bf16 Mon Sep 17 00:00:00 2001 From: Diogo Esteves Date: Sat, 24 Oct 2026 15:00:06 +0100 Subject: [PATCH] docs(examples): use spacer rows instead of translateY in the table examples The React, Vue, Svelte and Angular table examples moved each row with translateY inside a div sized to getTotalSize(). The itself only grew to the height of the rendered rows, so a sticky header scrolled away once the rows were translated past the end of the table. Render a spacer row above and below the visible rows instead, so the table is as tall as all of its rows, and make the header sticky. Also collapse the default border spacing so rows are exactly the estimated 34px, set explicit row heights in the Vue example, and turn off overflow-anchor on the scroll container. Fixes #585 --- .../angular/table/src/app/app.component.ts | 176 ++++++++++-------- examples/react/table/src/index.css | 11 ++ examples/react/table/src/main.tsx | 143 +++++++------- examples/svelte/table/src/App.svelte | 141 ++++++++------ examples/vue/table/src/App.vue | 135 ++++++++------ examples/vue/table/src/style.css | 11 ++ 6 files changed, 365 insertions(+), 252 deletions(-) diff --git a/examples/angular/table/src/app/app.component.ts b/examples/angular/table/src/app/app.component.ts index a90d2752e..24cb5ab7c 100644 --- a/examples/angular/table/src/app/app.component.ts +++ b/examples/angular/table/src/app/app.component.ts @@ -25,91 +25,107 @@ import { makeData, type Person } from './make-data' imports: [FlexRenderDirective], template: `

- For tables, the basis for the offset of the translate css function is from - the row's initial position itself. Because of this, we need to calculate - the translateY pixel count different and base it off the the index. + Table rows can't be absolutely positioned, so only the visible rows are + rendered, with a spacer row above and below them. The spacers keep the + table as tall as all of its rows, so the sticky header stays in place + while scrolling.


-
-
- - @for ( - headerGroup of table.getHeaderGroups(); - track headerGroup.id - ) { - - @for (header of headerGroup.headers; track header.id) { - - } - - } - - - @for ( - virtualRow of virtualizer.getVirtualItems(); - track data[virtualRow.index].id - ) { - - @for ( - cell of rows()[virtualRow.index].getVisibleCells(); - track cell.id - ) { - - } - - } - -
- @if (!header.isPlaceholder) { -
- - {{ headerText }} - {{ getSortIcon(header.column.getIsSorted()) }} - -
- } -
- + @if (!header.isPlaceholder) { +
- {{ cellText }} - -
- + + {{ headerText }} + {{ getSortIcon(header.column.getIsSorted()) }} + + + } + + } + + } + + + @if (paddingTop() > 0) { + + + + } + @for ( + virtualRow of virtualizer.getVirtualItems(); + track data[virtualRow.index].id + ) { + + @for ( + cell of rows()[virtualRow.index].getVisibleCells(); + track cell.id + ) { + + + {{ cellText }} + + + } + + } + @if (paddingBottom() > 0) { + + + + } + + `, styles: ` .scroll-container { height: 600px; overflow: auto; + overflow-anchor: none; + } + + table { + border-collapse: collapse; + } + + thead { + position: sticky; + top: 0; + background: white; } `, }) @@ -191,4 +207,18 @@ export class AppComponent { estimateSize: () => 34, overscan: 20, })) + + // Height of the spacer rows rendered above and below the visible rows + paddingTop = computed(() => { + const virtualRows = this.virtualizer.getVirtualItems() + return virtualRows.length > 0 ? virtualRows[0].start : 0 + }) + + paddingBottom = computed(() => { + const virtualRows = this.virtualizer.getVirtualItems() + return virtualRows.length > 0 + ? this.virtualizer.getTotalSize() - + virtualRows[virtualRows.length - 1].end + : 0 + }) } diff --git a/examples/react/table/src/index.css b/examples/react/table/src/index.css index c117a3a78..505f669ea 100644 --- a/examples/react/table/src/index.css +++ b/examples/react/table/src/index.css @@ -16,4 +16,15 @@ body { .container { height: 600px; overflow: auto; + overflow-anchor: none; +} + +table { + border-collapse: collapse; +} + +thead { + position: sticky; + top: 0; + background: white; } diff --git a/examples/react/table/src/main.tsx b/examples/react/table/src/main.tsx index 0bdbe7d69..ea7a484d8 100644 --- a/examples/react/table/src/main.tsx +++ b/examples/react/table/src/main.tsx @@ -86,74 +86,87 @@ function ReactTableVirtualized() { overscan: 20, }) + const virtualRows = virtualizer.getVirtualItems() + + // Height of the spacer rows rendered above and below the visible rows + const [paddingTop, paddingBottom] = + virtualRows.length > 0 + ? [ + virtualRows[0].start, + virtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end, + ] + : [0, 0] + + const columnCount = table.getVisibleLeafColumns().length + return (
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {paddingTop > 0 && ( + + + )} + {virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index] + return ( + + {row.getVisibleCells().map((cell) => { return ( - ) })} - ))} - - - {virtualizer.getVirtualItems().map((virtualRow, index) => { - const row = rows[virtualRow.index] - return ( - - {row.getVisibleCells().map((cell) => { - return ( - - ) - })} - - ) - })} - -
+ {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+
- {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - {{ - asc: ' 🔼', - desc: ' 🔽', - }[header.column.getIsSorted() as string] ?? null} -
+
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), )} - +
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
+ ) + })} + {paddingBottom > 0 && ( + + + + )} + +
) } @@ -162,10 +175,10 @@ function App() { return (

- For tables, the basis for the offset of the translate css function is - from the row's initial position itself. Because of this, we need to - calculate the translateY pixel count differently and base it off the - index. + Table rows can't be absolutely positioned, so only the visible rows are + rendered, with a spacer row above and below them. The spacers keep the + table as tall as all of its rows, so the sticky header stays in place + while scrolling.


diff --git a/examples/svelte/table/src/App.svelte b/examples/svelte/table/src/App.svelte index 427dcea2a..d91358fcd 100644 --- a/examples/svelte/table/src/App.svelte +++ b/examples/svelte/table/src/App.svelte @@ -94,69 +94,89 @@ estimateSize: () => 34, overscan: 20, }) + + $: virtualRows = $virtualizer.getVirtualItems() + + // Height of the spacer rows rendered above and below the visible rows + $: paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0 + $: paddingBottom = + virtualRows.length > 0 + ? $virtualizer.getTotalSize() - virtualRows[virtualRows.length - 1].end + : 0 + + $: columnCount = $table.getVisibleLeafColumns().length

- For tables, the basis for the offset of the translate css function is from - the row's initial position itself. Because of this, we need to calculate the - translateY pixel count differently and base it off the index. + Table rows can't be absolutely positioned, so only the visible rows are + rendered, with a spacer row above and below them. The spacers keep the table + as tall as all of its rows, so the sticky header stays in place while + scrolling.

-
- - - {#each $table.getHeaderGroups() as headerGroup (headerGroup.id)} - - {#each headerGroup.headers as header (header.id)} - - {/each} - - {/each} - - - {#each $virtualizer.getVirtualItems() as row, idx (row.index)} - - {#each rows[row.index].getVisibleCells() as cell (cell.id)} - - {/each} - - {/each} - -
- {#if !header.isPlaceholder} - - {/if} -
- -
-
+ + + {#each $table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {/each} + + {/each} + + + {#if paddingTop > 0} + + + + {/if} + {#each virtualRows as row (row.index)} + + {#each rows[row.index].getVisibleCells() as cell (cell.id)} + + {/each} + + {/each} + {#if paddingBottom > 0} + + + + {/if} + +
+ {#if !header.isPlaceholder} + + {/if} +
+ +
@@ -176,6 +196,15 @@ height: 600px; width: 100%; overflow: auto; + overflow-anchor: none; + } + table { + border-collapse: collapse; + } + thead { + position: sticky; + top: 0; + background: white; } .sortable-header { cursor: pointer; diff --git a/examples/vue/table/src/App.vue b/examples/vue/table/src/App.vue index ba12ad428..d3ca5a0fc 100644 --- a/examples/vue/table/src/App.vue +++ b/examples/vue/table/src/App.vue @@ -1,71 +1,77 @@ @@ -177,4 +183,17 @@ const rowVirtualizer = useVirtualizer(rowVirtualizerOptions) const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems()) const totalSize = computed(() => rowVirtualizer.value.getTotalSize()) + +// Height of the spacer rows rendered above and below the visible rows +const paddingTop = computed(() => + virtualRows.value.length > 0 ? virtualRows.value[0].start : 0, +) + +const paddingBottom = computed(() => + virtualRows.value.length > 0 + ? totalSize.value - virtualRows.value[virtualRows.value.length - 1].end + : 0, +) + +const columnCount = computed(() => table.getVisibleLeafColumns().length) diff --git a/examples/vue/table/src/style.css b/examples/vue/table/src/style.css index 7f784706f..9a1b9da09 100644 --- a/examples/vue/table/src/style.css +++ b/examples/vue/table/src/style.css @@ -16,6 +16,17 @@ body { .container { height: 600px; overflow: auto; + overflow-anchor: none; +} + +table { + border-collapse: collapse; +} + +thead { + position: sticky; + top: 0; + background: white; } .cursor-pointer {