lightning-base-components 1.13.8-alpha → 1.14.2-alpha

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 (46) hide show
  1. package/metadata/raptor.json +7 -0
  2. package/package.json +5 -1
  3. package/scopedImports/@salesforce-internal-core.appVersion.js +1 -1
  4. package/scopedImports/@salesforce-label-LightningLookup.recentItems.js +1 -0
  5. package/src/lightning/ariaObserver/__component__/ariaObserver.spec.js +103 -0
  6. package/src/lightning/{utilsPrivate/contentMutation.js → ariaObserver/ariaObserver.js} +51 -78
  7. package/src/lightning/baseCombobox/baseCombobox.html +1 -0
  8. package/src/lightning/baseCombobox/baseCombobox.js +14 -1
  9. package/src/lightning/combobox/combobox.css +12 -0
  10. package/src/lightning/combobox/combobox.html +1 -0
  11. package/src/lightning/datatable/columnWidthManager.js +7 -3
  12. package/src/lightning/datatable/datatable.js +27 -25
  13. package/src/lightning/datatable/inlineEdit.js +15 -3
  14. package/src/lightning/datatable/keyboard.js +1077 -933
  15. package/src/lightning/datatable/resizer.js +91 -108
  16. package/src/lightning/datatable/state.js +0 -9
  17. package/src/lightning/datatable/templates/div/div.css +19 -0
  18. package/src/lightning/datatable/templates/div/div.html +10 -8
  19. package/src/lightning/datatable/templates/table/table.html +8 -6
  20. package/src/lightning/datatable/widthManagerShared.js +1 -1
  21. package/src/lightning/formattedRichText/__docs__/formattedRichText.md +1 -0
  22. package/src/lightning/helptext/helptext.js +8 -0
  23. package/src/lightning/iconSvgTemplates/buildTemplates/templates.js +2 -1
  24. package/src/lightning/iconSvgTemplates/buildTemplates/utility/contract_alt.html +1 -2
  25. package/src/lightning/iconSvgTemplates/buildTemplates/utility/contract_doc.html +8 -0
  26. package/src/lightning/iconSvgTemplatesRtl/buildTemplates/templates.js +2 -1
  27. package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/contract_alt.html +1 -2
  28. package/src/lightning/iconSvgTemplatesRtl/buildTemplates/utility/contract_doc.html +8 -0
  29. package/src/lightning/iconSvgTemplatesUtility/buildTemplates/templates.js +2 -1
  30. package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/contract_alt.html +1 -2
  31. package/src/lightning/iconSvgTemplatesUtility/buildTemplates/utility/contract_doc.html +8 -0
  32. package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/templates.js +2 -1
  33. package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/contract_alt.html +1 -2
  34. package/src/lightning/iconSvgTemplatesUtilityRtl/buildTemplates/utility/contract_doc.html +8 -0
  35. package/src/lightning/input/input.html +0 -1
  36. package/src/lightning/input/input.js +69 -48
  37. package/src/lightning/positionLibrary/positionLibrary.js +43 -31
  38. package/src/lightning/primitiveDatatableIeditPanel/primitiveDatatableIeditPanel.js +20 -0
  39. package/src/lightning/primitiveDatatableIeditTypeFactory/primitiveDatatableIeditTypeFactory.js +10 -0
  40. package/src/lightning/primitiveHeaderFactory/nonsortableHeader.html +5 -4
  41. package/src/lightning/primitiveHeaderFactory/primitiveHeaderFactory.js +64 -47
  42. package/src/lightning/primitiveHeaderFactory/selectableHeader.html +25 -23
  43. package/src/lightning/primitiveHeaderFactory/sortableHeader.html +13 -9
  44. package/src/lightning/progressIndicator/progressIndicator.js +3 -5
  45. package/src/lightning/progressStep/progressStep.js +31 -22
  46. package/src/lightning/utilsPrivate/utilsPrivate.js +12 -1
@@ -6,15 +6,33 @@ import {
6
6
  } from './tree';
7
7
  import { isRTL, getShadowActiveElements } from 'lightning/utilsPrivate';
8
8
 
9
- export const ARROW_RIGHT = 39;
10
- export const ARROW_LEFT = 37;
11
- export const ARROW_DOWN = 40;
12
- export const ARROW_UP = 38;
13
- export const ENTER = 13;
14
- export const ESCAPE = 27;
15
- export const TAB = 9;
16
- export const SPACE = 32;
17
- export const NAVIGATION_DIR = (() => {
9
+ // Indicator/flag for a header row
10
+ const HEADER_ROW = 'HEADER';
11
+
12
+ // SLDS Class for Focus
13
+ const FOCUS_CLASS = 'slds-has-focus';
14
+
15
+ // Keyboard Navigation Modes
16
+ const NAVIGATION_MODE = 'NAVIGATION';
17
+ const ACTION_MODE = 'ACTION';
18
+
19
+ // Pixel Values
20
+ const TOP_MARGIN = 80;
21
+ const BOTTOM_MARGIN = 80;
22
+ const SCROLL_OFFSET = 20;
23
+
24
+ // Key Code Values
25
+ const ARROW_RIGHT = 39;
26
+ const ARROW_LEFT = 37;
27
+ const ARROW_DOWN = 40;
28
+ const ARROW_UP = 38;
29
+ const ENTER = 13;
30
+ const ESCAPE = 27;
31
+ const TAB = 9;
32
+ const SPACE = 32;
33
+
34
+ // Navigation Direction
35
+ const NAVIGATION_DIR = (() => {
18
36
  if (isRTL()) {
19
37
  return {
20
38
  RIGHT: -1,
@@ -36,618 +54,696 @@ export const NAVIGATION_DIR = (() => {
36
54
  };
37
55
  })();
38
56
 
39
- const TOP_MARGIN = 80;
40
- const BOTTOM_MARGIN = 80;
41
- const SCROLL_OFFSET = 20;
42
- const NAVIGATION_MODE = 'NAVIGATION';
57
+ // Selectors
58
+ const SELECTORS = {
59
+ headerRow: {
60
+ default: `thead > :nth-child(1)`,
61
+ roleBased: `[role="grid"] > [role="rowgroup"]:nth-child(1) > [role="row"]`,
62
+ },
63
+ dataRowRowGroup: {
64
+ default: `tbody`,
65
+ roleBased: `[role="grid"] > [role="rowgroup"]:nth-child(2)`,
66
+ },
67
+ cell: {
68
+ default: ['td', 'th'],
69
+ roleBased: ['rowheader', 'gridcell', 'columnheader'],
70
+ },
71
+ };
43
72
 
44
- export function getKeyboardDefaultState() {
45
- return {
46
- keyboardMode: NAVIGATION_MODE,
47
- rowMode: false,
48
- activeCell: undefined,
49
- tabindex: 0,
50
- cellToFocusNext: null,
51
- cellClicked: false,
52
- };
53
- }
73
+ /***************************** KEYDOWN HANDLERS *****************************/
54
74
 
55
75
  /**
56
- * It update the current activeCell in the state with the new rowKeyValue, colKeyValue
57
- * @param {object} state - datatable state
58
- * @param {string} rowKeyValue - the unique row key value
59
- * @param {string} colKeyValue {string} - the unique col key value
60
- * @returns {object} state - mutated datatable state
76
+ * Handler for the `privatecellkeydown` event that is fired by
77
+ * lightning-primitive-datatable-cell.
78
+ * This component is extended by primitive-cell-factory, primitive-cell-checkbox
79
+ * and primitive-header-factory.
80
+ *
81
+ * Typically this handler is invoked when the user is in ACTION mode and the
82
+ * user keys down on a cell that contains actionable items (ex. edit button, links,
83
+ * email, buttons).
84
+ *
85
+ * @param {Event} event - Custom DOM event (privatecellkeydown) sent by the cell
61
86
  */
62
- export const updateActiveCell = function (state, rowKeyValue, colKeyValue) {
63
- state.activeCell = {
64
- rowKeyValue,
65
- colKeyValue,
66
- };
67
- return state;
68
- };
87
+ export function handleKeydownOnCell(event) {
88
+ event.stopPropagation();
89
+ reactToKeyboardInActionMode(this.template, this.state, event);
90
+ }
69
91
 
70
92
  /**
71
- * It return if the pair rowKeyValue, colKeyValue are the current activeCell values
72
- * @param {object} state - datatable state
73
- * @param {string} rowKeyValue - the unique row key value
74
- * @param {string} colKeyValue {string} - the unique col key value
75
- * @returns {boolean} - true if rowKeyValue, colKeyValue are the current activeCell values.
93
+ * Handler for keydown on the <table> element or the corresponding [role="grid"]
94
+ * on the role-based table.
95
+ *
96
+ * This handler is invoked whenever a keydown occurs on the table. However, we
97
+ * only react to the keyboard here if the user is in Navigation mode OR in Action
98
+ * mode when the cell does not have actionable items (like buttons, links etc).
99
+ *
100
+ * The Action mode keydowns are filtered out here. If a keydown occurs on an actionable
101
+ * element, the target element will not be the cell element (td/th, role=gridcell etc).
102
+ * The target element in that case will likely be the components extending
103
+ * primitiveDatatableCell (primitive-cell-factory/primitive-cell-checkbox/primitive-header-factory)
104
+ * Those events are handled by `handleKeydownOnCell()` and the remaining are
105
+ * handled by this function.
106
+ *
107
+ * @param {*} event
76
108
  */
77
- export const isActiveCell = function (state, rowKeyValue, colKeyValue) {
78
- if (state.activeCell) {
79
- const {
80
- rowKeyValue: currentRowKeyValue,
81
- colKeyValue: currentColKeyValue,
82
- } = state.activeCell;
83
- return (
84
- currentRowKeyValue === rowKeyValue &&
85
- currentColKeyValue === colKeyValue
86
- );
109
+ export function handleKeydownOnTable(event) {
110
+ const targetTagName = event.target.tagName.toLowerCase();
111
+ const targetRole = event.target.getAttribute('role');
112
+
113
+ // Checks if the keydown happened on a cell element and not
114
+ // on an actionable element when in Action Mode.
115
+ if (isCellElement(targetTagName, targetRole)) {
116
+ reactToKeyboardInNavMode(this.template, this.state, event);
87
117
  }
88
- return false;
89
- };
118
+ }
90
119
 
91
120
  /**
92
- * It check if in the current (data, columns) the activeCell still valid.
93
- * When data changed the activeCell could be removed, then we check if there is cellToFocusNext
94
- * which is calculated from previously focused cell, if so we sync to that
95
- * If active cell is still valid we keep it the same
121
+ * Changes the datatable state based on the keyboard event sent from the cell component.
122
+ * The result of those changes may trigger a re-render on the table
96
123
  *
124
+ * @param {node} element - the custom element root `this.template`
97
125
  * @param {object} state - datatable state
98
- * @returns {object} state - mutated datatable state
126
+ * @param {event} event - custom DOM event sent by the cell
127
+ * @returns {object} - mutated state
99
128
  */
100
- export const syncActiveCell = function (state) {
101
- if (!state.activeCell || !stillValidActiveCell(state)) {
102
- if (state.activeCell && state.cellToFocusNext) {
103
- // there is previously focused cell
104
- setNextActiveCellFromPrev(state);
105
- } else {
106
- // there is no active cell or there is no previously focused cell
107
- setDefaultActiveCell(state);
108
- }
129
+ function reactToKeyboardInActionMode(element, state, event) {
130
+ switch (event.detail.keyCode) {
131
+ case ARROW_LEFT:
132
+ return reactToArrowLeft(element, state, event);
133
+ case ARROW_RIGHT:
134
+ return reactToArrowRight(element, state, event);
135
+ case ARROW_UP:
136
+ return reactToArrowUp(element, state, event);
137
+ case ARROW_DOWN:
138
+ return reactToArrowDown(element, state, event);
139
+ case ENTER:
140
+ case SPACE:
141
+ return reactToEnter(element, state, event);
142
+ case ESCAPE:
143
+ return reactToEscape(element, state, event);
144
+ case TAB:
145
+ return reactToTab(element, state, event);
146
+ default:
147
+ return state;
109
148
  }
110
- return state;
111
- };
149
+ }
112
150
 
113
- export const datatableHasFocus = function (state, template) {
114
- return isFocusInside(template) || state.cellClicked;
115
- };
151
+ function reactToKeyboardInNavMode(element, state, event) {
152
+ const syntheticEvent = {
153
+ detail: {
154
+ rowKeyValue: state.activeCell.rowKeyValue,
155
+ colKeyValue: state.activeCell.colKeyValue,
156
+ keyCode: event.keyCode,
157
+ shiftKey: event.shiftKey,
158
+ },
159
+ preventDefault: () => {},
160
+ stopPropagation: () => {},
161
+ };
116
162
 
117
- /**
118
- * Sets the row and col index of cell to focus next if
119
- * there is state.activecell
120
- * datatable has focus
121
- * there is state.indexes
122
- * there is no previously set state.cellToFocusNext
123
- * Indexes are calculated as to what to focus on next
124
- * @param {object} state - datatable state
125
- * @param {object} template - datatable element
126
- */
127
- export const setCellToFocusFromPrev = function (state, template) {
128
- if (
129
- state.activeCell &&
130
- datatableHasFocus(state, template) &&
131
- state.indexes &&
132
- !state.cellToFocusNext
133
- ) {
134
- let { rowIndex, colIndex } = getIndexesActiveCell(state);
135
- colIndex = 0; // default point to the first column
136
- if (state.rows && rowIndex === state.rows.length - 1) {
137
- // if it is last row, make it point to its previous row
138
- rowIndex = state.rows.length - 1;
139
- colIndex = state.columns ? state.columns.length - 1 : 0;
163
+ // We need event.preventDefault so that actions like arrow up or down
164
+ // does not scroll the table but instead sets focus on the right cells
165
+ switch (event.keyCode) {
166
+ case ARROW_LEFT:
167
+ event.preventDefault();
168
+ return reactToArrowLeft(element, state, syntheticEvent);
169
+ case ARROW_RIGHT:
170
+ event.preventDefault();
171
+ return reactToArrowRight(element, state, syntheticEvent);
172
+ case ARROW_UP:
173
+ event.preventDefault();
174
+ return reactToArrowUp(element, state, syntheticEvent);
175
+ case ARROW_DOWN:
176
+ event.preventDefault();
177
+ return reactToArrowDown(element, state, syntheticEvent);
178
+ case ENTER:
179
+ case SPACE:
180
+ event.preventDefault();
181
+ return reactToEnter(element, state, syntheticEvent);
182
+ case ESCAPE:
183
+ // td, th or div[role=gridcell/rowheader] is the active element in the
184
+ // action mode if cell doesn't have action elements; hence this can be
185
+ // reached and we should react to escape as exiting from action mode
186
+ syntheticEvent.detail.keyEvent = event;
187
+ return reactToEscape(element, state, syntheticEvent);
188
+ case TAB:
189
+ return reactToTab(element, state, syntheticEvent);
190
+ default:
191
+ return state;
192
+ }
193
+ }
194
+
195
+ function moveFromCellToRow(element, state) {
196
+ setBlurActiveCell(element, state);
197
+ setRowNavigationMode(state);
198
+ setFocusActiveRow(element, state);
199
+ }
200
+
201
+ function reactToArrowLeft(element, state, event) {
202
+ const { rowKeyValue, colKeyValue } = event.detail;
203
+ const { colIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
204
+ const { columns } = state;
205
+
206
+ // Move from navigation mode to row mode when user
207
+ // arrows left when in nav mode and on the first column
208
+ if (colIndex === 0 && canBeRowNavigationMode(state)) {
209
+ moveFromCellToRow(element, state);
210
+ } else {
211
+ const nextColIndex = getNextIndexLeft(state, colIndex);
212
+
213
+ if (nextColIndex === undefined) {
214
+ return;
140
215
  }
141
- state.cellToFocusNext = {
142
- rowIndex,
143
- colIndex,
216
+
217
+ setBlurActiveCell(element, state);
218
+
219
+ // update activeCell
220
+ state.activeCell = {
221
+ rowKeyValue,
222
+ colKeyValue: generateColKeyValue(
223
+ columns[nextColIndex],
224
+ nextColIndex
225
+ ),
144
226
  };
227
+ setFocusActiveCell(element, state, NAVIGATION_DIR.LEFT);
145
228
  }
146
- };
229
+ }
147
230
 
148
- /**
149
- * if the current new active still is valid ie exists then set the celltofocusnext to null
150
- * @param {object} state - datatable state
151
- */
152
- export const updateCellToFocusFromPrev = function (state) {
153
- if (
154
- state.activeCell &&
155
- state.cellToFocusNext &&
156
- stillValidActiveCell(state)
157
- ) {
158
- // if the previous focused is there and valid, dont set the prevActiveFocusedCell
159
- state.cellToFocusNext = null;
231
+ function reactToArrowRight(element, state, event) {
232
+ const { rowKeyValue, colKeyValue } = event.detail;
233
+ const { colIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
234
+ const nextColIndex = getNextIndexRight(state, colIndex);
235
+ const { columns } = state;
236
+
237
+ if (nextColIndex === undefined) {
238
+ return;
160
239
  }
161
- };
162
240
 
163
- /**
164
- * reset celltofocusnext to null (used after render)
165
- * @param {object} state - datatable state
166
- */
167
- export const resetCellToFocusFromPrev = function (state) {
168
- state.cellToFocusNext = null;
169
- };
241
+ setBlurActiveCell(element, state);
170
242
 
171
- /**
172
- * Sets the next active if there is a previously focused active cell
173
- * Logic is:
174
- * if the rowIndex is existing one - cell = (rowIndex, 0)
175
- * if the rowIndex is > the number of rows (focused was last row or more) = (lastRow, lastColumn)
176
- * for columns
177
- * same as above except if the colIndex is > the number of cols (means no data) = set it to null??
178
- * @param {object} state - datatable state
179
- */
180
- function setNextActiveCellFromPrev(state) {
181
- const { rowIndex, colIndex } = state.cellToFocusNext;
182
- let nextRowIndex = rowIndex;
183
- let nextColIndex = colIndex;
184
- const rowsCount = state.rows ? state.rows.length : 0;
185
- const colsCount = state.columns.length ? state.columns.length : 0;
243
+ // update activeCell
244
+ state.activeCell = {
245
+ rowKeyValue,
246
+ colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
247
+ };
248
+ setFocusActiveCell(element, state, NAVIGATION_DIR.RIGHT);
249
+ }
186
250
 
187
- if (nextRowIndex > rowsCount - 1) {
188
- // row index not existing after update to new 5 > 5-1, 6 > 5-1,
189
- nextRowIndex = rowsCount - 1;
251
+ function reactToArrowUp(element, state, event) {
252
+ const { rowKeyValue, colKeyValue, keyEvent } = event.detail;
253
+ const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
254
+ const nextRowIndex = getNextIndexUp(state, rowIndex);
255
+ const { rows } = state;
256
+
257
+ if (nextRowIndex === undefined) {
258
+ return;
190
259
  }
191
- if (nextColIndex > colsCount - 1) {
192
- // col index not existing after update to new
193
- nextColIndex = colsCount - 1;
260
+
261
+ if (state.hideTableHeader && nextRowIndex === -1) {
262
+ return;
194
263
  }
195
- const nextActiveCell = getCellFromIndexes(
196
- state,
197
- nextRowIndex,
198
- nextColIndex
199
- );
200
- if (nextActiveCell) {
201
- state.activeCell = nextActiveCell;
202
- } else {
203
- setDefaultActiveCell(state);
264
+
265
+ if (keyEvent) {
266
+ keyEvent.stopPropagation();
204
267
  }
205
- state.keyboardMode = 'NAVIGATION';
206
- }
207
268
 
208
- /**
209
- * It update the tabIndex value of a cell in the state for the rowIndex, colIndex passed
210
- * as consequence of this change
211
- * datatable is gonna re-render the cell affected with the new tabindex value
212
- *
213
- * @param {object} state - datatable state
214
- * @param {number} rowIndex - the row index
215
- * @param {number} colIndex - the column index
216
- * @param {number} [index = 0] - the value for the tabindex
217
- */
218
- export const updateTabIndex = function (state, rowIndex, colIndex, index = 0) {
219
- if (isHeaderRow(rowIndex)) {
220
- const { columns } = state;
221
- columns[colIndex].tabIndex = index;
222
- } else {
223
- state.rows[rowIndex].cells[colIndex].tabIndex = index;
269
+ setBlurActiveCell(element, state);
270
+
271
+ // update activeCell
272
+ state.activeCell = {
273
+ rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : HEADER_ROW,
274
+ colKeyValue,
275
+ };
276
+ setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
277
+ }
278
+
279
+ function reactToArrowDown(element, state, event) {
280
+ const { rowKeyValue, colKeyValue, keyEvent } = event.detail;
281
+ const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
282
+ const nextRowIndex = getNextIndexDown(state, rowIndex);
283
+ const { rows } = state;
284
+
285
+ if (nextRowIndex === undefined) {
286
+ return;
224
287
  }
225
- };
226
288
 
227
- /**
228
- * It updates the tabIndex value of a row in the state for the rowIndex passed
229
- * as consequence of this change
230
- * datatable is gonna re-render the row affected with the new tabindex value
231
- *
232
- * @param {object} state - datatable state
233
- * @param {number} rowIndex - the row index
234
- * @param {number} [index = 0] - the value for the tabindex
235
- */
236
- export const updateTabIndexRow = function (state, rowIndex, index = 0) {
237
- if (!isHeaderRow(rowIndex)) {
238
- // TODO what to do when rowIndex is header row
239
- state.rows[rowIndex].tabIndex = index;
289
+ if (state.hideTableHeader && nextRowIndex === -1) {
290
+ return;
240
291
  }
241
- };
242
- /**
243
- * It update the tabindex for the current activeCell.
244
- * @param {object} state - datatable state
245
- * @param {number} [index = 0] - the value for the tabindex
246
- * @returns {object} state - mutated state
247
- */
248
- export const updateTabIndexActiveCell = function (state, index = 0) {
249
- if (state.activeCell && !stillValidActiveCell(state)) {
250
- syncActiveCell(state);
292
+
293
+ if (keyEvent) {
294
+ keyEvent.stopPropagation();
251
295
  }
252
296
 
253
- // we need to check again because maybe there is no active cell after sync
254
- if (state.activeCell && !isRowNavigationMode(state)) {
297
+ setBlurActiveCell(element, state);
298
+
299
+ // update activeCell
300
+ state.activeCell = {
301
+ rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : HEADER_ROW,
302
+ colKeyValue,
303
+ };
304
+ setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
305
+ }
306
+
307
+ function reactToEnter(element, state, event) {
308
+ if (state.keyboardMode === NAVIGATION_MODE) {
309
+ state.keyboardMode = ACTION_MODE;
255
310
  const { rowIndex, colIndex } = getIndexesActiveCell(state);
256
- updateTabIndex(state, rowIndex, colIndex, index);
257
- }
258
- return state;
259
- };
260
311
 
261
- /**
262
- * It updates the tabindex for the row of the current activeCell.
263
- * This happens in rowMode of NAVIGATION_MODE
264
- * @param {object} state - datatable state
265
- * @param {number} [index = 0] - the value for the tabindex
266
- * @returns {object} state - mutated state
267
- */
268
- export const updateTabIndexActiveRow = function (state, index = 0) {
269
- if (state.activeCell && !stillValidActiveCell(state)) {
270
- syncActiveCell(state);
271
- }
312
+ const actionsMap = {};
313
+ actionsMap[SPACE] = 'space';
314
+ actionsMap[ENTER] = 'enter';
272
315
 
273
- // we need to check again because maybe there is no active cell after sync
274
- if (state.activeCell && isRowNavigationMode(state)) {
275
- const { rowIndex } = getIndexesActiveCell(state);
276
- updateTabIndexRow(state, rowIndex, index);
316
+ if (event.detail.keyEvent) {
317
+ event.detail.keyEvent.preventDefault();
318
+ }
319
+ setModeActiveCell(element, state, {
320
+ action: actionsMap[event.detail.keyCode],
321
+ });
322
+ updateTabIndex(state, rowIndex, colIndex, -1);
277
323
  }
278
- return state;
279
- };
324
+ }
280
325
 
281
- /**
282
- * If new set of columns doesnt have tree data mark it to false, as it
283
- * could be true earlier
284
- * Else if it has tree data, check if rowMode is false
285
- * Earlier it didnt have tree data, set rowMode to true to start
286
- * if rowMode is false and earlier it has tree data, keep it false
287
- * if rowMode is true and it has tree data, keep it true
288
- * @param {boolean} hadTreeDataTypePreviously - state object
289
- * @param {object} state - state object
290
- * @returns {object} state - mutated state
291
- */
292
- export function updateRowNavigationMode(hadTreeDataTypePreviously, state) {
293
- if (!hasTreeDataType(state)) {
294
- state.rowMode = false;
295
- } else if (state.rowMode === false && !hadTreeDataTypePreviously) {
296
- state.rowMode = true;
326
+ function reactToEscape(element, state, event) {
327
+ if (state.keyboardMode === ACTION_MODE) {
328
+ // When the table is in action mode this event shouldn't bubble
329
+ // because if the table in inside a modal it should prevent the modal closes
330
+ event.detail.keyEvent.stopPropagation();
331
+ state.keyboardMode = NAVIGATION_MODE;
332
+ setModeActiveCell(element, state);
333
+ setFocusActiveCell(element, state, NAVIGATION_DIR.RESET);
297
334
  }
298
- return state;
299
335
  }
300
336
 
301
- /**
302
- * It return the indexes { rowIndex, colIndex } of a cell based of the unique cell values
303
- * rowKeyValue, colKeyValue
304
- * @param {object} state - datatable state
305
- * @param {string} rowKeyValue - the row key value
306
- * @param {string} colKeyValue - the column key value
307
- * @returns {object} - {rowIndex, colIndex}
308
- */
309
- export const getIndexesByKeys = function (state, rowKeyValue, colKeyValue) {
310
- if (rowKeyValue === 'HEADER') {
311
- return {
312
- rowIndex: -1,
313
- colIndex: state.headerIndexes[colKeyValue],
314
- };
315
- }
337
+ function reactToTab(element, state, event) {
338
+ event.preventDefault();
339
+ event.stopPropagation();
316
340
 
317
- return {
318
- rowIndex: state.indexes[rowKeyValue][colKeyValue][0],
319
- colIndex: state.indexes[rowKeyValue][colKeyValue][1],
320
- };
321
- };
341
+ const { shiftKey } = event.detail;
342
+ const direction = getTabDirection(shiftKey);
343
+ const isExitCell = isActiveCellAnExitCell(state, direction);
322
344
 
323
- /**
324
- * It set the focus to the current activeCell, this operation imply multiple changes
325
- * - update the tabindex of the activeCell
326
- * - set the current keyboard mode
327
- * - set the focus to the cell
328
- * @param {node} element - the custom element template `this.template`
329
- * @param {object} state - datatable state
330
- * @param {int} direction - direction (-1 left, 1 right and 0 for no direction) its used to know which actionable element to activate.
331
- * @param {object} info - extra information when setting the cell mode.
332
- */
333
- export const setFocusActiveCell = function (element, state, direction, info) {
334
- const { keyboardMode } = state;
335
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
345
+ // if in ACTION mode
346
+ if (state.keyboardMode === ACTION_MODE) {
347
+ // if not on last or first cell, tab through each cell of the grid
348
+ if (isExitCell === false) {
349
+ // prevent default key event in action mode when actually moving within the grid
350
+ if (event.detail.keyEvent) {
351
+ event.detail.keyEvent.preventDefault();
352
+ }
353
+ // tab in proper direction based on shift key press
354
+ if (direction === 'BACKWARD') {
355
+ reactToTabBackward(element, state);
356
+ } else {
357
+ reactToTabForward(element, state);
358
+ }
359
+ } else {
360
+ // exit ACTION mode
361
+ state.keyboardMode = NAVIGATION_MODE;
362
+ setModeActiveCell(element, state);
363
+ state.isExitingActionMode = true;
364
+ }
365
+ } else {
366
+ state.isExitingActionMode = true;
367
+ }
368
+ }
336
369
 
337
- updateTabIndex(state, rowIndex, colIndex);
338
- return new Promise((resolve) => {
339
- // eslint-disable-next-line @lwc/lwc/no-async-operation
340
- setTimeout(() => {
341
- const cellElement = getCellElementByIndexes(
342
- element,
343
- rowIndex,
344
- colIndex
345
- );
346
- if (cellElement) {
347
- if (direction) {
348
- cellElement.resetCurrentInputIndex(direction, keyboardMode);
349
- }
350
- cellElement.addFocusStyles();
351
- cellElement.parentElement.classList.add('slds-has-focus');
352
- cellElement.parentElement.focus();
353
- cellElement.setMode(keyboardMode, info);
370
+ export function reactToTabForward(element, state) {
371
+ const { nextRowIndex, nextColIndex } = getNextIndexOnTab(state, 'FORWARD');
372
+ const { columns, rows } = state;
354
373
 
355
- const scrollableY = element.querySelector('.slds-scrollable_y');
356
- const scrollingParent = scrollableY.parentElement;
357
- const parentRect = scrollingParent.getBoundingClientRect();
358
- const findMeRect = cellElement.getBoundingClientRect();
359
- if (findMeRect.top < parentRect.top + TOP_MARGIN) {
360
- scrollableY.scrollTop -= SCROLL_OFFSET;
361
- } else if (
362
- findMeRect.bottom >
363
- parentRect.bottom - BOTTOM_MARGIN
364
- ) {
365
- scrollableY.scrollTop += SCROLL_OFFSET;
366
- }
367
- }
368
- resolve();
369
- }, 0);
374
+ setBlurActiveCell(element, state);
375
+
376
+ // update activeCell
377
+ state.activeCell = {
378
+ rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : HEADER_ROW,
379
+ colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
380
+ };
381
+ setFocusActiveCell(element, state, NAVIGATION_DIR.TAB_FORWARD, {
382
+ action: 'tab',
370
383
  });
371
- };
384
+ }
372
385
 
373
- /**
374
- * It adds and the focus classes to the th/td.
375
- *
376
- * @param {node} element - the custom element template `this.template`
377
- * @param {object} state - datatable state
378
- */
379
- export const addFocusStylesToActiveCell = function (element, state) {
380
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
386
+ export function reactToTabBackward(element, state) {
387
+ const { nextRowIndex, nextColIndex } = getNextIndexOnTab(state, 'BACKWARD');
388
+ const { columns, rows } = state;
381
389
 
382
- const cellElement = getCellElementByIndexes(element, rowIndex, colIndex);
390
+ setBlurActiveCell(element, state);
383
391
 
384
- if (cellElement) {
385
- cellElement.parentElement.classList.add('slds-has-focus');
386
- }
387
- };
392
+ // update activeCell
393
+ state.activeCell = {
394
+ rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : HEADER_ROW,
395
+ colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
396
+ };
397
+ setFocusActiveCell(element, state, NAVIGATION_DIR.TAB_BACKWARD, {
398
+ action: 'tab',
399
+ });
400
+ }
401
+
402
+ function getTabDirection(shiftKey) {
403
+ return shiftKey ? 'BACKWARD' : 'FORWARD';
404
+ }
388
405
 
389
406
  /**
390
- * It blur to the current activeCell, this operation imply multiple changes
391
- * - blur the activeCell
392
- * - update the tabindex to -1
393
- * @param {node} element - the custom element root `this.template`
394
- * @param {object} state - datatable state
395
- */
396
- export const setBlurActiveCell = function (element, state) {
397
- if (state.activeCell) {
398
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
399
- // eslint-disable-next-line @lwc/lwc/no-async-operation
400
- setTimeout(() => {
401
- const cellElement = getCellElementByIndexes(
402
- element,
403
- rowIndex,
404
- colIndex
405
- );
406
- // we need to check because of the tree,
407
- // at this point it may remove/change the rows/keys because opening or closing a row.
408
- if (cellElement) {
409
- if (document.activeElement === cellElement) {
410
- cellElement.blur();
411
- }
412
- cellElement.removeFocusStyles(true);
413
- cellElement.parentElement.classList.remove('slds-has-focus');
414
- }
415
- }, 0);
416
- updateTabIndex(state, rowIndex, colIndex, -1);
417
- }
418
- };
419
- /**
420
- * It set the focus to the current activeCell, this operation imply multiple changes
421
- * - update the tabindex of the activeCell
422
- * - set the current keyboard mode
423
- * - set the focus to the cell
424
- * @param {node} element - the custom element root `this.template`
407
+ * Retrieve the next index values for row & column when tab is pressed
425
408
  * @param {object} state - datatable state
409
+ * @param {string} direction - 'FORWARD' or 'BACKWARD'
410
+ * @returns {object} - nextRowIndex, nextColIndex values, isExitCell boolean
426
411
  */
427
- export const setFocusActiveRow = function (element, state) {
428
- const { rowIndex } = getIndexesActiveCell(state);
412
+ function getNextIndexOnTab(state, direction) {
413
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
429
414
 
430
- updateTabIndexRow(state, rowIndex);
431
- // eslint-disable-next-line @lwc/lwc/no-async-operation
432
- setTimeout(() => {
433
- const row = getRowElementByIndexes(element, rowIndex);
434
- row.focus();
415
+ // decide which function to use based on the value of direction
416
+ const nextTabFunc = {
417
+ FORWARD: getNextIndexOnTabForward,
418
+ BACKWARD: getNextIndexOnTabBackward,
419
+ };
435
420
 
436
- const scrollableY = element.querySelector('.slds-scrollable_y');
437
- const scrollingParent = scrollableY.parentElement;
438
- const parentRect = scrollingParent.getBoundingClientRect();
439
- const findMeRect = row.getBoundingClientRect();
440
- if (findMeRect.top < parentRect.top + TOP_MARGIN) {
441
- scrollableY.scrollTop -= SCROLL_OFFSET;
442
- } else if (findMeRect.bottom > parentRect.bottom - BOTTOM_MARGIN) {
443
- scrollableY.scrollTop += SCROLL_OFFSET;
444
- }
445
- }, 0);
446
- };
421
+ return nextTabFunc[direction](state, rowIndex, colIndex);
422
+ }
447
423
 
448
- /**
449
- * It blur the active Row, this operation imply multiple changes
450
- * - blur the active row
451
- * - update the tabindex to -1
452
- * @param {node} element - the custom element root `this.template`
453
- * @param {object} state - datatable state
454
- */
455
- export const setBlurActiveRow = function (element, state) {
456
- if (state.activeCell) {
457
- const { rowIndex } = getIndexesActiveCell(state);
458
- // eslint-disable-next-line @lwc/lwc/no-async-operation
459
- setTimeout(() => {
460
- const row = getRowElementByIndexes(element, rowIndex);
461
- if (document.activeElement === row) {
462
- row.blur();
463
- }
464
- }, 0);
465
- updateTabIndexRow(state, rowIndex, -1);
466
- }
467
- };
468
- /**
469
- * It changes the datable state based on the keyboard event sent from the cell component,
470
- * the result of those change may trigger re-render on the table
471
- * @param {node} element - the custom element root `this.template`
472
- * @param {object} state - datatable state
473
- * @param {event} event - custom DOM event sent by the cell
474
- * @returns {object} - mutated state
475
- */
476
- export const reactToKeyboard = function (element, state, event) {
477
- switch (event.detail.keyCode) {
478
- case ARROW_RIGHT:
479
- return reactToArrowRight(element, state, event);
480
- case ARROW_LEFT:
481
- return reactToArrowLeft(element, state, event);
482
- case ARROW_DOWN:
483
- return reactToArrowDown(element, state, event);
484
- case ARROW_UP:
485
- return reactToArrowUp(element, state, event);
486
- case ENTER:
487
- case SPACE:
488
- return reactToEnter(element, state, event);
489
- case ESCAPE:
490
- return reactToEscape(element, state, event);
491
- case TAB:
492
- return reactToTab(element, state, event);
493
- default:
494
- return state;
424
+ function getNextIndexOnTabForward(state, rowIndex, colIndex) {
425
+ const columnsCount = state.columns.length;
426
+ if (columnsCount > colIndex + 1) {
427
+ return {
428
+ nextRowIndex: rowIndex,
429
+ nextColIndex: colIndex + 1,
430
+ };
495
431
  }
496
- };
497
-
498
- function reactToKeyboardInNavMode(element, state, event) {
499
- const mockEvent = {
500
- detail: {
501
- rowKeyValue: state.activeCell.rowKeyValue,
502
- colKeyValue: state.activeCell.colKeyValue,
503
- keyCode: event.keyCode,
504
- shiftKey: event.shiftKey,
505
- },
506
- preventDefault: () => {},
507
- stopPropagation: () => {},
432
+ return {
433
+ nextRowIndex: getNextIndexDownWrapped(state, rowIndex),
434
+ nextColIndex: 0,
508
435
  };
436
+ }
509
437
 
510
- switch (event.keyCode) {
511
- case ARROW_RIGHT:
512
- event.preventDefault();
513
- return reactToArrowRight(element, state, mockEvent);
514
- case ARROW_LEFT:
515
- event.preventDefault();
516
- return reactToArrowLeft(element, state, mockEvent);
517
- case ARROW_DOWN:
518
- event.preventDefault();
519
- return reactToArrowDown(element, state, mockEvent);
520
- case ARROW_UP:
521
- event.preventDefault();
522
- return reactToArrowUp(element, state, mockEvent);
523
- case ENTER:
524
- case SPACE:
525
- event.preventDefault();
526
- return reactToEnter(element, state, mockEvent);
527
- case ESCAPE:
528
- // td, th is the active element in the action mode if cell doesnt have action elements
529
- // hence this can be reached and we should react to escape as exiting from action mode
530
- mockEvent.detail.keyEvent = event;
531
- return reactToEscape(element, state, mockEvent);
532
- case TAB:
533
- // event.preventDefault();
534
- return reactToTab(element, state, mockEvent);
535
- default:
536
- return state;
438
+ function getNextIndexOnTabBackward(state, rowIndex, colIndex) {
439
+ const columnsCount = state.columns.length;
440
+ if (colIndex > 0) {
441
+ return {
442
+ nextRowIndex: rowIndex,
443
+ nextColIndex: colIndex - 1,
444
+ };
537
445
  }
446
+ return {
447
+ nextRowIndex: getNextIndexUpWrapped(state, rowIndex),
448
+ nextColIndex: columnsCount - 1,
449
+ };
538
450
  }
539
451
 
540
- export const reactToKeyboardOnRow = function (dt, state, event) {
452
+ /**
453
+ * This set of keyboard actions is specific to tree-grid.
454
+ *
455
+ * When the user first tabs into the tree-grid, the user is set in row mode
456
+ * and the entire row is highlighted.
457
+ *
458
+ * Keyboard Interaction Model:
459
+ * Arrow Up: Moves focus to the row above
460
+ * Arrow Down: Moves focus to the row below
461
+ * Arrow Right: Expands the row to reveal nested items if any
462
+ * Pressing the right arrow again will set focus on a cell
463
+ * and will remove the user from row mode and place them in navigation mode
464
+ * Arrow Left: If cell is expanded, this will collapse the expanded row
465
+ *
466
+ * @param {*} datatable - The datatable component/instance
467
+ * @param {*} state - The datatable state object
468
+ * @param {*} event - The keydown event
469
+ * @returns Mutated state
470
+ */
471
+ export function reactToKeyboardOnRow(datatable, state, event) {
472
+ // TODO: Adapt this selector to also work in a role-based table once tree-grid is also migrated
541
473
  if (
542
474
  isRowNavigationMode(state) &&
543
475
  event.target.localName.indexOf('tr') !== -1
544
476
  ) {
545
- const element = dt.template;
477
+ const element = datatable.template;
546
478
  switch (event.detail.keyCode) {
547
- case ARROW_RIGHT:
548
- return reactToArrowRightOnRow.call(dt, element, state, event);
549
479
  case ARROW_LEFT:
550
- return reactToArrowLeftOnRow.call(dt, element, state, event);
551
- case ARROW_DOWN:
552
- return reactToArrowDownOnRow.call(dt, element, state, event);
480
+ return reactToArrowLeftOnRow.call(
481
+ datatable,
482
+ element,
483
+ state,
484
+ event
485
+ );
486
+ case ARROW_RIGHT:
487
+ return reactToArrowRightOnRow.call(
488
+ datatable,
489
+ element,
490
+ state,
491
+ event
492
+ );
553
493
  case ARROW_UP:
554
- return reactToArrowUpOnRow.call(dt, element, state, event);
494
+ return reactToArrowUpOnRow.call(
495
+ datatable,
496
+ element,
497
+ state,
498
+ event
499
+ );
500
+ case ARROW_DOWN:
501
+ return reactToArrowDownOnRow.call(
502
+ datatable,
503
+ element,
504
+ state,
505
+ event
506
+ );
555
507
  default:
556
508
  return state;
557
509
  }
558
510
  }
559
511
  return state;
560
- };
561
-
562
- function isRowNavigationMode(state) {
563
- return state.keyboardMode === 'NAVIGATION' && state.rowMode === true;
564
512
  }
565
513
 
566
- export function setRowNavigationMode(state) {
567
- if (hasTreeDataType(state) && state.keyboardMode === 'NAVIGATION') {
568
- state.rowMode = true;
514
+ function reactToArrowLeftOnRow(element, state, event) {
515
+ const { rowKeyValue, rowHasChildren, rowExpanded, rowLevel } = event.detail;
516
+ // check if row needs to be collapsed
517
+ // if not go to parent and focus there
518
+ if (rowHasChildren && rowExpanded) {
519
+ fireRowToggleEvent.call(this, rowKeyValue, rowExpanded);
520
+ } else if (rowLevel > 1) {
521
+ const treeColumn = getStateTreeColumn(state);
522
+ if (treeColumn) {
523
+ const colKeyValue = treeColumn.colKeyValue;
524
+ const { rowIndex } = getIndexesByKeys(
525
+ state,
526
+ rowKeyValue,
527
+ colKeyValue
528
+ );
529
+ const parentIndex = getRowParent(state, rowLevel, rowIndex);
530
+ if (parentIndex !== -1) {
531
+ const rows = state.rows;
532
+ setBlurActiveRow(element, state);
533
+ // update activeCell for the row
534
+ state.activeCell = {
535
+ rowKeyValue: rows[parentIndex].key,
536
+ colKeyValue,
537
+ };
538
+ setFocusActiveRow(element, state);
539
+ }
540
+ }
569
541
  }
570
542
  }
571
543
 
572
- export function unsetRowNavigationMode(state) {
573
- state.rowMode = false;
544
+ function moveFromRowToCell(element, state) {
545
+ setBlurActiveRow(element, state);
546
+ unsetRowNavigationMode(state);
547
+ setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
574
548
  }
575
549
 
576
- export function canBeRowNavigationMode(state) {
577
- return hasTreeDataType(state) && state.keyboardMode === 'NAVIGATION';
550
+ function reactToArrowRightOnRow(element, state, event) {
551
+ const { rowKeyValue, rowHasChildren, rowExpanded } = event.detail;
552
+ // check if row needs to be expanded
553
+ // expand row if has children and is collapsed
554
+ // otherwise make this.state.rowMode = false
555
+ // move tabindex 0 to first cell in the row and focus there
556
+ if (rowHasChildren && !rowExpanded) {
557
+ fireRowToggleEvent.call(this, rowKeyValue, rowExpanded);
558
+ } else {
559
+ moveFromRowToCell(element, state);
560
+ }
578
561
  }
579
562
 
580
- function isHeaderRow(rowIndex) {
581
- return rowIndex === -1;
582
- }
563
+ function reactToArrowUpOnRow(element, state, event) {
564
+ // move tabindex 0 one row down
565
+ const { rowKeyValue, keyEvent } = event.detail;
566
+ const treeColumn = getStateTreeColumn(state);
583
567
 
584
- export function getCellElementByIndexes(element, rowIndex, colIndex) {
585
- if (isHeaderRow(rowIndex)) {
586
- return element.querySelector(
587
- `thead > :nth-child(1) >
588
- :nth-child(${colIndex + 1}) > :first-child`
589
- );
590
- }
568
+ keyEvent.stopPropagation();
569
+ keyEvent.preventDefault();
591
570
 
592
- return element.querySelector(
593
- `tbody > :nth-child(${rowIndex + 1}) >
594
- :nth-child(${colIndex + 1}) > :first-child`
595
- );
571
+ if (treeColumn) {
572
+ const colKeyValue = treeColumn.colKeyValue;
573
+ const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
574
+ const prevRowIndex = getNextIndexUpWrapped(state, rowIndex);
575
+ const { rows } = state;
576
+ if (prevRowIndex !== -1) {
577
+ setBlurActiveRow(element, state);
578
+ // update activeCell for the row
579
+ state.activeCell = {
580
+ rowKeyValue: rows[prevRowIndex].key,
581
+ colKeyValue,
582
+ };
583
+ setFocusActiveRow(element, state);
584
+ }
585
+ }
596
586
  }
597
587
 
598
- function getRowElementByIndexes(element, rowIndex) {
599
- if (isHeaderRow(rowIndex)) {
600
- return element.querySelector(`thead > tr:nth-child(1)`);
588
+ function reactToArrowDownOnRow(element, state, event) {
589
+ // move tabindex 0 one row down
590
+ const { rowKeyValue, keyEvent } = event.detail;
591
+ const treeColumn = getStateTreeColumn(state);
592
+
593
+ keyEvent.stopPropagation();
594
+ keyEvent.preventDefault();
595
+
596
+ if (treeColumn) {
597
+ const colKeyValue = treeColumn.colKeyValue;
598
+ const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
599
+ const nextRowIndex = getNextIndexDownWrapped(state, rowIndex);
600
+ const { rows } = state;
601
+ if (nextRowIndex !== -1) {
602
+ setBlurActiveRow(element, state);
603
+ // update activeCell for the row
604
+ state.activeCell = {
605
+ rowKeyValue: rows[nextRowIndex].key,
606
+ colKeyValue,
607
+ };
608
+ setFocusActiveRow(element, state);
609
+ }
601
610
  }
602
- return element.querySelector(`tbody > tr:nth-child(${rowIndex + 1})`);
603
611
  }
604
612
 
605
- function reactToEnter(element, state, event) {
606
- if (state.keyboardMode === 'NAVIGATION') {
607
- state.keyboardMode = 'ACTION';
608
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
613
+ /***************************** ACTIVE CELL *****************************/
609
614
 
610
- const actionsMap = {};
611
- actionsMap[SPACE] = 'space';
612
- actionsMap[ENTER] = 'enter';
615
+ function getDefaultActiveCell(state) {
616
+ const { columns, rows } = state;
617
+ if (columns.length > 0) {
618
+ let colIndex;
619
+ const existCustomerColumn = columns.some((column, index) => {
620
+ colIndex = index;
621
+ return isCustomerColumn(column);
622
+ });
613
623
 
614
- if (event.detail.keyEvent) {
615
- event.detail.keyEvent.preventDefault();
624
+ if (!existCustomerColumn) {
625
+ colIndex = 0;
616
626
  }
617
- setModeActiveCell(element, state, {
618
- action: actionsMap[event.detail.keyCode],
619
- });
620
- updateTabIndex(state, rowIndex, colIndex, -1);
627
+
628
+ return {
629
+ rowKeyValue: rows.length > 0 ? rows[0].key : HEADER_ROW,
630
+ colKeyValue: generateColKeyValue(columns[colIndex], colIndex),
631
+ };
621
632
  }
633
+
634
+ return undefined;
622
635
  }
623
636
 
624
- function reactToEscape(element, state, event) {
625
- if (state.keyboardMode === 'ACTION') {
626
- // When the table is in action mode this event shouldn't bubble
627
- // because if the table in inside a modal it should prevent the model closes
628
- event.detail.keyEvent.stopPropagation();
629
- state.keyboardMode = 'NAVIGATION';
630
- setModeActiveCell(element, state);
631
- setFocusActiveCell(element, state, NAVIGATION_DIR.RESET);
632
- }
637
+ function setDefaultActiveCell(state) {
638
+ state.activeCell = getDefaultActiveCell(state);
633
639
  }
634
640
 
635
641
  /**
636
- * Retrieve the next tab index values for row & column
637
- * @param {object} state - datatable state
638
- * @param {string} direction - 'FORWARD' or 'BACKWARD'
639
- * @returns {object} - nextRowIndex, nextColIndex values, isExitCell boolean
642
+ * Given a datatable template and state, returns an LWC component reference that represents
643
+ * the currently active cell in the table.
644
+ *
645
+ * @param {Object} element - A reference to the datatable's template
646
+ * @param {Object} state - A reference to the datatable's state
640
647
  */
641
- function getNextTabIndex(state, direction) {
648
+ export function getActiveCellElement(element, state) {
642
649
  const { rowIndex, colIndex } = getIndexesActiveCell(state);
650
+ return getCellElementByIndexes(element, rowIndex, colIndex, state);
651
+ }
643
652
 
644
- // decide which function to use based on the value of direction
645
- const nextTabFunc = {
646
- FORWARD: getNextTabIndexForward,
647
- BACKWARD: getNextTabIndexBackward,
653
+ /**
654
+ * Returns if the pair rowKeyValue, colKeyValue are the current activeCell values
655
+ *
656
+ * @param {object} state - datatable state
657
+ * @param {string} rowKeyValue - the unique row key value
658
+ * @param {string} colKeyValue {string} - the unique col key value
659
+ * @returns {boolean} - true if rowKeyValue, colKeyValue are the current activeCell values.
660
+ */
661
+ export function isActiveCell(state, rowKeyValue, colKeyValue) {
662
+ if (state.activeCell) {
663
+ const {
664
+ rowKeyValue: currentRowKeyValue,
665
+ colKeyValue: currentColKeyValue,
666
+ } = state.activeCell;
667
+ return (
668
+ currentRowKeyValue === rowKeyValue &&
669
+ currentColKeyValue === colKeyValue
670
+ );
671
+ }
672
+ return false;
673
+ }
674
+
675
+ /**
676
+ * Updates the current activeCell in the state with the new rowKeyValue, colKeyValue
677
+ * @param {object} state - datatable state
678
+ * @param {string} rowKeyValue - the unique row key value
679
+ * @param {string} colKeyValue {string} - the unique col key value
680
+ * @returns {object} state - mutated datatable state
681
+ */
682
+ export function updateActiveCell(state, rowKeyValue, colKeyValue) {
683
+ state.activeCell = {
684
+ rowKeyValue,
685
+ colKeyValue,
648
686
  };
687
+ return state;
688
+ }
689
+
690
+ /**
691
+ * It check if in the current (data, columns) the activeCell still valid.
692
+ * When data changed the activeCell could be removed, then we check if there is cellToFocusNext
693
+ * which is calculated from previously focused cell, if so we sync to that
694
+ * If active cell is still valid we keep it the same
695
+ *
696
+ * @param {object} state - datatable state
697
+ * @returns {object} state - mutated datatable state
698
+ */
699
+ export function syncActiveCell(state) {
700
+ if (!state.activeCell || !stillValidActiveCell(state)) {
701
+ if (state.activeCell && state.cellToFocusNext) {
702
+ // there is previously focused cell
703
+ setNextActiveCellFromPrev(state);
704
+ } else {
705
+ // there is no active cell or there is no previously focused cell
706
+ setDefaultActiveCell(state);
707
+ }
708
+ }
709
+ return state;
710
+ }
711
+
712
+ /**
713
+ * Sets the next active if there is a previously focused active cell
714
+ * Logic is:
715
+ * if the rowIndex is existing one - cell = (rowIndex, 0)
716
+ * if the rowIndex is > the number of rows (focused was last row or more) = (lastRow, lastColumn)
717
+ * for columns
718
+ * same as above except if the colIndex is > the number of cols (means no data) = set it to null??
719
+ * @param {object} state - datatable state
720
+ */
721
+ function setNextActiveCellFromPrev(state) {
722
+ const { rowIndex, colIndex } = state.cellToFocusNext;
723
+ let nextRowIndex = rowIndex;
724
+ let nextColIndex = colIndex;
725
+ const rowsCount = state.rows ? state.rows.length : 0;
726
+ const colsCount = state.columns.length ? state.columns.length : 0;
649
727
 
650
- return nextTabFunc[direction](state, rowIndex, colIndex);
728
+ if (nextRowIndex > rowsCount - 1) {
729
+ // row index not existing after update to new 5 > 5-1, 6 > 5-1,
730
+ nextRowIndex = rowsCount - 1;
731
+ }
732
+ if (nextColIndex > colsCount - 1) {
733
+ // col index not existing after update to new
734
+ nextColIndex = colsCount - 1;
735
+ }
736
+ const nextActiveCell = getCellFromIndexes(
737
+ state,
738
+ nextRowIndex,
739
+ nextColIndex
740
+ );
741
+ if (nextActiveCell) {
742
+ state.activeCell = nextActiveCell;
743
+ } else {
744
+ setDefaultActiveCell(state);
745
+ }
746
+ state.keyboardMode = NAVIGATION_MODE;
651
747
  }
652
748
 
653
749
  /**
@@ -659,7 +755,7 @@ function getNextTabIndex(state, direction) {
659
755
  export function isActiveCellAnExitCell(state, direction) {
660
756
  // get next tab index values
661
757
  const { rowIndex, colIndex } = getIndexesActiveCell(state);
662
- const { nextRowIndex, nextColIndex } = getNextTabIndex(state, direction);
758
+ const { nextRowIndex, nextColIndex } = getNextIndexOnTab(state, direction);
663
759
  // is it an exit cell?
664
760
  if (
665
761
  // if first cell and moving backward
@@ -676,41 +772,11 @@ export function isActiveCellAnExitCell(state, direction) {
676
772
  return false;
677
773
  }
678
774
 
679
- function reactToTab(element, state, event) {
680
- event.preventDefault();
681
- event.stopPropagation();
682
-
683
- const { shiftKey } = event.detail;
684
- const direction = getTabDirection(shiftKey);
685
- const isExitCell = isActiveCellAnExitCell(state, direction);
686
-
687
- // if in ACTION mode
688
- if (state.keyboardMode === 'ACTION') {
689
- // if not on last or first cell, tab through each cell of the grid
690
- if (isExitCell === false) {
691
- // prevent default key event in action mode when actually moving within the grid
692
- if (event.detail.keyEvent) {
693
- event.detail.keyEvent.preventDefault();
694
- }
695
- // tab in proper direction based on shift key press
696
- if (direction === 'BACKWARD') {
697
- reactToTabBackward(element, state);
698
- } else {
699
- reactToTabForward(element, state);
700
- }
701
- } else {
702
- // exit ACTION mode
703
- state.keyboardMode = 'NAVIGATION';
704
- setModeActiveCell(element, state);
705
- state.isExiting = true;
706
- }
707
- } else {
708
- state.isExiting = true;
709
- }
710
- }
711
-
712
- function getTabDirection(shiftKey) {
713
- return shiftKey ? 'BACKWARD' : 'FORWARD';
775
+ export function getIndexesActiveCell(state) {
776
+ const {
777
+ activeCell: { rowKeyValue, colKeyValue },
778
+ } = state;
779
+ return getIndexesByKeys(state, rowKeyValue, colKeyValue);
714
780
  }
715
781
 
716
782
  function setModeActiveCell(element, state, info) {
@@ -720,260 +786,440 @@ function setModeActiveCell(element, state, info) {
720
786
  }
721
787
  }
722
788
 
723
- /**
724
- * Given a datatable template and state, returns an LWC component reference that represents
725
- * the currently active cell in the table.
726
- *
727
- * @param {Object} element - A reference to the datatable's template
728
- * @param {Object} state - A reference to the datatable's state
729
- */
730
- export function getActiveCellElement(element, state) {
731
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
732
- return getCellElementByIndexes(element, rowIndex, colIndex);
733
- }
734
-
735
- export function getIndexesActiveCell(state) {
789
+ function stillValidActiveCell(state) {
736
790
  const {
737
791
  activeCell: { rowKeyValue, colKeyValue },
738
792
  } = state;
739
- return getIndexesByKeys(state, rowKeyValue, colKeyValue);
793
+ if (rowKeyValue === HEADER_ROW) {
794
+ return state.headerIndexes[colKeyValue] !== undefined;
795
+ }
796
+ return !!(
797
+ state.indexes[rowKeyValue] && state.indexes[rowKeyValue][colKeyValue]
798
+ );
740
799
  }
741
800
 
742
- function reactToArrowRight(element, state, event) {
743
- const { rowKeyValue, colKeyValue } = event.detail;
744
- const { colIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
745
- const nextColIndex = getNextIndexRight(state, colIndex);
746
- const { columns } = state;
747
-
748
- if (nextColIndex === undefined) {
749
- return;
750
- }
801
+ /***************************** FOCUS MANAGEMENT *****************************/
751
802
 
752
- setBlurActiveCell(element, state);
753
- // update activeCell
754
- state.activeCell = {
755
- rowKeyValue,
756
- colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
757
- };
758
- setFocusActiveCell(element, state, NAVIGATION_DIR.RIGHT);
759
- }
803
+ /**
804
+ * It set the focus to the current activeCell, this operation imply multiple changes
805
+ * - update the tabindex of the activeCell
806
+ * - set the current keyboard mode
807
+ * - set the focus to the cell
808
+ * @param {node} element - the custom element template `this.template`
809
+ * @param {object} state - datatable state
810
+ * @param {int} direction - direction (-1 left, 1 right and 0 for no direction) its used to know which actionable element to activate.
811
+ * @param {object} info - extra information when setting the cell mode.
812
+ */
813
+ export function setFocusActiveCell(element, state, direction, info) {
814
+ const { keyboardMode } = state;
815
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
760
816
 
761
- function reactToArrowLeft(element, state, event) {
762
- const { rowKeyValue, colKeyValue } = event.detail;
763
- const { colIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
764
- if (colIndex === 0 && canBeRowNavigationMode(state)) {
765
- moveFromCellToRow(element, state);
766
- } else {
767
- const nextColIndex = getNextIndexLeft(state, colIndex);
817
+ updateTabIndex(state, rowIndex, colIndex);
818
+ return new Promise((resolve) => {
819
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
820
+ setTimeout(() => {
821
+ const cellElement = getCellElementByIndexes(
822
+ element,
823
+ rowIndex,
824
+ colIndex,
825
+ state
826
+ );
827
+ if (cellElement) {
828
+ if (direction) {
829
+ cellElement.resetCurrentInputIndex(direction, keyboardMode);
830
+ }
831
+ cellElement.addFocusStyles();
832
+ cellElement.parentElement.classList.add(FOCUS_CLASS);
833
+ cellElement.parentElement.focus();
834
+ cellElement.setMode(keyboardMode, info);
768
835
 
769
- if (nextColIndex === undefined) {
770
- return;
771
- }
836
+ const scrollableY = element.querySelector('.slds-scrollable_y');
837
+ const scrollingParent = scrollableY.parentElement;
838
+ const parentRect = scrollingParent.getBoundingClientRect();
839
+ const findMeRect = cellElement.getBoundingClientRect();
840
+ if (findMeRect.top < parentRect.top + TOP_MARGIN) {
841
+ scrollableY.scrollTop -= SCROLL_OFFSET;
842
+ } else if (
843
+ findMeRect.bottom >
844
+ parentRect.bottom - BOTTOM_MARGIN
845
+ ) {
846
+ scrollableY.scrollTop += SCROLL_OFFSET;
847
+ }
848
+ }
849
+ resolve();
850
+ }, 0);
851
+ });
852
+ }
772
853
 
773
- const { columns } = state;
774
- setBlurActiveCell(element, state);
775
- // update activeCell
776
- state.activeCell = {
777
- rowKeyValue,
778
- colKeyValue: generateColKeyValue(
779
- columns[nextColIndex],
780
- nextColIndex
781
- ),
782
- };
783
- setFocusActiveCell(element, state, NAVIGATION_DIR.LEFT);
854
+ /**
855
+ * It blur to the current activeCell, this operation imply multiple changes
856
+ * - blur the activeCell
857
+ * - update the tabindex to -1
858
+ * @param {node} element - the custom element root `this.template`
859
+ * @param {object} state - datatable state
860
+ */
861
+ export function setBlurActiveCell(element, state) {
862
+ if (state.activeCell) {
863
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
864
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
865
+ setTimeout(() => {
866
+ const cellElement = getCellElementByIndexes(
867
+ element,
868
+ rowIndex,
869
+ colIndex,
870
+ state
871
+ );
872
+ // we need to check because of the tree,
873
+ // at this point it may remove/change the rows/keys because opening or closing a row.
874
+ if (cellElement) {
875
+ if (document.activeElement === cellElement) {
876
+ cellElement.blur();
877
+ }
878
+ cellElement.removeFocusStyles(true);
879
+ cellElement.parentElement.classList.remove(FOCUS_CLASS);
880
+ }
881
+ }, 0);
882
+ updateTabIndex(state, rowIndex, colIndex, -1);
784
883
  }
785
884
  }
786
885
 
787
- function reactToArrowRightOnRow(element, state, event) {
788
- const { rowKeyValue, rowHasChildren, rowExpanded } = event.detail;
789
- // check if row needs to be expanded
790
- // expand row if has children and is collapsed
791
- // otherwise make this.state.rowMode = false
792
- // move tabindex 0 to first cell in the row and focus there
793
- if (rowHasChildren && !rowExpanded) {
794
- fireRowToggleEvent.call(this, rowKeyValue, rowExpanded);
795
- } else {
796
- moveFromRowToCell(element, state);
886
+ /**
887
+ * Sets the row and col index of cell to focus next if
888
+ * there is state.activecell
889
+ * datatable has focus
890
+ * there is state.indexes
891
+ * there is no previously set state.cellToFocusNext
892
+ * Indexes are calculated as to what to focus on next
893
+ * @param {object} state - datatable state
894
+ * @param {object} template - datatable element
895
+ */
896
+ export function setCellToFocusFromPrev(state, template) {
897
+ if (
898
+ state.activeCell &&
899
+ datatableHasFocus(state, template) &&
900
+ state.indexes &&
901
+ !state.cellToFocusNext
902
+ ) {
903
+ let { rowIndex, colIndex } = getIndexesActiveCell(state);
904
+ colIndex = 0; // default point to the first column
905
+ if (state.rows && rowIndex === state.rows.length - 1) {
906
+ // if it is last row, make it point to its previous row
907
+ rowIndex = state.rows.length - 1;
908
+ colIndex = state.columns ? state.columns.length - 1 : 0;
909
+ }
910
+ state.cellToFocusNext = {
911
+ rowIndex,
912
+ colIndex,
913
+ };
797
914
  }
798
915
  }
799
916
 
800
- function reactToArrowLeftOnRow(element, state, event) {
801
- const { rowKeyValue, rowHasChildren, rowExpanded, rowLevel } = event.detail;
802
- // check if row needs to be collapsed
803
- // if not go to parent and focus there
804
- if (rowHasChildren && rowExpanded) {
805
- fireRowToggleEvent.call(this, rowKeyValue, rowExpanded);
806
- } else if (rowLevel > 1) {
807
- const treeColumn = getStateTreeColumn(state);
808
- if (treeColumn) {
809
- const colKeyValue = treeColumn.colKeyValue;
810
- const { rowIndex } = getIndexesByKeys(
811
- state,
812
- rowKeyValue,
813
- colKeyValue
814
- );
815
- const parentIndex = getRowParent(state, rowLevel, rowIndex);
816
- if (parentIndex !== -1) {
817
- const rows = state.rows;
818
- setBlurActiveRow(element, state);
819
- // update activeCell for the row
820
- state.activeCell = {
821
- rowKeyValue: rows[parentIndex].key,
822
- colKeyValue,
823
- };
824
- setFocusActiveRow(element, state);
825
- }
826
- }
917
+ /**
918
+ * if the current new active still is valid (exists) then set the celltofocusnext to null
919
+ * @param {object} state - datatable state
920
+ */
921
+ export function updateCellToFocusFromPrev(state) {
922
+ if (
923
+ state.activeCell &&
924
+ state.cellToFocusNext &&
925
+ stillValidActiveCell(state)
926
+ ) {
927
+ // if the previous focus is there and valid, don't set the prevActiveFocusedCell
928
+ state.cellToFocusNext = null;
827
929
  }
828
930
  }
829
931
 
830
- function reactToArrowDownOnRow(element, state, event) {
831
- // move tabindex 0 one row down
832
- const { rowKeyValue } = event.detail;
833
- const treeColumn = getStateTreeColumn(state);
932
+ /**
933
+ * reset celltofocusnext to null (used after render)
934
+ * @param {object} state - datatable state
935
+ */
936
+ export function resetCellToFocusFromPrev(state) {
937
+ state.cellToFocusNext = null;
938
+ }
834
939
 
835
- event.detail.keyEvent.stopPropagation();
836
- event.detail.keyEvent.preventDefault();
940
+ /**
941
+ * It adds and the focus classes to the th/td or div[role=gridcell/rowheader].
942
+ *
943
+ * @param {node} element - the custom element template `this.template`
944
+ * @param {object} state - datatable state
945
+ */
946
+ export function addFocusStylesToActiveCell(element, state) {
947
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
837
948
 
838
- if (treeColumn) {
839
- const colKeyValue = treeColumn.colKeyValue;
840
- const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
841
- const nextRowIndex = getNextIndexDownWrapped(state, rowIndex);
842
- const { rows } = state;
843
- if (nextRowIndex !== -1) {
844
- setBlurActiveRow(element, state);
845
- // update activeCell for the row
846
- state.activeCell = {
847
- rowKeyValue: rows[nextRowIndex].key,
848
- colKeyValue,
849
- };
850
- setFocusActiveRow(element, state);
851
- }
949
+ const cellElement = getCellElementByIndexes(
950
+ element,
951
+ rowIndex,
952
+ colIndex,
953
+ state
954
+ );
955
+
956
+ if (cellElement) {
957
+ cellElement.parentElement.classList.add(FOCUS_CLASS);
852
958
  }
853
959
  }
854
960
 
855
- function reactToArrowUpOnRow(element, state, event) {
856
- // move tabindex 0 one row down
857
- // move tabindex 0 one row down
858
- const { rowKeyValue } = event.detail;
859
- const treeColumn = getStateTreeColumn(state);
961
+ /**
962
+ * It set the focus to the current activeCell, this operation imply multiple changes
963
+ * - update the tabindex of the activeCell
964
+ * - set the current keyboard mode
965
+ * - set the focus to the cell
966
+ * @param {node} element - the custom element root `this.template`
967
+ * @param {object} state - datatable state
968
+ */
969
+ function setFocusActiveRow(element, state) {
970
+ const { rowIndex } = getIndexesActiveCell(state);
860
971
 
861
- event.detail.keyEvent.stopPropagation();
862
- event.detail.keyEvent.preventDefault();
972
+ updateTabIndexRow(state, rowIndex);
973
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
974
+ setTimeout(() => {
975
+ const row = getRowElementByIndexes(element, rowIndex, state);
976
+ row.focus();
863
977
 
864
- if (treeColumn) {
865
- const colKeyValue = treeColumn.colKeyValue;
866
- const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
867
- const prevRowIndex = getNextIndexUpWrapped(state, rowIndex);
868
- const { rows } = state;
869
- if (prevRowIndex !== -1) {
870
- setBlurActiveRow(element, state);
871
- // update activeCell for the row
872
- state.activeCell = {
873
- rowKeyValue: rows[prevRowIndex].key,
874
- colKeyValue,
875
- };
876
- setFocusActiveRow(element, state);
978
+ const scrollableY = element.querySelector('.slds-scrollable_y');
979
+ const scrollingParent = scrollableY.parentElement;
980
+ const parentRect = scrollingParent.getBoundingClientRect();
981
+ const findMeRect = row.getBoundingClientRect();
982
+ if (findMeRect.top < parentRect.top + TOP_MARGIN) {
983
+ scrollableY.scrollTop -= SCROLL_OFFSET;
984
+ } else if (findMeRect.bottom > parentRect.bottom - BOTTOM_MARGIN) {
985
+ scrollableY.scrollTop += SCROLL_OFFSET;
877
986
  }
987
+ }, 0);
988
+ }
989
+
990
+ /**
991
+ * It blurs the active row, this operation implies multiple changes
992
+ * - blur the active row
993
+ * - update the tabindex to -1
994
+ * @param {node} element - the custom element root `this.template`
995
+ * @param {object} state - datatable state
996
+ */
997
+ function setBlurActiveRow(element, state) {
998
+ if (state.activeCell) {
999
+ const { rowIndex } = getIndexesActiveCell(state);
1000
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
1001
+ setTimeout(() => {
1002
+ const row = getRowElementByIndexes(element, rowIndex, state);
1003
+ if (document.activeElement === row) {
1004
+ row.blur();
1005
+ }
1006
+ }, 0);
1007
+ updateTabIndexRow(state, rowIndex, -1);
878
1008
  }
879
1009
  }
880
1010
 
881
- function moveFromCellToRow(element, state) {
882
- setBlurActiveCell(element, state);
883
- setRowNavigationMode(state);
884
- setFocusActiveRow(element, state);
1011
+ /**
1012
+ * This method is needed in IE11 where clicking on the cell (factory) makes the div or the span active element
1013
+ * It refocuses on the cell element td or th or div[role=gridcell/rowheader]
1014
+ * @param {object} template - datatable element
1015
+ * @param {object} state - datatable state
1016
+ * @param {boolean} needsRefocusOnCellElement - flag indicating whether or not to refocus on the cell td/th or div[role=gridcell/rowheader]
1017
+ */
1018
+ export function refocusCellElement(template, state, needsRefocusOnCellElement) {
1019
+ if (needsRefocusOnCellElement) {
1020
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
1021
+ const cellElement = getCellElementByIndexes(
1022
+ template,
1023
+ rowIndex,
1024
+ colIndex,
1025
+ state
1026
+ );
1027
+ if (cellElement) {
1028
+ cellElement.parentElement.focus();
1029
+ }
1030
+
1031
+ // setTimeout so that focusin happens and then we set state.cellClicked to true
1032
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
1033
+ setTimeout(() => {
1034
+ setCellClickedForFocus(state);
1035
+ }, 0);
1036
+ } else if (!datatableHasFocus(state, template)) {
1037
+ setCellClickedForFocus(state);
1038
+ }
885
1039
  }
886
1040
 
887
- function moveFromRowToCell(element, state) {
888
- setBlurActiveRow(element, state);
889
- unsetRowNavigationMode(state);
890
- setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
1041
+ export function datatableHasFocus(state, template) {
1042
+ return isFocusInside(template) || state.cellClicked;
891
1043
  }
892
1044
 
893
- export function reactToTabForward(element, state) {
894
- const { nextRowIndex, nextColIndex } = getNextTabIndex(state, 'FORWARD');
895
- const { columns, rows } = state;
1045
+ function isFocusInside(currentTarget) {
1046
+ const activeElements = getShadowActiveElements();
1047
+ return activeElements.some((element) => {
1048
+ return currentTarget.contains(element);
1049
+ });
1050
+ }
896
1051
 
897
- setBlurActiveCell(element, state);
1052
+ export function handleDatatableFocusIn(event) {
1053
+ const { state } = this;
1054
+ state.isExitingActionMode = false;
898
1055
 
899
- // update activeCell
900
- state.activeCell = {
901
- rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : 'HEADER',
902
- colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
903
- };
904
- setFocusActiveCell(element, state, NAVIGATION_DIR.TAB_FORWARD, {
905
- action: 'tab',
906
- });
1056
+ // workaround for delegatesFocus issue that focusin is called when not supposed to W-6220418
1057
+ if (isFocusInside(event.currentTarget)) {
1058
+ if (!state.rowMode && state.activeCell) {
1059
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
1060
+ const cellElement = getCellElementByIndexes(
1061
+ this.template,
1062
+ rowIndex,
1063
+ colIndex,
1064
+ state
1065
+ );
1066
+ // we need to check because of the tree,
1067
+ // at this point it may remove/change the rows/keys because opening or closing a row.
1068
+ if (cellElement) {
1069
+ cellElement.addFocusStyles();
1070
+ cellElement.parentElement.classList.add(FOCUS_CLASS);
1071
+ cellElement.tabindex = 0;
1072
+ }
1073
+ }
1074
+ resetCellClickedForFocus(state);
1075
+ }
907
1076
  }
908
1077
 
909
- export function reactToTabBackward(element, state) {
910
- const { nextRowIndex, nextColIndex } = getNextTabIndex(state, 'BACKWARD');
911
- const { columns, rows } = state;
1078
+ export function handleDatatableFocusOut(event) {
1079
+ const { state } = this;
1080
+ // workarounds for delegatesFocus issues
1081
+ if (
1082
+ // needed for initial focus where relatedTarget is empty
1083
+ !event.relatedTarget ||
1084
+ // needed when clicked outside
1085
+ (event.relatedTarget &&
1086
+ !event.currentTarget.contains(event.relatedTarget)) ||
1087
+ // needed when datatable leaves focus and related target is still within datatable W-6185154
1088
+ (event.relatedTarget &&
1089
+ event.currentTarget.contains(event.relatedTarget) &&
1090
+ state.isExitingActionMode)
1091
+ ) {
1092
+ if (state.activeCell && !state.rowMode) {
1093
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
1094
+ const cellElement = getCellElementByIndexes(
1095
+ this.template,
1096
+ rowIndex,
1097
+ colIndex,
1098
+ state
1099
+ );
1100
+ // we need to check because of the tree,
1101
+ // at this point it may remove/change the rows/keys because opening or closing a row.
1102
+ if (cellElement) {
1103
+ cellElement.removeFocusStyles();
1104
+ cellElement.parentElement.classList.remove(FOCUS_CLASS);
1105
+ }
1106
+ }
1107
+ }
1108
+ }
912
1109
 
913
- setBlurActiveCell(element, state);
1110
+ /**
1111
+ * This is needed to check if datatable has lost focus but cell has been clicked recently
1112
+ * @param {object} state - datatable state
1113
+ */
1114
+ export function setCellClickedForFocus(state) {
1115
+ state.cellClicked = true;
1116
+ }
914
1117
 
915
- // update activeCell
916
- state.activeCell = {
917
- rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : 'HEADER',
918
- colKeyValue: generateColKeyValue(columns[nextColIndex], nextColIndex),
919
- };
920
- setFocusActiveCell(element, state, NAVIGATION_DIR.TAB_BACKWARD, {
921
- action: 'tab',
922
- });
1118
+ /**
1119
+ * Once the dt regains focus there is no need to set this
1120
+ * @param {object} state - datatable state
1121
+ */
1122
+ function resetCellClickedForFocus(state) {
1123
+ state.cellClicked = false;
923
1124
  }
924
1125
 
925
- function reactToArrowDown(element, state, event) {
926
- const { rowKeyValue, colKeyValue } = event.detail;
927
- const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
928
- const nextRowIndex = getNextIndexDown(state, rowIndex);
929
- const { rows } = state;
1126
+ /***************************** TABINDEX MANAGEMENT *****************************/
930
1127
 
931
- if (nextRowIndex === undefined) {
932
- return;
1128
+ /**
1129
+ * It update the tabIndex value of a cell in the state for the rowIndex, colIndex passed
1130
+ * as consequence of this change
1131
+ * datatable is gonna re-render the cell affected with the new tabindex value
1132
+ *
1133
+ * @param {object} state - datatable state
1134
+ * @param {number} rowIndex - the row index
1135
+ * @param {number} colIndex - the column index
1136
+ * @param {number} [index = 0] - the value for the tabindex
1137
+ */
1138
+ export function updateTabIndex(state, rowIndex, colIndex, index = 0) {
1139
+ if (isHeaderRow(rowIndex)) {
1140
+ const { columns } = state;
1141
+ columns[colIndex].tabIndex = index;
1142
+ } else {
1143
+ state.rows[rowIndex].cells[colIndex].tabIndex = index;
933
1144
  }
1145
+ }
934
1146
 
935
- if (state.hideTableHeader && nextRowIndex === -1) {
936
- return;
1147
+ /**
1148
+ * It updates the tabIndex value of a row in the state for the rowIndex passed
1149
+ * as consequence of this change
1150
+ * datatable is gonna re-render the row affected with the new tabindex value
1151
+ *
1152
+ * @param {object} state - datatable state
1153
+ * @param {number} rowIndex - the row index
1154
+ * @param {number} [index = 0] - the value for the tabindex
1155
+ */
1156
+ export function updateTabIndexRow(state, rowIndex, index = 0) {
1157
+ if (!isHeaderRow(rowIndex)) {
1158
+ // TODO what to do when rowIndex is header row
1159
+ state.rows[rowIndex].tabIndex = index;
1160
+ }
1161
+ }
1162
+ /**
1163
+ * It update the tabindex for the current activeCell.
1164
+ * @param {object} state - datatable state
1165
+ * @param {number} [index = 0] - the value for the tabindex
1166
+ * @returns {object} state - mutated state
1167
+ */
1168
+ export function updateTabIndexActiveCell(state, index = 0) {
1169
+ if (state.activeCell && !stillValidActiveCell(state)) {
1170
+ syncActiveCell(state);
937
1171
  }
938
1172
 
939
- if (event.detail.keyEvent) {
940
- event.detail.keyEvent.stopPropagation();
1173
+ // we need to check again because maybe there is no active cell after sync
1174
+ if (state.activeCell && !isRowNavigationMode(state)) {
1175
+ const { rowIndex, colIndex } = getIndexesActiveCell(state);
1176
+ updateTabIndex(state, rowIndex, colIndex, index);
941
1177
  }
942
-
943
- setBlurActiveCell(element, state);
944
- // update activeCell
945
- state.activeCell = {
946
- rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : 'HEADER',
947
- colKeyValue,
948
- };
949
- setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
1178
+ return state;
950
1179
  }
951
1180
 
952
- function reactToArrowUp(element, state, event) {
953
- const { rowKeyValue, colKeyValue } = event.detail;
954
- const { rowIndex } = getIndexesByKeys(state, rowKeyValue, colKeyValue);
955
- const nextRowIndex = getNextIndexUp(state, rowIndex);
956
- const { rows } = state;
957
-
958
- if (nextRowIndex === undefined) {
959
- return;
1181
+ /**
1182
+ * It updates the tabindex for the row of the current activeCell.
1183
+ * This happens in rowMode of NAVIGATION_MODE
1184
+ * @param {object} state - datatable state
1185
+ * @param {number} [index = 0] - the value for the tabindex
1186
+ * @returns {object} state - mutated state
1187
+ */
1188
+ export function updateTabIndexActiveRow(state, index = 0) {
1189
+ if (state.activeCell && !stillValidActiveCell(state)) {
1190
+ syncActiveCell(state);
960
1191
  }
961
1192
 
962
- if (state.hideTableHeader && nextRowIndex === -1) {
963
- return;
1193
+ // we need to check again because maybe there is no active cell after sync
1194
+ if (state.activeCell && isRowNavigationMode(state)) {
1195
+ const { rowIndex } = getIndexesActiveCell(state);
1196
+ updateTabIndexRow(state, rowIndex, index);
964
1197
  }
1198
+ return state;
1199
+ }
965
1200
 
966
- if (event.detail.keyEvent) {
967
- event.detail.keyEvent.stopPropagation();
1201
+ /***************************** INDEX COMPUTATIONS *****************************/
1202
+
1203
+ /**
1204
+ * It return the indexes { rowIndex, colIndex } of a cell based of the unique cell values
1205
+ * rowKeyValue, colKeyValue
1206
+ * @param {object} state - datatable state
1207
+ * @param {string} rowKeyValue - the row key value
1208
+ * @param {string} colKeyValue - the column key value
1209
+ * @returns {object} - {rowIndex, colIndex}
1210
+ */
1211
+ export function getIndexesByKeys(state, rowKeyValue, colKeyValue) {
1212
+ if (rowKeyValue === HEADER_ROW) {
1213
+ return {
1214
+ rowIndex: -1,
1215
+ colIndex: state.headerIndexes[colKeyValue],
1216
+ };
968
1217
  }
969
1218
 
970
- setBlurActiveCell(element, state);
971
- // update activeCell
972
- state.activeCell = {
973
- rowKeyValue: nextRowIndex !== -1 ? rows[nextRowIndex].key : 'HEADER',
974
- colKeyValue,
1219
+ return {
1220
+ rowIndex: state.indexes[rowKeyValue][colKeyValue][0],
1221
+ colIndex: state.indexes[rowKeyValue][colKeyValue][1],
975
1222
  };
976
- setFocusActiveCell(element, state, NAVIGATION_DIR.USE_CURRENT);
977
1223
  }
978
1224
 
979
1225
  function getNextIndexUp(state, rowIndex) {
@@ -1018,215 +1264,113 @@ function getNextIndexDownWrapped(state, rowIndex) {
1018
1264
  return rowIndex + 1 < rowsCount ? rowIndex + 1 : -1;
1019
1265
  }
1020
1266
 
1021
- function getNextTabIndexForward(state, rowIndex, colIndex) {
1022
- const columnsCount = state.columns.length;
1023
- if (columnsCount > colIndex + 1) {
1024
- return {
1025
- nextRowIndex: rowIndex,
1026
- nextColIndex: colIndex + 1,
1027
- };
1028
- }
1029
- return {
1030
- nextRowIndex: getNextIndexDownWrapped(state, rowIndex),
1031
- nextColIndex: 0,
1032
- };
1033
- }
1267
+ /***************************** ROW NAVIGATION MODE *****************************/
1034
1268
 
1035
- function getNextTabIndexBackward(state, rowIndex, colIndex) {
1036
- const columnsCount = state.columns.length;
1037
- if (colIndex > 0) {
1038
- return {
1039
- nextRowIndex: rowIndex,
1040
- nextColIndex: colIndex - 1,
1041
- };
1042
- }
1043
- return {
1044
- nextRowIndex: getNextIndexUpWrapped(state, rowIndex),
1045
- nextColIndex: columnsCount - 1,
1046
- };
1269
+ function canBeRowNavigationMode(state) {
1270
+ return state.keyboardMode === NAVIGATION_MODE && hasTreeDataType(state);
1047
1271
  }
1048
1272
 
1049
- export function getRowParent(state, rowLevel, rowIndex) {
1050
- const parentIndex = rowIndex - 1;
1051
- const rows = state.rows;
1052
- for (let i = parentIndex; i >= 0; i--) {
1053
- if (rows[i].level === rowLevel - 1) {
1054
- return i;
1055
- }
1056
- }
1057
- return -1;
1273
+ function isRowNavigationMode(state) {
1274
+ return state.keyboardMode === NAVIGATION_MODE && state.rowMode === true;
1058
1275
  }
1059
1276
 
1060
- function stillValidActiveCell(state) {
1061
- const {
1062
- activeCell: { rowKeyValue, colKeyValue },
1063
- } = state;
1064
- if (rowKeyValue === 'HEADER') {
1065
- return state.headerIndexes[colKeyValue] !== undefined;
1277
+ function setRowNavigationMode(state) {
1278
+ if (hasTreeDataType(state) && state.keyboardMode === NAVIGATION_MODE) {
1279
+ state.rowMode = true;
1066
1280
  }
1067
- return !!(
1068
- state.indexes[rowKeyValue] && state.indexes[rowKeyValue][colKeyValue]
1069
- );
1070
1281
  }
1071
1282
 
1072
- function setDefaultActiveCell(state) {
1073
- state.activeCell = getDefaultActiveCell(state);
1283
+ export function unsetRowNavigationMode(state) {
1284
+ state.rowMode = false;
1074
1285
  }
1075
1286
 
1076
- function getDefaultActiveCell(state) {
1077
- const { columns, rows } = state;
1078
- if (columns.length > 0) {
1079
- let colIndex;
1080
- const existCustomerColumn = columns.some((column, index) => {
1081
- colIndex = index;
1082
- return isCustomerColumn(column);
1083
- });
1084
-
1085
- if (!existCustomerColumn) {
1086
- colIndex = 0;
1087
- }
1088
-
1089
- return {
1090
- rowKeyValue: rows.length > 0 ? rows[0].key : 'HEADER',
1091
- colKeyValue: generateColKeyValue(columns[colIndex], colIndex),
1092
- };
1287
+ /**
1288
+ * If new set of columns doesnt have tree data, mark it to false, as it
1289
+ * could be true earlier
1290
+ * Else if it has tree data, check if rowMode is false
1291
+ * Earlier it didnt have tree data, set rowMode to true to start
1292
+ * if rowMode is false and earlier it has tree data, keep it false
1293
+ * if rowMode is true and it has tree data, keep it true
1294
+ * @param {boolean} hadTreeDataTypePreviously - state object
1295
+ * @param {object} state - state object
1296
+ * @returns {object} state - mutated state
1297
+ */
1298
+ export function updateRowNavigationMode(hadTreeDataTypePreviously, state) {
1299
+ if (!hasTreeDataType(state)) {
1300
+ state.rowMode = false;
1301
+ } else if (state.rowMode === false && !hadTreeDataTypePreviously) {
1302
+ state.rowMode = true;
1093
1303
  }
1304
+ return state;
1305
+ }
1094
1306
 
1095
- return undefined;
1307
+ /***************************** HELPER FUNCTIONS *****************************/
1308
+
1309
+ function isCellElement(tagName, role) {
1310
+ return (
1311
+ SELECTORS.cell.default.includes(tagName) ||
1312
+ SELECTORS.cell.roleBased.includes(role)
1313
+ );
1096
1314
  }
1097
1315
 
1098
- function getCellFromIndexes(state, rowIndex, colIndex) {
1099
- const { columns, rows } = state;
1100
- if (columns.length > 0) {
1101
- return {
1102
- rowKeyValue: rowIndex === -1 ? 'HEADER' : rows[rowIndex].key,
1103
- colKeyValue: generateColKeyValue(columns[colIndex], colIndex),
1104
- };
1105
- }
1106
- return undefined;
1316
+ function isHeaderRow(rowIndex) {
1317
+ return rowIndex === -1;
1107
1318
  }
1108
1319
 
1109
- export function handleCellKeydown(event) {
1110
- event.stopPropagation();
1111
- reactToKeyboard(this.template, this.state, event);
1320
+ function getHeaderRow(isRenderModeRoleBased) {
1321
+ const selectors = SELECTORS.headerRow;
1322
+ return isRenderModeRoleBased ? selectors.roleBased : selectors.default;
1112
1323
  }
1113
1324
 
1114
- export function handleKeyDown(event) {
1115
- const targetTagName = event.target.tagName.toLowerCase();
1116
- // when the event came from the td is cause it has the focus.
1117
- if (targetTagName === 'td' || targetTagName === 'th') {
1118
- reactToKeyboardInNavMode(this.template, this.state, event);
1119
- }
1325
+ function getDataRow(rowIndex, isRenderModeRoleBased) {
1326
+ const dataRowRowGroupSelector = isRenderModeRoleBased
1327
+ ? SELECTORS.dataRowRowGroup.roleBased
1328
+ : SELECTORS.dataRowRowGroup.default;
1329
+ return `${dataRowRowGroupSelector} > :nth-child(${rowIndex + 1})`;
1120
1330
  }
1121
1331
 
1122
- /**
1123
- * This is needed to check if datatable has lost focus but cell has been clicked recently
1124
- * @param {object} state - datatable state
1125
- */
1126
- export const setCellClickedForFocus = function (state) {
1127
- state.cellClicked = true;
1128
- };
1332
+ export function getCellElementByIndexes(element, rowIndex, colIndex, state) {
1333
+ const isRenderModeRoleBased = state.renderModeRoleBased;
1334
+ let selector = '';
1129
1335
 
1130
- /**
1131
- * Once the dt regains focus there is no need to set this
1132
- * @param {object} state - datatable state
1133
- */
1134
- export const resetCellClickedForFocus = function (state) {
1135
- state.cellClicked = false;
1136
- };
1336
+ if (isHeaderRow(rowIndex)) {
1337
+ selector = `${getHeaderRow(isRenderModeRoleBased)}
1338
+ > :nth-child(${colIndex + 1}) > :first-child`;
1339
+ return element.querySelector(selector);
1340
+ }
1137
1341
 
1138
- /**
1139
- * This method is needed in IE11 where clicking on the cell (factory) makes the div or the span active element
1140
- * It refocuses on the cell element td or th
1141
- * @param {object} template - datatable element
1142
- * @param {object} state - datatable state
1143
- * @param {boolean} needsRefocusOnCellElement - flag indicating whether or not to refocus on the cell td/th
1144
- */
1145
- export const refocusCellElement = function (
1146
- template,
1147
- state,
1148
- needsRefocusOnCellElement
1149
- ) {
1150
- if (needsRefocusOnCellElement) {
1151
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
1152
- const cellElement = getCellElementByIndexes(
1153
- template,
1154
- rowIndex,
1155
- colIndex
1156
- );
1157
- if (cellElement) {
1158
- cellElement.parentElement.focus();
1159
- }
1342
+ selector = `${getDataRow(rowIndex, isRenderModeRoleBased)}
1343
+ > :nth-child(${colIndex + 1}) > :first-child`;
1344
+ return element.querySelector(selector);
1345
+ }
1160
1346
 
1161
- // setTimeout so that focusin happens and then we set state.cellClicked to true
1162
- // eslint-disable-next-line @lwc/lwc/no-async-operation
1163
- setTimeout(() => {
1164
- setCellClickedForFocus(state);
1165
- }, 0);
1166
- } else if (!datatableHasFocus(state, template)) {
1167
- setCellClickedForFocus(state);
1347
+ function getRowElementByIndexes(element, rowIndex, state) {
1348
+ const isRenderModeRoleBased = state.renderModeRoleBased;
1349
+ if (isHeaderRow(rowIndex)) {
1350
+ return element.querySelector(getHeaderRow(isRenderModeRoleBased));
1168
1351
  }
1169
- };
1170
1352
 
1171
- export const handleDatatableLosedFocus = function (event) {
1172
- const { state } = this;
1173
- // workarounds for delegatesFocus issues
1174
- if (
1175
- // needed for initial focus where relatedTarget is empty
1176
- !event.relatedTarget ||
1177
- // needed when clicked outside
1178
- (event.relatedTarget &&
1179
- !event.currentTarget.contains(event.relatedTarget)) ||
1180
- // needed when datatable leaves focus and related target is still within datatable W-6185154
1181
- (event.relatedTarget &&
1182
- event.currentTarget.contains(event.relatedTarget) &&
1183
- state.isExiting)
1184
- ) {
1185
- if (state.activeCell && !state.rowMode) {
1186
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
1187
- const cellElement = getCellElementByIndexes(
1188
- this.template,
1189
- rowIndex,
1190
- colIndex
1191
- );
1192
- // we need to check because of the tree,
1193
- // at this point it may remove/change the rows/keys because opening or closing a row.
1194
- if (cellElement) {
1195
- cellElement.removeFocusStyles();
1196
- cellElement.parentElement.classList.remove('slds-has-focus');
1197
- }
1353
+ return element.querySelector(getDataRow(rowIndex, isRenderModeRoleBased));
1354
+ }
1355
+
1356
+ export function getRowParent(state, rowLevel, rowIndex) {
1357
+ const parentIndex = rowIndex - 1;
1358
+ const rows = state.rows;
1359
+ for (let i = parentIndex; i >= 0; i--) {
1360
+ if (rows[i].level === rowLevel - 1) {
1361
+ return i;
1198
1362
  }
1199
1363
  }
1200
- };
1201
-
1202
- function isFocusInside(currentTarget) {
1203
- const activeElements = getShadowActiveElements();
1204
- return activeElements.some((element) => {
1205
- return currentTarget.contains(element);
1206
- });
1364
+ return -1;
1207
1365
  }
1208
1366
 
1209
- export const handleDatatableFocusIn = function (event) {
1210
- const { state } = this;
1211
- state.isExiting = false;
1212
-
1213
- // workaround for delegatesFocus issue that focusin is called when not supposed to W-6220418
1214
- if (isFocusInside(event.currentTarget)) {
1215
- if (!state.rowMode && state.activeCell) {
1216
- const { rowIndex, colIndex } = getIndexesActiveCell(state);
1217
- const cellElement = getCellElementByIndexes(
1218
- this.template,
1219
- rowIndex,
1220
- colIndex
1221
- );
1222
- // we need to check because of the tree,
1223
- // at this point it may remove/change the rows/keys because opening or closing a row.
1224
- if (cellElement) {
1225
- cellElement.addFocusStyles();
1226
- cellElement.parentElement.classList.add('slds-has-focus');
1227
- cellElement.tabindex = 0;
1228
- }
1229
- }
1230
- resetCellClickedForFocus(state);
1367
+ function getCellFromIndexes(state, rowIndex, colIndex) {
1368
+ const { columns, rows } = state;
1369
+ if (columns.length > 0) {
1370
+ return {
1371
+ rowKeyValue: rowIndex === -1 ? HEADER_ROW : rows[rowIndex].key,
1372
+ colKeyValue: generateColKeyValue(columns[colIndex], colIndex),
1373
+ };
1231
1374
  }
1232
- };
1375
+ return undefined;
1376
+ }