@vaadin/grid 24.2.0-dev.f254716fe → 24.2.0

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,1025 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2016 - 2023 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { isElementHidden } from '@vaadin/a11y-base/src/focus-utils.js';
7
+ import { TabindexMixin } from '@vaadin/a11y-base/src/tabindex-mixin.js';
8
+ import { animationFrame, microTask } from '@vaadin/component-base/src/async.js';
9
+ import { isAndroid, isChrome, isFirefox, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
10
+ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
11
+ import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
12
+ import { processTemplates } from '@vaadin/component-base/src/templates.js';
13
+ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
14
+ import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
15
+ import { A11yMixin } from './vaadin-grid-a11y-mixin.js';
16
+ import { ActiveItemMixin } from './vaadin-grid-active-item-mixin.js';
17
+ import { ArrayDataProviderMixin } from './vaadin-grid-array-data-provider-mixin.js';
18
+ import { ColumnReorderingMixin } from './vaadin-grid-column-reordering-mixin.js';
19
+ import { ColumnResizingMixin } from './vaadin-grid-column-resizing-mixin.js';
20
+ import { DataProviderMixin } from './vaadin-grid-data-provider-mixin.js';
21
+ import { DragAndDropMixin } from './vaadin-grid-drag-and-drop-mixin.js';
22
+ import { DynamicColumnsMixin } from './vaadin-grid-dynamic-columns-mixin.js';
23
+ import { EventContextMixin } from './vaadin-grid-event-context-mixin.js';
24
+ import { FilterMixin } from './vaadin-grid-filter-mixin.js';
25
+ import {
26
+ getBodyRowCells,
27
+ iterateChildren,
28
+ iterateRowCells,
29
+ updateBooleanRowStates,
30
+ updateCellsPart,
31
+ } from './vaadin-grid-helpers.js';
32
+ import { KeyboardNavigationMixin } from './vaadin-grid-keyboard-navigation-mixin.js';
33
+ import { RowDetailsMixin } from './vaadin-grid-row-details-mixin.js';
34
+ import { ScrollMixin } from './vaadin-grid-scroll-mixin.js';
35
+ import { SelectionMixin } from './vaadin-grid-selection-mixin.js';
36
+ import { SortMixin } from './vaadin-grid-sort-mixin.js';
37
+ import { StylingMixin } from './vaadin-grid-styling-mixin.js';
38
+
39
+ /**
40
+ * A mixin providing common grid functionality.
41
+ *
42
+ * @polymerMixin
43
+ * @mixes A11yMixin
44
+ * @mixes ActiveItemMixin
45
+ * @mixes ArrayDataProviderMixin
46
+ * @mixes ColumnResizingMixin
47
+ * @mixes DataProviderMixin
48
+ * @mixes DynamicColumnsMixin
49
+ * @mixes FilterMixin
50
+ * @mixes RowDetailsMixin
51
+ * @mixes ScrollMixin
52
+ * @mixes SelectionMixin
53
+ * @mixes SortMixin
54
+ * @mixes KeyboardNavigationMixin
55
+ * @mixes ColumnReorderingMixin
56
+ * @mixes EventContextMixin
57
+ * @mixes StylingMixin
58
+ * @mixes DragAndDropMixin
59
+ */
60
+ export const GridMixin = (superClass) =>
61
+ class extends DataProviderMixin(
62
+ ArrayDataProviderMixin(
63
+ DynamicColumnsMixin(
64
+ ActiveItemMixin(
65
+ ScrollMixin(
66
+ SelectionMixin(
67
+ SortMixin(
68
+ RowDetailsMixin(
69
+ KeyboardNavigationMixin(
70
+ A11yMixin(
71
+ FilterMixin(
72
+ ColumnReorderingMixin(
73
+ ColumnResizingMixin(
74
+ EventContextMixin(DragAndDropMixin(StylingMixin(TabindexMixin(superClass)))),
75
+ ),
76
+ ),
77
+ ),
78
+ ),
79
+ ),
80
+ ),
81
+ ),
82
+ ),
83
+ ),
84
+ ),
85
+ ),
86
+ ),
87
+ ) {
88
+ static get observers() {
89
+ return [
90
+ '_columnTreeChanged(_columnTree, _columnTree.*)',
91
+ '_effectiveSizeChanged(_effectiveSize, __virtualizer, _hasData, _columnTree)',
92
+ ];
93
+ }
94
+
95
+ static get properties() {
96
+ return {
97
+ /** @private */
98
+ _safari: {
99
+ type: Boolean,
100
+ value: isSafari,
101
+ },
102
+
103
+ /** @private */
104
+ _ios: {
105
+ type: Boolean,
106
+ value: isIOS,
107
+ },
108
+
109
+ /** @private */
110
+ _firefox: {
111
+ type: Boolean,
112
+ value: isFirefox,
113
+ },
114
+
115
+ /** @private */
116
+ _android: {
117
+ type: Boolean,
118
+ value: isAndroid,
119
+ },
120
+
121
+ /** @private */
122
+ _touchDevice: {
123
+ type: Boolean,
124
+ value: isTouch,
125
+ },
126
+
127
+ /**
128
+ * If true, the grid's height is defined by its rows.
129
+ *
130
+ * Effectively, this disables the grid's virtual scrolling so that all the rows are rendered in the DOM at once.
131
+ * If the grid has a large number of items, using the feature is discouraged to avoid performance issues.
132
+ * @attr {boolean} all-rows-visible
133
+ * @type {boolean}
134
+ */
135
+ allRowsVisible: {
136
+ type: Boolean,
137
+ value: false,
138
+ reflectToAttribute: true,
139
+ },
140
+
141
+ /** @private */
142
+ __pendingRecalculateColumnWidths: {
143
+ type: Boolean,
144
+ value: true,
145
+ },
146
+
147
+ /** @private */
148
+ isAttached: {
149
+ value: false,
150
+ },
151
+
152
+ /**
153
+ * An internal property that is mainly used by `vaadin-template-renderer`
154
+ * to identify grid elements.
155
+ *
156
+ * @private
157
+ */
158
+ __gridElement: {
159
+ type: Boolean,
160
+ value: true,
161
+ },
162
+ };
163
+ }
164
+
165
+ constructor() {
166
+ super();
167
+ this.addEventListener('animationend', this._onAnimationEnd);
168
+ }
169
+
170
+ /** @private */
171
+ get _firstVisibleIndex() {
172
+ const firstVisibleItem = this.__getFirstVisibleItem();
173
+ return firstVisibleItem ? firstVisibleItem.index : undefined;
174
+ }
175
+
176
+ /** @private */
177
+ get _lastVisibleIndex() {
178
+ const lastVisibleItem = this.__getLastVisibleItem();
179
+ return lastVisibleItem ? lastVisibleItem.index : undefined;
180
+ }
181
+
182
+ /** @protected */
183
+ connectedCallback() {
184
+ super.connectedCallback();
185
+ this.isAttached = true;
186
+ this.recalculateColumnWidths();
187
+ }
188
+
189
+ /** @protected */
190
+ disconnectedCallback() {
191
+ super.disconnectedCallback();
192
+ this.isAttached = false;
193
+ this._hideTooltip(true);
194
+ }
195
+
196
+ /** @private */
197
+ __getFirstVisibleItem() {
198
+ return this._getRenderedRows().find((row) => this._isInViewport(row));
199
+ }
200
+
201
+ /** @private */
202
+ __getLastVisibleItem() {
203
+ return this._getRenderedRows()
204
+ .reverse()
205
+ .find((row) => this._isInViewport(row));
206
+ }
207
+
208
+ /** @private */
209
+ _isInViewport(item) {
210
+ const scrollTargetRect = this.$.table.getBoundingClientRect();
211
+ const itemRect = item.getBoundingClientRect();
212
+ const headerHeight = this.$.header.getBoundingClientRect().height;
213
+ const footerHeight = this.$.footer.getBoundingClientRect().height;
214
+ return (
215
+ itemRect.bottom > scrollTargetRect.top + headerHeight && itemRect.top < scrollTargetRect.bottom - footerHeight
216
+ );
217
+ }
218
+
219
+ /** @private */
220
+ _getRenderedRows() {
221
+ return Array.from(this.$.items.children)
222
+ .filter((item) => !item.hidden)
223
+ .sort((a, b) => a.index - b.index);
224
+ }
225
+
226
+ /** @protected */
227
+ _getRowContainingNode(node) {
228
+ const content = getClosestElement('vaadin-grid-cell-content', node);
229
+ if (!content) {
230
+ return;
231
+ }
232
+
233
+ const cell = content.assignedSlot.parentElement;
234
+ return cell.parentElement;
235
+ }
236
+
237
+ /** @protected */
238
+ _isItemAssignedToRow(item, row) {
239
+ const model = this.__getRowModel(row);
240
+ return this.getItemId(item) === this.getItemId(model.item);
241
+ }
242
+
243
+ /** @protected */
244
+ ready() {
245
+ super.ready();
246
+
247
+ this.__virtualizer = new Virtualizer({
248
+ createElements: this._createScrollerRows.bind(this),
249
+ updateElement: this._updateScrollerItem.bind(this),
250
+ scrollContainer: this.$.items,
251
+ scrollTarget: this.$.table,
252
+ reorderElements: true,
253
+ });
254
+
255
+ new ResizeObserver(() =>
256
+ setTimeout(() => {
257
+ this.__updateFooterPositioning();
258
+ this.__updateColumnsBodyContentHidden();
259
+ this.__tryToRecalculateColumnWidthsIfPending();
260
+ }),
261
+ ).observe(this.$.table);
262
+
263
+ processTemplates(this);
264
+
265
+ this._tooltipController = new TooltipController(this);
266
+ this.addController(this._tooltipController);
267
+ this._tooltipController.setManual(true);
268
+ }
269
+
270
+ /** @private */
271
+ __getBodyCellCoordinates(cell) {
272
+ if (this.$.items.contains(cell) && cell.localName === 'td') {
273
+ return {
274
+ item: cell.parentElement._item,
275
+ column: cell._column,
276
+ };
277
+ }
278
+ }
279
+
280
+ /** @private */
281
+ __focusBodyCell({ item, column }) {
282
+ const row = this._getRenderedRows().find((row) => row._item === item);
283
+ const cell = row && [...row.children].find((cell) => cell._column === column);
284
+ if (cell) {
285
+ cell.focus();
286
+ }
287
+ }
288
+
289
+ /** @protected */
290
+ _focusFirstVisibleRow() {
291
+ const row = this.__getFirstVisibleItem();
292
+ this.__rowFocusMode = true;
293
+ row.focus();
294
+ }
295
+
296
+ /** @private */
297
+ _effectiveSizeChanged(effectiveSize, virtualizer, hasData, columnTree) {
298
+ if (virtualizer && hasData && columnTree) {
299
+ // Changing the virtualizer size may result in the row with focus getting hidden
300
+ const cell = this.shadowRoot.activeElement;
301
+ const cellCoordinates = this.__getBodyCellCoordinates(cell);
302
+
303
+ const previousSize = virtualizer.size || 0;
304
+ virtualizer.size = effectiveSize;
305
+
306
+ // Request an update for the previous last row to have the "last" state removed
307
+ virtualizer.update(previousSize - 1, previousSize - 1);
308
+ if (effectiveSize < previousSize) {
309
+ // Size was decreased, so the new last row requires an explicit update
310
+ virtualizer.update(effectiveSize - 1, effectiveSize - 1);
311
+ }
312
+
313
+ // If the focused cell's parent row got hidden by the size change, focus the corresponding new cell
314
+ if (cellCoordinates && cell.parentElement.hidden) {
315
+ this.__focusBodyCell(cellCoordinates);
316
+ }
317
+
318
+ // Make sure the body has a tabbable element
319
+ this._resetKeyboardNavigation();
320
+ }
321
+ }
322
+
323
+ /** @private */
324
+ __hasRowsWithClientHeight() {
325
+ return !!Array.from(this.$.items.children).filter((row) => row.clientHeight).length;
326
+ }
327
+
328
+ /** @private */
329
+ __getIntrinsicWidth(col) {
330
+ if (!this.__intrinsicWidthCache.has(col)) {
331
+ this.__calculateAndCacheIntrinsicWidths([col]);
332
+ }
333
+ return this.__intrinsicWidthCache.get(col);
334
+ }
335
+
336
+ /** @private */
337
+ __getDistributedWidth(col, innerColumn) {
338
+ if (col == null || col === this) {
339
+ return 0;
340
+ }
341
+
342
+ const columnWidth = Math.max(
343
+ this.__getIntrinsicWidth(col),
344
+ this.__getDistributedWidth((col.assignedSlot || col).parentElement, col),
345
+ );
346
+
347
+ // We're processing a regular grid-column and not a grid-column-group
348
+ if (!innerColumn) {
349
+ return columnWidth;
350
+ }
351
+
352
+ // At the end, the width of each vaadin-grid-column-group is determined by the sum of the width of its children.
353
+ // Here we determine how much space the vaadin-grid-column-group actually needs to render properly and then we distribute that space
354
+ // to its children, so when we actually do the summation it will be rendered properly.
355
+ // Check out vaadin-grid-column-group:_updateFlexAndWidth
356
+ const columnGroup = col;
357
+ const columnGroupWidth = columnWidth;
358
+ const sumOfWidthOfAllChildColumns = columnGroup._visibleChildColumns
359
+ .map((col) => this.__getIntrinsicWidth(col))
360
+ .reduce((sum, curr) => sum + curr, 0);
361
+
362
+ const extraNecessarySpaceForGridColumnGroup = Math.max(0, columnGroupWidth - sumOfWidthOfAllChildColumns);
363
+
364
+ // The distribution of the extra necessary space is done according to the intrinsic width of each child column.
365
+ // Lets say we need 100 pixels of extra space for the grid-column-group to render properly
366
+ // it has two grid-column children, |100px|300px| in total 400px
367
+ // the first column gets 25px of the additional space (100/400)*100 = 25
368
+ // the second column gets the 75px of the additional space (300/400)*100 = 75
369
+ const proportionOfExtraSpace = this.__getIntrinsicWidth(innerColumn) / sumOfWidthOfAllChildColumns;
370
+ const shareOfInnerColumnFromNecessaryExtraSpace = proportionOfExtraSpace * extraNecessarySpaceForGridColumnGroup;
371
+
372
+ return this.__getIntrinsicWidth(innerColumn) + shareOfInnerColumnFromNecessaryExtraSpace;
373
+ }
374
+
375
+ /**
376
+ * @param {!Array<!GridColumn>} cols the columns to auto size based on their content width
377
+ * @private
378
+ */
379
+ _recalculateColumnWidths(cols) {
380
+ // Flush to make sure DOM is up-to-date when measuring the column widths
381
+ this.__virtualizer.flush();
382
+ [...this.$.header.children, ...this.$.footer.children].forEach((row) => {
383
+ if (row.__debounceUpdateHeaderFooterRowVisibility) {
384
+ row.__debounceUpdateHeaderFooterRowVisibility.flush();
385
+ }
386
+ });
387
+
388
+ // Flush to account for any changes to the visibility of the columns
389
+ if (this._debouncerHiddenChanged) {
390
+ this._debouncerHiddenChanged.flush();
391
+ }
392
+
393
+ this.__intrinsicWidthCache = new Map();
394
+ // Cache the viewport rows to avoid unnecessary reflows while measuring the column widths
395
+ const fvi = this._firstVisibleIndex;
396
+ const lvi = this._lastVisibleIndex;
397
+ this.__viewportRowsCache = this._getRenderedRows().filter((row) => row.index >= fvi && row.index <= lvi);
398
+
399
+ // Pre-cache the intrinsic width of each column
400
+ this.__calculateAndCacheIntrinsicWidths(cols);
401
+
402
+ cols.forEach((col) => {
403
+ col.width = `${this.__getDistributedWidth(col)}px`;
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Toggles the cell content for the given column to use or not use auto width.
409
+ *
410
+ * While content for all the column cells uses auto width (instead of the default 100%),
411
+ * their offsetWidth can be used to calculate the collective intrinsic width of the column.
412
+ *
413
+ * @private
414
+ */
415
+ __setVisibleCellContentAutoWidth(col, autoWidth) {
416
+ col._allCells
417
+ .filter((cell) => {
418
+ if (this.$.items.contains(cell)) {
419
+ return this.__viewportRowsCache.includes(cell.parentElement);
420
+ }
421
+ return true;
422
+ })
423
+ .forEach((cell) => {
424
+ cell.__measuringAutoWidth = autoWidth;
425
+ cell._content.style.width = autoWidth ? 'auto' : '';
426
+ cell._content.style.position = autoWidth ? 'absolute' : '';
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Returns the maximum intrinsic width of the cell content in the given column.
432
+ * Only cells which are marked for measuring auto width are considered.
433
+ *
434
+ * @private
435
+ */
436
+ __getAutoWidthCellsMaxWidth(col) {
437
+ // Note: _allCells only contains cells which are currently rendered in DOM
438
+ return col._allCells.reduce((width, cell) => {
439
+ // Add 1px buffer to the offset width to avoid too narrow columns (sub-pixel rendering)
440
+ return cell.__measuringAutoWidth ? Math.max(width, cell._content.offsetWidth + 1) : width;
441
+ }, 0);
442
+ }
443
+
444
+ /**
445
+ * Calculates and caches the intrinsic width of each given column.
446
+ *
447
+ * @private
448
+ */
449
+ __calculateAndCacheIntrinsicWidths(cols) {
450
+ // Make all the columns use auto width at once before measuring to
451
+ // avoid reflows in between the measurements
452
+ cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, true));
453
+ // Measure and cache
454
+ cols.forEach((col) => {
455
+ const width = this.__getAutoWidthCellsMaxWidth(col);
456
+ this.__intrinsicWidthCache.set(col, width);
457
+ });
458
+ // Reset the columns to use 100% width
459
+ cols.forEach((col) => this.__setVisibleCellContentAutoWidth(col, false));
460
+ }
461
+
462
+ /**
463
+ * Updates the `width` of all columns which have `autoWidth` set to `true`.
464
+ */
465
+ recalculateColumnWidths() {
466
+ if (!this._columnTree) {
467
+ return; // No columns
468
+ }
469
+ if (isElementHidden(this) || this._cache.isLoading()) {
470
+ this.__pendingRecalculateColumnWidths = true;
471
+ return;
472
+ }
473
+ const cols = this._getColumns().filter((col) => !col.hidden && col.autoWidth);
474
+ this._recalculateColumnWidths(cols);
475
+ }
476
+
477
+ /** @private */
478
+ __tryToRecalculateColumnWidthsIfPending() {
479
+ if (
480
+ this.__pendingRecalculateColumnWidths &&
481
+ !isElementHidden(this) &&
482
+ !this._cache.isLoading() &&
483
+ this.__hasRowsWithClientHeight()
484
+ ) {
485
+ this.__pendingRecalculateColumnWidths = false;
486
+ this.recalculateColumnWidths();
487
+ }
488
+ }
489
+
490
+ /**
491
+ * @protected
492
+ * @override
493
+ */
494
+ _onDataProviderPageLoaded() {
495
+ super._onDataProviderPageLoaded();
496
+ this.__tryToRecalculateColumnWidthsIfPending();
497
+ }
498
+
499
+ /** @private */
500
+ _createScrollerRows(count) {
501
+ const rows = [];
502
+ for (let i = 0; i < count; i++) {
503
+ const row = document.createElement('tr');
504
+ row.setAttribute('part', 'row');
505
+ row.setAttribute('role', 'row');
506
+ row.setAttribute('tabindex', '-1');
507
+ if (this._columnTree) {
508
+ this._updateRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
509
+ }
510
+ rows.push(row);
511
+ }
512
+
513
+ if (this._columnTree) {
514
+ this._columnTree[this._columnTree.length - 1].forEach(
515
+ (c) => c.isConnected && c.notifyPath && c.notifyPath('_cells.*', c._cells),
516
+ );
517
+ }
518
+
519
+ this.__afterCreateScrollerRowsDebouncer = Debouncer.debounce(
520
+ this.__afterCreateScrollerRowsDebouncer,
521
+ animationFrame,
522
+ () => {
523
+ this._afterScroll();
524
+ this.__tryToRecalculateColumnWidthsIfPending();
525
+ },
526
+ );
527
+ return rows;
528
+ }
529
+
530
+ /** @private */
531
+ _createCell(tagName, column) {
532
+ const contentId = (this._contentIndex = this._contentIndex + 1 || 0);
533
+ const slotName = `vaadin-grid-cell-content-${contentId}`;
534
+
535
+ const cellContent = document.createElement('vaadin-grid-cell-content');
536
+ cellContent.setAttribute('slot', slotName);
537
+
538
+ const cell = document.createElement(tagName);
539
+ cell.id = slotName.replace('-content-', '-');
540
+ cell.setAttribute('role', tagName === 'td' ? 'gridcell' : 'columnheader');
541
+
542
+ // For now we only support tooltip on desktop
543
+ if (!isAndroid && !isIOS) {
544
+ cell.addEventListener('mouseenter', (event) => {
545
+ if (!this.$.scroller.hasAttribute('scrolling')) {
546
+ this._showTooltip(event);
547
+ }
548
+ });
549
+
550
+ cell.addEventListener('mouseleave', () => {
551
+ this._hideTooltip();
552
+ });
553
+
554
+ cell.addEventListener('mousedown', () => {
555
+ this._hideTooltip(true);
556
+ });
557
+ }
558
+
559
+ const slot = document.createElement('slot');
560
+ slot.setAttribute('name', slotName);
561
+
562
+ if (column && column._focusButtonMode) {
563
+ const div = document.createElement('div');
564
+ div.setAttribute('role', 'button');
565
+ div.setAttribute('tabindex', '-1');
566
+ cell.appendChild(div);
567
+
568
+ // Patch `focus()` to use the button
569
+ cell._focusButton = div;
570
+ cell.focus = function () {
571
+ cell._focusButton.focus();
572
+ };
573
+
574
+ div.appendChild(slot);
575
+ } else {
576
+ cell.setAttribute('tabindex', '-1');
577
+ cell.appendChild(slot);
578
+ }
579
+
580
+ cell._content = cellContent;
581
+
582
+ // With native Shadow DOM, mousedown on slotted element does not focus
583
+ // focusable slot wrapper, that is why cells are not focused with
584
+ // mousedown. Workaround: listen for mousedown and focus manually.
585
+ cellContent.addEventListener('mousedown', () => {
586
+ if (isChrome) {
587
+ // Chrome bug: focusing before mouseup prevents text selection, see http://crbug.com/771903
588
+ const mouseUpListener = (event) => {
589
+ // If focus is on element within the cell content - respect it, do not change
590
+ const contentContainsFocusedElement = cellContent.contains(this.getRootNode().activeElement);
591
+ // Only focus if mouse is released on cell content itself
592
+ const mouseUpWithinCell = event.composedPath().includes(cellContent);
593
+ if (!contentContainsFocusedElement && mouseUpWithinCell) {
594
+ cell.focus();
595
+ }
596
+ document.removeEventListener('mouseup', mouseUpListener, true);
597
+ };
598
+ document.addEventListener('mouseup', mouseUpListener, true);
599
+ } else {
600
+ // Focus on mouseup, on the other hand, removes selection on Safari.
601
+ // Watch out sync focus removal issue, only async focus works here.
602
+ setTimeout(() => {
603
+ if (!cellContent.contains(this.getRootNode().activeElement)) {
604
+ cell.focus();
605
+ }
606
+ });
607
+ }
608
+ });
609
+
610
+ return cell;
611
+ }
612
+
613
+ /**
614
+ * @param {!HTMLTableRowElement} row
615
+ * @param {!Array<!GridColumn>} columns
616
+ * @param {?string} section
617
+ * @param {boolean} isColumnRow
618
+ * @param {boolean} noNotify
619
+ * @protected
620
+ */
621
+ _updateRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
622
+ const contentsFragment = document.createDocumentFragment();
623
+
624
+ iterateRowCells(row, (cell) => {
625
+ cell._vacant = true;
626
+ });
627
+ row.innerHTML = '';
628
+ if (section === 'body') {
629
+ // Clear the cached cell references
630
+ row.__cells = [];
631
+ row.__detailsCell = null;
632
+ }
633
+
634
+ columns
635
+ .filter((column) => !column.hidden)
636
+ .forEach((column, index, cols) => {
637
+ let cell;
638
+
639
+ if (section === 'body') {
640
+ // Body
641
+ if (!column._cells) {
642
+ column._cells = [];
643
+ }
644
+ cell = column._cells.find((cell) => cell._vacant);
645
+ if (!cell) {
646
+ cell = this._createCell('td', column);
647
+ column._cells.push(cell);
648
+ }
649
+ cell.setAttribute('part', 'cell body-cell');
650
+ cell.__parentRow = row;
651
+ // Cache the cell reference
652
+ row.__cells.push(cell);
653
+ if (!column._bodyContentHidden) {
654
+ row.appendChild(cell);
655
+ }
656
+
657
+ if (row === this.$.sizer) {
658
+ column._sizerCell = cell;
659
+ }
660
+
661
+ if (index === cols.length - 1 && this.rowDetailsRenderer) {
662
+ // Add details cell as last cell to body rows
663
+ if (!this._detailsCells) {
664
+ this._detailsCells = [];
665
+ }
666
+ const detailsCell = this._detailsCells.find((cell) => cell._vacant) || this._createCell('td');
667
+ if (this._detailsCells.indexOf(detailsCell) === -1) {
668
+ this._detailsCells.push(detailsCell);
669
+ }
670
+ if (!detailsCell._content.parentElement) {
671
+ contentsFragment.appendChild(detailsCell._content);
672
+ }
673
+ this._configureDetailsCell(detailsCell);
674
+ row.appendChild(detailsCell);
675
+ // Cache the details cell reference
676
+ row.__detailsCell = detailsCell;
677
+ this._a11ySetRowDetailsCell(row, detailsCell);
678
+ detailsCell._vacant = false;
679
+ }
680
+
681
+ if (column.notifyPath && !noNotify) {
682
+ column.notifyPath('_cells.*', column._cells);
683
+ }
684
+ } else {
685
+ // Header & footer
686
+ const tagName = section === 'header' ? 'th' : 'td';
687
+ if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
688
+ cell = column[`_${section}Cell`] || this._createCell(tagName);
689
+ cell._column = column;
690
+ row.appendChild(cell);
691
+ column[`_${section}Cell`] = cell;
692
+ } else {
693
+ if (!column._emptyCells) {
694
+ column._emptyCells = [];
695
+ }
696
+ cell = column._emptyCells.find((cell) => cell._vacant) || this._createCell(tagName);
697
+ cell._column = column;
698
+ row.appendChild(cell);
699
+ if (column._emptyCells.indexOf(cell) === -1) {
700
+ column._emptyCells.push(cell);
701
+ }
702
+ }
703
+ cell.setAttribute('part', `cell ${section}-cell`);
704
+ }
705
+
706
+ if (!cell._content.parentElement) {
707
+ contentsFragment.appendChild(cell._content);
708
+ }
709
+ cell._vacant = false;
710
+ cell._column = column;
711
+ });
712
+
713
+ if (section !== 'body') {
714
+ this.__debounceUpdateHeaderFooterRowVisibility(row);
715
+ }
716
+
717
+ // Might be empty if only cache was used
718
+ this.appendChild(contentsFragment);
719
+
720
+ this._frozenCellsChanged();
721
+ this._updateFirstAndLastColumnForRow(row);
722
+ }
723
+
724
+ /**
725
+ * @param {HTMLTableRowElement} row
726
+ * @protected
727
+ */
728
+ __debounceUpdateHeaderFooterRowVisibility(row) {
729
+ row.__debounceUpdateHeaderFooterRowVisibility = Debouncer.debounce(
730
+ row.__debounceUpdateHeaderFooterRowVisibility,
731
+ microTask,
732
+ () => this.__updateHeaderFooterRowVisibility(row),
733
+ );
734
+ }
735
+
736
+ /**
737
+ * @param {HTMLTableRowElement} row
738
+ * @protected
739
+ */
740
+ __updateHeaderFooterRowVisibility(row) {
741
+ if (!row) {
742
+ return;
743
+ }
744
+
745
+ const visibleRowCells = Array.from(row.children).filter((cell) => {
746
+ const column = cell._column;
747
+ if (column._emptyCells && column._emptyCells.indexOf(cell) > -1) {
748
+ // The cell is an "empty cell" -> doesn't block hiding the row
749
+ return false;
750
+ }
751
+ if (row.parentElement === this.$.header) {
752
+ if (column.headerRenderer) {
753
+ // The cell is the header cell of a column that has a header renderer
754
+ // -> row should be visible
755
+ return true;
756
+ }
757
+ if (column.header === null) {
758
+ // The column header is explicilty set to null -> doesn't block hiding the row
759
+ return false;
760
+ }
761
+ if (column.path || column.header !== undefined) {
762
+ // The column has an explicit non-null header or a path that generates a header
763
+ // -> row should be visible
764
+ return true;
765
+ }
766
+ } else if (column.footerRenderer) {
767
+ // The cell is the footer cell of a column that has a footer renderer
768
+ // -> row should be visible
769
+ return true;
770
+ }
771
+ return false;
772
+ });
773
+
774
+ if (row.hidden !== !visibleRowCells.length) {
775
+ row.hidden = !visibleRowCells.length;
776
+ }
777
+
778
+ // Make sure the section has a tabbable element
779
+ this._resetKeyboardNavigation();
780
+ }
781
+
782
+ /** @private */
783
+ _updateScrollerItem(row, index) {
784
+ this._preventScrollerRotatingCellFocus(row, index);
785
+
786
+ if (!this._columnTree) {
787
+ return;
788
+ }
789
+
790
+ this._updateRowOrderParts(row, index);
791
+
792
+ this._a11yUpdateRowRowindex(row, index);
793
+ this._getItem(index, row);
794
+ }
795
+
796
+ /** @private */
797
+ _columnTreeChanged(columnTree) {
798
+ this._renderColumnTree(columnTree);
799
+ this.recalculateColumnWidths();
800
+ this.__updateColumnsBodyContentHidden();
801
+ }
802
+
803
+ /** @private */
804
+ _updateRowOrderParts(row, index = row.index) {
805
+ updateBooleanRowStates(row, {
806
+ first: index === 0,
807
+ last: index === this._effectiveSize - 1,
808
+ odd: index % 2 !== 0,
809
+ even: index % 2 === 0,
810
+ });
811
+ }
812
+
813
+ /** @private */
814
+ _updateRowStateParts(row, { expanded, selected, detailsOpened }) {
815
+ updateBooleanRowStates(row, {
816
+ expanded,
817
+ selected,
818
+ 'details-opened': detailsOpened,
819
+ });
820
+ }
821
+
822
+ /**
823
+ * @param {!Array<!GridColumn>} columnTree
824
+ * @protected
825
+ */
826
+ _renderColumnTree(columnTree) {
827
+ iterateChildren(this.$.items, (row) => {
828
+ this._updateRow(row, columnTree[columnTree.length - 1], 'body', false, true);
829
+
830
+ const model = this.__getRowModel(row);
831
+ this._updateRowOrderParts(row);
832
+ this._updateRowStateParts(row, model);
833
+ this._filterDragAndDrop(row, model);
834
+ });
835
+
836
+ while (this.$.header.children.length < columnTree.length) {
837
+ const headerRow = document.createElement('tr');
838
+ headerRow.setAttribute('part', 'row');
839
+ headerRow.setAttribute('role', 'row');
840
+ headerRow.setAttribute('tabindex', '-1');
841
+ this.$.header.appendChild(headerRow);
842
+
843
+ const footerRow = document.createElement('tr');
844
+ footerRow.setAttribute('part', 'row');
845
+ footerRow.setAttribute('role', 'row');
846
+ footerRow.setAttribute('tabindex', '-1');
847
+ this.$.footer.appendChild(footerRow);
848
+ }
849
+ while (this.$.header.children.length > columnTree.length) {
850
+ this.$.header.removeChild(this.$.header.firstElementChild);
851
+ this.$.footer.removeChild(this.$.footer.firstElementChild);
852
+ }
853
+
854
+ iterateChildren(this.$.header, (headerRow, index, rows) => {
855
+ this._updateRow(headerRow, columnTree[index], 'header', index === columnTree.length - 1);
856
+
857
+ const cells = getBodyRowCells(headerRow);
858
+ updateCellsPart(cells, 'first-header-row-cell', index === 0);
859
+ updateCellsPart(cells, 'last-header-row-cell', index === rows.length - 1);
860
+ });
861
+
862
+ iterateChildren(this.$.footer, (footerRow, index, rows) => {
863
+ this._updateRow(footerRow, columnTree[columnTree.length - 1 - index], 'footer', index === 0);
864
+
865
+ const cells = getBodyRowCells(footerRow);
866
+ updateCellsPart(cells, 'first-footer-row-cell', index === 0);
867
+ updateCellsPart(cells, 'last-footer-row-cell', index === rows.length - 1);
868
+ });
869
+
870
+ // Sizer rows
871
+ this._updateRow(this.$.sizer, columnTree[columnTree.length - 1]);
872
+
873
+ this._resizeHandler();
874
+ this._frozenCellsChanged();
875
+ this._updateFirstAndLastColumn();
876
+ this._resetKeyboardNavigation();
877
+ this._a11yUpdateHeaderRows();
878
+ this._a11yUpdateFooterRows();
879
+ this.__updateFooterPositioning();
880
+ this.generateCellClassNames();
881
+ this.generateCellPartNames();
882
+ }
883
+
884
+ /** @private */
885
+ __updateFooterPositioning() {
886
+ // TODO: fixed in Firefox 99, remove when we can drop Firefox ESR 91 support
887
+ if (this._firefox && parseFloat(navigator.userAgent.match(/Firefox\/(\d{2,3}.\d)/u)[1]) < 99) {
888
+ // Sticky (or translated) footer in a flexbox host doesn't get included in
889
+ // the scroll height calculation on FF. This is a workaround for the issue.
890
+ this.$.items.style.paddingBottom = 0;
891
+ if (!this.allRowsVisible) {
892
+ this.$.items.style.paddingBottom = `${this.$.footer.offsetHeight}px`;
893
+ }
894
+ }
895
+ }
896
+
897
+ /**
898
+ * @param {!HTMLElement} row
899
+ * @param {GridItem} item
900
+ * @protected
901
+ */
902
+ _updateItem(row, item) {
903
+ row._item = item;
904
+ const model = this.__getRowModel(row);
905
+
906
+ this._toggleDetailsCell(row, model.detailsOpened);
907
+
908
+ this._a11yUpdateRowLevel(row, model.level);
909
+ this._a11yUpdateRowSelected(row, model.selected);
910
+
911
+ this._updateRowStateParts(row, model);
912
+
913
+ this._generateCellClassNames(row, model);
914
+ this._generateCellPartNames(row, model);
915
+ this._filterDragAndDrop(row, model);
916
+
917
+ iterateChildren(row, (cell) => {
918
+ if (cell._renderer) {
919
+ const owner = cell._column || this;
920
+ cell._renderer.call(owner, cell._content, owner, model);
921
+ }
922
+ });
923
+
924
+ this._updateDetailsCellHeight(row);
925
+
926
+ this._a11yUpdateRowExpanded(row, model.expanded);
927
+ }
928
+
929
+ /** @private */
930
+ _resizeHandler() {
931
+ this._updateDetailsCellHeights();
932
+ this.__updateFooterPositioning();
933
+ this.__updateHorizontalScrollPosition();
934
+ }
935
+
936
+ /** @private */
937
+ _onAnimationEnd(e) {
938
+ // ShadyCSS applies scoping suffixes to animation names
939
+ if (e.animationName.indexOf('vaadin-grid-appear') === 0) {
940
+ e.stopPropagation();
941
+ this.__tryToRecalculateColumnWidthsIfPending();
942
+
943
+ requestAnimationFrame(() => {
944
+ this.__scrollToPendingIndexes();
945
+ });
946
+ }
947
+ }
948
+
949
+ /**
950
+ * @param {!HTMLTableRowElement} row
951
+ * @return {!GridItemModel}
952
+ * @protected
953
+ */
954
+ __getRowModel(row) {
955
+ return {
956
+ index: row.index,
957
+ item: row._item,
958
+ level: this._getIndexLevel(row.index),
959
+ expanded: this._isExpanded(row._item),
960
+ selected: this._isSelected(row._item),
961
+ detailsOpened: !!this.rowDetailsRenderer && this._isDetailsOpened(row._item),
962
+ };
963
+ }
964
+
965
+ /**
966
+ * @param {Event} event
967
+ * @protected
968
+ */
969
+ _showTooltip(event) {
970
+ // Check if there is a slotted vaadin-tooltip element.
971
+ const tooltip = this._tooltipController.node;
972
+ if (tooltip && tooltip.isConnected) {
973
+ this._tooltipController.setTarget(event.target);
974
+ this._tooltipController.setContext(this.getEventContext(event));
975
+
976
+ // Trigger opening using the corresponding delay.
977
+ tooltip._stateController.open({
978
+ focus: event.type === 'focusin',
979
+ hover: event.type === 'mouseenter',
980
+ });
981
+ }
982
+ }
983
+
984
+ /** @protected */
985
+ _hideTooltip(immediate) {
986
+ const tooltip = this._tooltipController.node;
987
+ if (tooltip) {
988
+ tooltip._stateController.close(immediate);
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Requests an update for the content of cells.
994
+ *
995
+ * While performing the update, the following renderers are invoked:
996
+ * - `Grid.rowDetailsRenderer`
997
+ * - `GridColumn.renderer`
998
+ * - `GridColumn.headerRenderer`
999
+ * - `GridColumn.footerRenderer`
1000
+ *
1001
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
1002
+ */
1003
+ requestContentUpdate() {
1004
+ if (this._columnTree) {
1005
+ // Header and footer renderers
1006
+ this._columnTree.forEach((level) => {
1007
+ level.forEach((column) => {
1008
+ if (column._renderHeaderAndFooter) {
1009
+ column._renderHeaderAndFooter();
1010
+ }
1011
+ });
1012
+ });
1013
+
1014
+ // Body and row details renderers
1015
+ this.__updateVisibleRows();
1016
+ }
1017
+ }
1018
+
1019
+ /** @protected */
1020
+ __updateVisibleRows(start, end) {
1021
+ if (this.__virtualizer) {
1022
+ this.__virtualizer.update(start, end);
1023
+ }
1024
+ }
1025
+ };