Skip to content

feat(Table): column resizing #4110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 53 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
09a6914
feat(stepper): add iconClass prop to stepper items
J-Michalek May 4, 2025
9124f12
test(stepper): add case for iconClass prop
J-Michalek May 4, 2025
1b906b5
docs(stepper): add iconClass prop
J-Michalek May 4, 2025
9902987
test(stepper): remove obsolete snapshot entry
J-Michalek May 4, 2025
16c68c7
test(stepper): update snapshot for vue tests
J-Michalek May 4, 2025
7331409
Merge branch 'nuxt:v3' into feat/stepper-icon-class
J-Michalek May 5, 2025
1197806
feat(accordion): add ui field in items
J-Michalek May 6, 2025
7c7a6f8
Merge branch 'feat/stepper-icon-class' of https://github.com/J-Michal…
J-Michalek May 6, 2025
2145147
test(accordion): add new snapshot entry for vue tests
J-Michalek May 6, 2025
161b2ba
docs(accordion): add ui field in items
J-Michalek May 6, 2025
4c8f564
feat(tabs): add ui field on items
J-Michalek May 6, 2025
31153d3
docs(tabs): add ui field on items
J-Michalek May 6, 2025
cac3e20
test(tabs): add ui field on items
J-Michalek May 6, 2025
47ae811
feat(stepper): add ui field on items
May 7, 2025
b3feac5
test(stepper): add ui field on items
May 7, 2025
c1ab96e
docs(stepper): add ui field on items
May 7, 2025
11daf42
feat(breadcrumb): add ui field on items
May 7, 2025
66a79a0
test(breadcrumb): add ui field on items
May 7, 2025
473b0b7
docs(breadcrumb): add ui field on items
May 7, 2025
9a4043e
feat(checkbox-group): add ui field on items
May 7, 2025
0feed77
test(checkbox-group): add ui field on items
May 7, 2025
591be49
docs(checkbox-group): add ui field on items
May 7, 2025
b3c19fa
docs(breadcrumb): fix description for object type
May 7, 2025
9ac59b5
feat(radio-group): add ui field on items
May 7, 2025
69a36a4
test(radio-group): add ui field on items
May 7, 2025
3402866
docs(radio-group): add ui field on items
May 7, 2025
824cecd
feat(input-menu): add ui field on items
May 7, 2025
402f06c
test(input-menu): add ui field on items
May 7, 2025
96d521a
docs(input-menu): add ui field on items
May 7, 2025
4269961
feat(tree): add ui field on items
May 7, 2025
68d04b1
test(tree): add ui field on items
May 7, 2025
ec91d84
docs(tree): add ui field on items
May 7, 2025
873de68
feat(table): column resizing WIP
J-Michalek May 8, 2025
b528214
feat(table): set default for columnResizeMode
J-Michalek May 9, 2025
9352b2c
feat(table-theme): adjust size and posizitoning of resize handle
J-Michalek May 9, 2025
03ca035
chore(table): enable resizing in table playground
J-Michalek May 9, 2025
cc49cab
docs(table): example of column resizing WIP
J-Michalek May 9, 2025
c68dbba
Merge branch 'nuxt:v3' into feat/table-column-resize
J-Michalek May 9, 2025
4a2f5bb
refactor(table): enable column resizing through columnSizingOptions
J-Michalek May 9, 2025
ddd3643
fix(table): enable column resizing through columnSizingOptions
J-Michalek May 9, 2025
baf34b2
chore(table): remove debugging leftover
J-Michalek May 9, 2025
cafda53
refactor(table-theme): the after element made it impossible for the t…
J-Michalek May 9, 2025
04fd5e2
fix(table): always set header width when table is resizable
J-Michalek May 9, 2025
bcc8514
style(table): adjust resizeHandle styles
J-Michalek May 9, 2025
feb4d08
Merge branch 'feat/table-column-resize' of https://github.com/J-Micha…
May 9, 2025
b2e84d7
feat(table): implement fit and expand resize modes
May 10, 2025
813d674
test(table): update snapshots
J-Michalek May 11, 2025
e9fd8c0
refactor(table): declutter template from sizing styles
J-Michalek May 11, 2025
b5d915f
Merge branch 'v3' of https://github.com/J-Michalek/ui into feat/table…
May 14, 2025
6393da5
Merge branch 'feat/table-column-resize' of https://github.com/J-Micha…
May 14, 2025
f911fc4
Merge branch 'v3' of https://github.com/J-Michalek/ui into feat/table…
May 14, 2025
9675d01
chore: resolve badly merged changes
May 14, 2025
d0963f6
Merge branch 'feat/table-column-resize' of https://github.com/J-Micha…
May 14, 2025
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
@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

interface User {
id: number
name: string
position: string
email: string
role: string
}

const data = ref<User[]>([{
id: 1,
name: 'Lindsay Walton',
position: 'Front-end Developer',
email: '[email protected]',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
position: 'Designer',
email: '[email protected]',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
position: 'Director of Product',
email: '[email protected]',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
position: 'Copywriter',
email: '[email protected]',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
position: 'Senior Designer',
email: '[email protected]',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
position: 'Principal Designer',
email: '[email protected]',
role: 'Member'
}])

const columns: TableColumn<User>[] = [{
accessorKey: 'id',
header: 'ID',
minSize: 200,
size: 300,
maxSize: 500
}, {
accessorKey: 'name',
header: 'Name',
maxSize: 200
}, {
accessorKey: 'email',
header: 'Email',
minSize: 225
}, {
accessorKey: 'role',
header: 'Role'
}, {
id: 'action'
}]
</script>

<template>
<UTable :data="data" :columns="columns" :column-sizing-options="{ enableColumnResizing: true }" class="flex-1" sticky />
</template>
15 changes: 15 additions & 0 deletions docs/content/3.components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,21 @@ class: '!p-0'
---
::

### With resizable columns

You can make the columns resizable by setting the `enableColumnResizing` prop.

Use the `enableResizing` field on the column to allow resizing for specific columns.

::component-example
---
prettier: true
collapse: true
name: 'table-resizing-example'
class: '!p-0'
---
::

## API

### Props
Expand Down
18 changes: 15 additions & 3 deletions playground/app/pages/components/table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,13 @@ const columns: TableColumn<Payment>[] = [{
'aria-label': 'Select row'
}),
enableSorting: false,
enableHiding: false
enableHiding: false,
enableResizing: false
}, {
accessorKey: 'id',
header: '#',
cell: ({ row }) => `#${row.getValue('id')}`
cell: ({ row }) => `#${row.getValue('id')}`,
enableResizing: false
}, {
accessorKey: 'date',
header: 'Date',
Expand Down Expand Up @@ -192,7 +194,8 @@ const columns: TableColumn<Payment>[] = [{
})[row.getValue('status') as string]

return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
}
},
maxSize: 200
}, {
accessorKey: 'email',
header: ({ column }) => {
Expand Down Expand Up @@ -279,6 +282,8 @@ const pagination = ref({
pageSize: 10
})

const resizeMode = ref<'fit' | 'expand'>('fit')

function addElement() {
data.value.unshift({
id: currentID.value.toString(),
Expand Down Expand Up @@ -318,6 +323,8 @@ onMounted(() => {
<UButton color="neutral" label="Randomize" @click="randomize" />
<UButton color="neutral" label="Add element" @click="addElement" />

<USelect v-model="resizeMode" class="min-w-[100px]" :items="['fit', 'expand']" />

<UDropdownMenu
:items="table?.tableApi?.getAllColumns().filter(column => column.getCanHide()).map(column => ({
label: upperFirst(column.id),
Expand Down Expand Up @@ -355,6 +362,11 @@ onMounted(() => {
:ui="{
tr: 'divide-x divide-default'
}"
:column-sizing-options="{
enableColumnResizing: true,
columnResizeMode: 'onChange'
}"
:resize-mode="resizeMode"
sticky
class="border border-accented rounded-sm"
@select="onSelect"
Expand Down
54 changes: 52 additions & 2 deletions src/runtime/components/Table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ export interface TableProps<T extends TableData> extends TableOptions<T> {
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-faceting)
*/
facetedOptions?: FacetedOptions<T>
/**
* Whether the table should fit its container's width or expand it when resizing the columns.
*
* @defaultValue 'fit'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@J-Michalek This is a lie, current default is 'expand'.

*/
resizeMode?: 'fit' | 'expand'
onSelect?: (row: TableRow<T>, e?: Event) => void
class?: any
ui?: Table['slots']
Expand Down Expand Up @@ -192,6 +198,7 @@ import { useLocale } from '../composables/useLocale'
import { tv } from '../utils/tv'

const props = withDefaults(defineProps<TableProps<T>>(), {
resizeMode: 'expand',
watchOptions: () => ({
deep: true
})
Expand All @@ -209,7 +216,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {})
sticky: props.sticky,
loading: props.loading,
loadingColor: props.loadingColor,
loadingAnimation: props.loadingAnimation
loadingAnimation: props.loadingAnimation,
resizable: props.columnSizingOptions?.enableColumnResizing
}))

const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
Expand Down Expand Up @@ -325,6 +333,34 @@ function handleRowSelect(row: TableRow<T>, e: Event) {
props.onSelect(row, e)
}

const tableStyle = computed(() => {
return props.columnSizingOptions?.enableColumnResizing && props.resizeMode === 'expand'
? {
width: `${tableApi.getTotalSize()}px`
}
: undefined
})

function getHeaderStyle(header: Header<T, unknown>) {
return props.columnSizingOptions?.enableColumnResizing ? { width: `${header.getSize()}px` } : undefined
}

function getResizeHandleStyle(header: Header<T, unknown>) {
return {
transform: props.columnSizingOptions?.columnResizeMode === 'onEnd'
&& header.column.getIsResizing()
? `translateX(${
(props.columnSizingOptions.columnResizeDirection
=== 'rtl'
? -1
: 1)
* (tableApi.getState().columnSizingInfo
.deltaOffset ?? 0)
}px)`
: undefined
}
}

watch(
() => props.data, () => {
data.value = props.data ? [...props.data] : []
Expand All @@ -339,7 +375,11 @@ defineExpose({

<template>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
<table ref="tableRef" :class="ui.base({ class: [props.ui?.base] })">
<table
ref="tableRef"
:class="ui.base({ class: [props.ui?.base] })"
:style="tableStyle"
>
<caption v-if="caption || !!slots.caption" :class="ui.caption({ class: [props.ui?.caption] })">
<slot name="caption">
{{ caption }}
Expand All @@ -360,10 +400,20 @@ defineExpose({
],
pinned: !!header.column.getIsPinned()
})"
:style="getHeaderStyle(header)"
>
<slot :name="`${header.id}-header`" v-bind="header.getContext()">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</slot>
<div
v-if="columnSizingOptions?.enableColumnResizing && header.column.getCanResize()"
:class="ui.resizeHandle({ class: props.ui?.resizeHandle })"
:aria-controls="header.id"
:style="getResizeHandleStyle(header)"
@doubleclick="header.column.resetSize"
@mousedown="header.getResizeHandler()($event)"
@touchstart="header.getResizeHandler()($event)"
/>
</th>
</tr>
</thead>
Expand Down
12 changes: 9 additions & 3 deletions src/theme/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ export default (options: Required<ModuleOptions>) => ({
root: 'relative overflow-auto',
base: 'min-w-full overflow-clip',
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)',
thead: 'relative before:absolute before:inset-x-0 before:bottom-0 before:h-px before:bg-(--ui-border-accented) before:z-1',
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
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',
empty: 'py-6 text-center text-sm text-muted',
loading: 'py-6 text-center'
loading: 'py-6 text-center',
resizeHandle: 'select-none cursor-ew-resize absolute top-0 bottom-0 bg-primary w-1 right-0 '
},
variants: {
pinned: {
Expand All @@ -27,7 +28,7 @@ export default (options: Required<ModuleOptions>) => ({
},
loading: {
true: {
thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px'
thead: 'after:absolute after:bottom-0 after:inset-x-0 after:h-px after:z-1'
}
},
loadingAnimation: {
Expand All @@ -39,6 +40,11 @@ export default (options: Required<ModuleOptions>) => ({
loadingColor: {
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, ''])),
neutral: ''
},
resizable: {
true: {
th: 'relative'
}
}
},
compoundVariants: [...(options.theme.colors || []).map((loadingColor: string) => ({
Expand Down
Loading