@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
@@ -4,10 +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, isFirefox, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
9
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
10
- import { getClosestElement } from '@vaadin/component-base/src/dom-utils.js';
7
+ import { isAndroid, isIOS, isSafari, isTouch } from '@vaadin/component-base/src/browser-utils.js';
11
8
  import { setTouchAction } from '@vaadin/component-base/src/gestures.js';
12
9
  import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
13
10
  import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
@@ -23,8 +20,10 @@ import { DragAndDropMixin } from './vaadin-grid-drag-and-drop-mixin.js';
23
20
  import { DynamicColumnsMixin } from './vaadin-grid-dynamic-columns-mixin.js';
24
21
  import { EventContextMixin } from './vaadin-grid-event-context-mixin.js';
25
22
  import { FilterMixin } from './vaadin-grid-filter-mixin.js';
23
+ import { HeaderFooterRenderingMixin } from './vaadin-grid-header-footer-rendering-mixin.js';
26
24
  import {
27
25
  getBodyRowCells,
26
+ getClosestCell,
28
27
  iterateChildren,
29
28
  iterateRowCells,
30
29
  updateBooleanRowStates,
@@ -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
  ),
@@ -91,12 +94,6 @@ export const GridMixin = (superClass) =>
91
94
  value: isIOS,
92
95
  },
93
96
 
94
- /** @private */
95
- _firefox: {
96
- type: Boolean,
97
- value: isFirefox,
98
- },
99
-
100
97
  /** @private */
101
98
  _android: {
102
99
  type: Boolean,
@@ -155,13 +152,22 @@ export const GridMixin = (superClass) =>
155
152
  /** @private */
156
153
  get _firstVisibleIndex() {
157
154
  const firstVisibleItem = this.__getFirstVisibleItem();
158
- return firstVisibleItem ? firstVisibleItem.index : undefined;
155
+ return firstVisibleItem?.index;
159
156
  }
160
157
 
161
158
  /** @private */
162
159
  get _lastVisibleIndex() {
163
160
  const lastVisibleItem = this.__getLastVisibleItem();
164
- return lastVisibleItem ? lastVisibleItem.index : undefined;
161
+ return lastVisibleItem?.index;
162
+ }
163
+
164
+ constructor() {
165
+ super();
166
+
167
+ this.__onCellMouseEnter = this.__onCellMouseEnter.bind(this);
168
+ this.__onCellMouseLeave = this.__onCellMouseLeave.bind(this);
169
+ this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
170
+ this.__onCellKeyDown = this.__onCellKeyDown.bind(this);
165
171
  }
166
172
 
167
173
  /** @protected */
@@ -210,13 +216,7 @@ export const GridMixin = (superClass) =>
210
216
 
211
217
  /** @protected */
212
218
  _getRowContainingNode(node) {
213
- const content = getClosestElement('vaadin-grid-cell-content', node);
214
- if (!content) {
215
- return;
216
- }
217
-
218
- const cell = content.assignedSlot.parentElement;
219
- return cell.parentElement;
219
+ return getClosestCell(node)?.parentElement;
220
220
  }
221
221
 
222
222
  /** @protected */
@@ -298,9 +298,7 @@ export const GridMixin = (superClass) =>
298
298
  __focusBodyCell({ item, column }) {
299
299
  const row = this._getRenderedRows().find((row) => row._item === item);
300
300
  const cell = row && [...row.children].find((cell) => cell._column === column);
301
- if (cell) {
302
- cell.focus();
303
- }
301
+ cell?.focus();
304
302
  }
305
303
 
306
304
  /** @protected */
@@ -347,7 +345,7 @@ export const GridMixin = (superClass) =>
347
345
  updatePart(row, 'row', true);
348
346
  updatePart(row, 'body-row', true);
349
347
  if (this._columnTree) {
350
- this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
348
+ this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', true);
351
349
  }
352
350
  rows.push(row);
353
351
  }
@@ -363,6 +361,36 @@ export const GridMixin = (superClass) =>
363
361
  return rows;
364
362
  }
365
363
 
364
+ /** @private */
365
+ __onCellKeyDown(event) {
366
+ const cell = event.currentTarget;
367
+ cell._column?.__onCellKeyDown?.(event);
368
+ }
369
+
370
+ /** @private */
371
+ __onCellMouseEnter(event) {
372
+ // For now we only support tooltip on desktop
373
+ if (!isAndroid && !isIOS && !this.$.scroller.hasAttribute('scrolling')) {
374
+ this._showTooltip(event);
375
+ }
376
+ }
377
+
378
+ /** @private */
379
+ __onCellMouseLeave() {
380
+ // For now we only support tooltip on desktop
381
+ if (!isAndroid && !isIOS) {
382
+ this._hideTooltip();
383
+ }
384
+ }
385
+
386
+ /** @private */
387
+ __onCellMouseDown() {
388
+ // For now we only support tooltip on desktop
389
+ if (!isAndroid && !isIOS) {
390
+ this._hideTooltip(true);
391
+ }
392
+ }
393
+
366
394
  /** @private */
367
395
  _createCell(tagName, column) {
368
396
  const contentId = (this._contentIndex = this._contentIndex + 1 || 0);
@@ -375,22 +403,10 @@ export const GridMixin = (superClass) =>
375
403
  cell.id = slotName.replace('-content-', '-');
376
404
  cell.setAttribute('role', tagName === 'td' ? 'gridcell' : 'columnheader');
377
405
 
378
- // For now we only support tooltip on desktop
379
- if (!isAndroid && !isIOS) {
380
- cell.addEventListener('mouseenter', (event) => {
381
- if (!this.$.scroller.hasAttribute('scrolling')) {
382
- this._showTooltip(event);
383
- }
384
- });
385
-
386
- cell.addEventListener('mouseleave', () => {
387
- this._hideTooltip();
388
- });
389
-
390
- cell.addEventListener('mousedown', () => {
391
- this._hideTooltip(true);
392
- });
393
- }
406
+ cell.addEventListener('mouseenter', this.__onCellMouseEnter);
407
+ cell.addEventListener('mouseleave', this.__onCellMouseLeave);
408
+ cell.addEventListener('mousedown', this.__onCellMouseDown);
409
+ cell.addEventListener('keydown', this.__onCellKeyDown);
394
410
 
395
411
  const slot = document.createElement('slot');
396
412
  slot.setAttribute('name', slotName);
@@ -415,34 +431,6 @@ export const GridMixin = (superClass) =>
415
431
 
416
432
  cell._content = cellContent;
417
433
 
418
- // With native Shadow DOM, mousedown on slotted element does not focus
419
- // focusable slot wrapper, that is why cells are not focused with
420
- // mousedown. Workaround: listen for mousedown and focus manually.
421
- cellContent.addEventListener('mousedown', () => {
422
- if (isChrome) {
423
- // Chrome bug: focusing before mouseup prevents text selection, see http://crbug.com/771903
424
- const mouseUpListener = (event) => {
425
- // If focus is on element within the cell content - respect it, do not change
426
- const contentContainsFocusedElement = cellContent.contains(this.getRootNode().activeElement);
427
- // Only focus if mouse is released on cell content itself
428
- const mouseUpWithinCell = event.composedPath().includes(cellContent);
429
- if (!contentContainsFocusedElement && mouseUpWithinCell) {
430
- cell.focus({ preventScroll: true });
431
- }
432
- document.removeEventListener('mouseup', mouseUpListener, true);
433
- };
434
- document.addEventListener('mouseup', mouseUpListener, true);
435
- } else {
436
- // Focus on mouseup, on the other hand, removes selection on Safari.
437
- // Watch out sync focus removal issue, only async focus works here.
438
- setTimeout(() => {
439
- if (!cellContent.contains(this.getRootNode().activeElement)) {
440
- cell.focus({ preventScroll: true });
441
- }
442
- });
443
- }
444
- });
445
-
446
434
  return cell;
447
435
  }
448
436
 
@@ -450,11 +438,10 @@ export const GridMixin = (superClass) =>
450
438
  * @param {!HTMLTableRowElement} row
451
439
  * @param {!Array<!GridColumn>} columns
452
440
  * @param {?string} section
453
- * @param {boolean} isColumnRow
454
441
  * @param {boolean} noNotify
455
442
  * @private
456
443
  */
457
- __initRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
444
+ __initRow(row, columns, section = 'body', noNotify = false) {
458
445
  const contentsFragment = document.createDocumentFragment();
459
446
 
460
447
  iterateRowCells(row, (cell) => {
@@ -481,9 +468,6 @@ export const GridMixin = (superClass) =>
481
468
  cell = column._cells.find((cell) => cell._vacant);
482
469
  if (!cell) {
483
470
  cell = this._createCell('td', column);
484
- if (column._onCellKeyDown) {
485
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
486
- }
487
471
  column._cells.push(cell);
488
472
  }
489
473
  updatePart(cell, 'cell', true);
@@ -525,33 +509,6 @@ export const GridMixin = (superClass) =>
525
509
  if (!noNotify) {
526
510
  column._cells = [...column._cells];
527
511
  }
528
- } else {
529
- // Header & footer
530
- const tagName = section === 'header' ? 'th' : 'td';
531
- if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
532
- cell = column[`_${section}Cell`];
533
- if (!cell) {
534
- cell = this._createCell(tagName);
535
- if (column._onCellKeyDown) {
536
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
537
- }
538
- }
539
- cell._column = column;
540
- row.appendChild(cell);
541
- column[`_${section}Cell`] = cell;
542
- } else {
543
- if (!column._emptyCells) {
544
- column._emptyCells = [];
545
- }
546
- cell = column._emptyCells.find((cell) => cell._vacant) || this._createCell(tagName);
547
- cell._column = column;
548
- row.appendChild(cell);
549
- if (column._emptyCells.indexOf(cell) === -1) {
550
- column._emptyCells.push(cell);
551
- }
552
- }
553
- updatePart(cell, 'cell', true);
554
- updatePart(cell, `${section}-cell`, true);
555
512
  }
556
513
 
557
514
  if (!cell._content.parentElement) {
@@ -561,10 +518,6 @@ export const GridMixin = (superClass) =>
561
518
  cell._column = column;
562
519
  });
563
520
 
564
- if (section !== 'body') {
565
- this.__debounceUpdateHeaderFooterRowVisibility(row);
566
- }
567
-
568
521
  // Might be empty if only cache was used
569
522
  this.appendChild(contentsFragment);
570
523
 
@@ -572,75 +525,6 @@ export const GridMixin = (superClass) =>
572
525
  this._updateFirstAndLastColumnForRow(row);
573
526
  }
574
527
 
575
- /**
576
- * @param {HTMLTableRowElement} row
577
- * @protected
578
- */
579
- __debounceUpdateHeaderFooterRowVisibility(row) {
580
- row.__debounceUpdateHeaderFooterRowVisibility = Debouncer.debounce(
581
- row.__debounceUpdateHeaderFooterRowVisibility,
582
- microTask,
583
- () => this.__updateHeaderFooterRowVisibility(row),
584
- );
585
- }
586
-
587
- /**
588
- * @param {HTMLTableRowElement} row
589
- * @protected
590
- */
591
- __updateHeaderFooterRowVisibility(row) {
592
- if (!row) {
593
- return;
594
- }
595
-
596
- const visibleRowCells = Array.from(row.children).filter((cell) => {
597
- const column = cell._column;
598
- if (column._emptyCells && column._emptyCells.indexOf(cell) > -1) {
599
- // The cell is an "empty cell" -> doesn't block hiding the row
600
- return false;
601
- }
602
- if (row.parentElement === this.$.header) {
603
- if (column.headerRenderer) {
604
- // The cell is the header cell of a column that has a header renderer
605
- // -> row should be visible
606
- return true;
607
- }
608
- if (column.header === null) {
609
- // The column header is explicilty set to null -> doesn't block hiding the row
610
- return false;
611
- }
612
- if (column.path || column.header !== undefined) {
613
- // The column has an explicit non-null header or a path that generates a header
614
- // -> row should be visible
615
- return true;
616
- }
617
- } else if (column.footerRenderer) {
618
- // The cell is the footer cell of a column that has a footer renderer
619
- // -> row should be visible
620
- return true;
621
- }
622
- return false;
623
- });
624
-
625
- if (row.hidden !== !visibleRowCells.length) {
626
- row.hidden = !visibleRowCells.length;
627
- }
628
-
629
- if (row.parentElement === this.$.header) {
630
- this.$.table.toggleAttribute('has-header', this.$.header.querySelector('tr:not([hidden])'));
631
- this.__updateHeaderFooterRowParts('header');
632
- }
633
-
634
- if (row.parentElement === this.$.footer) {
635
- this.$.table.toggleAttribute('has-footer', this.$.footer.querySelector('tr:not([hidden])'));
636
- this.__updateHeaderFooterRowParts('footer');
637
- }
638
-
639
- // Make sure the section has a tabbable element
640
- this._resetKeyboardNavigation();
641
- this.__a11yUpdateGridSize(this.size, this._columnTree, this.__emptyState);
642
- }
643
-
644
528
  /** @private */
645
529
  __updateVirtualizerElement(row, index) {
646
530
  this._preventScrollerRotatingCellFocus(row, index);
@@ -693,46 +577,17 @@ export const GridMixin = (superClass) =>
693
577
  */
694
578
  _renderColumnTree(columnTree) {
695
579
  iterateChildren(this.$.items, (row) => {
696
- this.__initRow(row, columnTree[columnTree.length - 1], 'body', false, true);
580
+ this.__initRow(row, columnTree[columnTree.length - 1], 'body', true);
697
581
  this.__updateRow(row);
698
582
  });
699
583
 
700
- while (this.$.header.children.length < columnTree.length) {
701
- const headerRow = document.createElement('tr');
702
- headerRow.setAttribute('role', 'row');
703
- headerRow.setAttribute('tabindex', '-1');
704
- updatePart(headerRow, 'row', true);
705
- updatePart(headerRow, 'header-row', true);
706
- this.$.header.appendChild(headerRow);
707
-
708
- const footerRow = document.createElement('tr');
709
- footerRow.setAttribute('role', 'row');
710
- footerRow.setAttribute('tabindex', '-1');
711
- updatePart(footerRow, 'row', true);
712
- updatePart(footerRow, 'footer-row', true);
713
- this.$.footer.appendChild(footerRow);
714
- }
715
- while (this.$.header.children.length > columnTree.length) {
716
- this.$.header.removeChild(this.$.header.firstElementChild);
717
- this.$.footer.removeChild(this.$.footer.firstElementChild);
718
- }
719
-
720
- iterateChildren(this.$.header, (headerRow, index) => {
721
- this.__initRow(headerRow, columnTree[index], 'header', index === columnTree.length - 1);
722
- });
723
-
724
- iterateChildren(this.$.footer, (footerRow, index) => {
725
- this.__initRow(footerRow, columnTree[columnTree.length - 1 - index], 'footer', index === 0);
726
- });
584
+ this.__renderHeaderFooter();
727
585
 
728
586
  // Sizer rows
729
587
  this.__initRow(this.$.sizer, columnTree[columnTree.length - 1]);
730
588
 
731
- this.__updateHeaderFooterRowParts('header');
732
- this.__updateHeaderFooterRowParts('footer');
733
589
  this._resizeHandler();
734
590
  this._frozenCellsChanged();
735
- this._updateFirstAndLastColumn();
736
591
  this._resetKeyboardNavigation();
737
592
  this.__a11yUpdateHeaderRows();
738
593
  this.__a11yUpdateFooterRows();
@@ -740,20 +595,6 @@ export const GridMixin = (superClass) =>
740
595
  this.__updateHeaderAndFooter();
741
596
  }
742
597
 
743
- /** @private */
744
- __updateHeaderFooterRowParts(section) {
745
- const visibleRows = [...this.$[section].querySelectorAll('tr:not([hidden])')];
746
- [...this.$[section].children].forEach((row) => {
747
- updatePart(row, `first-${section}-row`, row === visibleRows.at(0));
748
- updatePart(row, `last-${section}-row`, row === visibleRows.at(-1));
749
-
750
- getBodyRowCells(row).forEach((cell) => {
751
- updatePart(cell, `first-${section}-row-cell`, row === visibleRows.at(0));
752
- updatePart(cell, `last-${section}-row-cell`, row === visibleRows.at(-1));
753
- });
754
- });
755
- }
756
-
757
598
  /**
758
599
  * @param {!HTMLElement} row
759
600
  * @param {boolean} loading
@@ -180,7 +180,7 @@ export const RowDetailsMixin = (superClass) =>
180
180
  * @protected
181
181
  */
182
182
  _isDetailsOpened(item) {
183
- return this.__detailsOpenedKeys && this.__detailsOpenedKeys.has(this.getItemId(item));
183
+ return this.__detailsOpenedKeys?.has(this.getItemId(item));
184
184
  }
185
185
 
186
186
  /** @private */
@@ -6,6 +6,7 @@
6
6
  import { microTask, timeOut } from '@vaadin/component-base/src/async.js';
7
7
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
8
8
  import { getNormalizedScrollLeft } from '@vaadin/component-base/src/dir-utils.js';
9
+ import { setOrRemoveAttribute } from '@vaadin/component-base/src/dom-utils.js';
9
10
  import { OverflowController } from '@vaadin/component-base/src/overflow-controller.js';
10
11
 
11
12
  const timeouts = {
@@ -135,12 +136,6 @@ export const ScrollMixin = (superClass) =>
135
136
 
136
137
  this.__scrollIntoViewport(scrollTarget);
137
138
  }
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
139
  }
145
140
  });
146
141
 
@@ -377,11 +372,7 @@ export const ScrollMixin = (superClass) =>
377
372
 
378
373
  /** @private */
379
374
  __columnRenderingChanged(_columnTree, columnRendering) {
380
- if (columnRendering === 'eager') {
381
- this.$.scroller.removeAttribute('column-rendering');
382
- } else {
383
- this.$.scroller.setAttribute('column-rendering', columnRendering);
384
- }
375
+ setOrRemoveAttribute(this.$.scroller, 'column-rendering', columnRendering !== 'eager' && columnRendering);
385
376
 
386
377
  this.__updateColumnsBodyContentHidden();
387
378
  }
@@ -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
 
@@ -150,16 +150,33 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
150
150
  let checkbox = root.firstElementChild;
151
151
  if (!checkbox) {
152
152
  checkbox = document.createElement('vaadin-checkbox');
153
- checkbox.accessibleName = 'Select All';
154
153
  checkbox.classList.add('vaadin-grid-select-all-checkbox');
155
154
  checkbox.addEventListener('change', this.__onSelectAllCheckboxChange);
156
155
  root.appendChild(checkbox);
157
156
  }
158
157
 
158
+ const { selectAll, selectAllUnavailable } = this._grid.__effectiveI18n;
159
+ checkbox.accessibleName = selectAll;
160
+
159
161
  const checked = this.__isChecked(this.selectAll, this._indeterminate);
160
162
  checkbox.checked = checked;
161
163
  checkbox.indeterminate = this._indeterminate;
162
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
+ }
163
180
  }
164
181
 
165
182
  /**
@@ -171,15 +188,15 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
171
188
  let checkbox = root.firstElementChild;
172
189
  if (!checkbox) {
173
190
  checkbox = document.createElement('vaadin-checkbox');
174
- checkbox.accessibleName = 'Select Row';
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
 
199
+ this.__updateSelectRowAccessibleName(checkbox, root);
183
200
  checkbox.__item = item;
184
201
  checkbox.checked = selected;
185
202
 
@@ -190,6 +207,34 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
190
207
  checkbox.style.visibility = isHidden ? 'hidden' : '';
191
208
  }
192
209
 
210
+ /**
211
+ * Sets the Select Row checkbox accessible name based on the grid i18n.
212
+ * The `{rowHeader}` placeholder is replaced with the row header cell text content
213
+ * or row index if there is no row header column or it's cell is empty.
214
+ *
215
+ * @private
216
+ */
217
+ __updateSelectRowAccessibleName(checkbox, root) {
218
+ const ariaLabel = this._grid?.__effectiveI18n?.selectRow;
219
+ if (!ariaLabel || !ariaLabel.includes('{rowHeader}')) {
220
+ checkbox.accessibleName = ariaLabel;
221
+ return;
222
+ }
223
+
224
+ // Defer reading the row header cell text until all cell renderers of
225
+ // the row have run in the current task, as the selection column may
226
+ // render before the row header column.
227
+ queueMicrotask(() => {
228
+ const row = root.assignedSlot?.parentElement?.__parentRow;
229
+ if (!checkbox.isConnected || !row) {
230
+ return;
231
+ }
232
+ const rowHeaderCell = row.__cells.find((cell) => cell._column?.rowHeader);
233
+ const value = rowHeaderCell?._content?.textContent.trim() || String(row.index + 1);
234
+ checkbox.accessibleName = ariaLabel.replace('{rowHeader}', value);
235
+ });
236
+ }
237
+
193
238
  /**
194
239
  * Updates the select all state when the Select All checkbox is switched.
195
240
  * The listener handles only user-fired events.
@@ -225,7 +270,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
225
270
  }
226
271
 
227
272
  /** @private */
228
- __onCellTrack(event) {
273
+ __onCellContentTrack(event) {
229
274
  if (!this.dragSelect) {
230
275
  return;
231
276
  }
@@ -261,7 +306,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
261
306
  }
262
307
 
263
308
  /** @private */
264
- __onCellMouseDown(e) {
309
+ __onCellContentMouseDown(e) {
265
310
  if (this.dragSelect) {
266
311
  // Prevent text selection when starting to drag
267
312
  e.preventDefault();
@@ -269,7 +314,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
269
314
  }
270
315
 
271
316
  /** @private */
272
- __onTouchStart(e) {
317
+ __onGridTouchStart(e) {
273
318
  if (e.touches.length > 1) {
274
319
  this.__multiTouchActive = true;
275
320
  // Cancel in-progress drag-select on multi-touch (e.g. pinch-zoom)
@@ -281,7 +326,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
281
326
  }
282
327
 
283
328
  /** @private */
284
- __onCellClick(e) {
329
+ __onCellContentClick(e) {
285
330
  if (this.__dragStartIndex !== undefined) {
286
331
  // Stop the click event if drag was enabled. This click event should
287
332
  // only occur if drag started and stopped on the same item. In that case
@@ -293,7 +338,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
293
338
  }
294
339
 
295
340
  /** @private */
296
- _onCellKeyDown(e) {
341
+ __onCellKeyDown(e) {
297
342
  const target = e.composedPath()[0];
298
343
  // Toggle on Space without having to enter interaction mode first
299
344
  if (e.keyCode !== 32) {
@@ -312,7 +357,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
312
357
  }
313
358
 
314
359
  /** @private */
315
- __onActiveItemChanged(e) {
360
+ __onGridActiveItemChanged(e) {
316
361
  const activeItem = e.detail.value;
317
362
  if (this.autoSelect) {
318
363
  const item = activeItem || this.__previousActiveItem;
@@ -338,7 +383,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
338
383
 
339
384
  // Get the index of the row being hovered over or the first/last
340
385
  // visible row if hovering outside the grid
341
- let hoveredIndex = hoveredRow ? hoveredRow.index : undefined;
386
+ let hoveredIndex = hoveredRow?.index;
342
387
  const scrollableArea = this.__getScrollableArea();
343
388
  if (this.__dragCurrentY < scrollableArea.top) {
344
389
  hoveredIndex = this._grid._firstVisibleIndex;