@vaadin/grid 25.3.0-alpha6 → 25.3.0-alpha8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,8 @@ import { animationFrame } from '@vaadin/component-base/src/async.js';
7
7
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
8
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
9
9
  import { get } from '@vaadin/component-base/src/path-utils.js';
10
- import { updateCellState, updatePart } from './vaadin-grid-helpers.js';
10
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
11
+ import { updateCellState } from './vaadin-grid-helpers.js';
11
12
 
12
13
  export const ColumnBaseMixin = (superClass) =>
13
14
  class ColumnBaseMixin extends superClass {
@@ -92,8 +93,8 @@ export const ColumnBaseMixin = (superClass) =>
92
93
 
93
94
  /**
94
95
  * Aligns the columns cell content horizontally.
95
- * Supported values: "start", "center" and "end".
96
- * @attr {start|center|end} text-align
96
+ *
97
+ * @attr {start|center|end|left|right} text-align
97
98
  */
98
99
  textAlign: {
99
100
  type: String,
@@ -260,21 +261,21 @@ export const ColumnBaseMixin = (superClass) =>
260
261
 
261
262
  static get observers() {
262
263
  return [
263
- '_widthChanged(width, _headerCell, _footerCell, _cells)',
264
+ '_widthChanged(width, _cells)',
264
265
  '_frozenChanged(frozen, _headerCell, _footerCell, _cells)',
265
266
  '_frozenToEndChanged(frozenToEnd, _headerCell, _footerCell, _cells)',
266
- '_flexGrowChanged(flexGrow, _headerCell, _footerCell, _cells)',
267
- '_textAlignChanged(textAlign, _cells, _headerCell, _footerCell)',
267
+ '_flexGrowChanged(flexGrow, _cells)',
268
+ '_textAlignChanged(textAlign, _cells)',
268
269
  '_lastFrozenChanged(_lastFrozen)',
269
270
  '_firstFrozenToEndChanged(_firstFrozenToEnd)',
270
271
  '_onRendererOrBindingChanged(_renderer, _cells, _bodyContentHidden, path)',
271
272
  '_onHeaderRendererOrBindingChanged(_headerRenderer, _headerCell, path, header)',
272
273
  '_onFooterRendererOrBindingChanged(_footerRenderer, _footerCell)',
273
- '_resizableChanged(resizable, _headerCell)',
274
+ '_resizableChanged(resizable)',
274
275
  '_reorderStatusChanged(_reorderStatus, _headerCell, _footerCell, _cells)',
275
276
  '_hiddenChanged(hidden, _headerCell, _footerCell, _cells)',
276
277
  '_rowHeaderChanged(rowHeader, _cells)',
277
- '__headerFooterPartNameChanged(_headerCell, _footerCell, headerPartName, footerPartName)',
278
+ '__headerFooterPartNameChanged(headerPartName, footerPartName)',
278
279
  ];
279
280
  }
280
281
 
@@ -302,6 +303,19 @@ export const ColumnBaseMixin = (superClass) =>
302
303
  .filter((cell) => cell);
303
304
  }
304
305
 
306
+ constructor() {
307
+ super();
308
+
309
+ /**
310
+ * A stable unique id assigned once per column instance. Used to build
311
+ * cell content slot names that stay stable across re-renders, so lit
312
+ * can reuse the cell content elements when the column tree changes.
313
+ *
314
+ * @protected
315
+ */
316
+ this._id = generateUniqueId();
317
+ }
318
+
305
319
  /** @protected */
306
320
  connectedCallback() {
307
321
  super.connectedCallback();
@@ -313,7 +327,7 @@ export const ColumnBaseMixin = (superClass) =>
313
327
  return;
314
328
  }
315
329
 
316
- this._allCells.forEach((cell) => {
330
+ this._cells?.forEach((cell) => {
317
331
  if (!cell._content.parentNode) {
318
332
  this._grid.appendChild(cell._content);
319
333
  }
@@ -332,7 +346,7 @@ export const ColumnBaseMixin = (superClass) =>
332
346
  return;
333
347
  }
334
348
 
335
- this._allCells.forEach((cell) => {
349
+ this._cells?.forEach((cell) => {
336
350
  if (cell._content.parentNode) {
337
351
  cell._content.parentNode.removeChild(cell._content);
338
352
  }
@@ -368,7 +382,9 @@ export const ColumnBaseMixin = (superClass) =>
368
382
  this.parentElement._columnPropChanged('flexGrow');
369
383
  }
370
384
 
371
- this._allCells.forEach((cell) => {
385
+ this._grid?.__scheduleRenderHeaderFooter?.();
386
+
387
+ this._cells?.forEach((cell) => {
372
388
  cell.style.flexGrow = flexGrow;
373
389
  });
374
390
  }
@@ -379,7 +395,9 @@ export const ColumnBaseMixin = (superClass) =>
379
395
  this.parentElement._columnPropChanged('width');
380
396
  }
381
397
 
382
- this._allCells.forEach((cell) => {
398
+ this._grid?.__scheduleRenderHeaderFooter?.();
399
+
400
+ this._cells?.forEach((cell) => {
383
401
  cell.style.width = width;
384
402
  });
385
403
  }
@@ -485,27 +503,12 @@ export const ColumnBaseMixin = (superClass) =>
485
503
  }
486
504
 
487
505
  /** @private */
488
- _resizableChanged(resizable, headerCell) {
489
- if (resizable === undefined || headerCell === undefined) {
506
+ _resizableChanged(resizable) {
507
+ if (resizable === undefined || this._grid === undefined) {
490
508
  return;
491
509
  }
492
510
 
493
- if (headerCell) {
494
- [headerCell].concat(this._emptyCells).forEach((cell) => {
495
- if (cell) {
496
- const existingHandle = cell.querySelector('[part~="resize-handle"]');
497
- if (existingHandle) {
498
- cell.removeChild(existingHandle);
499
- }
500
-
501
- if (resizable) {
502
- const handle = document.createElement('div');
503
- updatePart(handle, 'resize-handle', true);
504
- cell.appendChild(handle);
505
- }
506
- }
507
- });
508
- }
511
+ this._grid.__scheduleRenderHeaderFooter?.();
509
512
  }
510
513
 
511
514
  /** @private */
@@ -513,12 +516,10 @@ export const ColumnBaseMixin = (superClass) =>
513
516
  if (textAlign === undefined || this._grid === undefined) {
514
517
  return;
515
518
  }
516
- if (['start', 'end', 'center'].indexOf(textAlign) === -1) {
517
- console.warn('textAlign can only be set as "start", "end" or "center"');
518
- return;
519
- }
520
519
 
521
- this._allCells.forEach((cell) => {
520
+ this._grid.__scheduleRenderHeaderFooter?.();
521
+
522
+ this._cells?.forEach((cell) => {
522
523
  cell._content.style.textAlign = textAlign;
523
524
  });
524
525
  }
@@ -531,7 +532,7 @@ export const ColumnBaseMixin = (superClass) =>
531
532
 
532
533
  if (!!hidden !== !!this._previousHidden && this._grid) {
533
534
  if (hidden === true) {
534
- this._allCells.forEach((cell) => {
535
+ this._cells?.forEach((cell) => {
535
536
  if (cell._content.parentNode) {
536
537
  cell._content.parentNode.removeChild(cell._content);
537
538
  }
@@ -632,9 +633,8 @@ export const ColumnBaseMixin = (superClass) =>
632
633
  }
633
634
 
634
635
  this.__renderCellsContent(headerRenderer, [headerCell]);
635
- if (this._grid && headerCell.parentElement) {
636
- this._grid.__debounceUpdateHeaderFooterRowVisibility(headerCell.parentElement);
637
- }
636
+
637
+ this._grid?.__scheduleRenderHeaderFooter();
638
638
  }
639
639
 
640
640
  /** @protected */
@@ -643,19 +643,8 @@ export const ColumnBaseMixin = (superClass) =>
643
643
  }
644
644
 
645
645
  /** @private */
646
- __headerFooterPartNameChanged(headerCell, footerCell, headerPartName, footerPartName) {
647
- [
648
- { cell: headerCell, partName: headerPartName },
649
- { cell: footerCell, partName: footerPartName },
650
- ].forEach(({ cell, partName }) => {
651
- if (cell) {
652
- const customParts = cell.__customParts || [];
653
- cell.part.remove(...customParts);
654
-
655
- cell.__customParts = partName ? partName.trim().split(' ') : [];
656
- cell.part.add(...cell.__customParts);
657
- }
658
- });
646
+ __headerFooterPartNameChanged() {
647
+ this._grid?.__scheduleRenderHeaderFooter?.();
659
648
  }
660
649
 
661
650
  /**
@@ -689,9 +678,8 @@ export const ColumnBaseMixin = (superClass) =>
689
678
  }
690
679
 
691
680
  this.__renderCellsContent(footerRenderer, [footerCell]);
692
- if (this._grid && footerCell.parentElement) {
693
- this._grid.__debounceUpdateHeaderFooterRowVisibility(footerCell.parentElement);
694
- }
681
+
682
+ this._grid?.__scheduleRenderHeaderFooter();
695
683
  }
696
684
 
697
685
  /** @protected */
@@ -203,6 +203,8 @@ export const ColumnReorderingMixin = (superClass) =>
203
203
  for (let i = startIndex; i !== endIndex; i += direction) {
204
204
  this._swapColumnOrders(this._draggedColumn, levelColumnsInOrder[i + direction]);
205
205
  }
206
+
207
+ this.__renderHeaderFooter();
206
208
  }
207
209
 
208
210
  this._updateGhostPosition(e.detail.x, this._touchDevice ? e.detail.y - 50 : e.detail.y);
@@ -434,7 +436,7 @@ export const ColumnReorderingMixin = (superClass) =>
434
436
  }
435
437
 
436
438
  /**
437
- * Swaps column orders and physically reorders cells in all rows.
439
+ * Swaps column orders and reorders cells in all rows.
438
440
  * @param {!GridColumn} column1
439
441
  * @param {!GridColumn} column2
440
442
  * @protected
@@ -444,8 +446,7 @@ export const ColumnReorderingMixin = (superClass) =>
444
446
  [column1._order, column2._order] = [column2._order, column1._order];
445
447
  const [firstColumn, secondColumn] = column1._order < column2._order ? [column1, column2] : [column2, column1];
446
448
 
447
- // Reorder cells in all rows (header, footer, body, sizer)
448
- [...this.$.header.children, ...this.$.footer.children, ...this.$.items.children, this.$.sizer].forEach((row) => {
449
+ [...this.$.items.children, this.$.sizer].forEach((row) => {
449
450
  const cells = getBodyRowCells(row);
450
451
  const firstColumnCells = cells.filter((cell) => firstColumn.contains(cell._column));
451
452
  const secondColumnFirstCell = cells.find((cell) => secondColumn.contains(cell._column));
@@ -456,8 +457,12 @@ export const ColumnReorderingMixin = (superClass) =>
456
457
  }
457
458
  });
458
459
 
460
+ [...this.$.items.children, this.$.sizer].forEach((row) => {
461
+ this._updateFirstAndLastColumnForRow(row);
462
+ });
463
+
464
+ this.__scheduleRenderHeaderFooter();
459
465
  this._debounceUpdateFrozenColumn();
460
- this._updateFirstAndLastColumn();
461
466
  }
462
467
 
463
468
  /**
@@ -98,6 +98,10 @@ export const ColumnResizingMixin = (superClass) =>
98
98
  cell._column.flexGrow = 0;
99
99
  });
100
100
 
101
+ // Flush the scheduled header/footer render to apply the new widths
102
+ // to the cells before measuring the layout below
103
+ this.__renderHeaderFooterDebouncer?.flush();
104
+
101
105
  const cellFrozenToEnd = this._frozenToEndCells[0];
102
106
 
103
107
  // When handle moves below the cell frozen to end, scroll into view.
@@ -143,11 +143,6 @@ export const DynamicColumnsMixin = (superClass) =>
143
143
  });
144
144
  }
145
145
 
146
- /** @protected */
147
- _updateFirstAndLastColumn() {
148
- Array.from(this.shadowRoot.querySelectorAll('tr')).forEach((row) => this._updateFirstAndLastColumnForRow(row));
149
- }
150
-
151
146
  /**
152
147
  * @param {!HTMLElement} row
153
148
  * @protected
@@ -37,6 +37,7 @@ import { GridFilterElementMixin } from './vaadin-grid-filter-element-mixin.js';
37
37
  *
38
38
  * @fires {CustomEvent} value-changed - Fired when the `value` property changes.
39
39
  *
40
+ * @attr {string} theme - The theme variants to apply to the component.
40
41
  * @customElement vaadin-grid-filter
41
42
  * @extends HTMLElement
42
43
  */
@@ -0,0 +1,296 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2016 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { html, nothing, render } from 'lit';
7
+ import { cache } from 'lit/directives/cache.js';
8
+ import { classMap } from 'lit/directives/class-map.js';
9
+ import { ifDefined } from 'lit/directives/if-defined.js';
10
+ import { repeat } from 'lit/directives/repeat.js';
11
+ import { styleMap } from 'lit/directives/style-map.js';
12
+ import { microTask } from '@vaadin/component-base/src/async.js';
13
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
14
+ import { partMap } from '@vaadin/component-base/src/directives/part-map.js';
15
+ import { cellContent } from './directives/cell-content-directive.js';
16
+
17
+ function isContentCell(column, level, columnTree) {
18
+ const isLastRow = level === columnTree.length - 1;
19
+ return isLastRow || column.localName === 'vaadin-grid-column-group';
20
+ }
21
+
22
+ function isHeaderRowVisible(columns, level, columnTree) {
23
+ return columns.some((column) => {
24
+ if (column.hidden || !isContentCell(column, level, columnTree)) {
25
+ return false;
26
+ }
27
+
28
+ if (column.headerRenderer) {
29
+ // The column has a header renderer -> row should be visible
30
+ return true;
31
+ }
32
+
33
+ if (column.header === null) {
34
+ // The column header is explicitly set to null -> doesn't block hiding the row
35
+ return false;
36
+ }
37
+
38
+ return column.path || column.header !== undefined;
39
+ });
40
+ }
41
+
42
+ function isFooterRowVisible(columns, level, columnTree) {
43
+ return columns.some((column) => {
44
+ if (column.hidden || !isContentCell(column, level, columnTree)) {
45
+ return false;
46
+ }
47
+
48
+ return column.footerRenderer;
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Converts a whitespace separated list of custom part names
54
+ * into an object accepted by the `partMap` directive.
55
+ */
56
+ function getCustomParts(partName) {
57
+ return Object.fromEntries(
58
+ (partName ?? '')
59
+ .split(' ')
60
+ .filter((name) => name !== '')
61
+ .map((name) => [name, true]),
62
+ );
63
+ }
64
+
65
+ /**
66
+ * A mixin providing rendering of header and footer rows based on the column tree.
67
+ */
68
+ export const HeaderFooterRenderingMixin = (superClass) =>
69
+ class HeaderFooterRenderingMixin extends superClass {
70
+ /** @private */
71
+ __scheduleRenderHeaderFooter() {
72
+ this.__renderHeaderFooterDebouncer = Debouncer.debounce(this.__renderHeaderFooterDebouncer, microTask, () => {
73
+ this.__renderHeaderFooter();
74
+ });
75
+ }
76
+
77
+ /** @private */
78
+ __renderHeaderFooter() {
79
+ this.__renderHeaderFooterDebouncer?.cancel();
80
+
81
+ const sortedColumnTree = (this._columnTree ?? []).map((columns) => {
82
+ return columns.toSorted((a, b) => a._order - b._order);
83
+ });
84
+
85
+ sortedColumnTree.flat().forEach((column) => {
86
+ column._emptyCells = [];
87
+ });
88
+
89
+ this.#renderHeader(sortedColumnTree);
90
+ this.#renderFooter(sortedColumnTree);
91
+
92
+ this._resetKeyboardNavigation();
93
+ this.__a11yUpdateGridSize(this.size, this._columnTree, this.__emptyState);
94
+ }
95
+
96
+ #renderHeader(columnTree) {
97
+ const rows = this.#getRows(columnTree, 'header');
98
+ render(rows.map(this.#renderHeaderRow), this.$.header, { host: this });
99
+
100
+ this.$.table.toggleAttribute('has-header', !!this.$.header.querySelector('tr:not([hidden])'));
101
+
102
+ this.$.header.querySelectorAll('.header-cell').forEach((cell) => {
103
+ const column = cell._column;
104
+ const isColumnRow = cell.parentElement === this.$.header.lastElementChild;
105
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
106
+ column._headerCell = cell;
107
+ } else {
108
+ column._emptyCells.push(cell);
109
+ }
110
+ });
111
+ }
112
+
113
+ #renderHeaderRow = ({ level, cells, isLastRow, isFirstRow, isRowVisible }) => {
114
+ const rowParts = {
115
+ 'first-header-row': isFirstRow,
116
+ 'last-header-row': isLastRow,
117
+ };
118
+
119
+ return html`
120
+ <tr
121
+ role="row"
122
+ part="row header-row${partMap(rowParts)}"
123
+ class="row header-row${classMap(rowParts)}"
124
+ tabindex="-1"
125
+ ?hidden=${!isRowVisible}
126
+ >
127
+ ${repeat(
128
+ cells,
129
+ ({ column }) => column._id,
130
+ ({ column, isFirstCell, isLastCell, isContentCell }) => {
131
+ // `cache` keeps the cell and its rendered content when the
132
+ // column gets hidden, so it can be restored as-is when the
133
+ // column is shown again.
134
+ if (column.hidden) {
135
+ return cache(nothing);
136
+ }
137
+
138
+ const cellParts = {
139
+ 'first-header-row-cell': isFirstRow,
140
+ 'last-header-row-cell': isLastRow,
141
+ 'first-column-cell': isFirstCell,
142
+ 'last-column-cell': isLastCell,
143
+ };
144
+
145
+ const customCellParts = isContentCell ? getCustomParts(column.headerPartName) : {};
146
+
147
+ return cache(html`
148
+ <th
149
+ role="columnheader"
150
+ part="cell header-cell${partMap({ ...cellParts, ...customCellParts })}"
151
+ class="cell header-cell${classMap(cellParts)}"
152
+ style="${styleMap({
153
+ width: column.width,
154
+ 'flex-grow': column.flexGrow,
155
+ })}"
156
+ ?first-column="${isFirstCell}"
157
+ ?last-column="${isLastCell}"
158
+ @keydown="${this.__onCellKeyDown}"
159
+ @mousedown=${this.__onCellMouseDown}
160
+ @mouseenter=${this.__onCellMouseEnter}
161
+ @mouseleave=${this.__onCellMouseLeave}
162
+ colspan="${ifDefined(column._colSpan)}"
163
+ aria-colspan="${ifDefined(column._colSpan)}"
164
+ tabindex="-1"
165
+ ._column=${column}
166
+ >
167
+ ${cellContent(this, `vaadin-grid-header-cell-content-${level}-${column._id}`, {
168
+ textAlign: column.textAlign,
169
+ })}
170
+ ${column.resizable ? html`<div part="resize-handle" class="resize-handle"></div>` : nothing}
171
+ </th>
172
+ `);
173
+ },
174
+ )}
175
+ </tr>
176
+ `;
177
+ };
178
+
179
+ #renderFooter(columnTree) {
180
+ const rows = this.#getRows(columnTree, 'footer');
181
+ render(rows.map(this.#renderFooterRow), this.$.footer, { host: this });
182
+
183
+ this.$.table.toggleAttribute('has-footer', !!this.$.footer.querySelector('tr:not([hidden])'));
184
+
185
+ this.$.footer.querySelectorAll('.footer-cell').forEach((cell) => {
186
+ const column = cell._column;
187
+ const isColumnRow = cell.parentElement === this.$.footer.firstElementChild;
188
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
189
+ column._footerCell = cell;
190
+ } else {
191
+ column._emptyCells.push(cell);
192
+ }
193
+ });
194
+ }
195
+
196
+ #renderFooterRow = ({ level, cells, isLastRow, isFirstRow, isRowVisible }) => {
197
+ const rowParts = {
198
+ 'first-footer-row': isFirstRow,
199
+ 'last-footer-row': isLastRow,
200
+ };
201
+
202
+ return html`
203
+ <tr
204
+ role="row"
205
+ part="row footer-row${partMap(rowParts)}"
206
+ class="row footer-row${classMap(rowParts)}"
207
+ tabindex="-1"
208
+ ?hidden=${!isRowVisible}
209
+ >
210
+ ${repeat(
211
+ cells,
212
+ ({ column }) => column._id,
213
+ ({ column, isFirstCell, isLastCell, isContentCell }) => {
214
+ // `cache` keeps the cell and its rendered content when the
215
+ // column gets hidden, so it can be restored as-is when the
216
+ // column is shown again.
217
+ if (column.hidden) {
218
+ return cache(nothing);
219
+ }
220
+
221
+ const cellParts = {
222
+ 'first-footer-row-cell': isFirstRow,
223
+ 'last-footer-row-cell': isLastRow,
224
+ 'first-column-cell': isFirstCell,
225
+ 'last-column-cell': isLastCell,
226
+ };
227
+
228
+ const customCellParts = isContentCell ? getCustomParts(column.footerPartName) : {};
229
+
230
+ return cache(html`
231
+ <td
232
+ role="gridcell"
233
+ part="cell footer-cell${partMap({ ...cellParts, ...customCellParts })}"
234
+ class="cell footer-cell${classMap(cellParts)}"
235
+ style="${styleMap({
236
+ width: column.width,
237
+ 'flex-grow': column.flexGrow,
238
+ })}"
239
+ ?first-column="${isFirstCell}"
240
+ ?last-column="${isLastCell}"
241
+ @keydown="${this.__onCellKeyDown}"
242
+ @mousedown=${this.__onCellMouseDown}
243
+ @mouseenter=${this.__onCellMouseEnter}
244
+ @mouseleave=${this.__onCellMouseLeave}
245
+ colspan="${ifDefined(column._colSpan)}"
246
+ aria-colspan="${ifDefined(column._colSpan)}"
247
+ tabindex="-1"
248
+ ._column=${column}
249
+ >
250
+ ${cellContent(this, `vaadin-grid-footer-cell-content-${level}-${column._id}`, {
251
+ textAlign: column.textAlign,
252
+ })}
253
+ </td>
254
+ `);
255
+ },
256
+ )}
257
+ </tr>
258
+ `;
259
+ };
260
+
261
+ #getRows(columnTree, section) {
262
+ let rows = columnTree.map((columns, level) => {
263
+ const visibleColumns = columns.filter((column) => !column.hidden);
264
+
265
+ return {
266
+ level,
267
+ cells: columns.map((column) => {
268
+ return {
269
+ column,
270
+ isFirstCell: column === visibleColumns.at(0),
271
+ isLastCell: column === visibleColumns.at(-1),
272
+ isContentCell: isContentCell(column, level, columnTree),
273
+ };
274
+ }),
275
+ isRowVisible:
276
+ section === 'header'
277
+ ? isHeaderRowVisible(columns, level, columnTree)
278
+ : isFooterRowVisible(columns, level, columnTree),
279
+ };
280
+ });
281
+
282
+ if (section === 'footer') {
283
+ rows = rows.toReversed();
284
+ }
285
+
286
+ const visibleRows = rows.filter((row) => row.isRowVisible);
287
+
288
+ return rows.map((row) => {
289
+ return {
290
+ ...row,
291
+ isFirstRow: row === visibleRows.at(0),
292
+ isLastRow: row === visibleRows.at(-1),
293
+ };
294
+ });
295
+ }
296
+ };