Skip to content

Commit 79d6444

Browse files
authored
feat: add option to freeze columns to end (#3566)
1 parent e64e89e commit 79d6444

29 files changed

+634
-42
lines changed

packages/grid/src/vaadin-grid-column-group.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class GridColumnGroup extends ColumnBaseMixin(PolymerElement) {
8888
'_updateVisibleChildColumns(_childColumns)',
8989
'_childColumnsChanged(_childColumns)',
9090
'_groupFrozenChanged(frozen, _rootColumns)',
91+
'_groupFrozenToEndChanged(frozenToEnd, _rootColumns)',
9192
'_groupHiddenChanged(hidden, _rootColumns)',
9293
'_visibleChildColumnsChanged(_visibleChildColumns)',
9394
'_colSpanChanged(_colSpan, _headerCell, _footerCell)',
@@ -135,6 +136,16 @@ class GridColumnGroup extends ColumnBaseMixin(PolymerElement) {
135136
// Don’t unfreeze the frozen group because of a non-frozen child
136137
this._lastFrozen = this._lastFrozen || value;
137138
}
139+
140+
if (path === 'frozenToEnd') {
141+
// Don’t unfreeze the frozen group because of a non-frozen child
142+
this.frozenToEnd = this.frozenToEnd || value;
143+
}
144+
145+
if (path === 'firstFrozenToEnd') {
146+
// Don’t unfreeze the frozen group because of a non-frozen child
147+
this._firstFrozenToEnd = this._firstFrozenToEnd || value;
148+
}
138149
}
139150

140151
/** @private */
@@ -239,6 +250,18 @@ class GridColumnGroup extends ColumnBaseMixin(PolymerElement) {
239250
}
240251
}
241252

253+
/** @private */
254+
_groupFrozenToEndChanged(frozenToEnd, rootColumns) {
255+
if (rootColumns === undefined || frozenToEnd === undefined) {
256+
return;
257+
}
258+
259+
// Don’t propagate the default `false` value.
260+
if (frozenToEnd !== false) {
261+
Array.from(rootColumns).forEach((col) => (col.frozenToEnd = frozenToEnd));
262+
}
263+
}
264+
242265
/** @private */
243266
_groupHiddenChanged(hidden, rootColumns) {
244267
if (rootColumns && !this._preventHiddenCascade) {

packages/grid/src/vaadin-grid-column-reordering-mixin.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,10 @@ export const ColumnReorderingMixin = (superClass) =>
317317
if (column1 && column2) {
318318
const differentColumns = column1 !== column2;
319319
const sameParent = column1.parentElement === column2.parentElement;
320-
const sameFrozen = column1.frozen === column2.frozen;
320+
const sameFrozen =
321+
(column1.frozen && column2.frozen) || // both columns are frozen
322+
(column1.frozenToEnd && column2.frozenToEnd) || // both columns are frozen to end
323+
(!column1.frozen && !column1.frozenToEnd && !column2.frozen && !column2.frozenToEnd);
321324
return differentColumns && sameParent && sameFrozen;
322325
}
323326
}
@@ -351,7 +354,7 @@ export const ColumnReorderingMixin = (superClass) =>
351354
const _order = column1._order;
352355
column1._order = column2._order;
353356
column2._order = _order;
354-
this._updateLastFrozen();
357+
this._updateFrozenColumn();
355358
this._updateFirstAndLastColumn();
356359
}
357360

packages/grid/src/vaadin-grid-column-resizing-mixin.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const ColumnResizingMixin = (superClass) =>
5353
.pop();
5454
}
5555

56+
const eventX = e.detail.x;
5657
const columnRowCells = Array.from(this.$.header.querySelectorAll('[part~="row"]:last-child [part~="cell"]'));
5758
const targetCell = columnRowCells.filter((cell) => cell._column === column)[0];
5859
// Resize the target column
@@ -66,11 +67,19 @@ export const ColumnResizingMixin = (superClass) =>
6667
parseInt(style.borderRightWidth) +
6768
parseInt(style.marginLeft) +
6869
parseInt(style.marginRight);
69-
const maxWidth =
70-
targetCell.offsetWidth +
71-
(this.__isRTL
72-
? targetCell.getBoundingClientRect().left - e.detail.x
73-
: e.detail.x - targetCell.getBoundingClientRect().right);
70+
71+
let maxWidth;
72+
73+
const cellWidth = targetCell.offsetWidth;
74+
const cellRect = targetCell.getBoundingClientRect();
75+
76+
// For cells frozen to end, resize handle is flipped horizontally.
77+
if (targetCell.hasAttribute('frozen-to-end')) {
78+
maxWidth = cellWidth + (this.__isRTL ? eventX - cellRect.right : cellRect.left - eventX);
79+
} else {
80+
maxWidth = cellWidth + (this.__isRTL ? cellRect.left - eventX : eventX - cellRect.right);
81+
}
82+
7483
column.width = Math.max(minWidth, maxWidth) + 'px';
7584
column.flexGrow = 0;
7685
}
@@ -86,6 +95,18 @@ export const ColumnResizingMixin = (superClass) =>
8695
}
8796
});
8897

98+
const cellFrozenToEnd = this._frozenToEndCells[0];
99+
100+
// When handle moves below the cell frozen to end, scroll into view.
101+
if (cellFrozenToEnd && this.$.table.scrollWidth > this.$.table.offsetWidth) {
102+
const frozenRect = cellFrozenToEnd.getBoundingClientRect();
103+
const offset = eventX - (this.__isRTL ? frozenRect.right : frozenRect.left);
104+
105+
if ((this.__isRTL && offset <= 0) || (!this.__isRTL && offset >= 0)) {
106+
this.$.table.scrollLeft += offset;
107+
}
108+
}
109+
89110
if (e.detail.state === 'end') {
90111
this.$.scroller.toggleAttribute('column-resizing', false);
91112
this.dispatchEvent(

packages/grid/src/vaadin-grid-column.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ export declare class ColumnBaseMixinClass<TItem> {
3232
*/
3333
frozen: boolean;
3434

35+
/**
36+
* When true, the column is frozen to end of grid.
37+
*
38+
* When a column inside of a column group is frozen to end, all of the sibling columns
39+
* inside the group will get frozen to end also.
40+
*
41+
* Column can not be set as `frozen` and `frozenToEnd` at the same time.
42+
* @attr {boolean} frozen-to-end
43+
*/
44+
frozenToEnd: boolean;
45+
3546
/**
3647
* When set to true, the cells for this column are hidden.
3748
*/

packages/grid/src/vaadin-grid-column.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ export const ColumnBaseMixin = (superClass) =>
4545
value: false
4646
},
4747

48+
/**
49+
* When true, the column is frozen to end of grid.
50+
*
51+
* When a column inside of a column group is frozen to end, all of the sibling columns
52+
* inside the group will get frozen to end also.
53+
*
54+
* Column can not be set as `frozen` and `frozenToEnd` at the same time.
55+
* @attr {boolean} frozen-to-end
56+
* @type {boolean}
57+
*/
58+
frozenToEnd: {
59+
type: Boolean,
60+
value: false
61+
},
62+
4863
/**
4964
* When set to true, the cells for this column are hidden.
5065
*/
@@ -79,6 +94,15 @@ export const ColumnBaseMixin = (superClass) =>
7994
value: false
8095
},
8196

97+
/**
98+
* @type {boolean}
99+
* @protected
100+
*/
101+
_firstFrozenToEnd: {
102+
type: Boolean,
103+
value: false
104+
},
105+
82106
/** @protected */
83107
_order: Number,
84108

@@ -176,10 +200,12 @@ export const ColumnBaseMixin = (superClass) =>
176200
return [
177201
'_widthChanged(width, _headerCell, _footerCell, _cells.*)',
178202
'_frozenChanged(frozen, _headerCell, _footerCell, _cells.*)',
203+
'_frozenToEndChanged(frozenToEnd, _headerCell, _footerCell, _cells.*)',
179204
'_flexGrowChanged(flexGrow, _headerCell, _footerCell, _cells.*)',
180205
'_textAlignChanged(textAlign, _cells.*, _headerCell, _footerCell)',
181206
'_orderChanged(_order, _headerCell, _footerCell, _cells.*)',
182207
'_lastFrozenChanged(_lastFrozen)',
208+
'_firstFrozenToEndChanged(_firstFrozenToEnd)',
183209
'_onRendererOrBindingChanged(_renderer, _cells, _cells.*, path)',
184210
'_onHeaderRendererOrBindingChanged(_headerRenderer, _headerCell, path, header)',
185211
'_onFooterRendererOrBindingChanged(_footerRenderer, _footerCell)',
@@ -314,6 +340,23 @@ export const ColumnBaseMixin = (superClass) =>
314340
this._grid && this._grid._frozenCellsChanged && this._grid._frozenCellsChanged();
315341
}
316342

343+
/** @private */
344+
_frozenToEndChanged(frozenToEnd) {
345+
if (this.parentElement && this.parentElement._columnPropChanged) {
346+
this.parentElement._columnPropChanged('frozenToEnd', frozenToEnd);
347+
}
348+
349+
this._allCells.forEach((cell) => {
350+
// Skip sizer cells to keep correct scrollWidth.
351+
if (this._grid && cell.parentElement === this._grid.$.sizer) {
352+
return;
353+
}
354+
cell.toggleAttribute('frozen-to-end', frozenToEnd);
355+
});
356+
357+
this._grid && this._grid._frozenCellsChanged && this._grid._frozenCellsChanged();
358+
}
359+
317360
/** @private */
318361
_lastFrozenChanged(lastFrozen) {
319362
this._allCells.forEach((cell) => cell.toggleAttribute('last-frozen', lastFrozen));
@@ -323,6 +366,22 @@ export const ColumnBaseMixin = (superClass) =>
323366
}
324367
}
325368

369+
/** @private */
370+
_firstFrozenToEndChanged(firstFrozenToEnd) {
371+
this._allCells.forEach((cell) => {
372+
// Skip sizer cells to keep correct scrollWidth.
373+
if (this._grid && cell.parentElement === this._grid.$.sizer) {
374+
return;
375+
}
376+
377+
cell.toggleAttribute('first-frozen-to-end', firstFrozenToEnd);
378+
});
379+
380+
if (this.parentElement && this.parentElement._columnPropChanged) {
381+
this.parentElement._firstFrozenToEnd = firstFrozenToEnd;
382+
}
383+
}
384+
326385
/**
327386
* @param {string} path
328387
* @return {string}
@@ -421,7 +480,7 @@ export const ColumnBaseMixin = (superClass) =>
421480
}
422481
);
423482

424-
this._grid._updateLastFrozen && this._grid._updateLastFrozen();
483+
this._grid._updateFrozenColumn && this._grid._updateFrozenColumn();
425484
this._grid._resetKeyboardNavigation && this._grid._resetKeyboardNavigation();
426485
}
427486
this._previousHidden = hidden;

packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ export const KeyboardNavigationMixin = (superClass) =>
854854
* @protected
855855
*/
856856
_scrollHorizontallyToCell(dstCell) {
857-
if (dstCell.hasAttribute('frozen') || this.__isDetailsCell(dstCell)) {
857+
if (dstCell.hasAttribute('frozen') || dstCell.hasAttribute('frozen-to-end') || this.__isDetailsCell(dstCell)) {
858858
// These cells are, by design, always visible, no need to scroll.
859859
return;
860860
}
@@ -870,7 +870,7 @@ export const KeyboardNavigationMixin = (superClass) =>
870870
if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
871871
continue;
872872
}
873-
if (cell.hasAttribute('frozen')) {
873+
if (cell.hasAttribute('frozen') || cell.hasAttribute('frozen-to-end')) {
874874
leftBoundary = cell.getBoundingClientRect().right;
875875
break;
876876
}
@@ -880,7 +880,7 @@ export const KeyboardNavigationMixin = (superClass) =>
880880
if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
881881
continue;
882882
}
883-
if (cell.hasAttribute('frozen')) {
883+
if (cell.hasAttribute('frozen') || cell.hasAttribute('frozen-to-end')) {
884884
rightBoundary = cell.getBoundingClientRect().left;
885885
break;
886886
}

packages/grid/src/vaadin-grid-scroll-mixin.js

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { animationFrame, microTask, timeOut } from '@vaadin/component-base/src/async.js';
77
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8+
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
89

910
const timeouts = {
1011
SCROLLING: 500
@@ -14,7 +15,7 @@ const timeouts = {
1415
* @polymerMixin
1516
*/
1617
export const ScrollMixin = (superClass) =>
17-
class ScrollMixin extends superClass {
18+
class ScrollMixin extends ResizeMixin(superClass) {
1819
static get properties() {
1920
return {
2021
/**
@@ -26,6 +27,15 @@ export const ScrollMixin = (superClass) =>
2627
value: () => []
2728
},
2829

30+
/**
31+
* Cached array of cells frozen to end
32+
* @private
33+
*/
34+
_frozenToEndCells: {
35+
type: Array,
36+
value: () => []
37+
},
38+
2939
/** @private */
3040
_rowWithFocusedElement: Element
3141
};
@@ -67,6 +77,15 @@ export const ScrollMixin = (superClass) =>
6777
this.$.table.addEventListener('scroll', () => this._afterScroll());
6878
}
6979

80+
/**
81+
* @protected
82+
* @override
83+
*/
84+
_onResize() {
85+
this._updateOverflow();
86+
this.__updateHorizontalScrollPosition();
87+
}
88+
7089
/**
7190
* Scroll to a specific row index in the virtual list. Note that the row index is
7291
* not always the same for any particular item. For example, sorting/filtering/expanding
@@ -177,13 +196,14 @@ export const ScrollMixin = (superClass) =>
177196
cell.style.transform = '';
178197
});
179198
this._frozenCells = Array.prototype.slice.call(this.$.table.querySelectorAll('[frozen]'));
199+
this._frozenToEndCells = Array.prototype.slice.call(this.$.table.querySelectorAll('[frozen-to-end]'));
180200
this.__updateHorizontalScrollPosition();
181201
});
182-
this._updateLastFrozen();
202+
this._updateFrozenColumn();
183203
}
184204

185205
/** @protected */
186-
_updateLastFrozen() {
206+
_updateFrozenColumn() {
187207
if (!this._columnTree) {
188208
return;
189209
}
@@ -192,26 +212,58 @@ export const ScrollMixin = (superClass) =>
192212
columnsRow.sort((a, b) => {
193213
return a._order - b._order;
194214
});
195-
const lastFrozen = columnsRow.reduce((prev, col, index) => {
215+
216+
let lastFrozen;
217+
let firstFrozenToEnd;
218+
219+
// Use for loop to only iterate columns once
220+
for (let i = 0; i < columnsRow.length; i++) {
221+
const col = columnsRow[i];
222+
196223
col._lastFrozen = false;
197-
return col.frozen && !col.hidden ? index : prev;
198-
}, undefined);
224+
col._firstFrozenToEnd = false;
225+
226+
if (firstFrozenToEnd === undefined && col.frozenToEnd && !col.hidden) {
227+
firstFrozenToEnd = i;
228+
}
229+
230+
if (col.frozen && !col.hidden) {
231+
lastFrozen = i;
232+
}
233+
}
234+
199235
if (lastFrozen !== undefined) {
200236
columnsRow[lastFrozen]._lastFrozen = true;
201237
}
238+
239+
if (firstFrozenToEnd !== undefined) {
240+
columnsRow[firstFrozenToEnd]._firstFrozenToEnd = true;
241+
}
202242
}
203243

204244
/** @private */
205245
__updateHorizontalScrollPosition() {
246+
const scrollWidth = this.$.table.scrollWidth;
247+
const clientWidth = this.$.table.clientWidth;
248+
const scrollLeft = this.__getNormalizedScrollLeft(this.$.table);
249+
250+
// Position cells frozen to end
251+
const remaining = scrollLeft + clientWidth - scrollWidth;
252+
253+
this.$.table.style.setProperty('--_grid-horizontal-scroll-remaining', remaining + 'px');
206254
this.$.table.style.setProperty('--_grid-horizontal-scroll-position', -this._scrollLeft + 'px');
207255

208256
if (this.__isRTL) {
209257
// Translating the sticky sections using a CSS variable works nicely on LTR.
210258
// On RTL, it causes jumpy behavior (on Desktop Safari) so we need to translate manually.
211-
const x = this.__getNormalizedScrollLeft(this.$.table) + this.$.table.clientWidth - this.$.table.scrollWidth;
212-
const transform = `translate(${x}px, 0)`;
259+
const transformFrozen = `translate(${remaining}px, 0)`;
213260
for (let i = 0; i < this._frozenCells.length; i++) {
214-
this._frozenCells[i].style.transform = transform;
261+
this._frozenCells[i].style.transform = transformFrozen;
262+
}
263+
264+
const transformFrozenToEnd = `translate(${scrollLeft}px, 0)`;
265+
for (let i = 0; i < this._frozenToEndCells.length; i++) {
266+
this._frozenToEndCells[i].style.transform = transformFrozenToEnd;
215267
}
216268
}
217269
}

0 commit comments

Comments
 (0)