@vaadin/grid 25.2.7 → 25.3.0-alpha10

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.
Files changed (33) hide show
  1. package/custom-elements.json +248 -128
  2. package/package.json +12 -12
  3. package/src/directives/cell-content-directive.js +33 -0
  4. package/src/styles/vaadin-grid-base-styles.js +0 -4
  5. package/src/vaadin-grid-a11y-mixin.js +20 -31
  6. package/src/vaadin-grid-active-item-mixin.js +1 -1
  7. package/src/vaadin-grid-column-auto-width-mixin.js +4 -13
  8. package/src/vaadin-grid-column-group-mixin.js +5 -20
  9. package/src/vaadin-grid-column-mixin.d.ts +3 -3
  10. package/src/vaadin-grid-column-mixin.js +57 -98
  11. package/src/vaadin-grid-column-reordering-mixin.js +10 -5
  12. package/src/vaadin-grid-column-resizing-mixin.js +4 -0
  13. package/src/vaadin-grid-data-provider-mixin.js +1 -1
  14. package/src/vaadin-grid-drag-and-drop-mixin.js +2 -5
  15. package/src/vaadin-grid-dynamic-columns-mixin.js +0 -5
  16. package/src/vaadin-grid-filter-column-mixin.js +1 -1
  17. package/src/vaadin-grid-filter-element-mixin.js +1 -3
  18. package/src/vaadin-grid-filter.js +1 -0
  19. package/src/vaadin-grid-header-footer-rendering-mixin.js +296 -0
  20. package/src/vaadin-grid-helpers.js +13 -0
  21. package/src/vaadin-grid-keyboard-navigation-mixin.js +3 -7
  22. package/src/vaadin-grid-mixin.js +69 -228
  23. package/src/vaadin-grid-row-details-mixin.js +1 -1
  24. package/src/vaadin-grid-scroll-mixin.js +2 -11
  25. package/src/vaadin-grid-selection-column-base-mixin.js +66 -21
  26. package/src/vaadin-grid-sort-column-mixin.js +11 -2
  27. package/src/vaadin-grid-sorter-mixin.js +22 -2
  28. package/src/vaadin-grid-sorter.js +2 -1
  29. package/src/vaadin-grid-tree-toggle.js +1 -0
  30. package/src/vaadin-grid.d.ts +63 -1
  31. package/src/vaadin-grid.js +51 -2
  32. package/web-types.json +29 -94
  33. package/web-types.lit.json +17 -10
@@ -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
+ };
@@ -5,6 +5,19 @@
5
5
  */
6
6
  import { microTask } from '@vaadin/component-base/src/async.js';
7
7
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
+ import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
9
+
10
+ /**
11
+ * Returns the grid cell that contains the given node, or `undefined` if the
12
+ * node is not inside a cell's content.
13
+ *
14
+ * @param {Node} node
15
+ * @return {HTMLElement | undefined}
16
+ */
17
+ export function getClosestCell(node) {
18
+ const content = getClosestElement('vaadin-grid-cell-content', node);
19
+ return content?.assignedSlot?.parentElement;
20
+ }
8
21
 
9
22
  /**
10
23
  * Returns the cells of the given row, excluding the details cell.
@@ -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,