Skip to content

Commit 415c267

Browse files
authored
feat(VDataTable): support fixed: 'end' columns (#21665)
resolves #20020 resolves #21153
1 parent cf0a5c2 commit 415c267

File tree

8 files changed

+108
-35
lines changed

8 files changed

+108
-35
lines changed

packages/api-generator/src/locale/en/VDataTableHeaders.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"sortAscIcon": "Icon used for ascending sort button.",
55
"sortDescIcon": "Icon used for descending sort button.",
66
"sticky": "Deprecated, use `fixed-header` instead.",
7-
"fixedHeader": "Sticks the header to the top of the table. From the left",
8-
"lastFixed": "Sticks the header to the top of the table. From the right.",
7+
"fixedHeader": "Sticks the header to the top of the table.",
8+
"lastFixed": "**FOR INTERNAL USE ONLY** Applies right border to the last column fixed to the left.",
9+
"firstFixedEnd": "**FOR INTERNAL USE ONLY** Applies left border to the first column fixed to the right.",
910
"multiSort": "Sort on multiple columns at the same time.",
1011
"headerProps": "Additional props to be be passed to the default header"
1112
},

packages/docs/src/data/new-in.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@
6262
},
6363
"VDataTable": {
6464
"props": {
65-
"headerProps": "3.5.0",
66-
"lastFixed": "3.9.0"
65+
"headerProps": "3.5.0"
6766
}
6867
},
6968
"VExpansionPanels": {

packages/vuetify/src/components/VDataTable/VDataTable.sass

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,27 @@
7777
height: fit-content
7878

7979
.v-data-table-column--fixed,
80+
.v-data-table-column--fixed-end,
8081
.v-data-table__th--sticky
8182
background: $table-background
8283
position: sticky !important
8384
left: 0
8485
z-index: 1
8586

87+
.v-data-table-column--fixed-end
88+
left: unset
89+
right: 0
90+
8691
.v-data-table-column--last-fixed
8792
border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity))
8893

89-
.v-data-table.v-table--fixed-header > .v-table__wrapper > table > thead > tr > th.v-data-table-column--fixed
90-
z-index: 2
94+
.v-data-table-column--first-fixed-end
95+
border-left: 1px solid rgba(var(--v-border-color), var(--v-border-opacity))
96+
97+
.v-data-table.v-table--fixed-header > .v-table__wrapper > table > thead > tr
98+
> th.v-data-table-column--fixed,
99+
> th.v-data-table-column--fixed-end
100+
z-index: 2
91101

92102
.v-data-table-group-header-row
93103
td

packages/vuetify/src/components/VDataTable/VDataTableColumn.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,37 @@ export const VDataTableColumn = defineFunctionalComponent({
99
type: String as PropType<'start' | 'center' | 'end'>,
1010
default: 'start',
1111
},
12-
fixed: Boolean,
12+
fixed: {
13+
type: [Boolean, String] as PropType<boolean | 'start' | 'end'>,
14+
default: false,
15+
},
1316
fixedOffset: [Number, String],
17+
fixedEndOffset: [Number, String],
1418
height: [Number, String],
1519
lastFixed: Boolean,
20+
firstFixedEnd: Boolean,
21+
1622
noPadding: Boolean,
1723
tag: String,
1824
width: [Number, String],
1925
maxWidth: [Number, String],
2026
nowrap: Boolean,
2127
}, (props, { slots }) => {
2228
const Tag = props.tag ?? 'td'
29+
30+
const fixedSide = typeof props.fixed === 'string' ? props.fixed
31+
: props.fixed ? 'start'
32+
: 'none'
33+
2334
return (
2435
<Tag
2536
class={[
2637
'v-data-table__td',
2738
{
28-
'v-data-table-column--fixed': props.fixed,
39+
'v-data-table-column--fixed': fixedSide === 'start',
40+
'v-data-table-column--fixed-end': fixedSide === 'end',
2941
'v-data-table-column--last-fixed': props.lastFixed,
42+
'v-data-table-column--first-fixed-end': props.firstFixedEnd,
3043
'v-data-table-column--no-padding': props.noPadding,
3144
'v-data-table-column--nowrap': props.nowrap,
3245
},
@@ -36,7 +49,8 @@ export const VDataTableColumn = defineFunctionalComponent({
3649
height: convertToUnit(props.height),
3750
width: convertToUnit(props.width),
3851
maxWidth: convertToUnit(props.maxWidth),
39-
left: convertToUnit(props.fixedOffset || null),
52+
left: fixedSide === 'start' ? convertToUnit(props.fixedOffset || null) : undefined,
53+
right: fixedSide === 'end' ? convertToUnit(props.fixedEndOffset || null) : undefined,
4054
}}
4155
>
4256
{ slots.default?.() }

packages/vuetify/src/components/VDataTable/VDataTableHeaders.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export const makeVDataTableHeadersProps = propsFactory({
6161
color: String,
6262
disableSort: Boolean,
6363
fixedHeader: Boolean,
64-
lastFixed: Boolean,
6564
multiSort: Boolean,
6665
sortAscIcon: {
6766
type: IconValue,
@@ -95,12 +94,16 @@ export const VDataTableHeaders = genericComponent<VDataTableHeadersSlots>()({
9594
const { loaderClasses } = useLoader(props)
9695

9796
function getFixedStyles (column: InternalDataTableHeader, y: number): CSSProperties | undefined {
98-
if (!(props.sticky || props.fixedHeader) && !(column.fixed || column.lastFixed)) return undefined
97+
if (!(props.sticky || props.fixedHeader) && !column.fixed) return undefined
98+
99+
const fixedSide = typeof column.fixed === 'string' ? column.fixed
100+
: column.fixed ? 'start'
101+
: 'none'
99102

100103
return {
101104
position: 'sticky',
102-
left: column.fixed || column.lastFixed ? convertToUnit(column.fixedOffset) : undefined,
103-
right: column.lastFixed ? convertToUnit(column.fixedOffset ?? 0) : undefined,
105+
left: fixedSide === 'start' ? convertToUnit(column.fixedOffset) : undefined,
106+
right: fixedSide === 'end' ? convertToUnit(column.fixedEndOffset) : undefined,
104107
top: (props.sticky || props.fixedHeader) ? `calc(var(--v-table-header-height) * ${y})` : undefined,
105108
}
106109
}
@@ -169,6 +172,7 @@ export const VDataTableHeaders = genericComponent<VDataTableHeadersSlots>()({
169172
fixed={ column.fixed }
170173
nowrap={ column.nowrap }
171174
lastFixed={ column.lastFixed }
175+
firstFixedEnd={ column.firstFixedEnd }
172176
noPadding={ noPadding }
173177
tabindex={ column.sortable ? 0 : undefined }
174178
onClick={ column.sortable ? () => toggleSort(column) : undefined }

packages/vuetify/src/components/VDataTable/VDataTableRow.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ export const VDataTableRow = genericComponent<new <T>(
130130
}}
131131
fixed={ column.fixed }
132132
fixedOffset={ column.fixedOffset }
133+
fixedEndOffset={ column.fixedEndOffset }
133134
lastFixed={ column.lastFixed }
135+
firstFixedEnd={ column.firstFixedEnd }
134136
maxWidth={ !mobile.value ? column.maxWidth : undefined }
135137
noPadding={ column.key === 'data-table-select' || column.key === 'data-table-expand' }
136138
nowrap={ column.nowrap }

packages/vuetify/src/components/VDataTable/composables/headers.ts

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,37 @@ function getDepth (item: InternalDataTableHeader, depth = 0): number {
100100

101101
function parseFixedColumns (items: InternalDataTableHeader[]) {
102102
let seenFixed = false
103-
function setFixed (item: InternalDataTableHeader, parentFixed = false) {
103+
104+
function setFixed (
105+
item: InternalDataTableHeader,
106+
side: 'start' | 'end',
107+
parentFixedSide: 'start' | 'end' | 'none' = 'none'
108+
) {
104109
if (!item) return
105110

106-
if (parentFixed) {
107-
item.fixed = true
111+
if (parentFixedSide !== 'none') {
112+
item.fixed = parentFixedSide
108113
}
109114

110-
if (item.fixed) {
111-
if (item.children) {
112-
for (let i = item.children.length - 1; i >= 0; i--) {
113-
setFixed(item.children[i], true)
115+
// normalize to simplify logic below
116+
if (item.fixed === true) {
117+
item.fixed = 'start'
118+
}
119+
120+
const orderedChildren = side === 'start'
121+
? item.children?.toReversed()
122+
: item.children
123+
124+
if (item.fixed === side) {
125+
if (orderedChildren) {
126+
for (const child of orderedChildren) {
127+
setFixed(child, side, side)
114128
}
115129
} else {
116-
if (!seenFixed) {
130+
if (!seenFixed && side === 'start') {
117131
item.lastFixed = true
132+
} else if (!seenFixed && side === 'end') {
133+
item.firstFixedEnd = true
118134
} else if (isNaN(Number(item.width))) {
119135
consoleError(`Multiple fixed columns should have a static width (key: ${item.key})`)
120136
} else {
@@ -123,40 +139,65 @@ function parseFixedColumns (items: InternalDataTableHeader[]) {
123139
seenFixed = true
124140
}
125141
} else {
126-
if (item.children) {
127-
for (let i = item.children.length - 1; i >= 0; i--) {
128-
setFixed(item.children[i])
142+
if (orderedChildren) {
143+
for (const child of orderedChildren) {
144+
setFixed(child, side)
129145
}
130146
} else {
131147
seenFixed = false
132148
}
133149
}
134150
}
135151

136-
for (let i = items.length - 1; i >= 0; i--) {
137-
setFixed(items[i])
152+
for (const item of items.toReversed()) {
153+
setFixed(item, 'start')
138154
}
139155

140-
function setFixedOffset (item: InternalDataTableHeader, fixedOffset = 0) {
141-
if (!item) return fixedOffset
156+
for (const item of items) {
157+
setFixed(item, 'end')
158+
}
159+
160+
function setFixedOffset (item: InternalDataTableHeader, offset = 0) {
161+
if (!item) return offset
142162

143163
if (item.children) {
144-
item.fixedOffset = fixedOffset
164+
item.fixedOffset = offset
145165
for (const child of item.children) {
146-
fixedOffset = setFixedOffset(child, fixedOffset)
166+
offset = setFixedOffset(child, offset)
147167
}
148-
} else if (item.fixed) {
149-
item.fixedOffset = fixedOffset
150-
fixedOffset += parseFloat(item.width || '0') || 0
168+
} else if (item.fixed && item.fixed !== 'end') {
169+
item.fixedOffset = offset
170+
offset += parseFloat(item.width || '0') || 0
151171
}
152172

153-
return fixedOffset
173+
return offset
154174
}
155175

156176
let fixedOffset = 0
157177
for (const item of items) {
158178
fixedOffset = setFixedOffset(item, fixedOffset)
159179
}
180+
181+
function setFixedEndOffset (item: InternalDataTableHeader, offset = 0) {
182+
if (!item) return offset
183+
184+
if (item.children) {
185+
item.fixedEndOffset = offset
186+
for (const child of item.children) {
187+
offset = setFixedEndOffset(child, offset)
188+
}
189+
} else if (item.fixed === 'end') {
190+
item.fixedEndOffset = offset
191+
offset += parseFloat(item.width || '0') || 0
192+
}
193+
194+
return offset
195+
}
196+
197+
let fixedEndOffset = 0
198+
for (const item of items.toReversed()) {
199+
fixedEndOffset = setFixedEndOffset(item, fixedEndOffset)
200+
}
160201
}
161202

162203
function parse (items: InternalDataTableHeader[], maxDepth: number) {

packages/vuetify/src/components/VDataTable/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type DataTableHeader<T = Record<string, any>> = {
1212
value?: SelectItemKey<T>
1313
title?: string
1414

15-
fixed?: boolean
15+
fixed?: boolean | 'start' | 'end'
1616
align?: 'start' | 'end' | 'center'
1717

1818
width?: number | string
@@ -36,7 +36,9 @@ export type InternalDataTableHeader = Omit<DataTableHeader, 'key' | 'value' | 'c
3636
value: SelectItemKey | null
3737
sortable: boolean
3838
fixedOffset?: number
39+
fixedEndOffset?: number
3940
lastFixed?: boolean
41+
firstFixedEnd?: boolean
4042
nowrap?: boolean
4143
colspan?: number
4244
rowspan?: number

0 commit comments

Comments
 (0)