@vaadin/grid 25.3.0-alpha6 → 25.3.0-dev.1fa5a51482

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.
@@ -0,0 +1,265 @@
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 { microTask } from '@vaadin/component-base/src/async.js';
12
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
13
+ import { partMap } from '@vaadin/component-base/src/directives/part-map.js';
14
+ import { cellContent } from './directives/cell-content-directive.js';
15
+
16
+ function isEmptyCell(column, level, columnTree) {
17
+ const isColumnRow = level === columnTree.length - 1;
18
+ return !isColumnRow && column.localName !== 'vaadin-grid-column-group';
19
+ }
20
+
21
+ function isHeaderRowVisible(columns, level, columnTree) {
22
+ return columns.some((column) => {
23
+ if (column.hidden || isEmptyCell(column, level, columnTree)) {
24
+ return false;
25
+ }
26
+
27
+ if (column.headerRenderer) {
28
+ // The column has a header renderer -> row should be visible
29
+ return true;
30
+ }
31
+
32
+ if (column.header === null) {
33
+ // The column header is explicitly set to null -> doesn't block hiding the row
34
+ return false;
35
+ }
36
+
37
+ return column.path || column.header !== undefined;
38
+ });
39
+ }
40
+
41
+ function isFooterRowVisible(columns, level, columnTree) {
42
+ return columns.some((column) => {
43
+ if (column.hidden || isEmptyCell(column, level, columnTree)) {
44
+ return false;
45
+ }
46
+
47
+ return column.footerRenderer;
48
+ });
49
+ }
50
+
51
+ /**
52
+ * A mixin providing rendering of header and footer rows based on the column tree.
53
+ */
54
+ export const HeaderFooterRenderingMixin = (superClass) =>
55
+ class HeaderFooterRenderingMixin extends superClass {
56
+ /** @private */
57
+ __scheduleRenderHeaderFooter() {
58
+ this.__renderHeaderFooterDebouncer = Debouncer.debounce(this.__renderHeaderFooterDebouncer, microTask, () => {
59
+ this.__renderHeaderFooter();
60
+ });
61
+ }
62
+
63
+ /** @private */
64
+ __renderHeaderFooter() {
65
+ this.__renderHeaderFooterDebouncer?.cancel();
66
+
67
+ const sortedColumnTree = (this._columnTree ?? []).map((columns) => {
68
+ return columns.toSorted((a, b) => a._order - b._order);
69
+ });
70
+
71
+ sortedColumnTree.flat().forEach((column) => {
72
+ column._emptyCells = [];
73
+ });
74
+
75
+ this.#renderHeader(sortedColumnTree);
76
+ this.#renderFooter(sortedColumnTree);
77
+
78
+ this._resetKeyboardNavigation();
79
+ this.__a11yUpdateGridSize(this.size, this._columnTree, this.__emptyState);
80
+ }
81
+
82
+ #renderHeader(columnTree) {
83
+ const rows = this.#getRows(columnTree, 'header');
84
+ render(rows.map(this.#renderHeaderRow), this.$.header, { host: this });
85
+
86
+ this.$.table.toggleAttribute('has-header', !!this.$.header.querySelector('tr:not([hidden])'));
87
+
88
+ this.$.header.querySelectorAll('.header-cell').forEach((cell) => {
89
+ const column = cell._column;
90
+ const isColumnRow = cell.parentElement === this.$.header.lastElementChild;
91
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
92
+ column._headerCell = cell;
93
+ } else {
94
+ column._emptyCells.push(cell);
95
+ }
96
+ });
97
+ }
98
+
99
+ #renderHeaderRow = ({ level, cells, isLastRow, isFirstRow, isRowVisible }) => {
100
+ const rowParts = {
101
+ 'first-header-row': isFirstRow,
102
+ 'last-header-row': isLastRow,
103
+ };
104
+
105
+ return html`
106
+ <tr
107
+ role="row"
108
+ part="row header-row${partMap(rowParts)}"
109
+ class="row header-row${classMap(rowParts)}"
110
+ tabindex="-1"
111
+ ?hidden=${!isRowVisible}
112
+ >
113
+ ${repeat(
114
+ cells,
115
+ ({ column }) => column._id,
116
+ ({ column, isFirstCell, isLastCell }) => {
117
+ // `cache` keeps the cell and its rendered content when the
118
+ // column gets hidden, so it can be restored as-is when the
119
+ // column is shown again.
120
+ if (column.hidden) {
121
+ return cache(nothing);
122
+ }
123
+
124
+ const cellParts = {
125
+ 'first-header-row-cell': isFirstRow,
126
+ 'last-header-row-cell': isLastRow,
127
+ 'first-column-cell': isFirstCell,
128
+ 'last-column-cell': isLastCell,
129
+ };
130
+
131
+ return cache(html`
132
+ <th
133
+ role="columnheader"
134
+ part="cell header-cell${partMap(cellParts)}"
135
+ class="cell header-cell${classMap(cellParts)}"
136
+ ?first-column="${isFirstCell}"
137
+ ?last-column="${isLastCell}"
138
+ @keydown="${this.__onCellKeyDown}"
139
+ @mousedown=${this.__onCellMouseDown}
140
+ @mouseenter=${this.__onCellMouseEnter}
141
+ @mouseleave=${this.__onCellMouseLeave}
142
+ colspan="${ifDefined(column._colSpan)}"
143
+ aria-colspan="${ifDefined(column._colSpan)}"
144
+ tabindex="-1"
145
+ ._column=${column}
146
+ >
147
+ ${cellContent(this, `vaadin-grid-header-cell-content-${level}-${column._id}`)}
148
+ ${column.resizable ? html`<div part="resize-handle" class="resize-handle"></div>` : nothing}
149
+ </th>
150
+ `);
151
+ },
152
+ )}
153
+ </tr>
154
+ `;
155
+ };
156
+
157
+ #renderFooter(columnTree) {
158
+ const rows = this.#getRows(columnTree, 'footer');
159
+ render(rows.map(this.#renderFooterRow), this.$.footer, { host: this });
160
+
161
+ this.$.table.toggleAttribute('has-footer', !!this.$.footer.querySelector('tr:not([hidden])'));
162
+
163
+ this.$.footer.querySelectorAll('.footer-cell').forEach((cell) => {
164
+ const column = cell._column;
165
+ const isColumnRow = cell.parentElement === this.$.footer.firstElementChild;
166
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
167
+ column._footerCell = cell;
168
+ } else {
169
+ column._emptyCells.push(cell);
170
+ }
171
+ });
172
+ }
173
+
174
+ #renderFooterRow = ({ level, cells, isLastRow, isFirstRow, isRowVisible }) => {
175
+ const rowParts = {
176
+ 'first-footer-row': isFirstRow,
177
+ 'last-footer-row': isLastRow,
178
+ };
179
+
180
+ return html`
181
+ <tr
182
+ role="row"
183
+ part="row footer-row${partMap(rowParts)}"
184
+ class="row footer-row${classMap(rowParts)}"
185
+ tabindex="-1"
186
+ ?hidden=${!isRowVisible}
187
+ >
188
+ ${repeat(
189
+ cells,
190
+ ({ column }) => column._id,
191
+ ({ column, isFirstCell, isLastCell }) => {
192
+ // `cache` keeps the cell and its rendered content when the
193
+ // column gets hidden, so it can be restored as-is when the
194
+ // column is shown again.
195
+ if (column.hidden) {
196
+ return cache(nothing);
197
+ }
198
+
199
+ const cellParts = {
200
+ 'first-footer-row-cell': isFirstRow,
201
+ 'last-footer-row-cell': isLastRow,
202
+ 'first-column-cell': isFirstCell,
203
+ 'last-column-cell': isLastCell,
204
+ };
205
+
206
+ return cache(html`
207
+ <td
208
+ role="gridcell"
209
+ part="cell footer-cell${partMap(cellParts)}"
210
+ class="cell footer-cell${classMap(cellParts)}"
211
+ ?first-column="${isFirstCell}"
212
+ ?last-column="${isLastCell}"
213
+ @keydown="${this.__onCellKeyDown}"
214
+ @mousedown=${this.__onCellMouseDown}
215
+ @mouseenter=${this.__onCellMouseEnter}
216
+ @mouseleave=${this.__onCellMouseLeave}
217
+ colspan="${ifDefined(column._colSpan)}"
218
+ aria-colspan="${ifDefined(column._colSpan)}"
219
+ tabindex="-1"
220
+ ._column=${column}
221
+ >
222
+ ${cellContent(this, `vaadin-grid-footer-cell-content-${level}-${column._id}`)}
223
+ </td>
224
+ `);
225
+ },
226
+ )}
227
+ </tr>
228
+ `;
229
+ };
230
+
231
+ #getRows(columnTree, section) {
232
+ let rows = columnTree.map((columns, level) => {
233
+ const visibleColumns = columns.filter((column) => !column.hidden);
234
+
235
+ return {
236
+ level,
237
+ cells: columns.map((column) => {
238
+ return {
239
+ column,
240
+ isFirstCell: column === visibleColumns.at(0),
241
+ isLastCell: column === visibleColumns.at(-1),
242
+ };
243
+ }),
244
+ isRowVisible:
245
+ section === 'header'
246
+ ? isHeaderRowVisible(columns, level, columnTree)
247
+ : isFooterRowVisible(columns, level, columnTree),
248
+ };
249
+ });
250
+
251
+ if (section === 'footer') {
252
+ rows = rows.toReversed();
253
+ }
254
+
255
+ const visibleRows = rows.filter((row) => row.isRowVisible);
256
+
257
+ return rows.map((row) => {
258
+ return {
259
+ ...row,
260
+ isFirstRow: row === visibleRows.at(0),
261
+ isLastRow: row === visibleRows.at(-1),
262
+ };
263
+ });
264
+ }
265
+ };
@@ -4,9 +4,7 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js';
7
- import { microTask } from '@vaadin/component-base/src/async.js';
8
7
  import { isAndroid, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
9
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
10
8
  import { setTouchAction } from '@vaadin/component-base/src/gestures.js';
11
9
  import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
12
10
  import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
@@ -22,6 +20,7 @@ import { DragAndDropMixin } from './vaadin-grid-drag-and-drop-mixin.js';
22
20
  import { DynamicColumnsMixin } from './vaadin-grid-dynamic-columns-mixin.js';
23
21
  import { EventContextMixin } from './vaadin-grid-event-context-mixin.js';
24
22
  import { FilterMixin } from './vaadin-grid-filter-mixin.js';
23
+ import { HeaderFooterRenderingMixin } from './vaadin-grid-header-footer-rendering-mixin.js';
25
24
  import {
26
25
  getBodyRowCells,
27
26
  getClosestCell,
@@ -48,17 +47,21 @@ export const GridMixin = (superClass) =>
48
47
  ArrayDataProviderMixin(
49
48
  DataProviderMixin(
50
49
  DynamicColumnsMixin(
51
- ActiveItemMixin(
52
- ScrollMixin(
53
- SelectionMixin(
54
- SortMixin(
55
- RowDetailsMixin(
56
- KeyboardNavigationMixin(
57
- A11yMixin(
58
- FilterMixin(
59
- ColumnReorderingMixin(
60
- ColumnResizingMixin(
61
- EventContextMixin(DragAndDropMixin(StylingMixin(TabindexMixin(ResizeMixin(superClass))))),
50
+ HeaderFooterRenderingMixin(
51
+ ActiveItemMixin(
52
+ ScrollMixin(
53
+ SelectionMixin(
54
+ SortMixin(
55
+ RowDetailsMixin(
56
+ KeyboardNavigationMixin(
57
+ A11yMixin(
58
+ FilterMixin(
59
+ ColumnReorderingMixin(
60
+ ColumnResizingMixin(
61
+ EventContextMixin(
62
+ DragAndDropMixin(StylingMixin(TabindexMixin(ResizeMixin(superClass)))),
63
+ ),
64
+ ),
62
65
  ),
63
66
  ),
64
67
  ),
@@ -351,7 +354,7 @@ export const GridMixin = (superClass) =>
351
354
  updatePart(row, 'row', true);
352
355
  updatePart(row, 'body-row', true);
353
356
  if (this._columnTree) {
354
- this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
357
+ this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', true);
355
358
  }
356
359
  rows.push(row);
357
360
  }
@@ -444,11 +447,10 @@ export const GridMixin = (superClass) =>
444
447
  * @param {!HTMLTableRowElement} row
445
448
  * @param {!Array<!GridColumn>} columns
446
449
  * @param {?string} section
447
- * @param {boolean} isColumnRow
448
450
  * @param {boolean} noNotify
449
451
  * @private
450
452
  */
451
- __initRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
453
+ __initRow(row, columns, section = 'body', noNotify = false) {
452
454
  const contentsFragment = document.createDocumentFragment();
453
455
 
454
456
  iterateRowCells(row, (cell) => {
@@ -516,30 +518,6 @@ export const GridMixin = (superClass) =>
516
518
  if (!noNotify) {
517
519
  column._cells = [...column._cells];
518
520
  }
519
- } else {
520
- // Header & footer
521
- const tagName = section === 'header' ? 'th' : 'td';
522
- if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
523
- cell = column[`_${section}Cell`];
524
- if (!cell) {
525
- cell = this._createCell(tagName);
526
- }
527
- cell._column = column;
528
- row.appendChild(cell);
529
- column[`_${section}Cell`] = cell;
530
- } else {
531
- if (!column._emptyCells) {
532
- column._emptyCells = [];
533
- }
534
- cell = column._emptyCells.find((cell) => cell._vacant) || this._createCell(tagName);
535
- cell._column = column;
536
- row.appendChild(cell);
537
- if (column._emptyCells.indexOf(cell) === -1) {
538
- column._emptyCells.push(cell);
539
- }
540
- }
541
- updatePart(cell, 'cell', true);
542
- updatePart(cell, `${section}-cell`, true);
543
521
  }
544
522
 
545
523
  if (!cell._content.parentElement) {
@@ -549,10 +527,6 @@ export const GridMixin = (superClass) =>
549
527
  cell._column = column;
550
528
  });
551
529
 
552
- if (section !== 'body') {
553
- this.__debounceUpdateHeaderFooterRowVisibility(row);
554
- }
555
-
556
530
  // Might be empty if only cache was used
557
531
  this.appendChild(contentsFragment);
558
532
 
@@ -560,75 +534,6 @@ export const GridMixin = (superClass) =>
560
534
  this._updateFirstAndLastColumnForRow(row);
561
535
  }
562
536
 
563
- /**
564
- * @param {HTMLTableRowElement} row
565
- * @protected
566
- */
567
- __debounceUpdateHeaderFooterRowVisibility(row) {
568
- row.__debounceUpdateHeaderFooterRowVisibility = Debouncer.debounce(
569
- row.__debounceUpdateHeaderFooterRowVisibility,
570
- microTask,
571
- () => this.__updateHeaderFooterRowVisibility(row),
572
- );
573
- }
574
-
575
- /**
576
- * @param {HTMLTableRowElement} row
577
- * @protected
578
- */
579
- __updateHeaderFooterRowVisibility(row) {
580
- if (!row) {
581
- return;
582
- }
583
-
584
- const visibleRowCells = Array.from(row.children).filter((cell) => {
585
- const column = cell._column;
586
- if (column._emptyCells && column._emptyCells.indexOf(cell) > -1) {
587
- // The cell is an "empty cell" -> doesn't block hiding the row
588
- return false;
589
- }
590
- if (row.parentElement === this.$.header) {
591
- if (column.headerRenderer) {
592
- // The cell is the header cell of a column that has a header renderer
593
- // -> row should be visible
594
- return true;
595
- }
596
- if (column.header === null) {
597
- // The column header is explicilty set to null -> doesn't block hiding the row
598
- return false;
599
- }
600
- if (column.path || column.header !== undefined) {
601
- // The column has an explicit non-null header or a path that generates a header
602
- // -> row should be visible
603
- return true;
604
- }
605
- } else if (column.footerRenderer) {
606
- // The cell is the footer cell of a column that has a footer renderer
607
- // -> row should be visible
608
- return true;
609
- }
610
- return false;
611
- });
612
-
613
- if (row.hidden !== !visibleRowCells.length) {
614
- row.hidden = !visibleRowCells.length;
615
- }
616
-
617
- if (row.parentElement === this.$.header) {
618
- this.$.table.toggleAttribute('has-header', this.$.header.querySelector('tr:not([hidden])'));
619
- this.__updateHeaderFooterRowParts('header');
620
- }
621
-
622
- if (row.parentElement === this.$.footer) {
623
- this.$.table.toggleAttribute('has-footer', this.$.footer.querySelector('tr:not([hidden])'));
624
- this.__updateHeaderFooterRowParts('footer');
625
- }
626
-
627
- // Make sure the section has a tabbable element
628
- this._resetKeyboardNavigation();
629
- this.__a11yUpdateGridSize(this.size, this._columnTree, this.__emptyState);
630
- }
631
-
632
537
  /** @private */
633
538
  __updateVirtualizerElement(row, index) {
634
539
  this._preventScrollerRotatingCellFocus(row, index);
@@ -681,46 +586,17 @@ export const GridMixin = (superClass) =>
681
586
  */
682
587
  _renderColumnTree(columnTree) {
683
588
  iterateChildren(this.$.items, (row) => {
684
- this.__initRow(row, columnTree[columnTree.length - 1], 'body', false, true);
589
+ this.__initRow(row, columnTree[columnTree.length - 1], 'body', true);
685
590
  this.__updateRow(row);
686
591
  });
687
592
 
688
- while (this.$.header.children.length < columnTree.length) {
689
- const headerRow = document.createElement('tr');
690
- headerRow.setAttribute('role', 'row');
691
- headerRow.setAttribute('tabindex', '-1');
692
- updatePart(headerRow, 'row', true);
693
- updatePart(headerRow, 'header-row', true);
694
- this.$.header.appendChild(headerRow);
695
-
696
- const footerRow = document.createElement('tr');
697
- footerRow.setAttribute('role', 'row');
698
- footerRow.setAttribute('tabindex', '-1');
699
- updatePart(footerRow, 'row', true);
700
- updatePart(footerRow, 'footer-row', true);
701
- this.$.footer.appendChild(footerRow);
702
- }
703
- while (this.$.header.children.length > columnTree.length) {
704
- this.$.header.removeChild(this.$.header.firstElementChild);
705
- this.$.footer.removeChild(this.$.footer.firstElementChild);
706
- }
707
-
708
- iterateChildren(this.$.header, (headerRow, index) => {
709
- this.__initRow(headerRow, columnTree[index], 'header', index === columnTree.length - 1);
710
- });
711
-
712
- iterateChildren(this.$.footer, (footerRow, index) => {
713
- this.__initRow(footerRow, columnTree[columnTree.length - 1 - index], 'footer', index === 0);
714
- });
593
+ this.__renderHeaderFooter();
715
594
 
716
595
  // Sizer rows
717
596
  this.__initRow(this.$.sizer, columnTree[columnTree.length - 1]);
718
597
 
719
- this.__updateHeaderFooterRowParts('header');
720
- this.__updateHeaderFooterRowParts('footer');
721
598
  this._resizeHandler();
722
599
  this._frozenCellsChanged();
723
- this._updateFirstAndLastColumn();
724
600
  this._resetKeyboardNavigation();
725
601
  this.__a11yUpdateHeaderRows();
726
602
  this.__a11yUpdateFooterRows();
@@ -728,20 +604,6 @@ export const GridMixin = (superClass) =>
728
604
  this.__updateHeaderAndFooter();
729
605
  }
730
606
 
731
- /** @private */
732
- __updateHeaderFooterRowParts(section) {
733
- const visibleRows = [...this.$[section].querySelectorAll('tr:not([hidden])')];
734
- [...this.$[section].children].forEach((row) => {
735
- updatePart(row, `first-${section}-row`, row === visibleRows.at(0));
736
- updatePart(row, `last-${section}-row`, row === visibleRows.at(-1));
737
-
738
- getBodyRowCells(row).forEach((cell) => {
739
- updatePart(cell, `first-${section}-row-cell`, row === visibleRows.at(0));
740
- updatePart(cell, `last-${section}-row-cell`, row === visibleRows.at(-1));
741
- });
742
- });
743
- }
744
-
745
607
  /**
746
608
  * @param {!HTMLElement} row
747
609
  * @param {boolean} loading
@@ -46,16 +46,25 @@ export const GridSortColumnMixin = (superClass) =>
46
46
  */
47
47
  _defaultHeaderRenderer(root, _column) {
48
48
  let sorter = root.firstElementChild;
49
- if (!sorter) {
49
+ const isNewSorter = !sorter;
50
+ if (isNewSorter) {
50
51
  sorter = document.createElement('vaadin-grid-sorter');
51
52
  sorter.addEventListener('direction-changed', this.__boundOnDirectionChanged);
52
- root.appendChild(sorter);
53
53
  }
54
54
 
55
55
  sorter.path = this.path;
56
56
  sorter.__rendererDirection = this.direction;
57
57
  sorter.direction = this.direction;
58
58
  sorter.textContent = this.__getHeader(this.header, this.path);
59
+
60
+ // Append the sorter only after its direction has been set. If the cell
61
+ // content is already connected (as with the declarative header rendering),
62
+ // appending an unconfigured sorter makes it notify its default `null`
63
+ // direction before `__rendererDirection` is set, which would reset the
64
+ // column's direction. See __onDirectionChanged.
65
+ if (isNewSorter) {
66
+ root.appendChild(sorter);
67
+ }
59
68
  }
60
69
 
61
70
  /**
@@ -51,6 +51,7 @@ import { GridSorterMixin } from './vaadin-grid-sorter-mixin.js';
51
51
  * @fires {CustomEvent} direction-changed - Fired when the `direction` property changes.
52
52
  * @fires {CustomEvent} sorter-changed - Fired when the `path` or `direction` property changes.
53
53
  *
54
+ * @attr {string} theme - The theme variants to apply to the component.
54
55
  * @customElement vaadin-grid-sorter
55
56
  * @extends HTMLElement
56
57
  */
@@ -59,6 +59,7 @@ import { GridTreeToggleMixin } from './vaadin-grid-tree-toggle-mixin.js';
59
59
  *
60
60
  * @fires {CustomEvent} expanded-changed - Fired when the `expanded` property changes.
61
61
  *
62
+ * @attr {string} theme - The theme variants to apply to the component.
62
63
  * @customElement vaadin-grid-tree-toggle
63
64
  * @extends HTMLElement
64
65
  */
@@ -274,6 +274,7 @@ const DEFAULT_I18N = {
274
274
  * @fires {CustomEvent} size-changed - Fired when the `size` property changes.
275
275
  * @fires {CustomEvent} item-toggle - Fired when the user selects or deselects an item through the selection column.
276
276
  *
277
+ * @attr {string} theme - The theme variants to apply to the component.
277
278
  * @customElement vaadin-grid
278
279
  * @extends HTMLElement
279
280
  */