@vaadin/grid 25.3.0-alpha1 → 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 +7 -0
  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 +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-helpers.js +13 -0
  21. package/src/vaadin-grid-keyboard-navigation-mixin.js +3 -7
  22. package/src/vaadin-grid-mixin.js +70 -225
  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, 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
  ),
@@ -149,13 +152,22 @@ export const GridMixin = (superClass) =>
149
152
  /** @private */
150
153
  get _firstVisibleIndex() {
151
154
  const firstVisibleItem = this.__getFirstVisibleItem();
152
- return firstVisibleItem ? firstVisibleItem.index : undefined;
155
+ return firstVisibleItem?.index;
153
156
  }
154
157
 
155
158
  /** @private */
156
159
  get _lastVisibleIndex() {
157
160
  const lastVisibleItem = this.__getLastVisibleItem();
158
- 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);
159
171
  }
160
172
 
161
173
  /** @protected */
@@ -204,13 +216,7 @@ export const GridMixin = (superClass) =>
204
216
 
205
217
  /** @protected */
206
218
  _getRowContainingNode(node) {
207
- const content = getClosestElement('vaadin-grid-cell-content', node);
208
- if (!content) {
209
- return;
210
- }
211
-
212
- const cell = content.assignedSlot.parentElement;
213
- return cell.parentElement;
219
+ return getClosestCell(node)?.parentElement;
214
220
  }
215
221
 
216
222
  /** @protected */
@@ -292,9 +298,7 @@ export const GridMixin = (superClass) =>
292
298
  __focusBodyCell({ item, column }) {
293
299
  const row = this._getRenderedRows().find((row) => row._item === item);
294
300
  const cell = row && [...row.children].find((cell) => cell._column === column);
295
- if (cell) {
296
- cell.focus();
297
- }
301
+ cell?.focus();
298
302
  }
299
303
 
300
304
  /** @protected */
@@ -341,7 +345,7 @@ export const GridMixin = (superClass) =>
341
345
  updatePart(row, 'row', true);
342
346
  updatePart(row, 'body-row', true);
343
347
  if (this._columnTree) {
344
- this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', false, true);
348
+ this.__initRow(row, this._columnTree[this._columnTree.length - 1], 'body', true);
345
349
  }
346
350
  rows.push(row);
347
351
  }
@@ -357,6 +361,36 @@ export const GridMixin = (superClass) =>
357
361
  return rows;
358
362
  }
359
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
+
360
394
  /** @private */
361
395
  _createCell(tagName, column) {
362
396
  const contentId = (this._contentIndex = this._contentIndex + 1 || 0);
@@ -369,22 +403,10 @@ export const GridMixin = (superClass) =>
369
403
  cell.id = slotName.replace('-content-', '-');
370
404
  cell.setAttribute('role', tagName === 'td' ? 'gridcell' : 'columnheader');
371
405
 
372
- // For now we only support tooltip on desktop
373
- if (!isAndroid && !isIOS) {
374
- cell.addEventListener('mouseenter', (event) => {
375
- if (!this.$.scroller.hasAttribute('scrolling')) {
376
- this._showTooltip(event);
377
- }
378
- });
379
-
380
- cell.addEventListener('mouseleave', () => {
381
- this._hideTooltip();
382
- });
383
-
384
- cell.addEventListener('mousedown', () => {
385
- this._hideTooltip(true);
386
- });
387
- }
406
+ cell.addEventListener('mouseenter', this.__onCellMouseEnter);
407
+ cell.addEventListener('mouseleave', this.__onCellMouseLeave);
408
+ cell.addEventListener('mousedown', this.__onCellMouseDown);
409
+ cell.addEventListener('keydown', this.__onCellKeyDown);
388
410
 
389
411
  const slot = document.createElement('slot');
390
412
  slot.setAttribute('name', slotName);
@@ -409,34 +431,6 @@ export const GridMixin = (superClass) =>
409
431
 
410
432
  cell._content = cellContent;
411
433
 
412
- // With native Shadow DOM, mousedown on slotted element does not focus
413
- // focusable slot wrapper, that is why cells are not focused with
414
- // mousedown. Workaround: listen for mousedown and focus manually.
415
- cellContent.addEventListener('mousedown', () => {
416
- if (isChrome) {
417
- // Chrome bug: focusing before mouseup prevents text selection, see http://crbug.com/771903
418
- const mouseUpListener = (event) => {
419
- // If focus is on element within the cell content - respect it, do not change
420
- const contentContainsFocusedElement = cellContent.contains(this.getRootNode().activeElement);
421
- // Only focus if mouse is released on cell content itself
422
- const mouseUpWithinCell = event.composedPath().includes(cellContent);
423
- if (!contentContainsFocusedElement && mouseUpWithinCell) {
424
- cell.focus({ preventScroll: true });
425
- }
426
- document.removeEventListener('mouseup', mouseUpListener, true);
427
- };
428
- document.addEventListener('mouseup', mouseUpListener, true);
429
- } else {
430
- // Focus on mouseup, on the other hand, removes selection on Safari.
431
- // Watch out sync focus removal issue, only async focus works here.
432
- setTimeout(() => {
433
- if (!cellContent.contains(this.getRootNode().activeElement)) {
434
- cell.focus({ preventScroll: true });
435
- }
436
- });
437
- }
438
- });
439
-
440
434
  return cell;
441
435
  }
442
436
 
@@ -444,11 +438,10 @@ export const GridMixin = (superClass) =>
444
438
  * @param {!HTMLTableRowElement} row
445
439
  * @param {!Array<!GridColumn>} columns
446
440
  * @param {?string} section
447
- * @param {boolean} isColumnRow
448
441
  * @param {boolean} noNotify
449
442
  * @private
450
443
  */
451
- __initRow(row, columns, section = 'body', isColumnRow = false, noNotify = false) {
444
+ __initRow(row, columns, section = 'body', noNotify = false) {
452
445
  const contentsFragment = document.createDocumentFragment();
453
446
 
454
447
  iterateRowCells(row, (cell) => {
@@ -475,9 +468,6 @@ export const GridMixin = (superClass) =>
475
468
  cell = column._cells.find((cell) => cell._vacant);
476
469
  if (!cell) {
477
470
  cell = this._createCell('td', column);
478
- if (column._onCellKeyDown) {
479
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
480
- }
481
471
  column._cells.push(cell);
482
472
  }
483
473
  updatePart(cell, 'cell', true);
@@ -519,33 +509,6 @@ export const GridMixin = (superClass) =>
519
509
  if (!noNotify) {
520
510
  column._cells = [...column._cells];
521
511
  }
522
- } else {
523
- // Header & footer
524
- const tagName = section === 'header' ? 'th' : 'td';
525
- if (isColumnRow || column.localName === 'vaadin-grid-column-group') {
526
- cell = column[`_${section}Cell`];
527
- if (!cell) {
528
- cell = this._createCell(tagName);
529
- if (column._onCellKeyDown) {
530
- cell.addEventListener('keydown', column._onCellKeyDown.bind(column));
531
- }
532
- }
533
- cell._column = column;
534
- row.appendChild(cell);
535
- column[`_${section}Cell`] = cell;
536
- } else {
537
- if (!column._emptyCells) {
538
- column._emptyCells = [];
539
- }
540
- cell = column._emptyCells.find((cell) => cell._vacant) || this._createCell(tagName);
541
- cell._column = column;
542
- row.appendChild(cell);
543
- if (column._emptyCells.indexOf(cell) === -1) {
544
- column._emptyCells.push(cell);
545
- }
546
- }
547
- updatePart(cell, 'cell', true);
548
- updatePart(cell, `${section}-cell`, true);
549
512
  }
550
513
 
551
514
  if (!cell._content.parentElement) {
@@ -555,10 +518,6 @@ export const GridMixin = (superClass) =>
555
518
  cell._column = column;
556
519
  });
557
520
 
558
- if (section !== 'body') {
559
- this.__debounceUpdateHeaderFooterRowVisibility(row);
560
- }
561
-
562
521
  // Might be empty if only cache was used
563
522
  this.appendChild(contentsFragment);
564
523
 
@@ -566,75 +525,6 @@ export const GridMixin = (superClass) =>
566
525
  this._updateFirstAndLastColumnForRow(row);
567
526
  }
568
527
 
569
- /**
570
- * @param {HTMLTableRowElement} row
571
- * @protected
572
- */
573
- __debounceUpdateHeaderFooterRowVisibility(row) {
574
- row.__debounceUpdateHeaderFooterRowVisibility = Debouncer.debounce(
575
- row.__debounceUpdateHeaderFooterRowVisibility,
576
- microTask,
577
- () => this.__updateHeaderFooterRowVisibility(row),
578
- );
579
- }
580
-
581
- /**
582
- * @param {HTMLTableRowElement} row
583
- * @protected
584
- */
585
- __updateHeaderFooterRowVisibility(row) {
586
- if (!row) {
587
- return;
588
- }
589
-
590
- const visibleRowCells = Array.from(row.children).filter((cell) => {
591
- const column = cell._column;
592
- if (column._emptyCells && column._emptyCells.indexOf(cell) > -1) {
593
- // The cell is an "empty cell" -> doesn't block hiding the row
594
- return false;
595
- }
596
- if (row.parentElement === this.$.header) {
597
- if (column.headerRenderer) {
598
- // The cell is the header cell of a column that has a header renderer
599
- // -> row should be visible
600
- return true;
601
- }
602
- if (column.header === null) {
603
- // The column header is explicilty set to null -> doesn't block hiding the row
604
- return false;
605
- }
606
- if (column.path || column.header !== undefined) {
607
- // The column has an explicit non-null header or a path that generates a header
608
- // -> row should be visible
609
- return true;
610
- }
611
- } else if (column.footerRenderer) {
612
- // The cell is the footer cell of a column that has a footer renderer
613
- // -> row should be visible
614
- return true;
615
- }
616
- return false;
617
- });
618
-
619
- if (row.hidden !== !visibleRowCells.length) {
620
- row.hidden = !visibleRowCells.length;
621
- }
622
-
623
- if (row.parentElement === this.$.header) {
624
- this.$.table.toggleAttribute('has-header', this.$.header.querySelector('tr:not([hidden])'));
625
- this.__updateHeaderFooterRowParts('header');
626
- }
627
-
628
- if (row.parentElement === this.$.footer) {
629
- this.$.table.toggleAttribute('has-footer', this.$.footer.querySelector('tr:not([hidden])'));
630
- this.__updateHeaderFooterRowParts('footer');
631
- }
632
-
633
- // Make sure the section has a tabbable element
634
- this._resetKeyboardNavigation();
635
- this.__a11yUpdateGridSize(this.size, this._columnTree, this.__emptyState);
636
- }
637
-
638
528
  /** @private */
639
529
  __updateVirtualizerElement(row, index) {
640
530
  this._preventScrollerRotatingCellFocus(row, index);
@@ -687,46 +577,17 @@ export const GridMixin = (superClass) =>
687
577
  */
688
578
  _renderColumnTree(columnTree) {
689
579
  iterateChildren(this.$.items, (row) => {
690
- this.__initRow(row, columnTree[columnTree.length - 1], 'body', false, true);
580
+ this.__initRow(row, columnTree[columnTree.length - 1], 'body', true);
691
581
  this.__updateRow(row);
692
582
  });
693
583
 
694
- while (this.$.header.children.length < columnTree.length) {
695
- const headerRow = document.createElement('tr');
696
- headerRow.setAttribute('role', 'row');
697
- headerRow.setAttribute('tabindex', '-1');
698
- updatePart(headerRow, 'row', true);
699
- updatePart(headerRow, 'header-row', true);
700
- this.$.header.appendChild(headerRow);
701
-
702
- const footerRow = document.createElement('tr');
703
- footerRow.setAttribute('role', 'row');
704
- footerRow.setAttribute('tabindex', '-1');
705
- updatePart(footerRow, 'row', true);
706
- updatePart(footerRow, 'footer-row', true);
707
- this.$.footer.appendChild(footerRow);
708
- }
709
- while (this.$.header.children.length > columnTree.length) {
710
- this.$.header.removeChild(this.$.header.firstElementChild);
711
- this.$.footer.removeChild(this.$.footer.firstElementChild);
712
- }
713
-
714
- iterateChildren(this.$.header, (headerRow, index) => {
715
- this.__initRow(headerRow, columnTree[index], 'header', index === columnTree.length - 1);
716
- });
717
-
718
- iterateChildren(this.$.footer, (footerRow, index) => {
719
- this.__initRow(footerRow, columnTree[columnTree.length - 1 - index], 'footer', index === 0);
720
- });
584
+ this.__renderHeaderFooter();
721
585
 
722
586
  // Sizer rows
723
587
  this.__initRow(this.$.sizer, columnTree[columnTree.length - 1]);
724
588
 
725
- this.__updateHeaderFooterRowParts('header');
726
- this.__updateHeaderFooterRowParts('footer');
727
589
  this._resizeHandler();
728
590
  this._frozenCellsChanged();
729
- this._updateFirstAndLastColumn();
730
591
  this._resetKeyboardNavigation();
731
592
  this.__a11yUpdateHeaderRows();
732
593
  this.__a11yUpdateFooterRows();
@@ -734,20 +595,6 @@ export const GridMixin = (superClass) =>
734
595
  this.__updateHeaderAndFooter();
735
596
  }
736
597
 
737
- /** @private */
738
- __updateHeaderFooterRowParts(section) {
739
- const visibleRows = [...this.$[section].querySelectorAll('tr:not([hidden])')];
740
- [...this.$[section].children].forEach((row) => {
741
- updatePart(row, `first-${section}-row`, row === visibleRows.at(0));
742
- updatePart(row, `last-${section}-row`, row === visibleRows.at(-1));
743
-
744
- getBodyRowCells(row).forEach((cell) => {
745
- updatePart(cell, `first-${section}-row-cell`, row === visibleRows.at(0));
746
- updatePart(cell, `last-${section}-row-cell`, row === visibleRows.at(-1));
747
- });
748
- });
749
- }
750
-
751
598
  /**
752
599
  * @param {!HTMLElement} row
753
600
  * @param {boolean} loading
@@ -925,9 +772,7 @@ export const GridMixin = (superClass) =>
925
772
 
926
773
  /** @protected */
927
774
  __updateVisibleRows(start, end) {
928
- if (this.__virtualizer) {
929
- this.__virtualizer.update(start, end);
930
- }
775
+ this.__virtualizer?.update(start, end);
931
776
  }
932
777
 
933
778
  /** @private */
@@ -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;