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