@ni/nimble-components 29.5.0 → 29.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/all-components-bundle.js +1332 -41
  2. package/dist/all-components-bundle.js.map +1 -1
  3. package/dist/all-components-bundle.min.js +4059 -3816
  4. package/dist/all-components-bundle.min.js.map +1 -1
  5. package/dist/esm/table/components/cell/index.d.ts +9 -0
  6. package/dist/esm/table/components/cell/index.js +20 -0
  7. package/dist/esm/table/components/cell/index.js.map +1 -1
  8. package/dist/esm/table/components/cell/styles.js +17 -1
  9. package/dist/esm/table/components/cell/styles.js.map +1 -1
  10. package/dist/esm/table/components/cell/template.js +10 -2
  11. package/dist/esm/table/components/cell/template.js.map +1 -1
  12. package/dist/esm/table/components/group-row/index.d.ts +11 -3
  13. package/dist/esm/table/components/group-row/index.js +13 -1
  14. package/dist/esm/table/components/group-row/index.js.map +1 -1
  15. package/dist/esm/table/components/group-row/styles.js +7 -1
  16. package/dist/esm/table/components/group-row/styles.js.map +1 -1
  17. package/dist/esm/table/components/group-row/template.js +1 -1
  18. package/dist/esm/table/components/group-row/template.js.map +1 -1
  19. package/dist/esm/table/components/header/styles.js +7 -1
  20. package/dist/esm/table/components/header/styles.js.map +1 -1
  21. package/dist/esm/table/components/row/index.d.ts +16 -4
  22. package/dist/esm/table/components/row/index.js +34 -6
  23. package/dist/esm/table/components/row/index.js.map +1 -1
  24. package/dist/esm/table/components/row/styles.js +39 -1
  25. package/dist/esm/table/components/row/styles.js.map +1 -1
  26. package/dist/esm/table/components/row/template.js +4 -2
  27. package/dist/esm/table/components/row/template.js.map +1 -1
  28. package/dist/esm/table/index.d.ts +22 -2
  29. package/dist/esm/table/index.js +58 -1
  30. package/dist/esm/table/index.js.map +1 -1
  31. package/dist/esm/table/models/keyboard-navigation-manager.d.ts +96 -0
  32. package/dist/esm/table/models/keyboard-navigation-manager.js +1015 -0
  33. package/dist/esm/table/models/keyboard-navigation-manager.js.map +1 -0
  34. package/dist/esm/table/models/table-update-tracker.d.ts +2 -1
  35. package/dist/esm/table/models/table-update-tracker.js +20 -3
  36. package/dist/esm/table/models/table-update-tracker.js.map +1 -1
  37. package/dist/esm/table/models/virtualizer.d.ts +6 -2
  38. package/dist/esm/table/models/virtualizer.js +16 -22
  39. package/dist/esm/table/models/virtualizer.js.map +1 -1
  40. package/dist/esm/table/styles.js +21 -0
  41. package/dist/esm/table/styles.js.map +1 -1
  42. package/dist/esm/table/template.js +21 -3
  43. package/dist/esm/table/template.js.map +1 -1
  44. package/dist/esm/table/testing/table.pageobject.d.ts +7 -2
  45. package/dist/esm/table/testing/table.pageobject.js +16 -9
  46. package/dist/esm/table/testing/table.pageobject.js.map +1 -1
  47. package/dist/esm/table/types.d.ts +38 -0
  48. package/dist/esm/table/types.js +14 -0
  49. package/dist/esm/table/types.js.map +1 -1
  50. package/dist/esm/table-column/anchor/cell-view/index.d.ts +3 -0
  51. package/dist/esm/table-column/anchor/cell-view/index.js +13 -0
  52. package/dist/esm/table-column/anchor/cell-view/index.js.map +1 -1
  53. package/dist/esm/table-column/anchor/cell-view/template.js +4 -2
  54. package/dist/esm/table-column/anchor/cell-view/template.js.map +1 -1
  55. package/dist/esm/table-column/base/cell-view/index.d.ts +5 -0
  56. package/dist/esm/table-column/base/cell-view/index.js +7 -0
  57. package/dist/esm/table-column/base/cell-view/index.js.map +1 -1
  58. package/dist/esm/theme-provider/design-token-comments.js +8 -0
  59. package/dist/esm/theme-provider/design-token-comments.js.map +1 -1
  60. package/dist/esm/theme-provider/design-token-names.js +8 -0
  61. package/dist/esm/theme-provider/design-token-names.js.map +1 -1
  62. package/dist/esm/theme-provider/design-tokens.d.ts +8 -0
  63. package/dist/esm/theme-provider/design-tokens.js +9 -1
  64. package/dist/esm/theme-provider/design-tokens.js.map +1 -1
  65. package/dist/tokens-internal.scss +48 -0
  66. package/dist/tokens.scss +24 -0
  67. package/package.json +1 -1
@@ -0,0 +1,1015 @@
1
+ import { Observable } from '@microsoft/fast-element';
2
+ import { keyArrowDown, keyArrowLeft, keyArrowRight, keyArrowUp, keyEnd, keyEnter, keyEscape, keyFunction2, keyHome, keyPageDown, keyPageUp, keySpace, keyTab } from '@microsoft/fast-web-utilities';
3
+ import { FoundationElement } from '@microsoft/fast-foundation';
4
+ import { TableFocusType } from '../types';
5
+ import { TableGroupRow } from '../components/group-row';
6
+ import { TableRow } from '../components/row';
7
+ import { TableCell } from '../components/cell';
8
+ import { tableHeaderTag } from '../components/header';
9
+ import { TableCellView } from '../../table-column/base/cell-view';
10
+ /**
11
+ * Manages the keyboard navigation and focus within the table.
12
+ * @internal
13
+ */
14
+ export class KeyboardNavigationManager {
15
+ get inNavigationMode() {
16
+ return (this.focusType !== TableFocusType.cellActionMenu
17
+ && this.focusType !== TableFocusType.cellContent);
18
+ }
19
+ constructor(table, virtualizer) {
20
+ this.table = table;
21
+ this.virtualizer = virtualizer;
22
+ this.focusType = TableFocusType.none;
23
+ this.headerActionIndex = -1;
24
+ this.rowIndex = -1;
25
+ this.cellContentIndex = -1;
26
+ this.columnIndex = -1;
27
+ this.focusWithinTable = false;
28
+ this.isCurrentlyFocusingElement = false;
29
+ this.visibleRowNotifiers = [];
30
+ this.onTableFocusIn = (event) => {
31
+ this.focusWithinTable = true;
32
+ this.updateFocusStateFromActiveElement(false);
33
+ // Sets initial focus on the appropriate table content
34
+ const actionMenuOpen = this.table.openActionMenuRecordId !== undefined;
35
+ if ((event.target === this.table
36
+ || this.focusType === TableFocusType.none)
37
+ && !actionMenuOpen) {
38
+ let focusHeader = true;
39
+ if (this.hasRowOrCellFocusType()
40
+ && this.scrollToAndFocusRow(this.rowIndex)) {
41
+ focusHeader = false;
42
+ }
43
+ if (focusHeader && !this.setFocusOnHeader()) {
44
+ // nothing to focus
45
+ this.table.blur();
46
+ }
47
+ }
48
+ };
49
+ this.onTableFocusOut = () => {
50
+ this.focusWithinTable = false;
51
+ };
52
+ this.onCellActionMenuBlur = (event) => {
53
+ event.stopPropagation();
54
+ const cell = event.detail;
55
+ if (cell.actionMenuButton) {
56
+ // Ensure that action menu buttons get hidden when no longer focused
57
+ this.setActionMenuButtonFocused(cell.actionMenuButton, false);
58
+ }
59
+ };
60
+ this.onCellViewFocusIn = (event) => {
61
+ event.stopPropagation();
62
+ this.updateFocusStateFromActiveElement(false);
63
+ };
64
+ this.onCellFocusIn = (event) => {
65
+ event.stopPropagation();
66
+ const cell = event.detail;
67
+ this.updateFocusStateFromActiveElement(true);
68
+ // Currently, clicking a non-interactive cell only updates the focus state to that row, it
69
+ // doesn't focus the cell. If we revisit this, we most likely need to set the cells to tabindex=-1
70
+ // upfront too, so their focusing behavior is consistent whether they've been previously keyboard
71
+ // focused or not.
72
+ if (this.focusType === TableFocusType.row
73
+ && this.getActiveElement() === cell) {
74
+ this.focusCurrentRow(false);
75
+ }
76
+ };
77
+ this.onCellBlur = (event) => {
78
+ event.stopPropagation();
79
+ const cell = event.detail;
80
+ this.setElementFocusable(cell, false);
81
+ };
82
+ this.onCaptureKeyDown = (event) => {
83
+ let handled = false;
84
+ if (event.key === keyTab) {
85
+ handled = this.onTabPressed(event.shiftKey);
86
+ }
87
+ else if (this.inNavigationMode) {
88
+ switch (event.key) {
89
+ case keyArrowLeft:
90
+ handled = this.onLeftArrowPressed();
91
+ break;
92
+ case keyArrowRight:
93
+ handled = this.onRightArrowPressed();
94
+ break;
95
+ case keyArrowUp:
96
+ handled = this.onUpArrowPressed();
97
+ break;
98
+ case keyArrowDown:
99
+ handled = this.onDownArrowPressed();
100
+ break;
101
+ case keyPageUp:
102
+ handled = this.onPageUpPressed();
103
+ break;
104
+ case keyPageDown:
105
+ handled = this.onPageDownPressed();
106
+ break;
107
+ case keyHome:
108
+ handled = this.onHomePressed(event.ctrlKey);
109
+ break;
110
+ case keyEnd:
111
+ handled = this.onEndPressed(event.ctrlKey);
112
+ break;
113
+ case keyEnter:
114
+ handled = this.onEnterPressed(event.ctrlKey);
115
+ break;
116
+ case keySpace:
117
+ handled = this.onSpacePressed(event.shiftKey);
118
+ break;
119
+ case keyFunction2:
120
+ handled = this.onF2Pressed();
121
+ break;
122
+ default:
123
+ break;
124
+ }
125
+ }
126
+ if (handled) {
127
+ event.preventDefault();
128
+ }
129
+ };
130
+ this.onKeyDown = (event) => {
131
+ if (!this.inNavigationMode && !event.defaultPrevented) {
132
+ if (event.key === keyEscape || event.key === keyFunction2) {
133
+ const row = this.getCurrentRow();
134
+ if (row) {
135
+ this.trySetCellFocus(row.getFocusableElements());
136
+ }
137
+ }
138
+ }
139
+ };
140
+ this.onViewportKeyDown = (event) => {
141
+ let handleEvent = !this.inNavigationMode
142
+ && (event.key === keyArrowUp || event.key === keyArrowDown);
143
+ switch (event.key) {
144
+ case keyPageUp:
145
+ case keyPageDown:
146
+ case keyHome:
147
+ case keyEnd:
148
+ handleEvent = true;
149
+ break;
150
+ default:
151
+ break;
152
+ }
153
+ if (handleEvent) {
154
+ // Swallow key presses that would cause table scrolling, independently of keyboard navigation
155
+ event.preventDefault();
156
+ event.stopImmediatePropagation();
157
+ }
158
+ };
159
+ this.tableNotifier = Observable.getNotifier(this.table);
160
+ this.tableNotifier.subscribe(this, 'rowElements');
161
+ this.virtualizerNotifier = Observable.getNotifier(this.virtualizer);
162
+ this.virtualizerNotifier.subscribe(this, 'visibleItems');
163
+ }
164
+ resetFocusState() {
165
+ this.focusType = TableFocusType.none;
166
+ const activeElement = this.getActiveElement();
167
+ if (activeElement && this.isInTable(activeElement)) {
168
+ this.setDefaultFocus();
169
+ }
170
+ }
171
+ get hasActiveRowOrCellFocus() {
172
+ return this.focusWithinTable && this.hasRowOrCellFocusType();
173
+ }
174
+ connect() {
175
+ this.table.addEventListener('keydown', this.onCaptureKeyDown, { capture: true });
176
+ this.table.addEventListener('keydown', this.onKeyDown);
177
+ this.table.addEventListener('focusin', this.onTableFocusIn);
178
+ this.table.addEventListener('focusout', this.onTableFocusOut);
179
+ this.table.viewport.addEventListener('keydown', this.onViewportKeyDown);
180
+ this.table.viewport.addEventListener('cell-action-menu-blur', this.onCellActionMenuBlur);
181
+ this.table.viewport.addEventListener('cell-view-focus-in', this.onCellViewFocusIn);
182
+ this.table.viewport.addEventListener('cell-focus-in', this.onCellFocusIn);
183
+ this.table.viewport.addEventListener('cell-blur', this.onCellBlur);
184
+ }
185
+ disconnect() {
186
+ this.table.removeEventListener('keydown', this.onCaptureKeyDown, { capture: true });
187
+ this.table.removeEventListener('keydown', this.onKeyDown);
188
+ this.table.removeEventListener('focusin', this.onTableFocusIn);
189
+ this.table.removeEventListener('focusout', this.onTableFocusOut);
190
+ this.table.viewport.removeEventListener('keydown', this.onViewportKeyDown);
191
+ this.table.viewport.removeEventListener('cell-action-menu-blur', this.onCellActionMenuBlur);
192
+ this.table.viewport.removeEventListener('cell-view-focus-in', this.onCellViewFocusIn);
193
+ this.table.viewport.removeEventListener('cell-focus-in', this.onCellFocusIn);
194
+ this.table.viewport.removeEventListener('cell-blur', this.onCellBlur);
195
+ }
196
+ handleChange(source, args) {
197
+ let focusRowAndCell = false;
198
+ if (source === this.virtualizer && args === 'visibleItems') {
199
+ focusRowAndCell = true;
200
+ }
201
+ else if (source === this.table && args === 'rowElements') {
202
+ for (const notifier of this.visibleRowNotifiers) {
203
+ notifier.unsubscribe(this);
204
+ }
205
+ this.visibleRowNotifiers = [];
206
+ for (const visibleRow of this.table.rowElements) {
207
+ const rowNotifier = Observable.getNotifier(visibleRow);
208
+ rowNotifier.subscribe(this, 'resolvedRowIndex');
209
+ if (visibleRow.resolvedRowIndex === this.rowIndex) {
210
+ focusRowAndCell = true;
211
+ }
212
+ }
213
+ }
214
+ else if (args === 'resolvedRowIndex'
215
+ && this.isResolvedRowType(source)) {
216
+ if (source.resolvedRowIndex === this.rowIndex) {
217
+ focusRowAndCell = true;
218
+ }
219
+ }
220
+ if (focusRowAndCell) {
221
+ // Focusable elements in cells, and action menus, are both blurred on scroll. To maintain our row/cell focus state,
222
+ // we focus the cell instead here. (We also don't want to refocus the cell content when the focusedRecycleCallback just
223
+ // blurred it.)
224
+ if (this.focusType === TableFocusType.cellActionMenu
225
+ || this.focusType === TableFocusType.cellContent) {
226
+ this.setCellFocusState(this.columnIndex, this.rowIndex, false);
227
+ }
228
+ if (this.inNavigationMode && this.hasRowOrCellFocusType()) {
229
+ if (this.rowIndex > this.table.tableData.length - 1) {
230
+ // Focused row index no longer valid, coerce to 1st row if possible
231
+ if (this.table.tableData.length > 0) {
232
+ this.rowIndex = 0;
233
+ }
234
+ else {
235
+ if (this.focusWithinTable) {
236
+ this.setDefaultFocus();
237
+ }
238
+ else {
239
+ this.focusType = TableFocusType.none;
240
+ }
241
+ return;
242
+ }
243
+ }
244
+ if (this.focusWithinTable) {
245
+ this.focusCurrentRow(false);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ handleFocusedCellRecycling(hadRowOrCellFocus) {
251
+ if (hadRowOrCellFocus && !this.focusWithinTable) {
252
+ this.focusCurrentRow(false);
253
+ }
254
+ }
255
+ onRowFocusIn(event) {
256
+ if (this.isCurrentlyFocusingElement) {
257
+ return;
258
+ }
259
+ const row = event.target;
260
+ if (this.isResolvedRowType(row)) {
261
+ if (this.rowIndex !== row.resolvedRowIndex) {
262
+ // If user focuses a row some other way (e.g. mouse), update our focus state so future keyboard nav
263
+ // will start from that row
264
+ this.setRowFocusState(row.resolvedRowIndex);
265
+ }
266
+ }
267
+ }
268
+ onRowBlur(event) {
269
+ const row = event.target;
270
+ if (this.isResolvedRowType(row)) {
271
+ this.setElementFocusable(row, false);
272
+ }
273
+ }
274
+ onRowActionMenuToggle(event) {
275
+ const isOpen = event.detail.newState;
276
+ if (isOpen) {
277
+ const row = event.target;
278
+ const columnIndex = this.table.visibleColumns.findIndex(column => column.columnId === event.detail.columnId);
279
+ this.setCellActionMenuFocusState(row.resolvedRowIndex, columnIndex, false);
280
+ }
281
+ }
282
+ onEnterPressed(ctrlKey) {
283
+ let row;
284
+ let rowElements;
285
+ if (this.hasRowOrCellFocusType()) {
286
+ row = this.getCurrentRow();
287
+ rowElements = row?.getFocusableElements();
288
+ }
289
+ if (this.focusType === TableFocusType.row) {
290
+ if (row instanceof TableGroupRow) {
291
+ this.toggleRowExpanded(row);
292
+ return true;
293
+ }
294
+ }
295
+ if (this.focusType === TableFocusType.cell) {
296
+ if (ctrlKey) {
297
+ const cell = rowElements?.cells[this.columnIndex];
298
+ if (cell?.actionMenuButton && !cell.actionMenuButton.open) {
299
+ cell.actionMenuButton.toggleButton.control.click();
300
+ return true;
301
+ }
302
+ }
303
+ return this.focusFirstInteractiveElementInCurrentCell(rowElements);
304
+ }
305
+ return false;
306
+ }
307
+ onF2Pressed() {
308
+ if (this.focusType === TableFocusType.cell) {
309
+ const row = this.getCurrentRow();
310
+ const rowElements = row?.getFocusableElements();
311
+ return this.focusFirstInteractiveElementInCurrentCell(rowElements);
312
+ }
313
+ return false;
314
+ }
315
+ onSpacePressed(shiftKey) {
316
+ if (this.focusType === TableFocusType.row
317
+ || this.focusType === TableFocusType.cell) {
318
+ if (this.focusType === TableFocusType.row || shiftKey) {
319
+ const row = this.getCurrentRow();
320
+ if (row instanceof TableRow && row.selectable) {
321
+ row.onSelectionChange(row.selected, !row.selected);
322
+ }
323
+ else if (row instanceof TableGroupRow) {
324
+ this.toggleRowExpanded(row);
325
+ }
326
+ }
327
+ // Default Space behavior scrolls down, which is redundant given the rest of our keyboard nav code, and we'd still try to focus a
328
+ // row that you scrolled away from. So suppress default Space behavior if a row or cell is selected, regardless of if we're
329
+ // toggling selection or not.
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+ onLeftArrowPressed() {
335
+ let row;
336
+ let rowElements;
337
+ let headerElements;
338
+ if (this.hasRowOrCellFocusType()) {
339
+ row = this.getCurrentRow();
340
+ rowElements = row?.getFocusableElements();
341
+ }
342
+ else if (this.hasHeaderFocusType()) {
343
+ headerElements = this.getTableHeaderFocusableElements();
344
+ }
345
+ switch (this.focusType) {
346
+ case TableFocusType.headerActions:
347
+ return this.trySetHeaderActionFocus(headerElements, this.headerActionIndex - 1);
348
+ case TableFocusType.columnHeader:
349
+ return (this.trySetColumnHeaderFocus(headerElements, this.columnIndex - 1)
350
+ || this.trySetHeaderActionFocus(headerElements, headerElements.headerActions.length - 1));
351
+ case TableFocusType.row:
352
+ if (this.isRowExpanded(row) === true) {
353
+ this.toggleRowExpanded(row);
354
+ return true;
355
+ }
356
+ return false;
357
+ case TableFocusType.rowSelectionCheckbox:
358
+ this.setRowFocusState();
359
+ return this.focusCurrentRow(true);
360
+ case TableFocusType.cell:
361
+ if (!this.trySetCellFocus(rowElements, this.columnIndex - 1)
362
+ && !this.trySetRowSelectionCheckboxFocus(rowElements)) {
363
+ this.setRowFocusState();
364
+ this.focusCurrentRow(true);
365
+ }
366
+ return true;
367
+ default:
368
+ break;
369
+ }
370
+ return false;
371
+ }
372
+ onRightArrowPressed() {
373
+ let row;
374
+ let rowElements;
375
+ let headerElements;
376
+ if (this.hasRowOrCellFocusType()) {
377
+ row = this.getCurrentRow();
378
+ rowElements = row?.getFocusableElements();
379
+ }
380
+ else if (this.hasHeaderFocusType()) {
381
+ headerElements = this.getTableHeaderFocusableElements();
382
+ }
383
+ switch (this.focusType) {
384
+ case TableFocusType.headerActions:
385
+ return (this.trySetHeaderActionFocus(headerElements, this.headerActionIndex + 1) || this.trySetColumnHeaderFocus(headerElements, 0));
386
+ case TableFocusType.columnHeader:
387
+ return this.trySetColumnHeaderFocus(headerElements, this.columnIndex + 1);
388
+ case TableFocusType.row:
389
+ if (this.isRowExpanded(row) === false) {
390
+ this.toggleRowExpanded(row);
391
+ return true;
392
+ }
393
+ return (this.trySetRowSelectionCheckboxFocus(rowElements)
394
+ || this.trySetCellFocus(rowElements, 0));
395
+ case TableFocusType.rowSelectionCheckbox:
396
+ return this.trySetCellFocus(rowElements, 0);
397
+ case TableFocusType.cell:
398
+ return this.trySetCellFocus(rowElements, this.columnIndex + 1);
399
+ default:
400
+ break;
401
+ }
402
+ return false;
403
+ }
404
+ onUpArrowPressed() {
405
+ this.onMoveUp(1);
406
+ // Always prevent default - prevents page scroll, and FireFox changing focus if focus is at table extents
407
+ return true;
408
+ }
409
+ onPageUpPressed() {
410
+ return this.onMoveUp(this.virtualizer.pageSize);
411
+ }
412
+ onHomePressed(ctrlKey) {
413
+ if (this.handleHomeEndWithinRow(ctrlKey)) {
414
+ const row = this.getCurrentRow();
415
+ const rowElements = row?.getFocusableElements();
416
+ return (this.trySetRowSelectionCheckboxFocus(rowElements)
417
+ || this.trySetCellFocus(rowElements, 0));
418
+ }
419
+ return this.onMoveUp(0, 0);
420
+ }
421
+ onDownArrowPressed() {
422
+ this.onMoveDown(1);
423
+ // Always prevent default - prevents page scroll, and FireFox changing focus if focus is at table extents
424
+ return true;
425
+ }
426
+ onPageDownPressed() {
427
+ return this.onMoveDown(this.virtualizer.pageSize);
428
+ }
429
+ onEndPressed(ctrlKey) {
430
+ if (this.handleHomeEndWithinRow(ctrlKey)) {
431
+ const row = this.getCurrentRow();
432
+ const rowElements = row?.getFocusableElements();
433
+ return this.trySetCellFocus(rowElements, this.table.visibleColumns.length - 1);
434
+ }
435
+ return this.onMoveDown(0, this.table.tableData.length - 1);
436
+ }
437
+ handleHomeEndWithinRow(ctrlKey) {
438
+ return ((this.focusType === TableFocusType.cell
439
+ || this.focusType === TableFocusType.rowSelectionCheckbox)
440
+ && !ctrlKey);
441
+ }
442
+ onTabPressed(shiftKeyPressed) {
443
+ const activeElement = this.getActiveElement();
444
+ if (activeElement === null || activeElement === this.table) {
445
+ return false;
446
+ }
447
+ const nextFocusState = this.hasRowOrCellFocusType()
448
+ ? this.getNextRowTabStop(shiftKeyPressed)
449
+ : this.getNextHeaderTabStop(shiftKeyPressed);
450
+ if (nextFocusState) {
451
+ this.focusType = nextFocusState.focusType;
452
+ this.rowIndex = nextFocusState.rowIndex ?? this.rowIndex;
453
+ this.columnIndex = nextFocusState.columnIndex ?? this.columnIndex;
454
+ this.headerActionIndex = nextFocusState.headerActionIndex ?? this.headerActionIndex;
455
+ this.cellContentIndex = nextFocusState.cellContentIndex ?? this.cellContentIndex;
456
+ if (this.hasRowOrCellFocusType()) {
457
+ this.focusCurrentRow(false);
458
+ }
459
+ else {
460
+ this.focusHeaderElement();
461
+ }
462
+ return true;
463
+ }
464
+ this.blurAfterLastTab(activeElement);
465
+ return false;
466
+ }
467
+ getNextRowTabStop(shiftKeyPressed) {
468
+ const row = this.getCurrentRow();
469
+ if (row === undefined) {
470
+ return undefined;
471
+ }
472
+ let startIndex = -1;
473
+ const focusStates = [];
474
+ const rowElements = row.getFocusableElements();
475
+ if (rowElements.selectionCheckbox) {
476
+ focusStates.push({
477
+ focusType: TableFocusType.rowSelectionCheckbox
478
+ });
479
+ if (this.focusType === TableFocusType.rowSelectionCheckbox) {
480
+ startIndex = 0;
481
+ }
482
+ }
483
+ let cellIndex = 0;
484
+ while (cellIndex < rowElements.cells.length) {
485
+ const firstCellTabbableIndex = focusStates.length;
486
+ const cellInfo = rowElements.cells[cellIndex];
487
+ const cellViewTabbableChildren = cellInfo.cell.cellView.tabbableChildren;
488
+ for (let i = 0; i < cellViewTabbableChildren.length; i++) {
489
+ focusStates.push({
490
+ focusType: TableFocusType.cellContent,
491
+ columnIndex: cellIndex,
492
+ cellContentIndex: i
493
+ });
494
+ if (this.focusType === TableFocusType.cellContent
495
+ && this.columnIndex === cellIndex
496
+ && this.cellContentIndex === i) {
497
+ startIndex = focusStates.length - 1;
498
+ }
499
+ }
500
+ if (cellInfo.actionMenuButton) {
501
+ focusStates.push({
502
+ focusType: TableFocusType.cellActionMenu,
503
+ columnIndex: cellIndex
504
+ });
505
+ if (this.focusType === TableFocusType.cellActionMenu
506
+ && this.columnIndex === cellIndex) {
507
+ startIndex = focusStates.length - 1;
508
+ }
509
+ }
510
+ const lastCellTabbableIndex = focusStates.length - 1;
511
+ if (this.focusType === TableFocusType.cell
512
+ && this.columnIndex === cellIndex) {
513
+ startIndex = shiftKeyPressed
514
+ ? lastCellTabbableIndex + 1
515
+ : firstCellTabbableIndex - 1;
516
+ }
517
+ cellIndex += 1;
518
+ }
519
+ if (this.focusType === TableFocusType.row) {
520
+ startIndex = shiftKeyPressed ? focusStates.length : -1;
521
+ }
522
+ const direction = shiftKeyPressed ? -1 : 1;
523
+ return focusStates[startIndex + direction];
524
+ }
525
+ getNextHeaderTabStop(shiftKeyPressed) {
526
+ let startIndex = -1;
527
+ const focusStates = [];
528
+ const headerTabbableElements = this.getTableHeaderFocusableElements().headerActions;
529
+ for (let i = 0; i < headerTabbableElements.length; i++) {
530
+ focusStates.push({
531
+ focusType: TableFocusType.headerActions,
532
+ headerActionIndex: i
533
+ });
534
+ }
535
+ if (this.focusType === TableFocusType.headerActions) {
536
+ startIndex = this.headerActionIndex;
537
+ }
538
+ else {
539
+ // TableFocusType.columnHeader
540
+ startIndex = focusStates.length;
541
+ }
542
+ const direction = shiftKeyPressed ? -1 : 1;
543
+ return focusStates[startIndex + direction];
544
+ }
545
+ blurAfterLastTab(activeElement) {
546
+ // In order to get the desired browser-provided Tab/Shift-Tab behavior of focusing the
547
+ // element before/after the table, the table shouldn't have tabIndex=0 when this event
548
+ // handling ends. However it needs to be tabIndex=0 so we can re-focus the table the next time
549
+ // it's tabbed to, so set tabIndex back to 0 after a rAF.
550
+ // Note: In Chrome this is only needed for Shift-Tab, but in Firefox both Tab and Shift-Tab need this
551
+ // to work as expected.
552
+ this.table.tabIndex = -1;
553
+ window.requestAnimationFrame(() => {
554
+ this.table.tabIndex = 0;
555
+ });
556
+ // Don't explicitly call blur() on activeElement (causes unexpected behavior on Safari / Mac Firefox)
557
+ this.setElementFocusable(activeElement, false);
558
+ }
559
+ onMoveUp(rowDelta, newRowIndex) {
560
+ const coerceRowIndex = rowDelta > 1;
561
+ switch (this.focusType) {
562
+ case TableFocusType.row:
563
+ case TableFocusType.rowSelectionCheckbox:
564
+ case TableFocusType.cell: {
565
+ const scrollOptions = {};
566
+ let rowIndex = this.rowIndex;
567
+ if (newRowIndex !== undefined) {
568
+ rowIndex = newRowIndex;
569
+ }
570
+ rowIndex -= rowDelta;
571
+ if (coerceRowIndex && rowIndex < 0) {
572
+ rowIndex = 0;
573
+ }
574
+ if (rowDelta > 1) {
575
+ scrollOptions.align = 'start';
576
+ }
577
+ if (rowIndex < this.rowIndex && rowIndex >= 0) {
578
+ return this.scrollToAndFocusRow(rowIndex, scrollOptions);
579
+ }
580
+ if (rowIndex === -1) {
581
+ const headerElements = this.getTableHeaderFocusableElements();
582
+ if (this.focusType === TableFocusType.row
583
+ || this.focusType === TableFocusType.rowSelectionCheckbox) {
584
+ return (this.trySetHeaderActionFocus(headerElements, 0)
585
+ || this.trySetColumnHeaderFocus(headerElements, 0));
586
+ }
587
+ return this.trySetColumnHeaderFocus(headerElements, this.columnIndex);
588
+ }
589
+ return false;
590
+ }
591
+ default:
592
+ break;
593
+ }
594
+ return false;
595
+ }
596
+ onMoveDown(rowDelta, newRowIndex) {
597
+ const coerceRowIndex = rowDelta > 1;
598
+ switch (this.focusType) {
599
+ case TableFocusType.headerActions: {
600
+ this.setRowFocusState(0);
601
+ return this.scrollToAndFocusRow(0);
602
+ }
603
+ case TableFocusType.columnHeader: {
604
+ this.setCellFocusState(this.columnIndex, 0, false);
605
+ return this.scrollToAndFocusRow(0);
606
+ }
607
+ case TableFocusType.row:
608
+ case TableFocusType.rowSelectionCheckbox:
609
+ case TableFocusType.cell: {
610
+ const scrollOptions = {};
611
+ let rowIndex = this.rowIndex;
612
+ if (newRowIndex !== undefined) {
613
+ rowIndex = newRowIndex;
614
+ }
615
+ rowIndex += rowDelta;
616
+ if (coerceRowIndex && rowIndex >= this.table.tableData.length) {
617
+ rowIndex = this.table.tableData.length - 1;
618
+ }
619
+ if (rowDelta > 1) {
620
+ scrollOptions.align = 'end';
621
+ }
622
+ if (rowIndex > this.rowIndex
623
+ && rowIndex < this.table.tableData.length) {
624
+ return this.scrollToAndFocusRow(rowIndex, scrollOptions);
625
+ }
626
+ return false;
627
+ }
628
+ default:
629
+ break;
630
+ }
631
+ return false;
632
+ }
633
+ updateFocusStateFromActiveElement(setRowFocus) {
634
+ // If the user is interacting with the table with non-keyboard methods (like mouse), we need to
635
+ // update our focus state based on the current active/focused element
636
+ const activeElement = this.getActiveElement();
637
+ if (activeElement) {
638
+ const row = this.getContainingRow(activeElement);
639
+ if (row) {
640
+ if (!(row instanceof TableGroupRow)) {
641
+ const cell = this.getContainingCell(activeElement);
642
+ if (cell) {
643
+ const columnIndex = this.table.visibleColumns.indexOf(cell.column);
644
+ if (cell.actionMenuButton === activeElement) {
645
+ this.setCellActionMenuFocusState(row.resolvedRowIndex, columnIndex, false);
646
+ return;
647
+ }
648
+ const contentIndex = cell.cellView.tabbableChildren.indexOf(activeElement);
649
+ if (contentIndex > -1) {
650
+ this.setCellContentFocusState(contentIndex, row.resolvedRowIndex, columnIndex, false);
651
+ return;
652
+ }
653
+ }
654
+ }
655
+ if (setRowFocus
656
+ && this.hasRowOrCellFocusType()
657
+ && this.rowIndex !== row.resolvedRowIndex) {
658
+ this.setRowFocusState(row.resolvedRowIndex);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ focusElement(element, focusOptions) {
664
+ const previousActiveElement = this.getActiveElement();
665
+ if (previousActiveElement !== element) {
666
+ this.setElementFocusable(element, true);
667
+ this.isCurrentlyFocusingElement = true;
668
+ element.focus(focusOptions);
669
+ this.isCurrentlyFocusingElement = false;
670
+ if (previousActiveElement
671
+ && this.isInTable(previousActiveElement)) {
672
+ this.setElementFocusable(previousActiveElement, false);
673
+ }
674
+ }
675
+ }
676
+ setElementFocusable(element, focusable) {
677
+ if (element === this.table) {
678
+ return;
679
+ }
680
+ element.tabIndex = focusable ? 0 : -1;
681
+ }
682
+ setActionMenuButtonFocused(menuButton, focused) {
683
+ // The action MenuButton needs to be visible in order to be focused, so this CSS class styling
684
+ // handles that (see cell/styles.ts).
685
+ if (focused) {
686
+ menuButton.classList.add('cell-action-menu-focused');
687
+ }
688
+ else {
689
+ menuButton.classList.remove('cell-action-menu-focused');
690
+ }
691
+ }
692
+ setFocusOnHeader() {
693
+ if (this.hasHeaderFocusType()) {
694
+ return this.focusHeaderElement();
695
+ }
696
+ this.setDefaultFocus();
697
+ return this.focusType !== TableFocusType.none;
698
+ }
699
+ setDefaultFocus() {
700
+ const headerElements = this.getTableHeaderFocusableElements();
701
+ if (!this.trySetHeaderActionFocus(headerElements, 0)
702
+ && !this.trySetColumnHeaderFocus(headerElements, 0)
703
+ && !this.scrollToAndFocusRow(0)) {
704
+ this.focusType = TableFocusType.none;
705
+ }
706
+ }
707
+ scrollToAndFocusRow(totalRowIndex, scrollOptions) {
708
+ if (totalRowIndex >= 0 && totalRowIndex < this.table.tableData.length) {
709
+ switch (this.focusType) {
710
+ case TableFocusType.none:
711
+ case TableFocusType.headerActions:
712
+ case TableFocusType.columnHeader:
713
+ this.setRowFocusState(totalRowIndex);
714
+ break;
715
+ default:
716
+ break;
717
+ }
718
+ this.rowIndex = totalRowIndex;
719
+ this.virtualizer.scrollToIndex(totalRowIndex, scrollOptions);
720
+ this.focusCurrentRow(true);
721
+ return true;
722
+ }
723
+ return false;
724
+ }
725
+ focusCurrentRow(allowScroll) {
726
+ const visibleRowIndex = this.getCurrentRowVisibleIndex();
727
+ if (visibleRowIndex < 0) {
728
+ return false;
729
+ }
730
+ const focusedRow = this.table.rowElements[visibleRowIndex];
731
+ let focusRowOnly = false;
732
+ switch (this.focusType) {
733
+ case TableFocusType.row:
734
+ focusRowOnly = true;
735
+ break;
736
+ case TableFocusType.cell:
737
+ case TableFocusType.cellActionMenu:
738
+ case TableFocusType.cellContent:
739
+ focusRowOnly = focusedRow instanceof TableGroupRow;
740
+ break;
741
+ default:
742
+ break;
743
+ }
744
+ const focusOptions = { preventScroll: !allowScroll };
745
+ if (focusRowOnly) {
746
+ this.focusElement(focusedRow, focusOptions);
747
+ return true;
748
+ }
749
+ this.focusRowElement(focusedRow, focusOptions);
750
+ return true;
751
+ }
752
+ focusRowElement(row, focusOptions) {
753
+ const rowElements = row.getFocusableElements();
754
+ let focusableElement;
755
+ switch (this.focusType) {
756
+ case TableFocusType.rowSelectionCheckbox:
757
+ focusableElement = rowElements.selectionCheckbox;
758
+ break;
759
+ case TableFocusType.cell: {
760
+ focusableElement = rowElements.cells[this.columnIndex].cell;
761
+ break;
762
+ }
763
+ case TableFocusType.cellActionMenu: {
764
+ const actionMenuButton = rowElements.cells[this.columnIndex]?.cell.actionMenuButton;
765
+ if (actionMenuButton) {
766
+ focusableElement = actionMenuButton;
767
+ this.setActionMenuButtonFocused(actionMenuButton, true);
768
+ }
769
+ break;
770
+ }
771
+ case TableFocusType.cellContent: {
772
+ focusableElement = rowElements.cells[this.columnIndex]?.cell.cellView
773
+ .tabbableChildren[this.cellContentIndex];
774
+ break;
775
+ }
776
+ default:
777
+ break;
778
+ }
779
+ if (focusableElement) {
780
+ this.focusElement(focusableElement, focusOptions);
781
+ }
782
+ }
783
+ focusHeaderElement() {
784
+ const headerElements = this.getTableHeaderFocusableElements();
785
+ let focusableElement;
786
+ switch (this.focusType) {
787
+ case TableFocusType.headerActions:
788
+ focusableElement = headerElements.headerActions[this.headerActionIndex];
789
+ break;
790
+ case TableFocusType.columnHeader:
791
+ focusableElement = headerElements.columnHeaders[this.columnIndex];
792
+ break;
793
+ default:
794
+ break;
795
+ }
796
+ if (focusableElement) {
797
+ this.focusElement(focusableElement);
798
+ return true;
799
+ }
800
+ return false;
801
+ }
802
+ getCurrentRowVisibleIndex() {
803
+ return this.table.rowElements.findIndex(row => row.resolvedRowIndex === this.rowIndex);
804
+ }
805
+ getTableHeaderFocusableElements() {
806
+ const headerActions = [];
807
+ if (this.table.selectionCheckbox?.getRootNode()
808
+ === this.table.shadowRoot) {
809
+ headerActions.push(this.table.selectionCheckbox);
810
+ }
811
+ if (this.table.showCollapseAll
812
+ && this.table.collapseAllButton?.getRootNode()
813
+ === this.table.shadowRoot) {
814
+ headerActions.push(this.table.collapseAllButton);
815
+ }
816
+ const columnHeaders = [];
817
+ if (this.canFocusColumnHeaders()) {
818
+ this.table.columnHeadersContainer
819
+ .querySelectorAll(tableHeaderTag)
820
+ .forEach(header => columnHeaders.push(header));
821
+ }
822
+ return { headerActions, columnHeaders };
823
+ }
824
+ canFocusColumnHeaders() {
825
+ return (this.table.columns.find(c => !c.columnInternals.sortingDisabled)
826
+ !== undefined);
827
+ }
828
+ getCurrentRow() {
829
+ return this.table.rowElements[this.getCurrentRowVisibleIndex()];
830
+ }
831
+ isRowExpanded(row) {
832
+ if ((row instanceof TableRow && row.isParentRow)
833
+ || row instanceof TableGroupRow) {
834
+ return row.expanded;
835
+ }
836
+ return undefined;
837
+ }
838
+ toggleRowExpanded(row) {
839
+ if (row instanceof TableGroupRow) {
840
+ row.onGroupExpandToggle();
841
+ }
842
+ else {
843
+ row.onRowExpandToggle();
844
+ }
845
+ this.focusRowElement(row);
846
+ }
847
+ getContainingRow(start) {
848
+ return this.getContainingElement(start, e => this.isResolvedRowType(e));
849
+ }
850
+ getContainingCell(start) {
851
+ return this.getContainingElement(start, e => e instanceof TableCell);
852
+ }
853
+ getContainingElement(start, isElementMatch) {
854
+ let possibleMatch = start;
855
+ while (possibleMatch && possibleMatch !== this.table) {
856
+ if (isElementMatch(possibleMatch)) {
857
+ return possibleMatch;
858
+ }
859
+ possibleMatch = possibleMatch.parentElement
860
+ ?? possibleMatch.parentNode?.host;
861
+ }
862
+ return undefined;
863
+ }
864
+ isInTable(start) {
865
+ let possibleMatch = start;
866
+ while (possibleMatch && possibleMatch !== this.table) {
867
+ possibleMatch = possibleMatch.parentElement
868
+ ?? possibleMatch.parentNode?.host;
869
+ }
870
+ return possibleMatch === this.table;
871
+ }
872
+ getActiveElement() {
873
+ let activeElement = document.activeElement;
874
+ while (activeElement?.shadowRoot?.activeElement) {
875
+ activeElement = activeElement.shadowRoot.activeElement;
876
+ // In some cases, the active element may be a sub-part of a control (example: MenuButton -> ToggleButton -> a div with tabindex=0). Stop at the outer control boundary, so that
877
+ // we can more simply check equality against the elements of getTableHeaderFocusableElements() / row.getFocusableElements().
878
+ // (For rows/cells/cell views, we do need to recurse into them, to get to the appropriate focused controls though)
879
+ if (activeElement instanceof FoundationElement
880
+ && !this.isResolvedRowType(activeElement)
881
+ && !(activeElement instanceof TableCell)
882
+ && !(activeElement instanceof TableCellView)) {
883
+ break;
884
+ }
885
+ }
886
+ return activeElement;
887
+ }
888
+ focusFirstInteractiveElementInCurrentCell(rowElements) {
889
+ if (!rowElements) {
890
+ return false;
891
+ }
892
+ return (this.trySetCellContentFocus(rowElements, 0)
893
+ || this.trySetCellActionMenuFocus(rowElements));
894
+ }
895
+ hasRowOrCellFocusType() {
896
+ switch (this.focusType) {
897
+ case TableFocusType.cell:
898
+ case TableFocusType.cellActionMenu:
899
+ case TableFocusType.cellContent:
900
+ case TableFocusType.row:
901
+ case TableFocusType.rowSelectionCheckbox:
902
+ return true;
903
+ default:
904
+ return false;
905
+ }
906
+ }
907
+ hasHeaderFocusType() {
908
+ switch (this.focusType) {
909
+ case TableFocusType.headerActions:
910
+ case TableFocusType.columnHeader:
911
+ return true;
912
+ default:
913
+ return false;
914
+ }
915
+ }
916
+ trySetRowSelectionCheckboxFocus(rowElements) {
917
+ if (rowElements?.selectionCheckbox) {
918
+ this.focusType = TableFocusType.rowSelectionCheckbox;
919
+ this.focusCurrentRow(true);
920
+ return true;
921
+ }
922
+ return false;
923
+ }
924
+ trySetColumnHeaderFocus(headerElements, columnIndex) {
925
+ if (columnIndex >= 0
926
+ && columnIndex < headerElements.columnHeaders.length) {
927
+ this.focusType = TableFocusType.columnHeader;
928
+ this.columnIndex = columnIndex;
929
+ this.focusHeaderElement();
930
+ return true;
931
+ }
932
+ return false;
933
+ }
934
+ trySetHeaderActionFocus(headerElements, headerActionIndex) {
935
+ if (headerActionIndex >= 0
936
+ && headerActionIndex < headerElements.headerActions.length) {
937
+ this.focusType = TableFocusType.headerActions;
938
+ this.headerActionIndex = headerActionIndex;
939
+ this.focusHeaderElement();
940
+ return true;
941
+ }
942
+ return false;
943
+ }
944
+ trySetCellFocus(rowElements, columnIndex, rowIndex) {
945
+ if (!rowElements) {
946
+ return false;
947
+ }
948
+ const newColumnIndex = columnIndex ?? this.columnIndex;
949
+ const newRowIndex = rowIndex ?? this.rowIndex;
950
+ if (newColumnIndex >= 0 && newColumnIndex < rowElements.cells.length) {
951
+ this.focusType = TableFocusType.cell;
952
+ this.setRowCellFocusState(newColumnIndex, newRowIndex, true);
953
+ return true;
954
+ }
955
+ return false;
956
+ }
957
+ trySetCellContentFocus(rowElements, cellContentIndex, columnIndex, rowIndex) {
958
+ if (!rowElements) {
959
+ return false;
960
+ }
961
+ const newColumnIndex = columnIndex ?? this.columnIndex;
962
+ const newRowIndex = rowIndex ?? this.rowIndex;
963
+ if (newColumnIndex >= 0
964
+ && newColumnIndex < rowElements.cells.length
965
+ && cellContentIndex >= 0
966
+ && cellContentIndex
967
+ < rowElements.cells[newColumnIndex].cell.cellView
968
+ .tabbableChildren.length) {
969
+ this.setCellContentFocusState(cellContentIndex, newRowIndex, newColumnIndex, true);
970
+ return true;
971
+ }
972
+ return false;
973
+ }
974
+ trySetCellActionMenuFocus(rowElements, columnIndex, rowIndex) {
975
+ const newColumnIndex = columnIndex ?? this.columnIndex;
976
+ const newRowIndex = rowIndex ?? this.rowIndex;
977
+ if (newColumnIndex >= 0
978
+ && newColumnIndex < rowElements.cells.length
979
+ && rowElements.cells[newColumnIndex].actionMenuButton) {
980
+ this.setCellActionMenuFocusState(newRowIndex, newColumnIndex, true);
981
+ return true;
982
+ }
983
+ return false;
984
+ }
985
+ setCellActionMenuFocusState(rowIndex, columnIndex, focusElement) {
986
+ this.focusType = TableFocusType.cellActionMenu;
987
+ this.setRowCellFocusState(columnIndex, rowIndex, focusElement);
988
+ }
989
+ setCellContentFocusState(cellContentIndex, rowIndex, columnIndex, focusElement) {
990
+ this.focusType = TableFocusType.cellContent;
991
+ this.cellContentIndex = cellContentIndex;
992
+ this.setRowCellFocusState(columnIndex, rowIndex, focusElement);
993
+ }
994
+ setRowFocusState(rowIndex) {
995
+ this.focusType = TableFocusType.row;
996
+ if (rowIndex !== undefined) {
997
+ this.rowIndex = rowIndex;
998
+ }
999
+ }
1000
+ setCellFocusState(columnIndex, rowIndex, focusElement) {
1001
+ this.focusType = TableFocusType.cell;
1002
+ this.setRowCellFocusState(columnIndex, rowIndex, focusElement);
1003
+ }
1004
+ setRowCellFocusState(columnIndex, rowIndex, focusElement) {
1005
+ this.rowIndex = rowIndex;
1006
+ this.columnIndex = columnIndex;
1007
+ if (focusElement) {
1008
+ this.focusCurrentRow(true);
1009
+ }
1010
+ }
1011
+ isResolvedRowType(row) {
1012
+ return row instanceof TableRow || row instanceof TableGroupRow;
1013
+ }
1014
+ }
1015
+ //# sourceMappingURL=keyboard-navigation-manager.js.map