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