@vaadin/grid 25.3.0-alpha7 → 25.3.0-alpha9

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,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
+ };
@@ -154,9 +154,7 @@ export const KeyboardNavigationMixin = (superClass) =>
154
154
 
155
155
  /** @private */
156
156
  _focusableChanged(focusable, oldFocusable) {
157
- if (oldFocusable) {
158
- oldFocusable.setAttribute('tabindex', '-1');
159
- }
157
+ oldFocusable?.setAttribute('tabindex', '-1');
160
158
  if (focusable) {
161
159
  this._updateGridSectionFocusTarget(focusable);
162
160
  }
@@ -415,9 +413,7 @@ export const KeyboardNavigationMixin = (superClass) =>
415
413
  _onRowNavigation(activeRow, dy) {
416
414
  const { dstRow } = this.__navigateRows(dy, activeRow);
417
415
 
418
- if (dstRow) {
419
- dstRow.focus();
420
- }
416
+ dstRow?.focus();
421
417
  }
422
418
 
423
419
  /** @private */
@@ -765,7 +761,7 @@ export const KeyboardNavigationMixin = (superClass) =>
765
761
  // When clicking a cell with only text nodes, skip activating the cell
766
762
  // on click, since that is already handled on keydown.
767
763
  const cell = e.composedPath()[0];
768
- const target = (cell._content && cell._content.firstElementChild) || cell;
764
+ const target = cell._content?.firstElementChild || cell;
769
765
  const wasNavigating = this.hasAttribute('navigating');
770
766
  const clickEvent = new MouseEvent('click', {
771
767
  shiftKey: e.shiftKey,
@@ -13,7 +13,6 @@ import { A11yMixin } from './vaadin-grid-a11y-mixin.js';
13
13
  import { ActiveItemMixin } from './vaadin-grid-active-item-mixin.js';
14
14
  import { ArrayDataProviderMixin } from './vaadin-grid-array-data-provider-mixin.js';
15
15
  import { ColumnAutoWidthMixin } from './vaadin-grid-column-auto-width-mixin.js';
16
- import { ColumnRenderingMixin } from './vaadin-grid-column-rendering-mixin.js';
17
16
  import { ColumnReorderingMixin } from './vaadin-grid-column-reordering-mixin.js';
18
17
  import { ColumnResizingMixin } from './vaadin-grid-column-resizing-mixin.js';
19
18
  import { DataProviderMixin } from './vaadin-grid-data-provider-mixin.js';
@@ -21,6 +20,7 @@ import { DragAndDropMixin } from './vaadin-grid-drag-and-drop-mixin.js';
21
20
  import { DynamicColumnsMixin } from './vaadin-grid-dynamic-columns-mixin.js';
22
21
  import { EventContextMixin } from './vaadin-grid-event-context-mixin.js';
23
22
  import { FilterMixin } from './vaadin-grid-filter-mixin.js';
23
+ import { HeaderFooterRenderingMixin } from './vaadin-grid-header-footer-rendering-mixin.js';
24
24
  import {
25
25
  getBodyRowCells,
26
26
  getClosestCell,
@@ -47,7 +47,7 @@ export const GridMixin = (superClass) =>
47
47
  ArrayDataProviderMixin(
48
48
  DataProviderMixin(
49
49
  DynamicColumnsMixin(
50
- ColumnRenderingMixin(
50
+ HeaderFooterRenderingMixin(
51
51
  ActiveItemMixin(
52
52
  ScrollMixin(
53
53
  SelectionMixin(
@@ -152,13 +152,13 @@ export const GridMixin = (superClass) =>
152
152
  /** @private */
153
153
  get _firstVisibleIndex() {
154
154
  const firstVisibleItem = this.__getFirstVisibleItem();
155
- return firstVisibleItem ? firstVisibleItem.index : undefined;
155
+ return firstVisibleItem?.index;
156
156
  }
157
157
 
158
158
  /** @private */
159
159
  get _lastVisibleIndex() {
160
160
  const lastVisibleItem = this.__getLastVisibleItem();
161
- return lastVisibleItem ? lastVisibleItem.index : undefined;
161
+ return lastVisibleItem?.index;
162
162
  }
163
163
 
164
164
  constructor() {
@@ -247,13 +247,6 @@ export const GridMixin = (superClass) =>
247
247
  // otherwise be triggered by this logic because it reads the row height
248
248
  // right after updating the rows' content.
249
249
  __disableHeightPlaceholder: true,
250
- // The virtualizer amortizes scroller height updates to avoid reflows while
251
- // scrolling. In `allRowsVisible` mode the grid has no scrolling and its
252
- // height must track the content exactly, so tell the virtualizer to always
253
- // apply the scroller height. Otherwise the items container can be left at a
254
- // stale, too-small height and clip rows when the grid grows (e.g. when
255
- // expanding a tree grid from a small size).
256
- __alwaysUpdateScrollerSize: () => this.allRowsVisible,
257
250
  });
258
251
 
259
252
  this._tooltipController = new TooltipController(this);
@@ -305,9 +298,7 @@ export const GridMixin = (superClass) =>
305
298
  __focusBodyCell({ item, column }) {
306
299
  const row = this._getRenderedRows().find((row) => row._item === item);
307
300
  const cell = row && [...row.children].find((cell) => cell._column === column);
308
- if (cell) {
309
- cell.focus();
310
- }
301
+ cell?.focus();
311
302
  }
312
303
 
313
304
  /** @protected */
@@ -597,7 +588,6 @@ export const GridMixin = (superClass) =>
597
588
 
598
589
  this._resizeHandler();
599
590
  this._frozenCellsChanged();
600
- this._updateFirstAndLastColumn();
601
591
  this._resetKeyboardNavigation();
602
592
  this.__a11yUpdateHeaderRows();
603
593
  this.__a11yUpdateFooterRows();
@@ -605,22 +595,6 @@ export const GridMixin = (superClass) =>
605
595
  this.__updateHeaderAndFooter();
606
596
  }
607
597
 
608
- /** @private */
609
- __updateHeaderFooterRowParts(section) {
610
- const visibleRows = [...this.$[section].querySelectorAll('tr:not([hidden])')];
611
- [...this.$[section].children].forEach((row) => {
612
- updatePart(row, `first-${section}-row`, row === visibleRows.at(0));
613
- updatePart(row, `last-${section}-row`, row === visibleRows.at(-1));
614
-
615
- getBodyRowCells(row).forEach((cell) => {
616
- updatePart(cell, `first-${section}-row-cell`, row === visibleRows.at(0));
617
- updatePart(cell, `last-${section}-row-cell`, row === visibleRows.at(-1));
618
- });
619
-
620
- this._updateFirstAndLastColumnForRow(row);
621
- });
622
- }
623
-
624
598
  /**
625
599
  * @param {!HTMLElement} row
626
600
  * @param {boolean} loading
@@ -180,7 +180,7 @@ export const RowDetailsMixin = (superClass) =>
180
180
  * @protected
181
181
  */
182
182
  _isDetailsOpened(item) {
183
- return this.__detailsOpenedKeys && this.__detailsOpenedKeys.has(this.getItemId(item));
183
+ return this.__detailsOpenedKeys?.has(this.getItemId(item));
184
184
  }
185
185
 
186
186
  /** @private */
@@ -6,6 +6,7 @@
6
6
  import { microTask, timeOut } from '@vaadin/component-base/src/async.js';
7
7
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
8
  import { getNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
9
+ import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
9
10
  import { OverflowController } from '@vaadin/component-base/src/overflow-controller.js';
10
11
 
11
12
  const timeouts = {
@@ -377,11 +378,7 @@ export const ScrollMixin = (superClass) =>
377
378
 
378
379
  /** @private */
379
380
  __columnRenderingChanged(_columnTree, columnRendering) {
380
- if (columnRendering === 'eager') {
381
- this.$.scroller.removeAttribute('column-rendering');
382
- } else {
383
- this.$.scroller.setAttribute('column-rendering', columnRendering);
384
- }
381
+ setOrRemoveAttribute(this.$.scroller, 'column-rendering', columnRendering !== 'eager' && columnRendering);
385
382
 
386
383
  this.__updateColumnsBodyContentHidden();
387
384
  }
@@ -174,8 +174,8 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
174
174
  this._headerCell.appendChild(label);
175
175
  }
176
176
  label.textContent = selectAllUnavailable;
177
- } else if (label) {
178
- label.remove();
177
+ } else {
178
+ label?.remove();
179
179
  }
180
180
  }
181
181
 
@@ -383,7 +383,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
383
383
 
384
384
  // Get the index of the row being hovered over or the first/last
385
385
  // visible row if hovering outside the grid
386
- let hoveredIndex = hoveredRow ? hoveredRow.index : undefined;
386
+ let hoveredIndex = hoveredRow?.index;
387
387
  const scrollableArea = this.__getScrollableArea();
388
388
  if (this.__dragCurrentY < scrollableArea.top) {
389
389
  hoveredIndex = this._grid._firstVisibleIndex;
@@ -3,6 +3,7 @@
3
3
  * Copyright (c) 2016 - 2026 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
6
7
  import { getClosestCell } from './vaadin-grid-helpers.js';
7
8
 
8
9
  /**
@@ -97,11 +98,7 @@ export const GridSorterMixin = (superClass) =>
97
98
  }
98
99
 
99
100
  const ariaLabel = grid.__effectiveI18n.sorter?.replace('{column}', this.textContent.trim());
100
- if (ariaLabel) {
101
- this.setAttribute('aria-label', ariaLabel);
102
- } else {
103
- this.removeAttribute('aria-label');
104
- }
101
+ setOrRemoveAttribute(this, 'aria-label', ariaLabel);
105
102
  }
106
103
 
107
104
  /** @private */
@@ -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
  */