diff --git a/playground/app/pages/components/table.vue b/playground/app/pages/components/table.vue index 46c3799b8b..8e5a7f8f63 100644 --- a/playground/app/pages/components/table.vue +++ b/playground/app/pages/components/table.vue @@ -211,6 +211,16 @@ const columns: TableColumn[] = [{ }, { accessorKey: 'amount', header: () => h('div', { class: 'text-right' }, 'Amount'), + footer: ({ column }) => { + const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow) => acc + Number.parseFloat(row.getValue('amount')), 0) + + const formatted = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'EUR' + }).format(total) + + return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`) + }, cell: ({ row }) => { const amount = Number.parseFloat(row.getValue('amount')) @@ -356,6 +366,7 @@ onMounted(() => { tr: 'divide-x divide-default' }" sticky + sticky-footer class="border border-accented rounded-sm" @select="onSelect" > diff --git a/src/runtime/components/Table.vue b/src/runtime/components/Table.vue index 627a2b90d0..17be0649f4 100644 --- a/src/runtime/components/Table.vue +++ b/src/runtime/components/Table.vue @@ -87,6 +87,11 @@ export interface TableProps extends TableOptions { * @defaultValue false */ sticky?: boolean + /** + * Whether the table should have a sticky footer. + * @defaultValue false + */ + stickyFooter?: boolean /** Whether the table should be in loading state. */ loading?: boolean /** @@ -207,11 +212,22 @@ const meta = computed(() => props.meta ?? {}) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {}) })({ sticky: props.sticky, + stickyFooter: props.stickyFooter, loading: props.loading, loadingColor: props.loadingColor, loadingAnimation: props.loadingAnimation })) +const hasFooter = computed(() => { + const queue: TableColumn[] = [...columns.value] + while (queue.length) { + const column = queue.shift()! + if ('footer' in column) return true + if ('columns' in column) queue.push(...column.columns!) + } + return false +}) + const globalFilterState = defineModel('globalFilter', { default: undefined }) const columnFiltersState = defineModel('columnFilters', { default: [] }) const columnOrderState = defineModel('columnOrder', { default: [] }) @@ -229,7 +245,7 @@ const paginationState = defineModel('pagination', { default: {} const tableRef = ref() const tableApi = useVueTable({ - ...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'), + ...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'stickyFooter', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'), data, columns: columns.value, meta: meta.value, @@ -424,6 +440,28 @@ defineExpose({ + + + + + + + + + + diff --git a/src/theme/table.ts b/src/theme/table.ts index 873f4092e8..de4e6c16d8 100644 --- a/src/theme/table.ts +++ b/src/theme/table.ts @@ -7,6 +7,7 @@ export default (options: Required) => ({ caption: 'sr-only', thead: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:bottom-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented)', tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary', + tfoot: 'relative [&>tr]:after:absolute [&>tr]:after:inset-x-0 [&>tr]:after:top-0 [&>tr]:after:h-px [&>tr]:after:bg-(--ui-border-accented) [&>tr>th]:empty:p-0 [&>tr>th]:empty:border-none', 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', td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0', @@ -25,6 +26,11 @@ export default (options: Required) => ({ thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur' } }, + stickyFooter: { + true: { + tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur' + } + }, loading: { true: { thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px' diff --git a/test/components/__snapshots__/Table-vue.spec.ts.snap b/test/components/__snapshots__/Table-vue.spec.ts.snap index d4438e9fe7..97145bf212 100644 --- a/test/components/__snapshots__/Table-vue.spec.ts.snap +++ b/test/components/__snapshots__/Table-vue.spec.ts.snap @@ -49,6 +49,7 @@ exports[`Table > renders with as correctly 1`] = ` + " `; @@ -102,6 +103,7 @@ exports[`Table > renders with caption correctly 1`] = ` + " `; @@ -155,6 +157,7 @@ exports[`Table > renders with caption slot correctly 1`] = ` + " `; @@ -208,6 +211,7 @@ exports[`Table > renders with cell slot correctly 1`] = ` + " `; @@ -261,6 +265,7 @@ exports[`Table > renders with class correctly 1`] = ` + " `; @@ -451,6 +456,7 @@ exports[`Table > renders with columns correctly 1`] = ` + " `; @@ -504,6 +510,7 @@ exports[`Table > renders with data correctly 1`] = ` + " `; @@ -520,6 +527,7 @@ exports[`Table > renders with empty correctly 1`] = ` There is no data + " `; @@ -558,6 +566,7 @@ exports[`Table > renders with empty slot correctly 1`] = ` Empty slot + " `; @@ -611,6 +620,7 @@ exports[`Table > renders with expanded slot correctly 1`] = ` + " `; @@ -664,6 +674,7 @@ exports[`Table > renders with header slot correctly 1`] = ` + " `; @@ -717,6 +728,7 @@ exports[`Table > renders with loading animation carousel correctly 1`] = ` + " `; @@ -770,6 +782,7 @@ exports[`Table > renders with loading animation carousel-inverse correctly 1`] = + " `; @@ -823,6 +836,7 @@ exports[`Table > renders with loading animation elastic correctly 1`] = ` + " `; @@ -876,6 +890,7 @@ exports[`Table > renders with loading animation swing correctly 1`] = ` + " `; @@ -929,6 +944,7 @@ exports[`Table > renders with loading color error correctly 1`] = ` + " `; @@ -982,6 +998,7 @@ exports[`Table > renders with loading color info correctly 1`] = ` + " `; @@ -1035,6 +1052,7 @@ exports[`Table > renders with loading color neutral correctly 1`] = ` + " `; @@ -1088,6 +1106,7 @@ exports[`Table > renders with loading color primary correctly 1`] = ` + " `; @@ -1141,6 +1160,7 @@ exports[`Table > renders with loading color secondary correctly 1`] = ` + " `; @@ -1194,6 +1214,7 @@ exports[`Table > renders with loading color success correctly 1`] = ` + " `; @@ -1247,6 +1268,7 @@ exports[`Table > renders with loading color warning correctly 1`] = ` + " `; @@ -1300,6 +1322,7 @@ exports[`Table > renders with loading correctly 1`] = ` + " `; @@ -1338,6 +1361,7 @@ exports[`Table > renders with loading slot correctly 1`] = ` Loading slot + " `; @@ -1391,6 +1415,7 @@ exports[`Table > renders with sticky correctly 1`] = ` + " `; @@ -1444,6 +1469,7 @@ exports[`Table > renders with ui correctly 1`] = ` + " `; @@ -1460,6 +1486,7 @@ exports[`Table > renders without data correctly 1`] = ` No data + " `; diff --git a/test/components/__snapshots__/Table.spec.ts.snap b/test/components/__snapshots__/Table.spec.ts.snap index cb973297ec..6d03c5300a 100644 --- a/test/components/__snapshots__/Table.spec.ts.snap +++ b/test/components/__snapshots__/Table.spec.ts.snap @@ -49,6 +49,7 @@ exports[`Table > renders with as correctly 1`] = ` + " `; @@ -102,6 +103,7 @@ exports[`Table > renders with caption correctly 1`] = ` + " `; @@ -155,6 +157,7 @@ exports[`Table > renders with caption slot correctly 1`] = ` + " `; @@ -208,6 +211,7 @@ exports[`Table > renders with cell slot correctly 1`] = ` + " `; @@ -261,6 +265,7 @@ exports[`Table > renders with class correctly 1`] = ` + " `; @@ -451,6 +456,7 @@ exports[`Table > renders with columns correctly 1`] = ` + " `; @@ -504,6 +510,7 @@ exports[`Table > renders with data correctly 1`] = ` + " `; @@ -520,6 +527,7 @@ exports[`Table > renders with empty correctly 1`] = ` There is no data + " `; @@ -558,6 +566,7 @@ exports[`Table > renders with empty slot correctly 1`] = ` Empty slot + " `; @@ -611,6 +620,7 @@ exports[`Table > renders with expanded slot correctly 1`] = ` + " `; @@ -664,6 +674,7 @@ exports[`Table > renders with header slot correctly 1`] = ` + " `; @@ -717,6 +728,7 @@ exports[`Table > renders with loading animation carousel correctly 1`] = ` + " `; @@ -770,6 +782,7 @@ exports[`Table > renders with loading animation carousel-inverse correctly 1`] = + " `; @@ -823,6 +836,7 @@ exports[`Table > renders with loading animation elastic correctly 1`] = ` + " `; @@ -876,6 +890,7 @@ exports[`Table > renders with loading animation swing correctly 1`] = ` + " `; @@ -929,6 +944,7 @@ exports[`Table > renders with loading color error correctly 1`] = ` + " `; @@ -982,6 +998,7 @@ exports[`Table > renders with loading color info correctly 1`] = ` + " `; @@ -1035,6 +1052,7 @@ exports[`Table > renders with loading color neutral correctly 1`] = ` + " `; @@ -1088,6 +1106,7 @@ exports[`Table > renders with loading color primary correctly 1`] = ` + " `; @@ -1141,6 +1160,7 @@ exports[`Table > renders with loading color secondary correctly 1`] = ` + " `; @@ -1194,6 +1214,7 @@ exports[`Table > renders with loading color success correctly 1`] = ` + " `; @@ -1247,6 +1268,7 @@ exports[`Table > renders with loading color warning correctly 1`] = ` + " `; @@ -1300,6 +1322,7 @@ exports[`Table > renders with loading correctly 1`] = ` + " `; @@ -1338,6 +1361,7 @@ exports[`Table > renders with loading slot correctly 1`] = ` Loading slot + " `; @@ -1391,6 +1415,7 @@ exports[`Table > renders with sticky correctly 1`] = ` + " `; @@ -1444,6 +1469,7 @@ exports[`Table > renders with ui correctly 1`] = ` + " `; @@ -1460,6 +1486,7 @@ exports[`Table > renders without data correctly 1`] = ` No data + " `;