@vaadin/grid 22.0.0-alpha7

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 (104) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +79 -0
  3. package/all-imports.js +1 -0
  4. package/package.json +58 -0
  5. package/src/all-imports.js +14 -0
  6. package/src/array-data-provider.js +145 -0
  7. package/src/interfaces.d.ts +75 -0
  8. package/src/vaadin-grid-a11y-mixin.js +158 -0
  9. package/src/vaadin-grid-active-item-mixin.d.ts +19 -0
  10. package/src/vaadin-grid-active-item-mixin.js +117 -0
  11. package/src/vaadin-grid-array-data-provider-mixin.d.ts +16 -0
  12. package/src/vaadin-grid-array-data-provider-mixin.js +75 -0
  13. package/src/vaadin-grid-column-group.d.ts +54 -0
  14. package/src/vaadin-grid-column-group.js +320 -0
  15. package/src/vaadin-grid-column-reordering-mixin.d.ts +19 -0
  16. package/src/vaadin-grid-column-reordering-mixin.js +387 -0
  17. package/src/vaadin-grid-column-resizing-mixin.js +111 -0
  18. package/src/vaadin-grid-column.d.ts +133 -0
  19. package/src/vaadin-grid-column.js +745 -0
  20. package/src/vaadin-grid-data-provider-mixin.d.ts +108 -0
  21. package/src/vaadin-grid-data-provider-mixin.js +520 -0
  22. package/src/vaadin-grid-drag-and-drop-mixin.d.ts +69 -0
  23. package/src/vaadin-grid-drag-and-drop-mixin.js +433 -0
  24. package/src/vaadin-grid-dynamic-columns-mixin.js +180 -0
  25. package/src/vaadin-grid-event-context-mixin.d.ts +33 -0
  26. package/src/vaadin-grid-event-context-mixin.js +57 -0
  27. package/src/vaadin-grid-filter-column.d.ts +35 -0
  28. package/src/vaadin-grid-filter-column.js +120 -0
  29. package/src/vaadin-grid-filter-mixin.js +76 -0
  30. package/src/vaadin-grid-filter.d.ts +67 -0
  31. package/src/vaadin-grid-filter.js +125 -0
  32. package/src/vaadin-grid-helpers.js +23 -0
  33. package/src/vaadin-grid-keyboard-navigation-mixin.js +891 -0
  34. package/src/vaadin-grid-row-details-mixin.d.ts +44 -0
  35. package/src/vaadin-grid-row-details-mixin.js +200 -0
  36. package/src/vaadin-grid-scroll-mixin.d.ts +18 -0
  37. package/src/vaadin-grid-scroll-mixin.js +202 -0
  38. package/src/vaadin-grid-selection-column.d.ts +71 -0
  39. package/src/vaadin-grid-selection-column.js +285 -0
  40. package/src/vaadin-grid-selection-mixin.d.ts +30 -0
  41. package/src/vaadin-grid-selection-mixin.js +93 -0
  42. package/src/vaadin-grid-sort-column.d.ts +63 -0
  43. package/src/vaadin-grid-sort-column.js +118 -0
  44. package/src/vaadin-grid-sort-mixin.d.ts +15 -0
  45. package/src/vaadin-grid-sort-mixin.js +139 -0
  46. package/src/vaadin-grid-sorter.d.ts +94 -0
  47. package/src/vaadin-grid-sorter.js +230 -0
  48. package/src/vaadin-grid-styles.js +297 -0
  49. package/src/vaadin-grid-styling-mixin.d.ts +37 -0
  50. package/src/vaadin-grid-styling-mixin.js +71 -0
  51. package/src/vaadin-grid-tree-column.d.ts +36 -0
  52. package/src/vaadin-grid-tree-column.js +119 -0
  53. package/src/vaadin-grid-tree-toggle.d.ts +104 -0
  54. package/src/vaadin-grid-tree-toggle.js +205 -0
  55. package/src/vaadin-grid.d.ts +397 -0
  56. package/src/vaadin-grid.js +1004 -0
  57. package/theme/lumo/all-imports.js +11 -0
  58. package/theme/lumo/vaadin-grid-column-group.js +1 -0
  59. package/theme/lumo/vaadin-grid-column.js +1 -0
  60. package/theme/lumo/vaadin-grid-filter-column.js +2 -0
  61. package/theme/lumo/vaadin-grid-filter.js +2 -0
  62. package/theme/lumo/vaadin-grid-selection-column.js +2 -0
  63. package/theme/lumo/vaadin-grid-sort-column.js +2 -0
  64. package/theme/lumo/vaadin-grid-sorter-styles.js +53 -0
  65. package/theme/lumo/vaadin-grid-sorter.js +2 -0
  66. package/theme/lumo/vaadin-grid-styles.js +378 -0
  67. package/theme/lumo/vaadin-grid-tree-column.js +2 -0
  68. package/theme/lumo/vaadin-grid-tree-toggle-styles.js +112 -0
  69. package/theme/lumo/vaadin-grid-tree-toggle.js +2 -0
  70. package/theme/lumo/vaadin-grid.js +9 -0
  71. package/theme/material/all-imports.js +11 -0
  72. package/theme/material/vaadin-grid-column-group.js +1 -0
  73. package/theme/material/vaadin-grid-column.js +1 -0
  74. package/theme/material/vaadin-grid-filter-column.js +2 -0
  75. package/theme/material/vaadin-grid-filter.js +2 -0
  76. package/theme/material/vaadin-grid-selection-column.js +2 -0
  77. package/theme/material/vaadin-grid-sort-column.js +2 -0
  78. package/theme/material/vaadin-grid-sorter-styles.js +72 -0
  79. package/theme/material/vaadin-grid-sorter.js +2 -0
  80. package/theme/material/vaadin-grid-styles.js +252 -0
  81. package/theme/material/vaadin-grid-tree-column.js +2 -0
  82. package/theme/material/vaadin-grid-tree-toggle-styles.js +42 -0
  83. package/theme/material/vaadin-grid-tree-toggle.js +2 -0
  84. package/theme/material/vaadin-grid.js +2 -0
  85. package/vaadin-grid-column-group.d.ts +1 -0
  86. package/vaadin-grid-column-group.js +3 -0
  87. package/vaadin-grid-column.d.ts +1 -0
  88. package/vaadin-grid-column.js +3 -0
  89. package/vaadin-grid-filter-column.d.ts +1 -0
  90. package/vaadin-grid-filter-column.js +3 -0
  91. package/vaadin-grid-filter.d.ts +1 -0
  92. package/vaadin-grid-filter.js +3 -0
  93. package/vaadin-grid-selection-column.d.ts +1 -0
  94. package/vaadin-grid-selection-column.js +3 -0
  95. package/vaadin-grid-sort-column.d.ts +1 -0
  96. package/vaadin-grid-sort-column.js +3 -0
  97. package/vaadin-grid-sorter.d.ts +1 -0
  98. package/vaadin-grid-sorter.js +3 -0
  99. package/vaadin-grid-tree-column.d.ts +1 -0
  100. package/vaadin-grid-tree-column.js +3 -0
  101. package/vaadin-grid-tree-toggle.d.ts +1 -0
  102. package/vaadin-grid-tree-toggle.js +3 -0
  103. package/vaadin-grid.d.ts +3 -0
  104. package/vaadin-grid.js +4 -0
@@ -0,0 +1,891 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+
7
+ /**
8
+ * @polymerMixin
9
+ */
10
+ export const KeyboardNavigationMixin = (superClass) =>
11
+ class KeyboardNavigationMixin extends superClass {
12
+ static get properties() {
13
+ return {
14
+ /** @private */
15
+ _headerFocusable: {
16
+ type: Object,
17
+ observer: '_focusableChanged'
18
+ },
19
+
20
+ /**
21
+ * @type {!HTMLElement | undefined}
22
+ * @protected
23
+ */
24
+ _itemsFocusable: {
25
+ type: Object,
26
+ observer: '_focusableChanged'
27
+ },
28
+
29
+ /** @private */
30
+ _footerFocusable: {
31
+ type: Object,
32
+ observer: '_focusableChanged'
33
+ },
34
+
35
+ /** @private */
36
+ _navigatingIsHidden: Boolean,
37
+
38
+ /**
39
+ * @type {number}
40
+ * @protected
41
+ */
42
+ _focusedItemIndex: {
43
+ type: Number,
44
+ value: 0
45
+ },
46
+
47
+ /** @private */
48
+ _focusedColumnOrder: Number,
49
+
50
+ /**
51
+ * Indicates whether the grid is currently in interaction mode.
52
+ * In interaction mode the user is currently interacting with a control,
53
+ * such as an input or a select, within a cell.
54
+ * In interaction mode keyboard navigation between cells is disabled.
55
+ * Interaction mode also prevents the focus target cell of that section of
56
+ * the grid from receiving focus, allowing the user to switch focus to
57
+ * controls in adjacent cells, rather than focussing the outer cell
58
+ * itself.
59
+ * @type {boolean}
60
+ * @private
61
+ */
62
+ interacting: {
63
+ type: Boolean,
64
+ value: false,
65
+ reflectToAttribute: true,
66
+ readOnly: true,
67
+ observer: '_interactingChanged'
68
+ }
69
+ };
70
+ }
71
+
72
+ /** @protected */
73
+ ready() {
74
+ super.ready();
75
+
76
+ if (this._ios || this._android) {
77
+ // Disable keyboard navigation on mobile devices
78
+ return;
79
+ }
80
+
81
+ this.addEventListener('keydown', this._onKeyDown);
82
+ this.addEventListener('keyup', this._onKeyUp);
83
+
84
+ this.addEventListener('focusin', this._onFocusIn);
85
+ this.addEventListener('focusout', this._onFocusOut);
86
+
87
+ // When focus goes from cell to another cell, focusin/focusout events do
88
+ // not escape the grid’s shadowRoot, thus listening inside the shadowRoot.
89
+ this.$.table.addEventListener('focusin', this._onContentFocusIn.bind(this));
90
+
91
+ this.addEventListener('mousedown', () => {
92
+ this.toggleAttribute('navigating', false);
93
+ this._isMousedown = true;
94
+ });
95
+ this.addEventListener('mouseup', () => (this._isMousedown = false));
96
+ }
97
+
98
+ /** @private */
99
+ get __rowFocusMode() {
100
+ return this.__isRow(this._itemsFocusable);
101
+ }
102
+
103
+ set __rowFocusMode(value) {
104
+ ['_itemsFocusable', '_footerFocusable', '_headerFocusable'].forEach((focusable) => {
105
+ if (value && this.__isCell(this[focusable])) {
106
+ this[focusable] = this[focusable].parentElement;
107
+ } else if (!value && this.__isRow(this[focusable])) {
108
+ this[focusable] = this[focusable].firstElementChild;
109
+ }
110
+ });
111
+ }
112
+
113
+ /** @private */
114
+ _focusableChanged(focusable, oldFocusable) {
115
+ if (oldFocusable) {
116
+ oldFocusable.setAttribute('tabindex', '-1');
117
+ }
118
+ if (focusable) {
119
+ this._updateGridSectionFocusTarget(focusable);
120
+ }
121
+ }
122
+
123
+ /** @private */
124
+ _interactingChanged() {
125
+ // Update focus targets when entering / exiting interaction mode
126
+ this._updateGridSectionFocusTarget(this._headerFocusable);
127
+ this._updateGridSectionFocusTarget(this._itemsFocusable);
128
+ this._updateGridSectionFocusTarget(this._footerFocusable);
129
+ }
130
+
131
+ /**
132
+ * @param {!KeyboardEvent} e
133
+ * @protected
134
+ */
135
+ _onKeyDown(e) {
136
+ const key = e.key;
137
+
138
+ let keyGroup;
139
+ switch (key) {
140
+ case 'ArrowUp':
141
+ case 'ArrowDown':
142
+ case 'ArrowLeft':
143
+ case 'ArrowRight':
144
+ case 'PageUp':
145
+ case 'PageDown':
146
+ case 'Home':
147
+ case 'End':
148
+ keyGroup = 'Navigation';
149
+ break;
150
+ case 'Enter':
151
+ case 'Escape':
152
+ case 'F2':
153
+ keyGroup = 'Interaction';
154
+ break;
155
+ case 'Tab':
156
+ keyGroup = 'Tab';
157
+ break;
158
+ case ' ':
159
+ keyGroup = 'Space';
160
+ break;
161
+ }
162
+
163
+ this._detectInteracting(e);
164
+ if (this.interacting && keyGroup !== 'Interaction') {
165
+ // When in the interacting mode, only the “Interaction” keys are handled.
166
+ keyGroup = undefined;
167
+ }
168
+
169
+ if (keyGroup) {
170
+ this[`_on${keyGroup}KeyDown`](e, key);
171
+ }
172
+ }
173
+
174
+ /** @private */
175
+ _ensureScrolledToIndex(index) {
176
+ const targetRowInDom = [...this.$.items.children].find((child) => child.index === index);
177
+ if (!targetRowInDom) {
178
+ this.scrollToIndex(index);
179
+ } else {
180
+ this.__scrollIntoViewport(index);
181
+ }
182
+ }
183
+
184
+ // TODO: A tree toggle component should not be the way to determine if the row is expandable
185
+ /** @private */
186
+ __isRowExpandable(row) {
187
+ const treeToggle = [...row.children].reduce(
188
+ (value, cell) => value || cell._content.querySelector('vaadin-grid-tree-toggle'),
189
+ null
190
+ );
191
+ return treeToggle && !treeToggle.expanded && !treeToggle.leaf;
192
+ }
193
+
194
+ /** @private */
195
+ __isRowCollapsible(row) {
196
+ return this._isExpanded(row._item);
197
+ }
198
+
199
+ /** @private */
200
+ __isDetailsCell(element) {
201
+ return element.matches('[part~="details-cell"]');
202
+ }
203
+
204
+ /** @private */
205
+ __isCell(element) {
206
+ return element instanceof HTMLTableCellElement;
207
+ }
208
+
209
+ /** @private */
210
+ __isRow(element) {
211
+ return element instanceof HTMLTableRowElement;
212
+ }
213
+
214
+ /** @private */
215
+ __getIndexOfChildElement(el) {
216
+ return Array.prototype.indexOf.call(el.parentNode.children, el);
217
+ }
218
+
219
+ /** @private */
220
+ _onNavigationKeyDown(e, key) {
221
+ e.preventDefault();
222
+
223
+ const visibleItemsCount = this._lastVisibleIndex - this._firstVisibleIndex - 1;
224
+
225
+ // Handle keyboard interaction as defined in:
226
+ // https://w3c.github.io/aria-practices/#keyboard-interaction-24
227
+
228
+ let dx = 0,
229
+ dy = 0;
230
+ switch (key) {
231
+ case 'ArrowRight':
232
+ dx = this.__isRTL ? -1 : 1;
233
+ break;
234
+ case 'ArrowLeft':
235
+ dx = this.__isRTL ? 1 : -1;
236
+ break;
237
+ case 'Home':
238
+ if (this.__rowFocusMode) {
239
+ // "If focus is on a row, moves focus to the first row. If focus is in the first row, focus does not move."
240
+ dy = -Infinity;
241
+ } else {
242
+ if (e.ctrlKey) {
243
+ // "If focus is on a cell, moves focus to the first cell in the column. If focus is in the first row, focus does not move."
244
+ dy = -Infinity;
245
+ } else {
246
+ // "If focus is on a cell, moves focus to the first cell in the row. If focus is in the first cell of the row, focus does not move."
247
+ dx = -Infinity;
248
+ }
249
+ }
250
+ break;
251
+ case 'End':
252
+ if (this.__rowFocusMode) {
253
+ // "If focus is on a row, moves focus to the last row. If focus is in the last row, focus does not move."
254
+ dy = Infinity;
255
+ } else {
256
+ if (e.ctrlKey) {
257
+ // "If focus is on a cell, moves focus to the last cell in the column. If focus is in the last row, focus does not move."
258
+ dy = Infinity;
259
+ } else {
260
+ // "If focus is on a cell, moves focus to the last cell in the row. If focus is in the last cell of the row, focus does not move."
261
+ dx = Infinity;
262
+ }
263
+ }
264
+ break;
265
+ case 'ArrowDown':
266
+ dy = 1;
267
+ break;
268
+ case 'ArrowUp':
269
+ dy = -1;
270
+ break;
271
+ case 'PageDown':
272
+ dy = visibleItemsCount;
273
+ break;
274
+ case 'PageUp':
275
+ dy = -visibleItemsCount;
276
+ break;
277
+ }
278
+
279
+ const activeRow = e.composedPath().find((el) => this.__isRow(el));
280
+ const activeCell = e.composedPath().find((el) => this.__isCell(el));
281
+
282
+ if ((this.__rowFocusMode && !activeRow) || (!this.__rowFocusMode && !activeCell)) {
283
+ // When using a screen reader, it's possible that neither a cell nor a row is focused.
284
+ return;
285
+ }
286
+
287
+ const forwardsKey = this.__isRTL ? 'ArrowLeft' : 'ArrowRight';
288
+ const backwardsKey = this.__isRTL ? 'ArrowRight' : 'ArrowLeft';
289
+ if (key === forwardsKey) {
290
+ // "Right Arrow:"
291
+ if (this.__rowFocusMode) {
292
+ // In row focus mode
293
+ if (this.__isRowExpandable(activeRow)) {
294
+ // "If focus is on a collapsed row, expands the row."
295
+ this.expandItem(activeRow._item);
296
+ return;
297
+ } else {
298
+ // "If focus is on an expanded row or is on a row that does not have child rows,
299
+ // moves focus to the first cell in the row."
300
+ this.__rowFocusMode = false;
301
+ this._onCellNavigation(activeRow.firstElementChild, 0, 0);
302
+ return;
303
+ }
304
+ }
305
+ } else if (key === backwardsKey) {
306
+ // "Left Arrow:"
307
+ if (this.__rowFocusMode) {
308
+ // In row focus mode
309
+ if (this.__isRowCollapsible(activeRow)) {
310
+ // "If focus is on an expanded row, collapses the row."
311
+ this.collapseItem(activeRow._item);
312
+ return;
313
+ }
314
+ } else {
315
+ // In cell focus mode
316
+ const activeRowCells = [...activeRow.children].sort((a, b) => a._order - b._order);
317
+ if (activeCell === activeRowCells[0] || this.__isDetailsCell(activeCell)) {
318
+ // "If focus is on the first cell in a row and row focus is supported, moves focus to the row."
319
+ this.__rowFocusMode = true;
320
+ this._onRowNavigation(activeRow, 0);
321
+ return;
322
+ }
323
+ }
324
+ }
325
+
326
+ // Navigate
327
+ if (this.__rowFocusMode) {
328
+ // Navigate the rows
329
+ this._onRowNavigation(activeRow, dy);
330
+ } else {
331
+ // Navigate the cells
332
+ this._onCellNavigation(activeCell, dx, dy);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Focuses the target row after navigating by the given dy offset.
338
+ * If the row is not in the viewport, it is first scrolled to.
339
+ * @private
340
+ **/
341
+ _onRowNavigation(activeRow, dy) {
342
+ const { dstRow } = this.__navigateRows(dy, activeRow);
343
+
344
+ if (dstRow) {
345
+ dstRow.focus();
346
+ }
347
+ }
348
+
349
+ /** @private */
350
+ __getIndexInGroup(row, bodyFallbackIndex) {
351
+ const rowGroup = row.parentNode;
352
+ // Body rows have index property, otherwise DOM child index of the row is used.
353
+ if (rowGroup === this.$.items) {
354
+ return bodyFallbackIndex !== undefined ? bodyFallbackIndex : row.index;
355
+ } else {
356
+ return this.__getIndexOfChildElement(row);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Returns the target row after navigating by the given dy offset.
362
+ * Also returns infromation whether the details cell should be the target on the target row.
363
+ * If the row is not in the viewport, it is first scrolled to.
364
+ * @private
365
+ **/
366
+ __navigateRows(dy, activeRow, activeCell) {
367
+ const currentRowIndex = this.__getIndexInGroup(activeRow, this._focusedItemIndex);
368
+ const activeRowGroup = activeRow.parentNode;
369
+ const maxRowIndex = (activeRowGroup === this.$.items ? this._effectiveSize : activeRowGroup.children.length) - 1;
370
+
371
+ // Index of the destination row
372
+ let dstRowIndex = Math.max(0, Math.min(currentRowIndex + dy, maxRowIndex));
373
+
374
+ if (activeRowGroup !== this.$.items) {
375
+ // Navigating header/footer rows
376
+
377
+ // Header and footer could have hidden rows, e. g., if none of the columns
378
+ // or groups on the given column tree level define template. Skip them
379
+ // in vertical keyboard navigation.
380
+ if (dstRowIndex > currentRowIndex) {
381
+ while (dstRowIndex < maxRowIndex && activeRowGroup.children[dstRowIndex].hidden) {
382
+ dstRowIndex++;
383
+ }
384
+ } else if (dstRowIndex < currentRowIndex) {
385
+ while (dstRowIndex > 0 && activeRowGroup.children[dstRowIndex].hidden) {
386
+ dstRowIndex--;
387
+ }
388
+ }
389
+
390
+ this.toggleAttribute('navigating', true);
391
+
392
+ return { dstRow: activeRowGroup.children[dstRowIndex] };
393
+ } else {
394
+ // Navigating body rows
395
+
396
+ let dstIsRowDetails = false;
397
+ if (activeCell) {
398
+ const isRowDetails = this.__isDetailsCell(activeCell);
399
+ // Row details navigation logic
400
+ if (activeRowGroup === this.$.items) {
401
+ const item = activeRow._item;
402
+ const dstItem = this._cache.getItemForIndex(dstRowIndex);
403
+ // Should we navigate to row details?
404
+ if (isRowDetails) {
405
+ dstIsRowDetails = dy === 0;
406
+ } else {
407
+ dstIsRowDetails =
408
+ (dy === 1 && this._isDetailsOpened(item)) ||
409
+ (dy === -1 && dstRowIndex !== currentRowIndex && this._isDetailsOpened(dstItem));
410
+ }
411
+ // Should we navigate between details and regular cells of the same row?
412
+ if (
413
+ dstIsRowDetails !== isRowDetails &&
414
+ ((dy === 1 && dstIsRowDetails) || (dy === -1 && !dstIsRowDetails))
415
+ ) {
416
+ dstRowIndex = currentRowIndex;
417
+ }
418
+ }
419
+ }
420
+
421
+ // Ensure correct vertical scroll position, destination row is visible
422
+ this._ensureScrolledToIndex(dstRowIndex);
423
+
424
+ // When scrolling with repeated keydown, sometimes FocusEvent listeners
425
+ // are too late to update _focusedItemIndex. Ensure next keydown
426
+ // listener invocation gets updated _focusedItemIndex value.
427
+ this._focusedItemIndex = dstRowIndex;
428
+
429
+ // This has to be set after scrolling, otherwise it can be removed by
430
+ // `_preventScrollerRotatingCellFocus(row, index)` during scrolling.
431
+ this.toggleAttribute('navigating', true);
432
+
433
+ return {
434
+ dstRow: [...activeRowGroup.children].find((el) => !el.hidden && el.index === dstRowIndex),
435
+ dstIsRowDetails
436
+ };
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Focuses the target cell after navigating by the given dx and dy offset.
442
+ * If the cell is not in the viewport, it is first scrolled to.
443
+ * @private
444
+ **/
445
+ _onCellNavigation(activeCell, dx, dy) {
446
+ const activeRow = activeCell.parentNode;
447
+ const { dstRow, dstIsRowDetails } = this.__navigateRows(dy, activeRow, activeCell);
448
+ if (!dstRow) {
449
+ return;
450
+ }
451
+
452
+ const columnIndex = this.__getIndexOfChildElement(activeCell);
453
+ const isCurrentCellRowDetails = this.__isDetailsCell(activeCell);
454
+ const activeRowGroup = activeRow.parentNode;
455
+ const currentRowIndex = this.__getIndexInGroup(activeRow, this._focusedItemIndex);
456
+
457
+ // _focusedColumnOrder is memoized — this is to ensure predictable
458
+ // navigation when entering and leaving detail and column group cells.
459
+ if (this._focusedColumnOrder === undefined) {
460
+ if (isCurrentCellRowDetails) {
461
+ this._focusedColumnOrder = 0;
462
+ } else {
463
+ this._focusedColumnOrder = this._getColumns(activeRowGroup, currentRowIndex).filter((c) => !c.hidden)[
464
+ columnIndex
465
+ ]._order;
466
+ }
467
+ }
468
+
469
+ if (dstIsRowDetails) {
470
+ // Focusing a row details cell on the destination row
471
+ const dstCell = [...dstRow.children].find((el) => this.__isDetailsCell(el));
472
+ dstCell.focus();
473
+ } else {
474
+ // Focusing a regular cell on the destination row
475
+
476
+ // Find orderedColumnIndex — the index of order closest matching the
477
+ // original _focusedColumnOrder in the sorted array of orders
478
+ // of the visible columns on the destination row.
479
+ const dstRowIndex = this.__getIndexInGroup(dstRow, this._focusedItemIndex);
480
+ const dstColumns = this._getColumns(activeRowGroup, dstRowIndex).filter((c) => !c.hidden);
481
+ const dstSortedColumnOrders = dstColumns.map((c) => c._order).sort((b, a) => b - a);
482
+ const maxOrderedColumnIndex = dstSortedColumnOrders.length - 1;
483
+ const orderedColumnIndex = dstSortedColumnOrders.indexOf(
484
+ dstSortedColumnOrders
485
+ .slice(0)
486
+ .sort((b, a) => Math.abs(b - this._focusedColumnOrder) - Math.abs(a - this._focusedColumnOrder))[0]
487
+ );
488
+
489
+ // Index of the destination column order
490
+ const dstOrderedColumnIndex =
491
+ dy === 0 && isCurrentCellRowDetails
492
+ ? orderedColumnIndex
493
+ : Math.max(0, Math.min(orderedColumnIndex + dx, maxOrderedColumnIndex));
494
+
495
+ if (dstOrderedColumnIndex !== orderedColumnIndex) {
496
+ // Horizontal movement invalidates stored _focusedColumnOrder
497
+ this._focusedColumnOrder = undefined;
498
+ }
499
+
500
+ const columnIndexByOrder = dstColumns.reduce((acc, col, i) => ((acc[col._order] = i), acc), {});
501
+ const dstColumnIndex = columnIndexByOrder[dstSortedColumnOrders[dstOrderedColumnIndex]];
502
+ const dstCell = dstRow.children[dstColumnIndex];
503
+
504
+ this._scrollHorizontallyToCell(dstCell);
505
+ dstCell.focus();
506
+ }
507
+ }
508
+
509
+ /** @private */
510
+ _onInteractionKeyDown(e, key) {
511
+ const localTarget = e.composedPath()[0];
512
+ const localTargetIsTextInput =
513
+ localTarget.localName === 'input' &&
514
+ !/^(button|checkbox|color|file|image|radio|range|reset|submit)$/i.test(localTarget.type);
515
+
516
+ let wantInteracting;
517
+ switch (key) {
518
+ case 'Enter':
519
+ wantInteracting = this.interacting ? !localTargetIsTextInput : true;
520
+ break;
521
+ case 'Escape':
522
+ wantInteracting = false;
523
+ break;
524
+ case 'F2':
525
+ wantInteracting = !this.interacting;
526
+ break;
527
+ }
528
+
529
+ const { cell } = this._getGridEventLocation(e);
530
+
531
+ if (this.interacting !== wantInteracting && cell !== null) {
532
+ if (wantInteracting) {
533
+ const focusTarget = cell._content.querySelector('[focus-target]') || cell._content.firstElementChild;
534
+ if (focusTarget) {
535
+ e.preventDefault();
536
+ focusTarget.focus();
537
+ this._setInteracting(true);
538
+ this.toggleAttribute('navigating', false);
539
+ }
540
+ } else {
541
+ e.preventDefault();
542
+ this._focusedColumnOrder = undefined;
543
+ cell.focus();
544
+ this._setInteracting(false);
545
+ this.toggleAttribute('navigating', true);
546
+ }
547
+ }
548
+ }
549
+
550
+ /** @private */
551
+ _predictFocusStepTarget(srcElement, step) {
552
+ const tabOrder = [
553
+ this.$.table,
554
+ this._headerFocusable,
555
+ this._itemsFocusable,
556
+ this._footerFocusable,
557
+ this.$.focusexit
558
+ ];
559
+
560
+ let index = tabOrder.indexOf(srcElement);
561
+
562
+ index += step;
563
+ while (index >= 0 && index <= tabOrder.length - 1) {
564
+ const rowElement = this.__rowFocusMode ? tabOrder[index] : tabOrder[index].parentNode;
565
+
566
+ if (!rowElement || rowElement.hidden) {
567
+ index += step;
568
+ } else {
569
+ break;
570
+ }
571
+ }
572
+
573
+ return tabOrder[index];
574
+ }
575
+
576
+ /** @private */
577
+ _onTabKeyDown(e) {
578
+ const focusTarget = this._predictFocusStepTarget(e.composedPath()[0], e.shiftKey ? -1 : 1);
579
+
580
+ if (focusTarget === this.$.table) {
581
+ // The focus is about to exit the grid to the top.
582
+ this.$.table.focus();
583
+ } else if (focusTarget === this.$.focusexit) {
584
+ // The focus is about to exit the grid to the bottom.
585
+ this.$.focusexit.focus();
586
+ } else if (focusTarget === this._itemsFocusable) {
587
+ let itemsFocusTarget = focusTarget;
588
+ const targetRow = this.__isRow(focusTarget) ? focusTarget : focusTarget.parentNode;
589
+ this._ensureScrolledToIndex(this._focusedItemIndex);
590
+ if (targetRow.index !== this._focusedItemIndex && this.__isCell(focusTarget)) {
591
+ // The target row, which is about to be focused next, has been
592
+ // assigned with a new index since last focus, probably because of
593
+ // scrolling. Focus the row for the stored focused item index instead.
594
+ const columnIndex = Array.from(targetRow.children).indexOf(this._itemsFocusable);
595
+ const focusedItemRow = Array.from(this.$.items.children).filter(
596
+ (row) => row.index === this._focusedItemIndex
597
+ )[0];
598
+ if (focusedItemRow) {
599
+ itemsFocusTarget = focusedItemRow.children[columnIndex];
600
+ }
601
+ }
602
+ e.preventDefault();
603
+ itemsFocusTarget.focus();
604
+ } else {
605
+ e.preventDefault();
606
+ focusTarget.focus();
607
+ }
608
+
609
+ this.toggleAttribute('navigating', true);
610
+ }
611
+
612
+ /** @private */
613
+ _onSpaceKeyDown(e) {
614
+ e.preventDefault();
615
+
616
+ const element = e.composedPath()[0];
617
+ const isRow = this.__isRow(element);
618
+ if (isRow || !element._content || !element._content.firstElementChild) {
619
+ this.dispatchEvent(
620
+ new CustomEvent(isRow ? 'row-activate' : 'cell-activate', {
621
+ detail: {
622
+ model: this.__getRowModel(isRow ? element : element.parentElement)
623
+ }
624
+ })
625
+ );
626
+ }
627
+ }
628
+
629
+ /** @private */
630
+ _onKeyUp(e) {
631
+ if (!/^( |SpaceBar)$/.test(e.key) || this.interacting) {
632
+ return;
633
+ }
634
+
635
+ e.preventDefault();
636
+
637
+ const cell = e.composedPath()[0];
638
+ if (cell._content && cell._content.firstElementChild) {
639
+ const wasNavigating = this.hasAttribute('navigating');
640
+ cell._content.firstElementChild.click();
641
+ this.toggleAttribute('navigating', wasNavigating);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * @param {!FocusEvent} e
647
+ * @protected
648
+ */
649
+ _onFocusIn(e) {
650
+ if (!this._isMousedown) {
651
+ this.toggleAttribute('navigating', true);
652
+ }
653
+
654
+ const rootTarget = e.composedPath()[0];
655
+
656
+ if (rootTarget === this.$.table || rootTarget === this.$.focusexit) {
657
+ // The focus enters the top (bottom) of the grid, meaning that user has
658
+ // tabbed (shift-tabbed) into the grid. Move the focus to
659
+ // the first (the last) focusable.
660
+ this._predictFocusStepTarget(rootTarget, rootTarget === this.$.table ? 1 : -1).focus();
661
+ this._setInteracting(false);
662
+ } else {
663
+ this._detectInteracting(e);
664
+ }
665
+ }
666
+
667
+ /**
668
+ * @param {!FocusEvent} e
669
+ * @protected
670
+ */
671
+ _onFocusOut(e) {
672
+ this.toggleAttribute('navigating', false);
673
+ this._detectInteracting(e);
674
+ }
675
+
676
+ /** @private */
677
+ _onContentFocusIn(e) {
678
+ const { section, cell, row } = this._getGridEventLocation(e);
679
+ this._detectInteracting(e);
680
+
681
+ if (section && (cell || row)) {
682
+ this._activeRowGroup = section;
683
+ if (this.$.header === section) {
684
+ this._headerFocusable = this.__rowFocusMode ? row : cell;
685
+ } else if (this.$.items === section) {
686
+ this._itemsFocusable = this.__rowFocusMode ? row : cell;
687
+ } else if (this.$.footer === section) {
688
+ this._footerFocusable = this.__rowFocusMode ? row : cell;
689
+ }
690
+
691
+ if (cell) {
692
+ // Fire a public event for cell.
693
+ const context = this.getEventContext(e);
694
+ cell.dispatchEvent(
695
+ new CustomEvent('cell-focus', { bubbles: true, composed: true, detail: { context: context } })
696
+ );
697
+ }
698
+ }
699
+
700
+ this._detectFocusedItemIndex(e);
701
+ }
702
+
703
+ /** @private
704
+ * Enables interaction mode if a cells descendant receives focus or keyboard
705
+ * input. Disables it if the event is not related to cell content.
706
+ * @param {!KeyboardEvent|!FocusEvent} e
707
+ */
708
+ _detectInteracting(e) {
709
+ const isInteracting = e.composedPath().some((el) => el.localName === 'vaadin-grid-cell-content');
710
+ this._setInteracting(isInteracting);
711
+ }
712
+
713
+ /** @private */
714
+ _detectFocusedItemIndex(e) {
715
+ const { section, row } = this._getGridEventLocation(e);
716
+ if (section === this.$.items) {
717
+ this._focusedItemIndex = row.index;
718
+ }
719
+ }
720
+
721
+ /** @private
722
+ * Enables or disables the focus target of the containing section of the
723
+ * grid from receiving focus, based on whether the user is interacting with
724
+ * that section of the grid.
725
+ * @param {HTMLElement} focusTarget
726
+ */
727
+ _updateGridSectionFocusTarget(focusTarget) {
728
+ if (!focusTarget) return;
729
+
730
+ const section = this._getGridSectionFromFocusTarget(focusTarget);
731
+ const isInteractingWithinActiveSection = this.interacting && section === this._activeRowGroup;
732
+
733
+ focusTarget.tabIndex = isInteractingWithinActiveSection ? -1 : 0;
734
+ }
735
+
736
+ /**
737
+ * @param {!HTMLTableRowElement} row
738
+ * @param {number} index
739
+ * @protected
740
+ */
741
+ _preventScrollerRotatingCellFocus(row, index) {
742
+ if (
743
+ row.index === this._focusedItemIndex &&
744
+ this.hasAttribute('navigating') &&
745
+ this._activeRowGroup === this.$.items
746
+ ) {
747
+ // Focused item has went, hide navigation mode
748
+ this._navigatingIsHidden = true;
749
+ this.toggleAttribute('navigating', false);
750
+ }
751
+ if (index === this._focusedItemIndex && this._navigatingIsHidden) {
752
+ // Focused item is back, restore navigation mode
753
+ this._navigatingIsHidden = false;
754
+ this.toggleAttribute('navigating', true);
755
+ }
756
+ }
757
+
758
+ /**
759
+ * @param {HTMLTableSectionElement=} rowGroup
760
+ * @param {number=} rowIndex
761
+ * @return {!Array<!GridColumn>}
762
+ * @protected
763
+ */
764
+ _getColumns(rowGroup, rowIndex) {
765
+ let columnTreeLevel = this._columnTree.length - 1;
766
+ if (rowGroup === this.$.header) {
767
+ columnTreeLevel = rowIndex;
768
+ } else if (rowGroup === this.$.footer) {
769
+ columnTreeLevel = this._columnTree.length - 1 - rowIndex;
770
+ }
771
+ return this._columnTree[columnTreeLevel];
772
+ }
773
+
774
+ /** @private */
775
+ __isValidFocusable(element) {
776
+ return this.$.table.contains(element) && element.offsetHeight;
777
+ }
778
+
779
+ /** @protected */
780
+ _resetKeyboardNavigation() {
781
+ if (!this.__isValidFocusable(this._headerFocusable) && this.$.header.firstElementChild) {
782
+ this._headerFocusable = Array.from(this.$.header.firstElementChild.children).filter((el) => !el.hidden)[0];
783
+ }
784
+
785
+ if (!this.__isValidFocusable(this._itemsFocusable) && this.$.items.firstElementChild) {
786
+ const firstVisibleIndexRow = this.__getFirstVisibleItem();
787
+ if (firstVisibleIndexRow) {
788
+ this._itemsFocusable = Array.from(firstVisibleIndexRow.children).filter((el) => !el.hidden)[0];
789
+ }
790
+ }
791
+
792
+ if (!this.__isValidFocusable(this._footerFocusable) && this.$.footer.firstElementChild) {
793
+ this._footerFocusable = Array.from(this.$.footer.firstElementChild.children).filter((el) => !el.hidden)[0];
794
+ }
795
+ }
796
+
797
+ /**
798
+ * @param {!HTMLElement} dstCell
799
+ * @protected
800
+ */
801
+ _scrollHorizontallyToCell(dstCell) {
802
+ if (dstCell.hasAttribute('frozen') || this.__isDetailsCell(dstCell)) {
803
+ // These cells are, by design, always visible, no need to scroll.
804
+ return;
805
+ }
806
+
807
+ const dstCellRect = dstCell.getBoundingClientRect();
808
+ const dstRow = dstCell.parentNode;
809
+ const dstCellIndex = Array.from(dstRow.children).indexOf(dstCell);
810
+ const tableRect = this.$.table.getBoundingClientRect();
811
+ let leftBoundary = tableRect.left,
812
+ rightBoundary = tableRect.right;
813
+ for (let i = dstCellIndex - 1; i >= 0; i--) {
814
+ const cell = dstRow.children[i];
815
+ if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
816
+ continue;
817
+ }
818
+ if (cell.hasAttribute('frozen')) {
819
+ leftBoundary = cell.getBoundingClientRect().right;
820
+ break;
821
+ }
822
+ }
823
+ for (let i = dstCellIndex + 1; i < dstRow.children.length; i++) {
824
+ const cell = dstRow.children[i];
825
+ if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
826
+ continue;
827
+ }
828
+ if (cell.hasAttribute('frozen')) {
829
+ rightBoundary = cell.getBoundingClientRect().left;
830
+ break;
831
+ }
832
+ }
833
+
834
+ if (dstCellRect.left < leftBoundary) {
835
+ this.$.table.scrollLeft += Math.round(dstCellRect.left - leftBoundary);
836
+ }
837
+ if (dstCellRect.right > rightBoundary) {
838
+ this.$.table.scrollLeft += Math.round(dstCellRect.right - rightBoundary);
839
+ }
840
+ }
841
+
842
+ /**
843
+ * @typedef {Object} GridEventLocation
844
+ * @property {HTMLTableSectionElement | null} section - The table section element that the event occurred in (header, body, or footer), or null if the event did not occur in a section
845
+ * @property {HTMLTableRowElement | null} row - The row element that the event occurred in, or null if the event did not occur in a row
846
+ * @property {HTMLTableCellElement | null} cell - The cell element that the event occurred in, or null if the event did not occur in a cell
847
+ * @private
848
+ */
849
+ /**
850
+ * Takes an event and returns a location object describing in which part of the grid the event occurred.
851
+ * The event may either target table section, a row, a cell or contents of a cell.
852
+ * @param {Event} e
853
+ * @returns {GridEventLocation}
854
+ * @private
855
+ */
856
+ _getGridEventLocation(e) {
857
+ const path = e.composedPath();
858
+ const tableIndex = path.indexOf(this.$.table);
859
+ // Assuming ascending path to table is: [...,] th|td, tr, thead|tbody, table [,...]
860
+ const section = tableIndex >= 1 ? path[tableIndex - 1] : null;
861
+ const row = tableIndex >= 2 ? path[tableIndex - 2] : null;
862
+ const cell = tableIndex >= 3 ? path[tableIndex - 3] : null;
863
+
864
+ return {
865
+ section,
866
+ row,
867
+ cell
868
+ };
869
+ }
870
+
871
+ /**
872
+ * Helper method that maps a focus target cell to the containing grid section
873
+ * @param {HTMLElement} focusTarget
874
+ * @returns {HTMLTableSectionElement | null}
875
+ * @private
876
+ */
877
+ _getGridSectionFromFocusTarget(focusTarget) {
878
+ if (focusTarget === this._headerFocusable) return this.$.header;
879
+ if (focusTarget === this._itemsFocusable) return this.$.items;
880
+ if (focusTarget === this._footerFocusable) return this.$.footer;
881
+ return null;
882
+ }
883
+
884
+ /**
885
+ * Fired when a cell is focused with click or keyboard navigation.
886
+ *
887
+ * Use context property of @see {@link GridCellFocusEvent} to get detail information about the event.
888
+ *
889
+ * @event cell-focus
890
+ */
891
+ };