@vaadin/grid 25.3.0-dev.3a3c2d7d2a → 25.3.0-rc1

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 +194 -135
  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 +7 -0
  5. package/src/vaadin-grid-a11y-mixin.js +15 -28
  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 -109
  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 +2 -2
  14. package/src/vaadin-grid-drag-and-drop-mixin.js +14 -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-keyboard-navigation-mixin.js +3 -7
  21. package/src/vaadin-grid-mixin.js +67 -229
  22. package/src/vaadin-grid-row-details-mixin.js +1 -1
  23. package/src/vaadin-grid-scroll-mixin.js +7 -32
  24. package/src/vaadin-grid-selection-column-base-mixin.js +36 -20
  25. package/src/vaadin-grid-sort-column-mixin.js +11 -2
  26. package/src/vaadin-grid-sorter-mixin.js +3 -7
  27. package/src/vaadin-grid-sorter.js +1 -0
  28. package/src/vaadin-grid-tree-toggle.d.ts +5 -3
  29. package/src/vaadin-grid-tree-toggle.js +6 -3
  30. package/src/vaadin-grid.d.ts +33 -0
  31. package/src/vaadin-grid.js +30 -1
  32. package/web-types.json +22 -96
  33. package/web-types.lit.json +12 -12
@@ -4,6 +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 { isChrome, isSafari } from '@vaadin/component-base/src/browser-utils.js';
7
+ import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
7
8
  import {
8
9
  getBodyRowCells,
9
10
  iterateChildren,
@@ -481,12 +482,20 @@ export const DragAndDropMixin = (superClass) =>
481
482
  const dragDisabled = !this.rowsDraggable || loading || (this.dragFilter && !this.dragFilter(model));
482
483
  const dropDisabled = !this.dropMode || loading || (this.dropFilter && !this.dropFilter(model));
483
484
 
485
+ // Chromium computes an empty accessible name for cells whose content
486
+ // element has the `draggable` attribute when the accessibility tree is
487
+ // built (https://issues.chromium.org/issues/534049472,
488
+ // https://github.com/vaadin/web-components/issues/11726). Blink maps
489
+ // `draggable="true"` to the `-webkit-user-drag: element` and
490
+ // `user-select: none` styles, and the accessible-name bug is keyed to the
491
+ // attribute, not the styles. So on Chromium, mark the draggable cell
492
+ // content with the `draggable-source` attribute, which applies the
493
+ // equivalent styles from the grid styles, instead of the `draggable`
494
+ // attribute. This can be removed once the Chromium issue is fixed.
495
+ const draggableAttribute = isChrome ? 'draggable-source' : 'draggable';
496
+
484
497
  iterateRowCells(row, (cell) => {
485
- if (dragDisabled) {
486
- cell._content.removeAttribute('draggable');
487
- } else {
488
- cell._content.setAttribute('draggable', true);
489
- }
498
+ setOrRemoveAttribute(cell._content, draggableAttribute, !dragDisabled);
490
499
  });
491
500
 
492
501
  updateBooleanRowStates(row, {
@@ -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,7 +37,7 @@ export const GridFilterColumnMixin = (superClass) =>
37
37
  */
38
38
  _defaultHeaderRenderer(root, _column) {
39
39
  let filter = root.firstElementChild;
40
- let textField = filter ? filter.firstElementChild : undefined;
40
+ let textField = filter?.firstElementChild;
41
41
 
42
42
  if (!filter) {
43
43
  filter = document.createElement('vaadin-grid-filter');
@@ -71,8 +71,6 @@ export const GridFilterElementMixin = (superClass) =>
71
71
  }
72
72
 
73
73
  focus() {
74
- if (this._textField) {
75
- this._textField.focus();
76
- }
74
+ this._textField?.focus();
77
75
  }
78
76
  };
@@ -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._flatSize, 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,