@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,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
- import { isAndroid, isChrome, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
9
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
7
+ import { isAndroid, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.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
  ),
@@ -121,17 +124,6 @@ export const GridMixin = (superClass) =>
121
124
  value: false,
122
125
  },
123
126
 
124
- /**
125
- * An internal property that is mainly used by `vaadin-template-renderer`
126
- * to identify grid elements.
127
- *
128
- * @private
129
- */
130
- __gridElement: {
131
- type: Boolean,
132
- value: true,
133
- },
134
-
135
127
  /** @private */
136
128
  __hasEmptyStateContent: {
137
129
  type: Boolean,
@@ -149,13 +141,22 @@ export const GridMixin = (superClass) =>
149
141
  /** @private */
150
142
  get _firstVisibleIndex() {
151
143
  const firstVisibleItem = this.__getFirstVisibleItem();
152
- return firstVisibleItem ? firstVisibleItem.index : undefined;
144
+ return firstVisibleItem?.index;
153
145
  }
154
146
 
155
147
  /** @private */
156
148
  get _lastVisibleIndex() {
157
149
  const lastVisibleItem = this.__getLastVisibleItem();
158
- return lastVisibleItem ? lastVisibleItem.index : undefined;
150
+ return lastVisibleItem?.index;
151
+ }
152
+
153
+ constructor() {
154
+ super();
155
+
156
+ this.__onCellMouseEnter = this.__onCellMouseEnter.bind(this);
157
+ this.__onCellMouseLeave = this.__onCellMouseLeave.bind(this);
158
+ this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
159
+ this.__onCellKeyDown = this.__onCellKeyDown.bind(this);
159
160
  }
160
161
 
161
162
  /** @protected */
@@ -286,9 +287,7 @@ export const GridMixin = (superClass) =>
286
287
  __focusBodyCell({ item, column }) {
287
288
  const row = this._getRenderedRows().find((row) => row._item === item);
288
289
  const cell = row && [...row.children].find((cell) => cell._column === column);
289
- if (cell) {
290
- cell.focus();
291
- }
290
+ cell?.focus();
292
291
  }
293
292
 
294
293
  /** @protected */
@@ -335,7 +334,7 @@ export const GridMixin = (superClass) =>
335
334
  updatePart(row, 'row', true);
336
335
  updatePart(row, 'body-row', true);
337
336
  if (this._columnTree) {
338
- this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
337
+ this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', true);
339
338
  }
340
339
  rows.push(row);
341
340
  }
@@ -351,6 +350,36 @@ export const GridMixin = (superClass) =>
351
350
  return rows;
352
351
  }
353
352
 
353
+ /** @private */
354
+ __onCellKeyDown(event) {
355
+ const cell = event.currentTarget;
356
+ cell._column?.__onCellKeyDown?.(event);
357
+ }
358
+
359
+ /** @private */
360
+ __onCellMouseEnter(event) {
361
+ // For now we only support tooltip on desktop
362
+ if (!isAndroid && !isIOS && !this.$.scroller.hasAttribute('scrolling')) {
363
+ this._showTooltip(event);
364
+ }
365
+ }
366
+
367
+ /** @private */
368
+ __onCellMouseLeave() {
369
+ // For now we only support tooltip on desktop
370
+ if (!isAndroid && !isIOS) {
371
+ this._hideTooltip();
372
+ }
373
+ }
374
+
375
+ /** @private */
376
+ __onCellMouseDown() {
377
+ // For now we only support tooltip on desktop
378
+ if (!isAndroid && !isIOS) {
379
+ this._hideTooltip(true);
380
+ }
381
+ }
382
+
354
383
  /** @private */
355
384
  _createCell(tagName, column) {
356
385
  const contentId = (this._contentIndex = this._contentIndex + 1 || 0);
@@ -363,22 +392,10 @@ export const GridMixin = (superClass) =>
363
392
  cell.id = slotName.replace('-content-', '-');
364
393
  cell.setAttribute('role', tagName === 'td' ? 'gridcell' : 'columnheader');
365
394
 
366
- // For now we only support tooltip on desktop
367
- if (!isAndroid && !isIOS) {
368
- cell.addEventListener('mouseenter', (event) => {
369
- if (!this.$.scroller.hasAttribute('scrolling')) {
370
- this._showTooltip(event);
371
- }
372
- });
373
-
374
- cell.addEventListener('mouseleave', () => {
375
- this._hideTooltip();
376
- });
377
-
378
- cell.addEventListener('mousedown', () => {
379
- this._hideTooltip(true);
380
- });
381
- }
395
+ cell.addEventListener('mouseenter', this.__onCellMouseEnter);
396
+ cell.addEventListener('mouseleave', this.__onCellMouseLeave);
397
+ cell.addEventListener('mousedown', this.__onCellMouseDown);
398
+ cell.addEventListener('keydown', this.__onCellKeyDown);
382
399
 
383
400
  const slot = document.createElement('slot');
384
401
  slot.setAttribute('name', slotName);
@@ -403,34 +420,6 @@ export const GridMixin = (superClass) =>
403
420
 
404
421
  cell._content = cellContent;
405
422
 
406
- // With native Shadow DOM, mousedown on slotted element does not focus
407
- // focusable slot wrapper, that is why cells are not focused with
408
- // mousedown. Workaround: listen for mousedown and focus manually.
409
- cellContent.addEventListener('mousedown', () => {
410
- if (isChrome) {
411
- // Chrome bug: focusing before mouseup prevents text selection, see http://crbug.com/771903
412
- const mouseUpListener = (event) => {
413
- // If focus is on element within the cell content - respect it, do not change
414
- const contentContainsFocusedElement = cellContent.contains(this.getRootNode().activeElement);
415
- // Only focus if mouse is released on cell content itself
416
- const mouseUpWithinCell = event.composedPath().includes(cellContent);
417
- if (!contentContainsFocusedElement && mouseUpWithinCell) {
418
- cell.focus({ preventScroll: true });
419
- }
420
- document.removeEventListener('mouseup', mouseUpListener, true);
421
- };
422
- document.addEventListener('mouseup', mouseUpListener, true);
423
- } else {
424
- // Focus on mouseup, on the other hand, removes selection on Safari.
425
- // Watch out sync focus removal issue, only async focus works here.
426
- setTimeout(() => {
427
- if (!cellContent.contains(this.getRootNode().activeElement)) {
428
- cell.focus({ preventScroll: true });
429
- }
430
- });
431
- }
432
- });
433
-
434
423
  return cell;
435
424
  }
436
425
 
@@ -438,11 +427,10 @@ export const GridMixin = (superClass) =>
438
427
  * @param {!HTMLTableRowElement} row
439
428
  * @param {!Array<!GridColumn>} columns
440
429
  * @param {?string} section
441
- * @param {boolean} isColumnRow
442
430
  * @param {boolean} noNotify
443
431
  * @private
444
432
  */
445
- __initRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
433
+ __initRow(row, columns, section = 'body', noNotify = false) {
446
434
  const contentsFragment = document.createDocumentFragment();
447
435
 
448
436
  iterateRowCells(row, (cell) => {
@@ -469,9 +457,6 @@ export const GridMixin = (superClass) =>
469
457
  cell = column._cells.find((cell) => cell._vacant);
470
458
  if (!cell) {
471
459
  cell = this._createCell('td', column);
472
- if (column._onCellKeyDown) {
473
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
474
- }
475
460
  column._cells.push(cell);
476
461
  }
477
462
  updatePart(cell, 'cell', true);
@@ -513,33 +498,6 @@ export const GridMixin = (superClass) =>
513
498
  if (!noNotify) {
514
499
  column._cells = [...column._cells];
515
500
  }
516
- } else {
517
- // Header & footer
518
- const tagName = section === 'header' ? 'th' : 'td';
519
- if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
520
- cell = column[`_${section}Cell`];
521
- if (!cell) {
522
- cell = this._createCell(tagName);
523
- if (column._onCellKeyDown) {
524
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
525
- }
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
501
  }
544
502
 
545
503
  if (!cell._content.parentElement) {
@@ -549,86 +507,12 @@ export const GridMixin = (superClass) =>
549
507
  cell._column = column;
550
508
  });
551
509
 
552
- if (section !== 'body') {
553
- this.__debounceUpdateHeaderFooterRowVisibility(row);
554
- }
555
-
556
510
  // Might be empty if only cache was used
557
511
  this.appendChild(contentsFragment);
558
512
 
559
- this._frozenCellsChanged();
560
513
  this._updateFirstAndLastColumnForRow(row);
561
514
  }
562
515
 
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
516
  /** @private */
633
517
  __updateVirtualizerElement(row, index) {
634
518
  this._preventScrollerRotatingCellFocus(row, index);
@@ -681,67 +565,21 @@ export const GridMixin = (superClass) =>
681
565
  */
682
566
  _renderColumnTree(columnTree) {
683
567
  iterateChildren(this.$.items, (row) => {
684
- this.__initRow(row, columnTree[columnTree.length - 1], 'body', false, true);
568
+ this.__initRow(row, columnTree[columnTree.length - 1], 'body', true);
685
569
  this.__updateRow(row);
686
570
  });
687
571
 
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
- });
572
+ this.__renderHeaderFooter();
715
573
 
716
574
  // Sizer rows
717
575
  this.__initRow(this.$.sizer, columnTree[columnTree.length - 1]);
718
576
 
719
- this.__updateHeaderFooterRowParts('header');
720
- this.__updateHeaderFooterRowParts('footer');
721
577
  this._resizeHandler();
722
- this._frozenCellsChanged();
723
- this._updateFirstAndLastColumn();
724
- this._resetKeyboardNavigation();
725
578
  this.__a11yUpdateHeaderRows();
726
579
  this.__a11yUpdateFooterRows();
727
- this.generateCellPartNames();
728
580
  this.__updateHeaderAndFooter();
729
581
  }
730
582
 
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
583
  /**
746
584
  * @param {!HTMLElement} row
747
585
  * @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 = {
@@ -88,19 +89,6 @@ export const ScrollMixin = (superClass) =>
88
89
  return this.$.table.scrollLeft;
89
90
  }
90
91
 
91
- /** @private */
92
- get _scrollTop() {
93
- return this.$.table.scrollTop;
94
- }
95
-
96
- /**
97
- * Override (from iron-scroll-target-behavior) to avoid document scroll
98
- * @private
99
- */
100
- set _scrollTop(top) {
101
- this.$.table.scrollTop = top;
102
- }
103
-
104
92
  /** @protected */
105
93
  get _lazyColumns() {
106
94
  return this.columnRendering === 'lazy';
@@ -123,24 +111,20 @@ export const ScrollMixin = (superClass) =>
123
111
  // is inside the viewport. If the whole row fits into the viewport, then scroll
124
112
  // the row into view. This ensures that labels, helper texts and other related
125
113
  // elements of focusable elements within cells also become visible. When the row
126
- // is larger than the viewport, scroll the focus event target into the viewport.
114
+ // is larger than the viewport, scroll the actually focused element into the viewport.
127
115
  // This works better when focusing elements within cells, which could otherwise
128
116
  // still be outside the viewport when scrolling to the top or bottom of the row.
117
+ // The composed path is used instead of the event target, which would be
118
+ // retargeted to the host element when focus lands inside a nested shadow root.
129
119
  const tableHeight = this.$.table.clientHeight;
130
120
  const headerHeight = this.$.header.clientHeight;
131
121
  const footerHeight = this.$.footer.clientHeight;
132
122
  const viewportHeight = tableHeight - headerHeight - footerHeight;
133
123
  const isRowLargerThanViewport = row.clientHeight > viewportHeight;
134
- const scrollTarget = isRowLargerThanViewport ? e.target : row;
124
+ const scrollTarget = isRowLargerThanViewport ? composedPath[0] : row;
135
125
 
136
126
  this.__scrollIntoViewport(scrollTarget);
137
127
  }
138
-
139
- if (!this.$.table.contains(e.relatedTarget)) {
140
- // Virtualizer can't catch the event because if orginates from the light DOM.
141
- // Dispatch a virtualizer-element-focused event for virtualizer to catch.
142
- this.$.table.dispatchEvent(new CustomEvent('virtualizer-element-focused', { detail: { element: row } }));
143
- }
144
128
  }
145
129
  });
146
130
 
@@ -244,13 +228,8 @@ export const ScrollMixin = (superClass) =>
244
228
 
245
229
  /** @private */
246
230
  _scheduleScrolling() {
247
- if (!this._scrollingFrame) {
248
- // Defer setting state attributes to avoid Edge hiccups
249
- this._scrollingFrame = requestAnimationFrame(() => this.$.scroller.toggleAttribute('scrolling', true));
250
- }
231
+ this.$.scroller.toggleAttribute('scrolling', true);
251
232
  this._debounceScrolling = Debouncer.debounce(this._debounceScrolling, timeOut.after(timeouts.SCROLLING), () => {
252
- cancelAnimationFrame(this._scrollingFrame);
253
- delete this._scrollingFrame;
254
233
  this.$.scroller.toggleAttribute('scrolling', false);
255
234
  });
256
235
  }
@@ -377,11 +356,7 @@ export const ScrollMixin = (superClass) =>
377
356
 
378
357
  /** @private */
379
358
  __columnRenderingChanged(_columnTree, columnRendering) {
380
- if (columnRendering === 'eager') {
381
- this.$.scroller.removeAttribute('column-rendering');
382
- } else {
383
- this.$.scroller.setAttribute('column-rendering', columnRendering);
384
- }
359
+ setOrRemoveAttribute(this.$.scroller, 'column-rendering', columnRendering !== 'eager' && columnRendering);
385
360
 
386
361
  this.__updateColumnsBodyContentHidden();
387
362
  }
@@ -107,12 +107,12 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
107
107
 
108
108
  constructor() {
109
109
  super();
110
- this.__onCellTrack = this.__onCellTrack.bind(this);
111
- this.__onCellClick = this.__onCellClick.bind(this);
112
- this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
110
+ this.__onCellContentTrack = this.__onCellContentTrack.bind(this);
111
+ this.__onCellContentClick = this.__onCellContentClick.bind(this);
112
+ this.__onCellContentMouseDown = this.__onCellContentMouseDown.bind(this);
113
113
  this.__onGridInteraction = this.__onGridInteraction.bind(this);
114
- this.__onActiveItemChanged = this.__onActiveItemChanged.bind(this);
115
- this.__onTouchStart = this.__onTouchStart.bind(this);
114
+ this.__onGridActiveItemChanged = this.__onGridActiveItemChanged.bind(this);
115
+ this.__onGridTouchStart = this.__onGridTouchStart.bind(this);
116
116
  this.__onSelectRowCheckboxChange = this.__onSelectRowCheckboxChange.bind(this);
117
117
  this.__onSelectAllCheckboxChange = this.__onSelectAllCheckboxChange.bind(this);
118
118
  }
@@ -124,8 +124,8 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
124
124
  this._grid.addEventListener('keyup', this.__onGridInteraction);
125
125
  this._grid.addEventListener('keydown', this.__onGridInteraction, { capture: true });
126
126
  this._grid.addEventListener('mousedown', this.__onGridInteraction);
127
- this._grid.addEventListener('active-item-changed', this.__onActiveItemChanged);
128
- this._grid.addEventListener('touchstart', this.__onTouchStart);
127
+ this._grid.addEventListener('active-item-changed', this.__onGridActiveItemChanged);
128
+ this._grid.addEventListener('touchstart', this.__onGridTouchStart);
129
129
  }
130
130
  }
131
131
 
@@ -136,8 +136,8 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
136
136
  this._grid.removeEventListener('keyup', this.__onGridInteraction);
137
137
  this._grid.removeEventListener('keydown', this.__onGridInteraction, { capture: true });
138
138
  this._grid.removeEventListener('mousedown', this.__onGridInteraction);
139
- this._grid.removeEventListener('active-item-changed', this.__onActiveItemChanged);
140
- this._grid.removeEventListener('touchstart', this.__onTouchStart);
139
+ this._grid.removeEventListener('active-item-changed', this.__onGridActiveItemChanged);
140
+ this._grid.removeEventListener('touchstart', this.__onGridTouchStart);
141
141
  }
142
142
  }
143
143
 
@@ -155,12 +155,28 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
155
155
  root.appendChild(checkbox);
156
156
  }
157
157
 
158
- checkbox.accessibleName = this._grid?.__effectiveI18n?.selectAll;
158
+ const { selectAll, selectAllUnavailable } = this._grid.__effectiveI18n;
159
+ checkbox.accessibleName = selectAll;
159
160
 
160
161
  const checked = this.__isChecked(this.selectAll, this._indeterminate);
161
162
  checkbox.checked = checked;
162
163
  checkbox.indeterminate = this._indeterminate;
163
164
  checkbox.style.visibility = this._selectAllHidden ? 'hidden' : '';
165
+
166
+ // When the Select All checkbox is hidden, the header cell would otherwise
167
+ // be announced as blank. Render a screen-reader-only label into the cell
168
+ // so that screen readers announce the `selectAllUnavailable` text.
169
+ let label = this._headerCell.querySelector(':scope > .sr-only');
170
+ if (this._selectAllHidden && selectAllUnavailable) {
171
+ if (!label) {
172
+ label = document.createElement('span');
173
+ label.classList.add('sr-only');
174
+ this._headerCell.appendChild(label);
175
+ }
176
+ label.textContent = selectAllUnavailable;
177
+ } else {
178
+ label?.remove();
179
+ }
164
180
  }
165
181
 
166
182
  /**
@@ -174,10 +190,10 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
174
190
  checkbox = document.createElement('vaadin-checkbox');
175
191
  checkbox.addEventListener('change', this.__onSelectRowCheckboxChange);
176
192
  root.appendChild(checkbox);
177
- addListener(root, 'track', this.__onCellTrack);
193
+ addListener(root, 'track', this.__onCellContentTrack);
178
194
  setTouchAction(root, 'pinch-zoom');
179
- root.addEventListener('mousedown', this.__onCellMouseDown);
180
- root.addEventListener('click', this.__onCellClick);
195
+ root.addEventListener('mousedown', this.__onCellContentMouseDown);
196
+ root.addEventListener('click', this.__onCellContentClick);
181
197
  }
182
198
 
183
199
  this.__updateSelectRowAccessibleName(checkbox, root);
@@ -254,7 +270,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
254
270
  }
255
271
 
256
272
  /** @private */
257
- __onCellTrack(event) {
273
+ __onCellContentTrack(event) {
258
274
  if (!this.dragSelect) {
259
275
  return;
260
276
  }
@@ -290,7 +306,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
290
306
  }
291
307
 
292
308
  /** @private */
293
- __onCellMouseDown(e) {
309
+ __onCellContentMouseDown(e) {
294
310
  if (this.dragSelect) {
295
311
  // Prevent text selection when starting to drag
296
312
  e.preventDefault();
@@ -298,7 +314,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
298
314
  }
299
315
 
300
316
  /** @private */
301
- __onTouchStart(e) {
317
+ __onGridTouchStart(e) {
302
318
  if (e.touches.length > 1) {
303
319
  this.__multiTouchActive = true;
304
320
  // Cancel in-progress drag-select on multi-touch (e.g. pinch-zoom)
@@ -310,7 +326,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
310
326
  }
311
327
 
312
328
  /** @private */
313
- __onCellClick(e) {
329
+ __onCellContentClick(e) {
314
330
  if (this.__dragStartIndex !== undefined) {
315
331
  // Stop the click event if drag was enabled. This click event should
316
332
  // only occur if drag started and stopped on the same item. In that case
@@ -322,7 +338,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
322
338
  }
323
339
 
324
340
  /** @private */
325
- _onCellKeyDown(e) {
341
+ __onCellKeyDown(e) {
326
342
  const target = e.composedPath()[0];
327
343
  // Toggle on Space without having to enter interaction mode first
328
344
  if (e.keyCode !== 32) {
@@ -341,7 +357,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
341
357
  }
342
358
 
343
359
  /** @private */
344
- __onActiveItemChanged(e) {
360
+ __onGridActiveItemChanged(e) {
345
361
  const activeItem = e.detail.value;
346
362
  if (this.autoSelect) {
347
363
  const item = activeItem || this.__previousActiveItem;
@@ -367,7 +383,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
367
383
 
368
384
  // Get the index of the row being hovered over or the first/last
369
385
  // visible row if hovering outside the grid
370
- let hoveredIndex = hoveredRow ? hoveredRow.index : undefined;
386
+ let hoveredIndex = hoveredRow?.index;
371
387
  const scrollableArea = this.__getScrollableArea();
372
388
  if (this.__dragCurrentY < scrollableArea.top) {
373
389
  hoveredIndex = this._grid._firstVisibleIndex;