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