Skip to content

Commit 3194abd

Browse files
committed
fix(Table): re-measure virtualized rows when expanded state toggles
Tanstack's ResizeObserver is attached to the main <tr>, but expanding a row inserts a *sibling* <tr> that doesn't resize the main row — the RO never fires, so the virtualizer keeps its stale un-expanded cached height. Watch the TanStack expanded state and call virtualizer.measureElement on every rendered row after the DOM updates, which picks up the new combined height via the existing expansion-aware measureElement we ship. Also clarify the virtualize JSDoc: expansion height is added on top of whatever the user's measureElement returns, so user callbacks should measure the main row only (or return collapsed height if they already account for expansion) to avoid double-counting. Addresses CodeRabbit review feedback on nuxt#6379.
1 parent 66f8abd commit 3194abd

4 files changed

Lines changed: 61 additions & 11 deletions

File tree

dist/runtime/components/Table.d.vue.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
5252
meta?: TableMeta<T>;
5353
/**
5454
* Enable virtualization for large datasets.
55-
* Pass a `measureElement` function to opt into dynamic row heights — the virtualizer
56-
* will measure each rendered row and automatically include the expanded row's height
57-
* when `row.getIsExpanded()` is `true`.
55+
*
56+
* Pass a `measureElement` function to opt into dynamic row heights. The value returned by
57+
* your `measureElement` is used as the height of the main row; when `row.getIsExpanded()`
58+
* is `true`, the immediate next-sibling `<tr>`'s height is **added on top of** that value
59+
* automatically, so your callback should measure the main row only. If your custom
60+
* `measureElement` already includes the expanded region (e.g. by measuring a wrapper),
61+
* return only the collapsed-row height to avoid double-counting.
62+
*
5863
* Note: when enabled, the divider between rows, sticky and row pinning properties are not supported.
5964
* @see https://tanstack.com/virtual/latest/docs/api/virtualizer#options
6065
* @defaultValue false

dist/runtime/components/Table.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import theme from "#build/ui/table";
33
</script>
44

55
<script setup>
6-
import { computed, useTemplateRef, watch, toRef } from "vue";
6+
import { computed, nextTick, useTemplateRef, watch, toRef } from "vue";
77
import { Primitive, useForwardProps } from "reka-ui";
88
import { upperFirst } from "scule";
99
import { defu } from "defu";
@@ -259,6 +259,21 @@ function measureRowRef(el) {
259259
if (!virtualizer || !el) return;
260260
virtualizer.value.measureElement(el);
261261
}
262+
if (virtualizer) {
263+
watch(
264+
() => tableApi.getState().expanded,
265+
() => {
266+
nextTick(() => {
267+
const root = rootRef.value?.$el;
268+
if (!virtualizer) return;
269+
root?.querySelectorAll("[data-index]").forEach((row) => {
270+
virtualizer.value.measureElement(row);
271+
});
272+
});
273+
},
274+
{ deep: true }
275+
);
276+
}
262277
function valueUpdater(updaterOrValue, ref) {
263278
ref.value = typeof updaterOrValue === "function" ? updaterOrValue(ref.value) : updaterOrValue;
264279
}

dist/runtime/components/Table.vue.d.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
5252
meta?: TableMeta<T>;
5353
/**
5454
* Enable virtualization for large datasets.
55-
* Pass a `measureElement` function to opt into dynamic row heights — the virtualizer
56-
* will measure each rendered row and automatically include the expanded row's height
57-
* when `row.getIsExpanded()` is `true`.
55+
*
56+
* Pass a `measureElement` function to opt into dynamic row heights. The value returned by
57+
* your `measureElement` is used as the height of the main row; when `row.getIsExpanded()`
58+
* is `true`, the immediate next-sibling `<tr>`'s height is **added on top of** that value
59+
* automatically, so your callback should measure the main row only. If your custom
60+
* `measureElement` already includes the expanded region (e.g. by measuring a wrapper),
61+
* return only the collapsed-row height to avoid double-counting.
62+
*
5863
* Note: when enabled, the divider between rows, sticky and row pinning properties are not supported.
5964
* @see https://tanstack.com/virtual/latest/docs/api/virtualizer#options
6065
* @defaultValue false

src/runtime/components/Table.vue

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,14 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
9999
meta?: TableMeta<T>
100100
/**
101101
* Enable virtualization for large datasets.
102-
* Pass a `measureElement` function to opt into dynamic row heights — the virtualizer
103-
* will measure each rendered row and automatically include the expanded row's height
104-
* when `row.getIsExpanded()` is `true`.
102+
*
103+
* Pass a `measureElement` function to opt into dynamic row heights. The value returned by
104+
* your `measureElement` is used as the height of the main row; when `row.getIsExpanded()`
105+
* is `true`, the immediate next-sibling `<tr>`'s height is **added on top of** that value
106+
* automatically, so your callback should measure the main row only. If your custom
107+
* `measureElement` already includes the expanded region (e.g. by measuring a wrapper),
108+
* return only the collapsed-row height to avoid double-counting.
109+
*
105110
* Note: when enabled, the divider between rows, sticky and row pinning properties are not supported.
106111
* @see https://tanstack.com/virtual/latest/docs/api/virtualizer#options
107112
* @defaultValue false
@@ -229,7 +234,7 @@ export type TableSlots<T extends TableData = TableData> = {
229234
</script>
230235

231236
<script setup lang="ts" generic="T extends TableData">
232-
import { computed, useTemplateRef, watch, toRef } from 'vue'
237+
import { computed, nextTick, useTemplateRef, watch, toRef } from 'vue'
233238
import { Primitive, useForwardProps } from 'reka-ui'
234239
import { upperFirst } from 'scule'
235240
import { defu } from 'defu'
@@ -467,6 +472,26 @@ function measureRowRef(el: Element | ComponentPublicInstance | null) {
467472
virtualizer.value.measureElement(el as Element)
468473
}
469474
475+
// Re-measure rendered rows whenever the expanded state changes. Tanstack's
476+
// ResizeObserver is attached to the main `<tr>`, but expansion inserts a
477+
// *sibling* `<tr>` that doesn't resize the main row, so the RO never fires —
478+
// this manual re-measure keeps the virtualizer's cached heights in sync.
479+
if (virtualizer) {
480+
watch(
481+
() => tableApi.getState().expanded,
482+
() => {
483+
nextTick(() => {
484+
const root = rootRef.value?.$el as HTMLElement | undefined
485+
if (!virtualizer) return
486+
root?.querySelectorAll<HTMLElement>('[data-index]').forEach((row) => {
487+
virtualizer.value.measureElement(row)
488+
})
489+
})
490+
},
491+
{ deep: true }
492+
)
493+
}
494+
470495
function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
471496
ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
472497
}

0 commit comments

Comments
 (0)