Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const columns: TableColumn<Payment>[] = [{

<template>
<UTable
sticky
virtualize
:data="data"
:columns="columns"
Expand Down
5 changes: 3 additions & 2 deletions docs/content/docs/2.components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,16 +666,17 @@ class: '!p-0'

### With virtualization :badge{label="4.1+" class="align-text-top"}

Use the `virtualize` prop to enable virtualization for large datasets as a boolean or an object with options like `{ estimateSize: 65, overscan: 12 }`. You can also pass other [TanStack Virtual options](https://tanstack.com/virtual/latest/docs/api/virtualizer#optional-options) to customize the virtualization behavior.
Use the `virtualize` prop to enable virtualization for large datasets as a boolean or an object with options like `{ estimateSize: 65, overscan: 12 }`. You can also pass other [TanStack Virtual options](https://tanstack.com/virtual/latest/docs/api/virtualizer#optional-options) to customize the virtualization behavior. The `sticky` prop works in combination with `virtualize` to keep the header or footer visible while scrolling through large datasets.

::warning
When virtualization is enabled, the divider between rows and sticky properties are not supported.
Row pinning is not supported when virtualization is enabled.
::

::component-example
---
prettier: true
collapse: true
overflowHidden: true
name: 'table-virtualize-example'
class: '!p-0'
---
Expand Down
50 changes: 18 additions & 32 deletions src/runtime/components/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
meta?: TableMeta<T>
/**
* Enable virtualization for large datasets.
* Note: when enabled, the divider between rows, sticky and row pinning properties are not supported.
* Note: row pinning is not supported when virtualization is enabled.
* @see https://tanstack.com/virtual/latest/docs/api/virtualizer#options
* @defaultValue false
*/
Expand All @@ -122,7 +122,6 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
empty?: string
/**
* Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only.
* Note: this prop is not supported when `virtualize` is true.
* @defaultValue false
*/
sticky?: boolean | 'header' | 'footer'
Expand Down Expand Up @@ -278,11 +277,10 @@ function processColumns(columns: TableColumn<T>[]): TableColumn<T>[] {
}

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {}) })({
sticky: props.virtualize ? false : props.sticky,
sticky: props.sticky,
loading: props.loading,
loadingColor: props.loadingColor,
loadingAnimation: props.loadingAnimation,
virtualize: !!props.virtualize
loadingAnimation: props.loadingAnimation
}))

const [DefineTableTemplate, ReuseTableTemplate] = createReusableTemplate()
Expand Down Expand Up @@ -437,18 +435,13 @@ const virtualizer = !!props.virtualize && useVirtualizer({
}
})

const renderedSize = computed(() => {
if (!virtualizer) {
return 0
}
const virtualItems = computed(() => virtualizer ? virtualizer.value.getVirtualItems() : [])

const virtualItems = virtualizer.value.getVirtualItems()
if (!virtualItems?.length) {
return 0
}
const virtualPaddingTop = computed(() => virtualItems.value[0]?.start ?? 0)

// Sum up the actual sizes of virtual items
return virtualItems.reduce((sum: number, item: any) => sum + item.size, 0)
const virtualPaddingBottom = computed(() => {
if (!virtualizer || !virtualItems.value.length) return 0
return virtualizer.value.getTotalSize() - (virtualItems.value[virtualItems.value.length - 1]?.end ?? 0)
})

function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
Expand Down Expand Up @@ -625,15 +618,19 @@ defineExpose({
<ReuseRowTemplate v-for="row in topRows" :key="row.id" :row="row" />

<template v-if="virtualizer">
<template v-for="(virtualRow, index) in virtualizer.getVirtualItems()" :key="centerRows[virtualRow.index]?.id">
<tr v-if="virtualPaddingTop > 0" :style="{ height: `${virtualPaddingTop}px` }" aria-hidden="true">
<td :colspan="tableApi.getAllLeafColumns().length" />
</tr>
<template v-for="virtualRow in virtualItems" :key="centerRows[virtualRow.index]?.id ?? `virtual-${virtualRow.index}`">
<ReuseRowTemplate
v-if="centerRows[virtualRow.index]"
:row="centerRows[virtualRow.index]!"
:style="{
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`
}"
:style="{ height: `${virtualRow.size}px` }"
/>
</template>
<tr v-if="virtualPaddingBottom > 0" :style="{ height: `${virtualPaddingBottom}px` }" aria-hidden="true">
<td :colspan="tableApi.getAllLeafColumns().length" />
</tr>
</template>

<template v-else>
Expand Down Expand Up @@ -664,9 +661,6 @@ defineExpose({
v-if="hasFooter"
data-slot="tfoot"
:class="ui.tfoot({ class: [uiProp?.tfoot] })"
:style="virtualizer ? {
transform: `translateY(${virtualizer.getTotalSize() - renderedSize}px)`
} : undefined"
>
<tr data-slot="separator" :class="ui.separator({ class: [uiProp?.separator] })" />

Expand Down Expand Up @@ -700,14 +694,6 @@ defineExpose({
</DefineTableTemplate>

<Primitive ref="rootRef" :as="as" v-bind="$attrs" data-slot="root" :class="ui.root({ class: [uiProp?.root, props.class] })">
<div
v-if="virtualizer"
:style="{
height: `${virtualizer.getTotalSize()}px`
}"
>
<ReuseTableTemplate />
</div>
<ReuseTableTemplate v-else />
<ReuseTableTemplate />
</Primitive>
</template>
10 changes: 2 additions & 8 deletions src/theme/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { ModuleOptions } from '../module'
export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'relative overflow-auto',
base: 'min-w-full',
base: 'min-w-full overflow-clip',
caption: 'sr-only',
thead: 'relative',
tbody: 'isolate [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
tbody: 'isolate [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary divide-y divide-default',
tfoot: 'relative',
tr: 'data-[selected=true]:bg-elevated/50',
th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
Expand All @@ -16,12 +16,6 @@ export default (options: Required<ModuleOptions>) => ({
loading: 'py-6 text-center'
},
variants: {
virtualize: {
false: {
base: 'overflow-clip',
tbody: 'divide-y divide-default'
}
},
pinned: {
true: {
th: 'sticky bg-default/75 z-1',
Expand Down
1 change: 1 addition & 0 deletions test/components/Table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ describe('Table', () => {
['with meta prop', { props: { ...props, meta: { class: { tr: 'custom-row-class' }, style: { tr: { backgroundColor: 'lightgray' } } } } }],
['with meta field on columns', { props: { ...props, columns: columns.map(c => ({ ...c, meta: { class: { th: 'custom-heading-class', td: 'custom-cell-class' }, style: { th: { backgroundColor: 'black' }, td: { backgroundColor: 'lightgray' } } } })) } }],
['with virtualize', { props: { ...props, virtualize: true } }],
['with virtualize and sticky', { props: { ...props, columns, virtualize: true, sticky: true } }],
['with row pinning', { props: { ...props, rowPinning: { top: ['2'], bottom: ['3'] } } }],
['with row pinning and virtualization', { props: { ...props, virtualize: true, rowPinning: { top: ['2'], bottom: ['3'] } } }],
['with as', { props: { ...props, as: 'section' } }],
Expand Down
128 changes: 96 additions & 32 deletions test/components/__snapshots__/Table-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1780,22 +1780,23 @@ exports[`Table > renders with meta prop correctly 1`] = `

exports[`Table > renders with row pinning and virtualization correctly 1`] = `
"<div data-slot="root" class="relative overflow-auto">
<div style="height: 325px;">
<table data-slot="base" class="min-w-full">
<!--v-if-->
<thead data-slot="thead" class="relative">
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Id</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Amount</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Status</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Email</th>
</tr>
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
</thead>
<tbody data-slot="tbody" class="isolate [&amp;>tr]:data-[selectable=true]:hover:bg-elevated/50 [&amp;>tr]:data-[selectable=true]:focus-visible:outline-primary"></tbody>
<!--v-if-->
</table>
</div>
<table data-slot="base" class="min-w-full overflow-clip">
<!--v-if-->
<thead data-slot="thead" class="relative">
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Id</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Amount</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Status</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Email</th>
</tr>
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
</thead>
<tbody data-slot="tbody" class="isolate [&amp;>tr]:data-[selectable=true]:hover:bg-elevated/50 [&amp;>tr]:data-[selectable=true]:focus-visible:outline-primary divide-y divide-default">
<!--v-if-->
<!--v-if-->
</tbody>
<!--v-if-->
</table>
</div>"
`;

Expand Down Expand Up @@ -1964,24 +1965,87 @@ exports[`Table > renders with ui correctly 1`] = `
</div>"
`;

exports[`Table > renders with virtualize and sticky correctly 1`] = `
"<div data-slot="root" class="relative overflow-auto">
<table data-slot="base" class="min-w-full overflow-clip">
<!--v-if-->
<thead data-slot="thead" class="sticky top-0 inset-x-0 bg-default/75 backdrop-blur z-1">
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<div data-slot="root" class="relative flex items-start flex-row">
<div data-slot="container" class="flex items-center h-5"><button data-slot="base" class="rounded-sm ring ring-inset ring-accented overflow-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary size-4" id="v-0" role="checkbox" type="button" aria-checked="false" aria-required="false" data-state="unchecked">
<!---->
<!--v-if-->
</button></div>
<div data-slot="wrapper" class="w-full ms-2 text-sm"><label for="v-0" data-slot="label" class="block font-medium text-default">Select all</label>
<!--v-if-->
</div>
</div>
</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">#</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Date</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Status</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0"><button type="button" data-slot="base" class="rounded-md font-medium inline-flex items-center disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1.5 text-sm gap-1.5 text-default hover:bg-elevated active:bg-elevated focus:outline-none focus-visible:bg-elevated hover:disabled:bg-transparent dark:hover:disabled:bg-transparent hover:aria-disabled:bg-transparent dark:hover:aria-disabled:bg-transparent -mx-2.5"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="leadingIcon" class="shrink-0 size-5"></svg><span data-slot="label" class="truncate">Email</span>
<!--v-if-->
</button></th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0 text-right">Amount</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
</tr>
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
</thead>
<tbody data-slot="tbody" class="isolate [&amp;>tr]:data-[selectable=true]:hover:bg-elevated/50 [&amp;>tr]:data-[selectable=true]:focus-visible:outline-primary divide-y divide-default">
<!--v-if-->
<!--v-if-->
</tbody>
<tfoot data-slot="tfoot" class="sticky bottom-0 inset-x-0 bg-default/75 backdrop-blur z-1">
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0 text-right">Total: €2,990.00</th>
<th data-pinned="false" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">
<!---->
</th>
</tr>
</tfoot>
</table>
</div>"
`;

exports[`Table > renders with virtualize correctly 1`] = `
"<div data-slot="root" class="relative overflow-auto">
<div style="height: 325px;">
<table data-slot="base" class="min-w-full">
<!--v-if-->
<thead data-slot="thead" class="relative">
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Id</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Amount</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Status</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Email</th>
</tr>
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
</thead>
<tbody data-slot="tbody" class="isolate [&amp;>tr]:data-[selectable=true]:hover:bg-elevated/50 [&amp;>tr]:data-[selectable=true]:focus-visible:outline-primary"></tbody>
<!--v-if-->
</table>
</div>
<table data-slot="base" class="min-w-full overflow-clip">
<!--v-if-->
<thead data-slot="thead" class="relative">
<tr data-slot="tr" class="data-[selected=true]:bg-elevated/50">
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Id</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Amount</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Status</th>
<th data-pinned="false" scope="col" data-slot="th" class="px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&amp;:has([role=checkbox])]:pe-0">Email</th>
</tr>
<tr data-slot="separator" class="absolute z-1 left-0 w-full h-px bg-(--ui-border-accented)"></tr>
</thead>
<tbody data-slot="tbody" class="isolate [&amp;>tr]:data-[selectable=true]:hover:bg-elevated/50 [&amp;>tr]:data-[selectable=true]:focus-visible:outline-primary divide-y divide-default">
<!--v-if-->
<!--v-if-->
</tbody>
<!--v-if-->
</table>
</div>"
`;

Expand Down
Loading
Loading