lightning-base-components 1.13.10-alpha → 1.14.4-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 (69) hide show
  1. package/metadata/raptor.json +24 -0
  2. package/package.json +20 -4
  3. package/scopedImports/@salesforce-internal-core.appVersion.js +1 -1
  4. package/scopedImports/@salesforce-label-LightningDualListbox.movedOptionsPlural.js +1 -0
  5. package/scopedImports/@salesforce-label-LightningDualListbox.movedOptionsSingular.js +1 -0
  6. package/scopedImports/@salesforce-label-LightningErrorMessage.validitySelectAtleastOne.js +1 -0
  7. package/scopedImports/@salesforce-label-LightningMap.titleWithAddress.js +1 -0
  8. package/scopedImports/@salesforce-label-LightningModalBase.cancelandclose.js +1 -0
  9. package/src/lightning/ariaObserver/__component__/ariaObserver.spec.js +112 -0
  10. package/src/lightning/ariaObserver/__docs__/ariaObserver.md +142 -0
  11. package/src/lightning/{utilsPrivate/contentMutation.js → ariaObserver/ariaObserver.js} +60 -98
  12. package/src/lightning/buttonMenu/keyboard.js +0 -10
  13. package/src/lightning/card/card.html +6 -0
  14. package/src/lightning/checkboxGroup/checkboxGroup.html +2 -2
  15. package/src/lightning/checkboxGroup/checkboxGroup.js +6 -1
  16. package/src/lightning/colorPickerCustom/colorPickerCustom.js +20 -1
  17. package/src/lightning/datatable/__docs__/datatable.md +55 -0
  18. package/src/lightning/datatable/__examples__/basic/basic.html +1 -1
  19. package/src/lightning/datatable/columns-shared.js +1 -1
  20. package/src/lightning/datatable/datatable.js +98 -30
  21. package/src/lightning/datatable/errors.js +20 -9
  22. package/src/lightning/datatable/headerActions.js +77 -49
  23. package/src/lightning/datatable/infiniteLoading.js +100 -28
  24. package/src/lightning/datatable/inlineEdit.js +505 -379
  25. package/src/lightning/datatable/inlineEditShared.js +24 -0
  26. package/src/lightning/datatable/keyboard.js +162 -127
  27. package/src/lightning/datatable/renderManager.js +201 -133
  28. package/src/lightning/datatable/rowLevelActions.js +17 -13
  29. package/src/lightning/datatable/rowNumber.js +54 -20
  30. package/src/lightning/datatable/rowSelection.js +760 -0
  31. package/src/lightning/datatable/rowSelectionShared.js +79 -0
  32. package/src/lightning/datatable/rows.js +17 -6
  33. package/src/lightning/datatable/state.js +16 -2
  34. package/src/lightning/datatable/templates/div/div.css +4 -0
  35. package/src/lightning/datatable/templates/div/div.html +6 -0
  36. package/src/lightning/datatable/templates/table/table.html +5 -0
  37. package/src/lightning/datatable/utils.js +14 -0
  38. package/src/lightning/datatable/wrapText.js +77 -47
  39. package/src/lightning/dualListbox/dualListbox.html +1 -1
  40. package/src/lightning/dualListbox/dualListbox.js +42 -0
  41. package/src/lightning/formattedDateTime/__docs__/formattedDateTime.md +36 -3
  42. package/src/lightning/formattedDateTime/__examples__/datetime/datetime.html +2 -2
  43. package/src/lightning/formattedDateTime/__examples__/datetime/datetime.js +3 -1
  44. package/src/lightning/formattedDateTime/__examples__/time/time.html +1 -1
  45. package/src/lightning/formattedDateTime/__examples__/time/time.js +3 -1
  46. package/src/lightning/formattedDateTime/formattedDateTime.js +1 -0
  47. package/src/lightning/input/input.html +1 -5
  48. package/src/lightning/input/input.js +69 -48
  49. package/src/lightning/inputUtils/validity.js +12 -1
  50. package/src/lightning/pillContainer/__docs__/pillContainer.md +45 -1
  51. package/src/lightning/primitiveCellActions/primitiveCellActions.js +69 -12
  52. package/src/lightning/primitiveCellFactory/cellWithStandardLayout.html +13 -11
  53. package/src/lightning/primitiveCellFactory/primitiveCellFactory.js +13 -8
  54. package/src/lightning/primitiveDatatableIeditPanel/primitiveDatatableIeditPanel.html +17 -14
  55. package/src/lightning/primitiveDatatableIeditPanel/primitiveDatatableIeditPanel.js +167 -98
  56. package/src/lightning/primitiveDatatableIeditTypeFactory/primitiveDatatableIeditTypeFactory.js +94 -69
  57. package/src/lightning/primitiveDatatableStatusBar/primitiveDatatableStatusBar.html +4 -4
  58. package/src/lightning/primitiveDatatableStatusBar/primitiveDatatableStatusBar.js +4 -4
  59. package/src/lightning/primitiveHeaderActions/primitiveHeaderActions.js +99 -37
  60. package/src/lightning/progressIndicator/progressIndicator.js +1 -1
  61. package/src/lightning/progressStep/progressStep.js +30 -22
  62. package/src/lightning/staticMap/staticMap.html +1 -0
  63. package/src/lightning/staticMap/staticMap.js +39 -2
  64. package/src/lightning/utils/classSet.js +4 -1
  65. package/src/lightning/utilsPrivate/utilsPrivate.js +12 -1
  66. package/scopedImports/@salesforce-label-LightningModalBase.close.js +0 -1
  67. package/src/lightning/datatable/inlineEdit-shared.js +0 -14
  68. package/src/lightning/datatable/selector-shared.js +0 -38
  69. package/src/lightning/datatable/selector.js +0 -527
@@ -8,14 +8,16 @@ import {
8
8
  reactToTabBackward,
9
9
  reactToTabForward,
10
10
  getActiveCellElement,
11
- getCellElementByIndexes,
12
11
  updateActiveCell,
12
+ isActiveCellEditable,
13
+ isValidCell,
13
14
  } from './keyboard';
14
15
  import {
15
16
  updateRowsAndCellIndexes,
16
17
  getRowByKey,
17
18
  getKeyField,
18
19
  getUserRowByCellKeys,
20
+ isCellEditable,
19
21
  } from './rows';
20
22
  import {
21
23
  getColumnIndexByColumnKey,
@@ -25,56 +27,143 @@ import {
25
27
  } from './columns';
26
28
  import { setErrors } from './errors';
27
29
  import {
28
- markDeselectedCell,
29
- markSelectedCell,
30
+ setAriaSelectedOnCell,
31
+ unsetAriaSelectedOnCell,
30
32
  isSelectedRow,
31
33
  getCurrentSelectionLength,
32
34
  getSelectedRowsKeys,
33
- } from './selector';
35
+ } from './rowSelection';
34
36
  import { isObjectLike } from './utils';
35
37
 
36
- export { getDirtyValue } from './inlineEdit-shared';
38
+ export { getDirtyValueFromCell } from './inlineEditShared';
37
39
 
38
- const PANEL_SEL = '[data-iedit-panel="true"]';
40
+ const IEDIT_PANEL_SELECTOR = '[data-iedit-panel="true"]';
41
+ const HIDE_PANEL_THRESHOLD = 5; // hide panel on scroll
39
42
 
40
- export function getInlineEditDefaultState() {
41
- return {
42
- inlineEdit: {
43
- dirtyValues: {},
44
- },
45
- };
43
+ /************************** EVENT HANDLERS **************************/
44
+
45
+ /**
46
+ * Event handler to open/start the inline edit flows that are triggered by datatable cells
47
+ *
48
+ * @param {CustomEvent} event - An object representing the event that was fired by the datatable cell for
49
+ * which to open the inline edit panel. Must be valid and truthy.
50
+ */
51
+ export function handleEditCell(event) {
52
+ openInlineEdit(this, event.target);
46
53
  }
47
54
 
48
55
  /**
49
- * @param {Object} state - Datatable instance.
50
- * @return {Array} - An array of objects, each object describing the dirty values in the form { colName : dirtyValue }.
51
- * A special key is the { [keyField]: value } pair used to identify the row containing this changed values.
56
+ * Handles the completion of inline edit.
57
+ * Closes and destroys the panel and processes completion of the edit
58
+ *
59
+ * @param {CustomEvent} event - `ieditfinished`
52
60
  */
53
- export function getDirtyValues(state) {
54
- return getChangesForCustomer(state, state.inlineEdit.dirtyValues);
61
+ export function handleInlineEditFinish(event) {
62
+ stopPanelPositioning(this);
63
+
64
+ const { reason, rowKeyValue, colKeyValue } = event.detail;
65
+
66
+ processInlineEditFinish(this, reason, rowKeyValue, colKeyValue);
55
67
  }
56
68
 
57
69
  /**
58
- * Sets the dirty values in the datatable.
70
+ * Sets the `aria-selected` value on the cell based on the checked value
71
+ * If the mass update checkbox is checked, set aria-selected on those cells
72
+ * which are to be updated to true
73
+ * If not, set aria-selected to true on only the cell that is being edited
74
+ */
75
+ export function handleMassCheckboxChange(event) {
76
+ const state = this.state;
77
+ if (event.detail.checked) {
78
+ setAriaSelectedOnAllSelectedRows(state);
79
+ } else {
80
+ unsetAriaSelectedOnAllSelectedRows(this.state);
81
+ setAriaSelectedOnCell(
82
+ state,
83
+ state.inlineEdit.rowKeyValue,
84
+ state.inlineEdit.colKeyValue
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Handles management of the inline edit panel when user scrolls horizontally or vertically.
91
+ * On either horizontal or vertical scroll:
92
+ * - If the user scrolls past the pre-determined threshold,
93
+ * hide the inline edit panel and process the completion of inline edit.
94
+ * - If the user scrolls within the pre-determined threshold,
95
+ * keep the panel open but reposition it to align with the cell
59
96
  *
60
- * @param {Object} state Datatable state for the inline edit.
61
- * @param {Array} value An array of objects, each object describing the dirty values in the form { colName : dirtyValue }.
62
- * A special key is the { [keyField]: value } pair used to identify the row containing this changed values.
97
+ * @param {Event} event - `scroll`
98
+ * @returns
63
99
  */
64
- export function setDirtyValues(state, value) {
65
- const keyField = getKeyField(state);
66
- const dirtyValues = Array.isArray(value) ? value : [];
100
+ export function handleInlineEditPanelScroll(event) {
101
+ const { isPanelVisible, rowKeyValue, colKeyValue } = this.state.inlineEdit;
67
102
 
68
- state.inlineEdit.dirtyValues = dirtyValues.reduce((result, rowValues) => {
69
- const changes = getRowChangesFromCustomer(state, rowValues);
70
- delete changes[keyField];
103
+ if (!isPanelVisible) {
104
+ return;
105
+ }
71
106
 
72
- result[rowValues[keyField]] = changes;
107
+ let delta = 0;
108
+ const container = event.target;
73
109
 
74
- return result;
75
- }, {});
110
+ // When user scrolls horizontally
111
+ if (container.classList.contains('slds-scrollable_x')) {
112
+ const scrollX = container.scrollLeft;
113
+ if (this.privateLastScrollX == null) {
114
+ this.privateLastScrollX = scrollX;
115
+ } else {
116
+ delta = Math.abs(this.privateLastScrollX - scrollX);
117
+ }
118
+ } else {
119
+ // When user scrolls vertically
120
+ const scrollY = container.scrollTop;
121
+ if (this.privateLastScrollY == null) {
122
+ this.privateLastScrollY = scrollY;
123
+ } else {
124
+ delta = Math.abs(this.privateLastScrollY - scrollY);
125
+ }
126
+ }
127
+
128
+ // If user has scrolled past threshold,
129
+ // reset stored scroll values, hide panel and
130
+ // process inline edit completion
131
+ if (delta > HIDE_PANEL_THRESHOLD) {
132
+ this.privateLastScrollX = null;
133
+ this.privateLastScrollY = null;
134
+ stopPanelPositioning(this);
135
+ processInlineEditFinish(this, 'lost-focus', rowKeyValue, colKeyValue);
136
+ } else {
137
+ // we want to keep the panel attached to the cell before
138
+ // reaching the threshold and hiding the panel
139
+ repositionPanel(this);
140
+ }
76
141
  }
77
142
 
143
+ /************************** EVENT DISPATCHER **************************/
144
+
145
+ /**
146
+ * Dispatches the `cellchange` event with the `draftValues` in the
147
+ * detail object.
148
+ *
149
+ * @param {Object} dtInstance - datatable instance
150
+ * @param {Object} cellChange - object containing cell changes
151
+ */
152
+ function dispatchCellChangeEvent(dtInstance, cellChange) {
153
+ dtInstance.dispatchEvent(
154
+ new CustomEvent('cellchange', {
155
+ detail: {
156
+ draftValues: getResolvedCellChanges(
157
+ dtInstance.state,
158
+ cellChange
159
+ ),
160
+ },
161
+ })
162
+ );
163
+ }
164
+
165
+ /************************** INLINE EDIT STATE MANAGEMENT **************************/
166
+
78
167
  export function isInlineEditTriggered(state) {
79
168
  return Object.keys(state.inlineEdit.dirtyValues).length > 0;
80
169
  }
@@ -85,102 +174,129 @@ export function cancelInlineEdit(dt) {
85
174
  updateRowsAndCellIndexes.call(dt);
86
175
  }
87
176
 
88
- /**
89
- * An event handler for open inline edit flows that are triggered by datatable cells
90
- *
91
- * @param {CustomEvent} event - An object representing the event that was fired by the datatable cell for
92
- * which to open the inline edit panel. Must be valid and truthy.
93
- */
94
- export function handleEditCell(event) {
95
- openInlineEdit(this, event.target);
177
+ export function closeInlineEdit(dt) {
178
+ const inlineEditState = dt.state.inlineEdit;
179
+
180
+ if (inlineEditState.isPanelVisible) {
181
+ processInlineEditFinish(
182
+ dt,
183
+ 'lost-focus',
184
+ inlineEditState.rowKeyValue,
185
+ inlineEditState.colKeyValue
186
+ );
187
+ }
96
188
  }
97
189
 
98
190
  /**
99
- * Attempts to open the inline edit panel for the datatable's currently active cell. If the active cell is not
100
- * editable, then the panel is instead opened for the first editable cell in the table. Used to open inline edit
101
- * in a direct, programmatic fashion.
191
+ * Handles processing when the datatable has finished an inline edit flow.
192
+ * Evaluates if data from the inline edit panel should be saved or not.
193
+ * Data should be saved
194
+ * - if inline edit was not canceled by the user and
195
+ * - if in mass inline edit, the 'Apply' button is clicked (don't save when focus is lost) and
196
+ * - if the cell being edited is a valid cell
102
197
  *
103
- * If there is no data in the table or there are no editable cells in the table then calling this function
104
- * results in a no-op.
198
+ * If the data should be saved, check that the value has changed or if mass edit is enabled.
199
+ * If so, one or more cells need to reflect the updated value.
200
+ * All changes to the cell(s) (`cellChange`) are stored in the following format:
201
+ * cellChange = {
202
+ * rowKeyValue1: {
203
+ * colKeyValue: 'changed value'
204
+ * },
205
+ * rowKeyValue2: {
206
+ * colKeyValue: 'changed value'
207
+ * }
208
+ * }
105
209
  *
106
- * @param {Object} dt - The datatable instance. Must be a truthy and valid datatable reference.
210
+ * The above cell changes are used to update state.inlineEdit.dirtyValues.
211
+ * The draft values are retrieved using the cell changes that were gathered here and
212
+ * the `cellchange` event is dispatched passing the draftValues in the detail object.
213
+ *
214
+ * If the user inline edit panel lost focus, the datatable should react accordingly.
215
+ *
216
+ * @param {Object} dt - datatable instance
217
+ * @param {string} reason - reason to finish the edit; valid reasons are: edit-canceled | lost-focus | tab-pressed | submit-action
218
+ * @param {string} rowKeyValue - row key of the edited cell
219
+ * @param {string} colKeyValue - column key of the edited cell
107
220
  */
108
- export function openInlineEditOnActiveCell(dt) {
109
- const hasData = dt.state.data && dt.state.data.length > 0;
110
- if (hasData) {
111
- const activeCellElement = getActiveCellElement(dt.template, dt.state);
112
- const isEditable = activeCellElement.editable;
113
- if (isEditable) {
114
- setFocusAndOpenInlineEdit(dt, activeCellElement);
115
- } else {
116
- const firstEditableCell = getFirstEditableCell(dt);
117
- if (firstEditableCell) {
118
- updateActiveCell(
119
- dt.state,
120
- firstEditableCell.rowKeyValue,
121
- firstEditableCell.colKeyValue
122
- );
123
- setFocusAndOpenInlineEdit(dt, firstEditableCell);
221
+ function processInlineEditFinish(dt, reason, rowKeyValue, colKeyValue) {
222
+ const state = dt.state;
223
+ const inlineEditState = state.inlineEdit;
224
+
225
+ const shouldSaveData =
226
+ reason !== 'edit-canceled' &&
227
+ !(inlineEditState.massEditEnabled && reason === 'lost-focus') &&
228
+ isValidCell(dt.state, rowKeyValue, colKeyValue);
229
+
230
+ if (shouldSaveData) {
231
+ const panel = dt.template.querySelector(IEDIT_PANEL_SELECTOR);
232
+ const editValue = panel.value;
233
+ const isValidEditValue = panel.validity.valid;
234
+ const updateAllSelectedRows = panel.isMassEditChecked;
235
+ const currentValue = getCellValue(state, rowKeyValue, colKeyValue);
236
+
237
+ if (
238
+ isValidEditValue &&
239
+ (editValue !== currentValue || updateAllSelectedRows)
240
+ ) {
241
+ const cellChange = {};
242
+ cellChange[rowKeyValue] = {};
243
+ cellChange[rowKeyValue][colKeyValue] = editValue;
244
+
245
+ if (updateAllSelectedRows) {
246
+ const selectedRowKeys = getSelectedRowsKeys(state);
247
+ selectedRowKeys.forEach((rowKey) => {
248
+ cellChange[rowKey] = {};
249
+ cellChange[rowKey][colKeyValue] = editValue;
250
+ });
124
251
  }
125
- }
126
- }
127
- }
128
252
 
129
- /**
130
- * async function to await setting focus on an editable cell before opening inline-edit panel
131
- * @param {Object} dt - The datatable instance
132
- * @param {Object} cell - editable cell to be focused before open inline-edit panel
133
- */
134
- // eslint-disable-next-line @lwc/lwc/no-async-await
135
- async function setFocusAndOpenInlineEdit(dt, cell) {
136
- await setFocusActiveCell(dt.template, dt.state, 0);
137
- openInlineEdit(dt, cell);
138
- }
253
+ updateDirtyValues(state, cellChange);
139
254
 
140
- /**
141
- * Returns a reference to the first editable cell in the table. If no editable cells exist in the table
142
- * then undefined is returned.
143
- *
144
- * @param {Object} dt - The datatable instance. Must be a truthy and valid datatable reference.
145
- */
146
- function getFirstEditableCell(dt) {
147
- const columns = getColumns(dt.state);
148
- const editableColumns = getEditableColumns(columns);
255
+ dispatchCellChangeEvent(dt, cellChange);
149
256
 
150
- if (editableColumns.length > 0) {
151
- const rows = dt.state.rows;
152
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
153
- for (let i = 0; i < editableColumns.length; i++) {
154
- // Loop through the editable columns in order and examine the corresponding cells
155
- // in the current row for editability, returning the first such cell that is editable
156
- const editableColumn = editableColumns[i];
157
- const editableColumnIndex = getStateColumnIndex(
158
- dt.state,
159
- editableColumn.colKeyValue
160
- );
161
- const cell = getCellElementByIndexes(
162
- dt.template,
163
- rowIndex,
164
- editableColumnIndex
165
- );
166
- if (cell.editable) {
167
- return cell;
168
- }
257
+ // TODO: do we need to update all rows in the dt or just the one that was modified?
258
+ updateRowsAndCellIndexes.call(dt);
259
+ }
260
+ }
261
+
262
+ if (reason !== 'lost-focus') {
263
+ switch (reason) {
264
+ case 'tab-pressed-next': {
265
+ reactToTabForward(dt.template, state);
266
+ break;
267
+ }
268
+ case 'tab-pressed-prev': {
269
+ reactToTabBackward(dt.template, state);
270
+ break;
271
+ }
272
+ default: {
273
+ setFocusActiveCell(dt.template, state, 0);
169
274
  }
170
275
  }
171
276
  }
172
277
 
173
- return undefined;
278
+ unsetAriaSelectedOnAllSelectedRows(state);
279
+ unsetAriaSelectedOnCell(state, rowKeyValue, colKeyValue);
280
+
281
+ inlineEditState.isPanelVisible = false;
174
282
  }
175
283
 
284
+ /************************** INLINE EDIT **************************/
285
+
176
286
  /**
177
- * Opens the inline edit panel for the given target element. This function is the endpoint of all
287
+ * Opens the inline edit panel for the given target element/cell. This function is the endpoint of all
178
288
  * event-driven open inline edit flows but can also be used to open the inline edit panel in a direct
179
289
  * programmatic fashion.
180
290
  *
291
+ * - Open and position the inline edit panel relative to the cell it was opened from.
292
+ * - Retrieve and set the required inline edit properties in the state object.
293
+ * - Resolve typeAttributes from the column definition so that it can be passed down to the inline edit panel input
294
+ * - Set aria-selected to `true` on the cell which is being edited
295
+ * - Once the panel is open, set focus on the input element
296
+ *
181
297
  * @param {Object} dt - The datatable instance. Must be a truthy and valid datatable reference.
182
- * @param {Object} target - The LWC component instance representing the cell in the datatable for which
183
- * the inline edit panel is to be opened. Must be a truthy and valid reference.
298
+ * @param {Object} target - The LWC component instance (lightning-primitive-cell-factory) representing the cell in the
299
+ * datatable for which the inline edit panel is to be opened. Must be a truthy and valid reference.
184
300
  */
185
301
  function openInlineEdit(dt, target) {
186
302
  startPanelPositioning(dt, target.parentElement);
@@ -193,7 +309,7 @@ function openInlineEdit(dt, target) {
193
309
  // in this case we will need to process the values before re-open the edit panel with the new values or we may lose the edition.
194
310
  processInlineEditFinish(
195
311
  dt,
196
- 'loosed-focus',
312
+ 'lost-focus',
197
313
  inlineEdit.rowKeyValue,
198
314
  inlineEdit.colKeyValue
199
315
  );
@@ -201,6 +317,11 @@ function openInlineEdit(dt, target) {
201
317
 
202
318
  const { rowKeyValue, colKeyValue } = target;
203
319
 
320
+ // ensure that focus remains on inline edit panel instead of active cell
321
+ if (state.activeCell) {
322
+ state.activeCell.focused = false;
323
+ }
324
+
204
325
  inlineEdit.isPanelVisible = true;
205
326
  inlineEdit.rowKeyValue = rowKeyValue;
206
327
  inlineEdit.colKeyValue = colKeyValue;
@@ -217,7 +338,7 @@ function openInlineEdit(dt, target) {
217
338
  const typeAttributesFromColumnDef =
218
339
  inlineEdit.columnDef && inlineEdit.columnDef.typeAttributes;
219
340
  if (typeAttributesFromColumnDef) {
220
- // when open the inline edit panel resolve the typeAttributes if it's available
341
+ // when the inline edit panel is opened resolve the typeAttributes if available
221
342
  // then assign the resolved values to inlineEdit.resolvedTypeAttributes
222
343
  inlineEdit.resolvedTypeAttributes = resolveNestedTypeAttributes(
223
344
  state,
@@ -229,7 +350,7 @@ function openInlineEdit(dt, target) {
229
350
  );
230
351
  }
231
352
 
232
- markSelectedCell(state, rowKeyValue, colKeyValue);
353
+ setAriaSelectedOnCell(state, rowKeyValue, colKeyValue);
233
354
 
234
355
  // eslint-disable-next-line @lwc/lwc/no-async-operation
235
356
  setTimeout(() => {
@@ -248,9 +369,243 @@ function openInlineEdit(dt, target) {
248
369
  // if panel can be edited, focus
249
370
  panel.focus();
250
371
  }
251
- }, 0);
372
+ }, 0);
373
+ }
374
+
375
+ /**
376
+ * Attempts to open the inline edit panel for the datatable's currently active cell. If the active cell is not
377
+ * editable, then the panel is instead opened for the first editable cell in the table. Used to open inline edit
378
+ * in a direct, programmatic fashion.
379
+ *
380
+ * If there is no data in the table or there are no editable cells in the table then calling this function
381
+ * results in a no-op.
382
+ *
383
+ * @param {Object} dt - The datatable instance. Must be a truthy and valid datatable reference.
384
+ */
385
+ export function openInlineEditOnActiveCell(dt) {
386
+ const hasData = dt.state.data && dt.state.data.length > 0;
387
+ if (hasData) {
388
+ if (!isActiveCellEditable(dt.state)) {
389
+ const firstEditableCell = getFirstEditableCell(dt);
390
+ if (firstEditableCell) {
391
+ updateActiveCell(
392
+ dt.state,
393
+ firstEditableCell.rowKeyValue,
394
+ firstEditableCell.colKeyValue
395
+ );
396
+ setFocusAndOpenInlineEdit(dt, dt.state.activeCell);
397
+ }
398
+ } else {
399
+ setFocusAndOpenInlineEdit(dt, dt.state.activeCell);
400
+ }
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Async function to await setting focus on an editable cell before opening inline-edit panel
406
+ *
407
+ * @param {Object} dt - The datatable instance
408
+ */
409
+ // eslint-disable-next-line @lwc/lwc/no-async-await
410
+ async function setFocusAndOpenInlineEdit(dt) {
411
+ await setFocusActiveCell(dt.template, dt.state, 0);
412
+ const cell = getActiveCellElement(dt.template, dt.state);
413
+ openInlineEdit(dt, cell);
414
+ }
415
+
416
+ /************************** PANEL POSITIONING **************************/
417
+
418
+ /**
419
+ * Begin positioning the inline edit panel based on the following constraints:
420
+ * Align to the 'top-left' edge of the inline edit panel to the `top-left` edge of the cell
421
+ *
422
+ * `align` refers to the alignment of the inline edit panel
423
+ * - horizontal - Left -> align left edge of panel
424
+ * - vertical - Top -> align top of panel
425
+ *
426
+ * `targetAlign` refers to the cell against which the panel should be aligned
427
+ * - horizontal - Left -> align panel to left edge of cell
428
+ * - vertical - Top -> align panel to top of the cell
429
+ *
430
+ * @param {Object} dt - datatable instance
431
+ * @param {HTMLElement} target - cell on which inline edit should open
432
+ */
433
+ function startPanelPositioning(dt, target) {
434
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
435
+ requestAnimationFrame(() => {
436
+ // we need to discard previous binding otherwise the panel
437
+ // will retain previous alignment
438
+ stopPanelPositioning(dt);
439
+
440
+ dt.privatePositionRelationship = startPositioning(dt, {
441
+ target,
442
+ element: () =>
443
+ dt.template
444
+ .querySelector(IEDIT_PANEL_SELECTOR)
445
+ .getPositionedElement(),
446
+ align: {
447
+ horizontal: Direction.Left,
448
+ vertical: Direction.Top,
449
+ },
450
+ targetAlign: {
451
+ horizontal: Direction.Left,
452
+ vertical: Direction.Top,
453
+ },
454
+ autoFlip: true,
455
+ });
456
+ });
457
+ }
458
+
459
+ function stopPanelPositioning(dt) {
460
+ if (dt.privatePositionRelationship) {
461
+ stopPositioning(dt.privatePositionRelationship);
462
+ dt.privatePositionRelationship = null;
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Repositions the inline edit panel. this does not realign the element,
468
+ * so it doesn't fix alignment when size of panel changes
469
+ *
470
+ * @param {Object} dt - datatable instance
471
+ */
472
+ function repositionPanel(dt) {
473
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
474
+ requestAnimationFrame(() => {
475
+ if (dt.privatePositionRelationship) {
476
+ dt.privatePositionRelationship.reposition();
477
+ }
478
+ });
479
+ }
480
+
481
+ /************************** DIRTY/UNSAVED VALUES **************************/
482
+
483
+ /**
484
+ * @param {Object} state - Datatable state object.
485
+ * @returns {Array} - An array of objects, each object describing the dirty values in the form { colName : dirtyValue }.
486
+ * A special key is the { [keyField]: value } pair used to identify the row containing this changed values.
487
+ * The returned array will be in the form - [{colName : dirtyValue, ... , [keyField]: value }, {...}, {...}]
488
+ */
489
+ export function getDirtyValues(state) {
490
+ return getResolvedCellChanges(state, state.inlineEdit.dirtyValues);
491
+ }
492
+
493
+ /**
494
+ * Sets the dirty values in the datatable.
495
+ *
496
+ * @param {Object} state Datatable state object.
497
+ * @param {Array} value An array of objects, each object describing the dirty values in the form { colName : dirtyValue }.
498
+ * A special key is the { [keyField]: value } pair used to identify the row containing this changed values.
499
+ */
500
+ export function setDirtyValues(state, value) {
501
+ const keyField = getKeyField(state);
502
+ const dirtyValues = Array.isArray(value) ? value : [];
503
+
504
+ state.inlineEdit.dirtyValues = dirtyValues.reduce((result, rowValues) => {
505
+ const changes = getCellChangesFromCustomer(state, rowValues);
506
+ delete changes[keyField];
507
+
508
+ result[rowValues[keyField]] = changes;
509
+
510
+ return result;
511
+ }, {});
512
+ }
513
+
514
+ /**
515
+ * Updates the dirty values specified in rowColKeyValues
516
+ *
517
+ * @param {Object} state - state of the datatable
518
+ * @param {Object} rowColKeyValues - An object in the form of { rowKeyValue: { colKeyValue1: value, ..., colKeyValueN: value } ... }
519
+ */
520
+ function updateDirtyValues(state, rowColKeyValues) {
521
+ const dirtyValues = state.inlineEdit.dirtyValues;
522
+
523
+ Object.keys(rowColKeyValues).forEach((rowKey) => {
524
+ if (!Object.prototype.hasOwnProperty.call(dirtyValues, rowKey)) {
525
+ dirtyValues[rowKey] = {};
526
+ }
527
+
528
+ Object.assign(dirtyValues[rowKey], rowColKeyValues[rowKey]);
529
+ });
530
+ }
531
+
532
+ /**
533
+ * Constructs and returns an object that contains the cell changes which can
534
+ * be referenced by the column key value. It follows this format:
535
+ * { <colKeyValue: "<editedValue>"> }; Ex. { "name-text-2": "My changes" }
536
+ *
537
+ * @param {Object} state - datatable's state object
538
+ * @param {Object} changes - internal representation of changes in a row
539
+ * @returns {Object} - changes in a column that can be referenced by the column key
540
+ */
541
+ function getCellChangesFromCustomer(state, changes) {
542
+ return Object.keys(changes).reduce((result, externalColumnKey) => {
543
+ const columns = getColumns(state);
544
+ const columnIndex = getColumnIndexByColumnKey(state, externalColumnKey);
545
+
546
+ if (columnIndex >= 0) {
547
+ const colKey = columns[columnIndex].colKeyValue;
548
+ result[colKey] = changes[externalColumnKey];
549
+ }
550
+
551
+ return result;
552
+ }, {});
553
+ }
554
+
555
+ /**
556
+ * Retrieves the changes in cells in a particular column
557
+ * Returns an object where each item follows this format:
558
+ * { <columnName>: "<changes>"} -> Ex. { name: "My changes" }
559
+ *
560
+ * @param {Object} state - Datatable state
561
+ * @param {Object} changes - The internal representation of changes in a row
562
+ * @returns {Object} - the list of customer changes in a column
563
+ */
564
+ function getCellChangesByColumn(state, changes) {
565
+ return Object.keys(changes).reduce((result, colKey) => {
566
+ const columns = getColumns(state);
567
+ const columnIndex = getStateColumnIndex(state, colKey);
568
+ const columnDef = columns[columnIndex];
569
+
570
+ result[columnDef.columnKey || columnDef.fieldName] = changes[colKey];
571
+
572
+ return result;
573
+ }, {});
574
+ }
575
+
576
+ /**
577
+ * Constructs an array of resolved cell changes made via inline edit
578
+ * Each array item consists of an identifier of the row and column in order to locate
579
+ * the cell in which the changes were made
580
+ *
581
+ * It follows this format: [{ <columnName>: "<changes>", <keyField>: "<keyFieldIdentifier>" }]
582
+ * Ex. [{ name: "My changes", id: "2" }]; where column name is 'name' and 'id' is the keyField
583
+ * The keyField can be used to identify the row.
584
+ *
585
+ * @param {Object} state - datatable state object
586
+ * @param {Object} changes - list of cell changes to be resolved
587
+ * @returns {Array} - array containing changes and identifiers of column and row where the changes
588
+ * should be applied
589
+ */
590
+ function getResolvedCellChanges(state, changes) {
591
+ const keyField = getKeyField(state);
592
+
593
+ return Object.keys(changes).reduce((result, rowKey) => {
594
+ // Get the changes made by column
595
+ const cellChanges = getCellChangesByColumn(state, changes[rowKey]);
596
+
597
+ if (Object.keys(cellChanges).length > 0) {
598
+ // Add identifier for which row has change
599
+ cellChanges[keyField] = rowKey;
600
+ result.push(cellChanges);
601
+ }
602
+
603
+ return result;
604
+ }, []);
252
605
  }
253
606
 
607
+ /************************** TYPE ATTRIBUTES RESOLUTION **************************/
608
+
254
609
  /**
255
610
  * Returns the resolved typeAttributes
256
611
  *
@@ -261,7 +616,7 @@ function openInlineEdit(dt, target) {
261
616
  * @param {object} typeAttributesFromColumnDef - values of typeAttributes from column definition
262
617
  * @param {number} stateColIndex - state column index
263
618
  *
264
- * @return {Object} the resolved typeAttributes.
619
+ * @returns {Object} the resolved typeAttributes.
265
620
  */
266
621
  export function resolveNestedTypeAttributes(
267
622
  state,
@@ -307,7 +662,9 @@ export function resolveNestedTypeAttributes(
307
662
  * fieldName: 'name'
308
663
  * }
309
664
  * }
310
- * } to be {
665
+ * }
666
+ * to be ...
667
+ * {
311
668
  * editTypeAttributes: {
312
669
  * value: 'resolvedValue'
313
670
  * }
@@ -361,84 +718,36 @@ function resolveNestedTypeAttributesHelper(rowData, typeAttributesValue) {
361
718
  return resolvedTypeAttributes;
362
719
  }
363
720
 
364
- export function handleInlineEditFinish(event) {
365
- stopPanelPositioning(this);
366
-
367
- const { reason, rowKeyValue, colKeyValue } = event.detail;
368
-
369
- processInlineEditFinish(this, reason, rowKeyValue, colKeyValue);
370
- }
371
-
372
- export function handleMassCheckboxChange(event) {
373
- const state = this.state;
374
- if (event.detail.checked) {
375
- markAllSelectedRowsAsSelectedCell(state);
376
- } else {
377
- markAllSelectedRowsAsDeselectedCell(this.state);
378
- markSelectedCell(
379
- state,
380
- state.inlineEdit.rowKeyValue,
381
- state.inlineEdit.colKeyValue
382
- );
383
- }
384
- }
385
-
386
- // hide panel on scroll
387
- const HIDE_PANEL_THRESHOLD = 5;
388
- export function handleInlineEditPanelScroll(event) {
389
- const { isPanelVisible, rowKeyValue, colKeyValue } = this.state.inlineEdit;
390
-
391
- if (!isPanelVisible) {
392
- return;
393
- }
394
-
395
- let delta = 0;
396
-
397
- const container = event.target;
398
- if (container.classList.contains('slds-scrollable_x')) {
399
- const scrollX = container.scrollLeft;
400
- if (this.privateLastScrollX == null) {
401
- this.privateLastScrollX = scrollX;
402
- } else {
403
- delta = Math.abs(this.privateLastScrollX - scrollX);
404
- }
405
- } else {
406
- const scrollY = container.scrollTop;
407
- if (this.privateLastScrollY == null) {
408
- this.privateLastScrollY = scrollY;
409
- } else {
410
- delta = Math.abs(this.privateLastScrollY - scrollY);
411
- }
412
- }
413
-
414
- if (delta > HIDE_PANEL_THRESHOLD) {
415
- this.privateLastScrollX = null;
416
- this.privateLastScrollY = null;
417
- stopPanelPositioning(this);
418
- processInlineEditFinish(this, 'loosed-focus', rowKeyValue, colKeyValue);
419
- } else {
420
- // we want to keep the panel attached to the cell before
421
- // reaching the threshold and hiding the panel
422
- repositionPanel(this);
423
- }
424
- }
721
+ /************************** HELPER FUNCTIONS **************************/
425
722
 
426
723
  /**
427
- * Will update the dirty values specified in rowColKeyValues
724
+ * Returns the row and column keys of the first editable cell in the table.
725
+ * If no editable cells exist in the table then undefined is returned.
428
726
  *
429
- * @param {Object} state - state of the datatable
430
- * @param {Object} rowColKeyValues - An object in the form of { rowKeyValue: { colKeyValue1: value, ..., colKeyValueN: value } ... }
727
+ * @param {Object} dt - The datatable instance. Must be a truthy and valid datatable reference.
431
728
  */
432
- function updateDirtyValues(state, rowColKeyValues) {
433
- const dirtyValues = state.inlineEdit.dirtyValues;
729
+ function getFirstEditableCell(dt) {
730
+ const columns = getColumns(dt.state);
731
+ const editableColumns = getEditableColumns(columns);
434
732
 
435
- Object.keys(rowColKeyValues).forEach((rowKey) => {
436
- if (!Object.prototype.hasOwnProperty.call(dirtyValues, rowKey)) {
437
- dirtyValues[rowKey] = {};
733
+ if (editableColumns.length > 0) {
734
+ const rows = dt.state.rows;
735
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
736
+ for (let i = 0; i < editableColumns.length; i++) {
737
+ // Loop through the editable columns in order and examine the corresponding cells
738
+ // in the current row for editability, returning the first such cell that is editable
739
+ const editableColumn = editableColumns[i];
740
+ if (isCellEditable(rows[rowIndex], editableColumn)) {
741
+ return {
742
+ rowKeyValue: rows[rowIndex].key,
743
+ colKeyValue: editableColumn.colKeyValue,
744
+ };
745
+ }
746
+ }
438
747
  }
748
+ }
439
749
 
440
- Object.assign(dirtyValues[rowKey], rowColKeyValues[rowKey]);
441
- });
750
+ return undefined;
442
751
  }
443
752
 
444
753
  /**
@@ -448,7 +757,7 @@ function updateDirtyValues(state, rowColKeyValues) {
448
757
  * @param {String} rowKeyValue - row key
449
758
  * @param {String} colKeyValue - column key
450
759
  *
451
- * @return {Object} the value for the current cell.
760
+ * @returns {Object} the value for the current cell.
452
761
  */
453
762
  function getCellValue(state, rowKeyValue, colKeyValue) {
454
763
  const row = getRowByKey(state, rowKeyValue);
@@ -458,214 +767,31 @@ function getCellValue(state, rowKeyValue, colKeyValue) {
458
767
  }
459
768
 
460
769
  /**
770
+ * Sets `aria-selected` to true on cells whose rows are selected
771
+ * and are in the same column as the cell being currently edited
461
772
  *
462
- * @param {Object} state - Datatable state
463
- * @param {Object} changes - The internal representation of changes in a row
464
- * @returns {Object} - the list of customer changes in a row
465
- */
466
- function getColumnsChangesForCustomer(state, changes) {
467
- return Object.keys(changes).reduce((result, colKey) => {
468
- const columns = getColumns(state);
469
- const columnIndex = getStateColumnIndex(state, colKey);
470
- const columnDef = columns[columnIndex];
471
-
472
- result[columnDef.columnKey || columnDef.fieldName] = changes[colKey];
473
-
474
- return result;
475
- }, {});
476
- }
477
-
478
- function getRowChangesFromCustomer(state, changes) {
479
- return Object.keys(changes).reduce((result, externalColumnKey) => {
480
- const columns = getColumns(state);
481
- const columnIndex = getColumnIndexByColumnKey(state, externalColumnKey);
482
-
483
- if (columnIndex >= 0) {
484
- const colKey = columns[columnIndex].colKeyValue;
485
- result[colKey] = changes[externalColumnKey];
486
- }
487
-
488
- return result;
489
- }, {});
490
- }
491
-
492
- function getChangesForCustomer(state, changes) {
493
- const keyField = getKeyField(state);
494
-
495
- return Object.keys(changes).reduce((result, rowKey) => {
496
- const rowChanges = getColumnsChangesForCustomer(state, changes[rowKey]);
497
-
498
- if (Object.keys(rowChanges).length > 0) {
499
- rowChanges[keyField] = rowKey;
500
-
501
- result.push(rowChanges);
502
- }
503
-
504
- return result;
505
- }, []);
506
- }
507
-
508
- function dispatchCellChangeEvent(dtInstance, cellChange) {
509
- dtInstance.dispatchEvent(
510
- new CustomEvent('cellchange', {
511
- detail: {
512
- draftValues: getChangesForCustomer(
513
- dtInstance.state,
514
- cellChange
515
- ),
516
- },
517
- })
518
- );
519
- }
520
-
521
- export function closeInlineEdit(dt) {
522
- const inlineEditState = dt.state.inlineEdit;
523
-
524
- if (inlineEditState.isPanelVisible) {
525
- processInlineEditFinish(
526
- dt,
527
- 'loosed-focus',
528
- inlineEditState.rowKeyValue,
529
- inlineEditState.colKeyValue
530
- );
531
- }
532
- }
533
-
534
- function isValidCell(state, rowKeyValue, colKeyValue) {
535
- const row = getRowByKey(state, rowKeyValue);
536
- const colIndex = getStateColumnIndex(state, colKeyValue);
537
-
538
- return row && row.cells[colIndex];
539
- }
540
-
541
- /**
542
- * It will process when the datatable had finished an edition.
543
- *
544
- * @param {Object} dt - the datatable instance
545
- * @param {string} reason - the reason to finish the edition. valid reasons are: edit-canceled | loosed-focus | tab-pressed | submit-action
546
- * @param {string} rowKeyValue - the row key of the edited cell
547
- * @param {string} colKeyValue - the column key of the edited cell
773
+ * @param {Object} state - datatable's state object
548
774
  */
549
- function processInlineEditFinish(dt, reason, rowKeyValue, colKeyValue) {
550
- const state = dt.state;
551
- const inlineEditState = state.inlineEdit;
552
-
553
- const shouldSaveData =
554
- reason !== 'edit-canceled' &&
555
- !(inlineEditState.massEditEnabled && reason === 'loosed-focus') &&
556
- isValidCell(dt.state, rowKeyValue, colKeyValue);
557
-
558
- if (shouldSaveData) {
559
- const panel = dt.template.querySelector(PANEL_SEL);
560
- const editValue = panel.value;
561
- const isValidEditValue = panel.validity.valid;
562
- const updateAllSelectedRows = panel.isMassEditChecked;
563
- const currentValue = getCellValue(state, rowKeyValue, colKeyValue);
564
-
565
- if (
566
- isValidEditValue &&
567
- (editValue !== currentValue || updateAllSelectedRows)
568
- ) {
569
- const cellChange = {};
570
- cellChange[rowKeyValue] = {};
571
- cellChange[rowKeyValue][colKeyValue] = editValue;
572
-
573
- if (updateAllSelectedRows) {
574
- const selectedRowKeys = getSelectedRowsKeys(state);
575
- selectedRowKeys.forEach((rowKey) => {
576
- cellChange[rowKey] = {};
577
- cellChange[rowKey][colKeyValue] = editValue;
578
- });
579
- }
580
-
581
- updateDirtyValues(state, cellChange);
582
-
583
- dispatchCellChangeEvent(dt, cellChange);
584
-
585
- // @todo: do we need to update all rows in the dt or just the one that was modified?
586
- updateRowsAndCellIndexes.call(dt);
587
- }
588
- }
589
-
590
- if (reason !== 'loosed-focus') {
591
- switch (reason) {
592
- case 'tab-pressed-next': {
593
- reactToTabForward(dt.template, state);
594
- break;
595
- }
596
- case 'tab-pressed-prev': {
597
- reactToTabBackward(dt.template, state);
598
- break;
599
- }
600
- default: {
601
- setFocusActiveCell(dt.template, state, 0);
602
- }
603
- }
604
- }
605
-
606
- markAllSelectedRowsAsDeselectedCell(state);
607
- markDeselectedCell(state, rowKeyValue, colKeyValue);
608
-
609
- inlineEditState.isPanelVisible = false;
610
- }
611
-
612
- function startPanelPositioning(dt, target) {
613
- // eslint-disable-next-line @lwc/lwc/no-async-operation
614
- requestAnimationFrame(() => {
615
- // we need to discard previous binding otherwise the panel
616
- // will retain previous alignment
617
- stopPanelPositioning(dt);
618
-
619
- dt.privatePositionRelationship = startPositioning(dt, {
620
- target,
621
- element: () =>
622
- dt.template.querySelector(PANEL_SEL).getPositionedElement(),
623
- align: {
624
- horizontal: Direction.Left,
625
- vertical: Direction.Top,
626
- },
627
- targetAlign: {
628
- horizontal: Direction.Left,
629
- vertical: Direction.Top,
630
- },
631
- autoFlip: true,
632
- });
633
- });
634
- }
635
-
636
- function stopPanelPositioning(dt) {
637
- if (dt.privatePositionRelationship) {
638
- stopPositioning(dt.privatePositionRelationship);
639
- dt.privatePositionRelationship = null;
640
- }
641
- }
642
-
643
- // reposition inline edit panel
644
- // this does not realign the element, so it doesn't fix alignment
645
- // when size of panel changes
646
- function repositionPanel(dt) {
647
- // eslint-disable-next-line @lwc/lwc/no-async-operation
648
- requestAnimationFrame(() => {
649
- if (dt.privatePositionRelationship) {
650
- dt.privatePositionRelationship.reposition();
651
- }
652
- });
653
- }
654
-
655
- function markAllSelectedRowsAsSelectedCell(state) {
775
+ function setAriaSelectedOnAllSelectedRows(state) {
656
776
  const { colKeyValue } = state.inlineEdit;
657
777
  const selectedRowKeys = getSelectedRowsKeys(state);
658
778
 
659
779
  selectedRowKeys.forEach((rowKeyValue) => {
660
- markSelectedCell(state, rowKeyValue, colKeyValue);
780
+ setAriaSelectedOnCell(state, rowKeyValue, colKeyValue);
661
781
  });
662
782
  }
663
783
 
664
- function markAllSelectedRowsAsDeselectedCell(state) {
784
+ /**
785
+ * Sets `aria-selected` to false on cells whose rows are selected
786
+ * and are in the same column as the cell being currently edited
787
+ *
788
+ * @param {Object} state - datatable's state object
789
+ */
790
+ function unsetAriaSelectedOnAllSelectedRows(state) {
665
791
  const { colKeyValue } = state.inlineEdit;
666
792
  const selectedRowKeys = getSelectedRowsKeys(state);
667
793
 
668
794
  selectedRowKeys.forEach((rowKeyValue) => {
669
- markDeselectedCell(state, rowKeyValue, colKeyValue);
795
+ unsetAriaSelectedOnCell(state, rowKeyValue, colKeyValue);
670
796
  });
671
797
  }