@vaadin/grid 22.0.0-alpha7
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/LICENSE +190 -0
- package/README.md +79 -0
- package/all-imports.js +1 -0
- package/package.json +58 -0
- package/src/all-imports.js +14 -0
- package/src/array-data-provider.js +145 -0
- package/src/interfaces.d.ts +75 -0
- package/src/vaadin-grid-a11y-mixin.js +158 -0
- package/src/vaadin-grid-active-item-mixin.d.ts +19 -0
- package/src/vaadin-grid-active-item-mixin.js +117 -0
- package/src/vaadin-grid-array-data-provider-mixin.d.ts +16 -0
- package/src/vaadin-grid-array-data-provider-mixin.js +75 -0
- package/src/vaadin-grid-column-group.d.ts +54 -0
- package/src/vaadin-grid-column-group.js +320 -0
- package/src/vaadin-grid-column-reordering-mixin.d.ts +19 -0
- package/src/vaadin-grid-column-reordering-mixin.js +387 -0
- package/src/vaadin-grid-column-resizing-mixin.js +111 -0
- package/src/vaadin-grid-column.d.ts +133 -0
- package/src/vaadin-grid-column.js +745 -0
- package/src/vaadin-grid-data-provider-mixin.d.ts +108 -0
- package/src/vaadin-grid-data-provider-mixin.js +520 -0
- package/src/vaadin-grid-drag-and-drop-mixin.d.ts +69 -0
- package/src/vaadin-grid-drag-and-drop-mixin.js +433 -0
- package/src/vaadin-grid-dynamic-columns-mixin.js +180 -0
- package/src/vaadin-grid-event-context-mixin.d.ts +33 -0
- package/src/vaadin-grid-event-context-mixin.js +57 -0
- package/src/vaadin-grid-filter-column.d.ts +35 -0
- package/src/vaadin-grid-filter-column.js +120 -0
- package/src/vaadin-grid-filter-mixin.js +76 -0
- package/src/vaadin-grid-filter.d.ts +67 -0
- package/src/vaadin-grid-filter.js +125 -0
- package/src/vaadin-grid-helpers.js +23 -0
- package/src/vaadin-grid-keyboard-navigation-mixin.js +891 -0
- package/src/vaadin-grid-row-details-mixin.d.ts +44 -0
- package/src/vaadin-grid-row-details-mixin.js +200 -0
- package/src/vaadin-grid-scroll-mixin.d.ts +18 -0
- package/src/vaadin-grid-scroll-mixin.js +202 -0
- package/src/vaadin-grid-selection-column.d.ts +71 -0
- package/src/vaadin-grid-selection-column.js +285 -0
- package/src/vaadin-grid-selection-mixin.d.ts +30 -0
- package/src/vaadin-grid-selection-mixin.js +93 -0
- package/src/vaadin-grid-sort-column.d.ts +63 -0
- package/src/vaadin-grid-sort-column.js +118 -0
- package/src/vaadin-grid-sort-mixin.d.ts +15 -0
- package/src/vaadin-grid-sort-mixin.js +139 -0
- package/src/vaadin-grid-sorter.d.ts +94 -0
- package/src/vaadin-grid-sorter.js +230 -0
- package/src/vaadin-grid-styles.js +297 -0
- package/src/vaadin-grid-styling-mixin.d.ts +37 -0
- package/src/vaadin-grid-styling-mixin.js +71 -0
- package/src/vaadin-grid-tree-column.d.ts +36 -0
- package/src/vaadin-grid-tree-column.js +119 -0
- package/src/vaadin-grid-tree-toggle.d.ts +104 -0
- package/src/vaadin-grid-tree-toggle.js +205 -0
- package/src/vaadin-grid.d.ts +397 -0
- package/src/vaadin-grid.js +1004 -0
- package/theme/lumo/all-imports.js +11 -0
- package/theme/lumo/vaadin-grid-column-group.js +1 -0
- package/theme/lumo/vaadin-grid-column.js +1 -0
- package/theme/lumo/vaadin-grid-filter-column.js +2 -0
- package/theme/lumo/vaadin-grid-filter.js +2 -0
- package/theme/lumo/vaadin-grid-selection-column.js +2 -0
- package/theme/lumo/vaadin-grid-sort-column.js +2 -0
- package/theme/lumo/vaadin-grid-sorter-styles.js +53 -0
- package/theme/lumo/vaadin-grid-sorter.js +2 -0
- package/theme/lumo/vaadin-grid-styles.js +378 -0
- package/theme/lumo/vaadin-grid-tree-column.js +2 -0
- package/theme/lumo/vaadin-grid-tree-toggle-styles.js +112 -0
- package/theme/lumo/vaadin-grid-tree-toggle.js +2 -0
- package/theme/lumo/vaadin-grid.js +9 -0
- package/theme/material/all-imports.js +11 -0
- package/theme/material/vaadin-grid-column-group.js +1 -0
- package/theme/material/vaadin-grid-column.js +1 -0
- package/theme/material/vaadin-grid-filter-column.js +2 -0
- package/theme/material/vaadin-grid-filter.js +2 -0
- package/theme/material/vaadin-grid-selection-column.js +2 -0
- package/theme/material/vaadin-grid-sort-column.js +2 -0
- package/theme/material/vaadin-grid-sorter-styles.js +72 -0
- package/theme/material/vaadin-grid-sorter.js +2 -0
- package/theme/material/vaadin-grid-styles.js +252 -0
- package/theme/material/vaadin-grid-tree-column.js +2 -0
- package/theme/material/vaadin-grid-tree-toggle-styles.js +42 -0
- package/theme/material/vaadin-grid-tree-toggle.js +2 -0
- package/theme/material/vaadin-grid.js +2 -0
- package/vaadin-grid-column-group.d.ts +1 -0
- package/vaadin-grid-column-group.js +3 -0
- package/vaadin-grid-column.d.ts +1 -0
- package/vaadin-grid-column.js +3 -0
- package/vaadin-grid-filter-column.d.ts +1 -0
- package/vaadin-grid-filter-column.js +3 -0
- package/vaadin-grid-filter.d.ts +1 -0
- package/vaadin-grid-filter.js +3 -0
- package/vaadin-grid-selection-column.d.ts +1 -0
- package/vaadin-grid-selection-column.js +3 -0
- package/vaadin-grid-sort-column.d.ts +1 -0
- package/vaadin-grid-sort-column.js +3 -0
- package/vaadin-grid-sorter.d.ts +1 -0
- package/vaadin-grid-sorter.js +3 -0
- package/vaadin-grid-tree-column.d.ts +1 -0
- package/vaadin-grid-tree-column.js +3 -0
- package/vaadin-grid-tree-toggle.d.ts +1 -0
- package/vaadin-grid-tree-toggle.js +3 -0
- package/vaadin-grid.d.ts +3 -0
- package/vaadin-grid.js +4 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @polymerMixin
|
|
9
|
+
*/
|
|
10
|
+
export const KeyboardNavigationMixin = (superClass) =>
|
|
11
|
+
class KeyboardNavigationMixin extends superClass {
|
|
12
|
+
static get properties() {
|
|
13
|
+
return {
|
|
14
|
+
/** @private */
|
|
15
|
+
_headerFocusable: {
|
|
16
|
+
type: Object,
|
|
17
|
+
observer: '_focusableChanged'
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @type {!HTMLElement | undefined}
|
|
22
|
+
* @protected
|
|
23
|
+
*/
|
|
24
|
+
_itemsFocusable: {
|
|
25
|
+
type: Object,
|
|
26
|
+
observer: '_focusableChanged'
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/** @private */
|
|
30
|
+
_footerFocusable: {
|
|
31
|
+
type: Object,
|
|
32
|
+
observer: '_focusableChanged'
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/** @private */
|
|
36
|
+
_navigatingIsHidden: Boolean,
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @type {number}
|
|
40
|
+
* @protected
|
|
41
|
+
*/
|
|
42
|
+
_focusedItemIndex: {
|
|
43
|
+
type: Number,
|
|
44
|
+
value: 0
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/** @private */
|
|
48
|
+
_focusedColumnOrder: Number,
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Indicates whether the grid is currently in interaction mode.
|
|
52
|
+
* In interaction mode the user is currently interacting with a control,
|
|
53
|
+
* such as an input or a select, within a cell.
|
|
54
|
+
* In interaction mode keyboard navigation between cells is disabled.
|
|
55
|
+
* Interaction mode also prevents the focus target cell of that section of
|
|
56
|
+
* the grid from receiving focus, allowing the user to switch focus to
|
|
57
|
+
* controls in adjacent cells, rather than focussing the outer cell
|
|
58
|
+
* itself.
|
|
59
|
+
* @type {boolean}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
interacting: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
value: false,
|
|
65
|
+
reflectToAttribute: true,
|
|
66
|
+
readOnly: true,
|
|
67
|
+
observer: '_interactingChanged'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @protected */
|
|
73
|
+
ready() {
|
|
74
|
+
super.ready();
|
|
75
|
+
|
|
76
|
+
if (this._ios || this._android) {
|
|
77
|
+
// Disable keyboard navigation on mobile devices
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.addEventListener('keydown', this._onKeyDown);
|
|
82
|
+
this.addEventListener('keyup', this._onKeyUp);
|
|
83
|
+
|
|
84
|
+
this.addEventListener('focusin', this._onFocusIn);
|
|
85
|
+
this.addEventListener('focusout', this._onFocusOut);
|
|
86
|
+
|
|
87
|
+
// When focus goes from cell to another cell, focusin/focusout events do
|
|
88
|
+
// not escape the grid’s shadowRoot, thus listening inside the shadowRoot.
|
|
89
|
+
this.$.table.addEventListener('focusin', this._onContentFocusIn.bind(this));
|
|
90
|
+
|
|
91
|
+
this.addEventListener('mousedown', () => {
|
|
92
|
+
this.toggleAttribute('navigating', false);
|
|
93
|
+
this._isMousedown = true;
|
|
94
|
+
});
|
|
95
|
+
this.addEventListener('mouseup', () => (this._isMousedown = false));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @private */
|
|
99
|
+
get __rowFocusMode() {
|
|
100
|
+
return this.__isRow(this._itemsFocusable);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
set __rowFocusMode(value) {
|
|
104
|
+
['_itemsFocusable', '_footerFocusable', '_headerFocusable'].forEach((focusable) => {
|
|
105
|
+
if (value && this.__isCell(this[focusable])) {
|
|
106
|
+
this[focusable] = this[focusable].parentElement;
|
|
107
|
+
} else if (!value && this.__isRow(this[focusable])) {
|
|
108
|
+
this[focusable] = this[focusable].firstElementChild;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** @private */
|
|
114
|
+
_focusableChanged(focusable, oldFocusable) {
|
|
115
|
+
if (oldFocusable) {
|
|
116
|
+
oldFocusable.setAttribute('tabindex', '-1');
|
|
117
|
+
}
|
|
118
|
+
if (focusable) {
|
|
119
|
+
this._updateGridSectionFocusTarget(focusable);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @private */
|
|
124
|
+
_interactingChanged() {
|
|
125
|
+
// Update focus targets when entering / exiting interaction mode
|
|
126
|
+
this._updateGridSectionFocusTarget(this._headerFocusable);
|
|
127
|
+
this._updateGridSectionFocusTarget(this._itemsFocusable);
|
|
128
|
+
this._updateGridSectionFocusTarget(this._footerFocusable);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {!KeyboardEvent} e
|
|
133
|
+
* @protected
|
|
134
|
+
*/
|
|
135
|
+
_onKeyDown(e) {
|
|
136
|
+
const key = e.key;
|
|
137
|
+
|
|
138
|
+
let keyGroup;
|
|
139
|
+
switch (key) {
|
|
140
|
+
case 'ArrowUp':
|
|
141
|
+
case 'ArrowDown':
|
|
142
|
+
case 'ArrowLeft':
|
|
143
|
+
case 'ArrowRight':
|
|
144
|
+
case 'PageUp':
|
|
145
|
+
case 'PageDown':
|
|
146
|
+
case 'Home':
|
|
147
|
+
case 'End':
|
|
148
|
+
keyGroup = 'Navigation';
|
|
149
|
+
break;
|
|
150
|
+
case 'Enter':
|
|
151
|
+
case 'Escape':
|
|
152
|
+
case 'F2':
|
|
153
|
+
keyGroup = 'Interaction';
|
|
154
|
+
break;
|
|
155
|
+
case 'Tab':
|
|
156
|
+
keyGroup = 'Tab';
|
|
157
|
+
break;
|
|
158
|
+
case ' ':
|
|
159
|
+
keyGroup = 'Space';
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this._detectInteracting(e);
|
|
164
|
+
if (this.interacting && keyGroup !== 'Interaction') {
|
|
165
|
+
// When in the interacting mode, only the “Interaction” keys are handled.
|
|
166
|
+
keyGroup = undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (keyGroup) {
|
|
170
|
+
this[`_on${keyGroup}KeyDown`](e, key);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @private */
|
|
175
|
+
_ensureScrolledToIndex(index) {
|
|
176
|
+
const targetRowInDom = [...this.$.items.children].find((child) => child.index === index);
|
|
177
|
+
if (!targetRowInDom) {
|
|
178
|
+
this.scrollToIndex(index);
|
|
179
|
+
} else {
|
|
180
|
+
this.__scrollIntoViewport(index);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// TODO: A tree toggle component should not be the way to determine if the row is expandable
|
|
185
|
+
/** @private */
|
|
186
|
+
__isRowExpandable(row) {
|
|
187
|
+
const treeToggle = [...row.children].reduce(
|
|
188
|
+
(value, cell) => value || cell._content.querySelector('vaadin-grid-tree-toggle'),
|
|
189
|
+
null
|
|
190
|
+
);
|
|
191
|
+
return treeToggle && !treeToggle.expanded && !treeToggle.leaf;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @private */
|
|
195
|
+
__isRowCollapsible(row) {
|
|
196
|
+
return this._isExpanded(row._item);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @private */
|
|
200
|
+
__isDetailsCell(element) {
|
|
201
|
+
return element.matches('[part~="details-cell"]');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @private */
|
|
205
|
+
__isCell(element) {
|
|
206
|
+
return element instanceof HTMLTableCellElement;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @private */
|
|
210
|
+
__isRow(element) {
|
|
211
|
+
return element instanceof HTMLTableRowElement;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @private */
|
|
215
|
+
__getIndexOfChildElement(el) {
|
|
216
|
+
return Array.prototype.indexOf.call(el.parentNode.children, el);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @private */
|
|
220
|
+
_onNavigationKeyDown(e, key) {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
|
|
223
|
+
const visibleItemsCount = this._lastVisibleIndex - this._firstVisibleIndex - 1;
|
|
224
|
+
|
|
225
|
+
// Handle keyboard interaction as defined in:
|
|
226
|
+
// https://w3c.github.io/aria-practices/#keyboard-interaction-24
|
|
227
|
+
|
|
228
|
+
let dx = 0,
|
|
229
|
+
dy = 0;
|
|
230
|
+
switch (key) {
|
|
231
|
+
case 'ArrowRight':
|
|
232
|
+
dx = this.__isRTL ? -1 : 1;
|
|
233
|
+
break;
|
|
234
|
+
case 'ArrowLeft':
|
|
235
|
+
dx = this.__isRTL ? 1 : -1;
|
|
236
|
+
break;
|
|
237
|
+
case 'Home':
|
|
238
|
+
if (this.__rowFocusMode) {
|
|
239
|
+
// "If focus is on a row, moves focus to the first row. If focus is in the first row, focus does not move."
|
|
240
|
+
dy = -Infinity;
|
|
241
|
+
} else {
|
|
242
|
+
if (e.ctrlKey) {
|
|
243
|
+
// "If focus is on a cell, moves focus to the first cell in the column. If focus is in the first row, focus does not move."
|
|
244
|
+
dy = -Infinity;
|
|
245
|
+
} else {
|
|
246
|
+
// "If focus is on a cell, moves focus to the first cell in the row. If focus is in the first cell of the row, focus does not move."
|
|
247
|
+
dx = -Infinity;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
case 'End':
|
|
252
|
+
if (this.__rowFocusMode) {
|
|
253
|
+
// "If focus is on a row, moves focus to the last row. If focus is in the last row, focus does not move."
|
|
254
|
+
dy = Infinity;
|
|
255
|
+
} else {
|
|
256
|
+
if (e.ctrlKey) {
|
|
257
|
+
// "If focus is on a cell, moves focus to the last cell in the column. If focus is in the last row, focus does not move."
|
|
258
|
+
dy = Infinity;
|
|
259
|
+
} else {
|
|
260
|
+
// "If focus is on a cell, moves focus to the last cell in the row. If focus is in the last cell of the row, focus does not move."
|
|
261
|
+
dx = Infinity;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
case 'ArrowDown':
|
|
266
|
+
dy = 1;
|
|
267
|
+
break;
|
|
268
|
+
case 'ArrowUp':
|
|
269
|
+
dy = -1;
|
|
270
|
+
break;
|
|
271
|
+
case 'PageDown':
|
|
272
|
+
dy = visibleItemsCount;
|
|
273
|
+
break;
|
|
274
|
+
case 'PageUp':
|
|
275
|
+
dy = -visibleItemsCount;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const activeRow = e.composedPath().find((el) => this.__isRow(el));
|
|
280
|
+
const activeCell = e.composedPath().find((el) => this.__isCell(el));
|
|
281
|
+
|
|
282
|
+
if ((this.__rowFocusMode && !activeRow) || (!this.__rowFocusMode && !activeCell)) {
|
|
283
|
+
// When using a screen reader, it's possible that neither a cell nor a row is focused.
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const forwardsKey = this.__isRTL ? 'ArrowLeft' : 'ArrowRight';
|
|
288
|
+
const backwardsKey = this.__isRTL ? 'ArrowRight' : 'ArrowLeft';
|
|
289
|
+
if (key === forwardsKey) {
|
|
290
|
+
// "Right Arrow:"
|
|
291
|
+
if (this.__rowFocusMode) {
|
|
292
|
+
// In row focus mode
|
|
293
|
+
if (this.__isRowExpandable(activeRow)) {
|
|
294
|
+
// "If focus is on a collapsed row, expands the row."
|
|
295
|
+
this.expandItem(activeRow._item);
|
|
296
|
+
return;
|
|
297
|
+
} else {
|
|
298
|
+
// "If focus is on an expanded row or is on a row that does not have child rows,
|
|
299
|
+
// moves focus to the first cell in the row."
|
|
300
|
+
this.__rowFocusMode = false;
|
|
301
|
+
this._onCellNavigation(activeRow.firstElementChild, 0, 0);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} else if (key === backwardsKey) {
|
|
306
|
+
// "Left Arrow:"
|
|
307
|
+
if (this.__rowFocusMode) {
|
|
308
|
+
// In row focus mode
|
|
309
|
+
if (this.__isRowCollapsible(activeRow)) {
|
|
310
|
+
// "If focus is on an expanded row, collapses the row."
|
|
311
|
+
this.collapseItem(activeRow._item);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
// In cell focus mode
|
|
316
|
+
const activeRowCells = [...activeRow.children].sort((a, b) => a._order - b._order);
|
|
317
|
+
if (activeCell === activeRowCells[0] || this.__isDetailsCell(activeCell)) {
|
|
318
|
+
// "If focus is on the first cell in a row and row focus is supported, moves focus to the row."
|
|
319
|
+
this.__rowFocusMode = true;
|
|
320
|
+
this._onRowNavigation(activeRow, 0);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Navigate
|
|
327
|
+
if (this.__rowFocusMode) {
|
|
328
|
+
// Navigate the rows
|
|
329
|
+
this._onRowNavigation(activeRow, dy);
|
|
330
|
+
} else {
|
|
331
|
+
// Navigate the cells
|
|
332
|
+
this._onCellNavigation(activeCell, dx, dy);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Focuses the target row after navigating by the given dy offset.
|
|
338
|
+
* If the row is not in the viewport, it is first scrolled to.
|
|
339
|
+
* @private
|
|
340
|
+
**/
|
|
341
|
+
_onRowNavigation(activeRow, dy) {
|
|
342
|
+
const { dstRow } = this.__navigateRows(dy, activeRow);
|
|
343
|
+
|
|
344
|
+
if (dstRow) {
|
|
345
|
+
dstRow.focus();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @private */
|
|
350
|
+
__getIndexInGroup(row, bodyFallbackIndex) {
|
|
351
|
+
const rowGroup = row.parentNode;
|
|
352
|
+
// Body rows have index property, otherwise DOM child index of the row is used.
|
|
353
|
+
if (rowGroup === this.$.items) {
|
|
354
|
+
return bodyFallbackIndex !== undefined ? bodyFallbackIndex : row.index;
|
|
355
|
+
} else {
|
|
356
|
+
return this.__getIndexOfChildElement(row);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns the target row after navigating by the given dy offset.
|
|
362
|
+
* Also returns infromation whether the details cell should be the target on the target row.
|
|
363
|
+
* If the row is not in the viewport, it is first scrolled to.
|
|
364
|
+
* @private
|
|
365
|
+
**/
|
|
366
|
+
__navigateRows(dy, activeRow, activeCell) {
|
|
367
|
+
const currentRowIndex = this.__getIndexInGroup(activeRow, this._focusedItemIndex);
|
|
368
|
+
const activeRowGroup = activeRow.parentNode;
|
|
369
|
+
const maxRowIndex = (activeRowGroup === this.$.items ? this._effectiveSize : activeRowGroup.children.length) - 1;
|
|
370
|
+
|
|
371
|
+
// Index of the destination row
|
|
372
|
+
let dstRowIndex = Math.max(0, Math.min(currentRowIndex + dy, maxRowIndex));
|
|
373
|
+
|
|
374
|
+
if (activeRowGroup !== this.$.items) {
|
|
375
|
+
// Navigating header/footer rows
|
|
376
|
+
|
|
377
|
+
// Header and footer could have hidden rows, e. g., if none of the columns
|
|
378
|
+
// or groups on the given column tree level define template. Skip them
|
|
379
|
+
// in vertical keyboard navigation.
|
|
380
|
+
if (dstRowIndex > currentRowIndex) {
|
|
381
|
+
while (dstRowIndex < maxRowIndex && activeRowGroup.children[dstRowIndex].hidden) {
|
|
382
|
+
dstRowIndex++;
|
|
383
|
+
}
|
|
384
|
+
} else if (dstRowIndex < currentRowIndex) {
|
|
385
|
+
while (dstRowIndex > 0 && activeRowGroup.children[dstRowIndex].hidden) {
|
|
386
|
+
dstRowIndex--;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.toggleAttribute('navigating', true);
|
|
391
|
+
|
|
392
|
+
return { dstRow: activeRowGroup.children[dstRowIndex] };
|
|
393
|
+
} else {
|
|
394
|
+
// Navigating body rows
|
|
395
|
+
|
|
396
|
+
let dstIsRowDetails = false;
|
|
397
|
+
if (activeCell) {
|
|
398
|
+
const isRowDetails = this.__isDetailsCell(activeCell);
|
|
399
|
+
// Row details navigation logic
|
|
400
|
+
if (activeRowGroup === this.$.items) {
|
|
401
|
+
const item = activeRow._item;
|
|
402
|
+
const dstItem = this._cache.getItemForIndex(dstRowIndex);
|
|
403
|
+
// Should we navigate to row details?
|
|
404
|
+
if (isRowDetails) {
|
|
405
|
+
dstIsRowDetails = dy === 0;
|
|
406
|
+
} else {
|
|
407
|
+
dstIsRowDetails =
|
|
408
|
+
(dy === 1 && this._isDetailsOpened(item)) ||
|
|
409
|
+
(dy === -1 && dstRowIndex !== currentRowIndex && this._isDetailsOpened(dstItem));
|
|
410
|
+
}
|
|
411
|
+
// Should we navigate between details and regular cells of the same row?
|
|
412
|
+
if (
|
|
413
|
+
dstIsRowDetails !== isRowDetails &&
|
|
414
|
+
((dy === 1 && dstIsRowDetails) || (dy === -1 && !dstIsRowDetails))
|
|
415
|
+
) {
|
|
416
|
+
dstRowIndex = currentRowIndex;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Ensure correct vertical scroll position, destination row is visible
|
|
422
|
+
this._ensureScrolledToIndex(dstRowIndex);
|
|
423
|
+
|
|
424
|
+
// When scrolling with repeated keydown, sometimes FocusEvent listeners
|
|
425
|
+
// are too late to update _focusedItemIndex. Ensure next keydown
|
|
426
|
+
// listener invocation gets updated _focusedItemIndex value.
|
|
427
|
+
this._focusedItemIndex = dstRowIndex;
|
|
428
|
+
|
|
429
|
+
// This has to be set after scrolling, otherwise it can be removed by
|
|
430
|
+
// `_preventScrollerRotatingCellFocus(row, index)` during scrolling.
|
|
431
|
+
this.toggleAttribute('navigating', true);
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
dstRow: [...activeRowGroup.children].find((el) => !el.hidden && el.index === dstRowIndex),
|
|
435
|
+
dstIsRowDetails
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Focuses the target cell after navigating by the given dx and dy offset.
|
|
442
|
+
* If the cell is not in the viewport, it is first scrolled to.
|
|
443
|
+
* @private
|
|
444
|
+
**/
|
|
445
|
+
_onCellNavigation(activeCell, dx, dy) {
|
|
446
|
+
const activeRow = activeCell.parentNode;
|
|
447
|
+
const { dstRow, dstIsRowDetails } = this.__navigateRows(dy, activeRow, activeCell);
|
|
448
|
+
if (!dstRow) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const columnIndex = this.__getIndexOfChildElement(activeCell);
|
|
453
|
+
const isCurrentCellRowDetails = this.__isDetailsCell(activeCell);
|
|
454
|
+
const activeRowGroup = activeRow.parentNode;
|
|
455
|
+
const currentRowIndex = this.__getIndexInGroup(activeRow, this._focusedItemIndex);
|
|
456
|
+
|
|
457
|
+
// _focusedColumnOrder is memoized — this is to ensure predictable
|
|
458
|
+
// navigation when entering and leaving detail and column group cells.
|
|
459
|
+
if (this._focusedColumnOrder === undefined) {
|
|
460
|
+
if (isCurrentCellRowDetails) {
|
|
461
|
+
this._focusedColumnOrder = 0;
|
|
462
|
+
} else {
|
|
463
|
+
this._focusedColumnOrder = this._getColumns(activeRowGroup, currentRowIndex).filter((c) => !c.hidden)[
|
|
464
|
+
columnIndex
|
|
465
|
+
]._order;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (dstIsRowDetails) {
|
|
470
|
+
// Focusing a row details cell on the destination row
|
|
471
|
+
const dstCell = [...dstRow.children].find((el) => this.__isDetailsCell(el));
|
|
472
|
+
dstCell.focus();
|
|
473
|
+
} else {
|
|
474
|
+
// Focusing a regular cell on the destination row
|
|
475
|
+
|
|
476
|
+
// Find orderedColumnIndex — the index of order closest matching the
|
|
477
|
+
// original _focusedColumnOrder in the sorted array of orders
|
|
478
|
+
// of the visible columns on the destination row.
|
|
479
|
+
const dstRowIndex = this.__getIndexInGroup(dstRow, this._focusedItemIndex);
|
|
480
|
+
const dstColumns = this._getColumns(activeRowGroup, dstRowIndex).filter((c) => !c.hidden);
|
|
481
|
+
const dstSortedColumnOrders = dstColumns.map((c) => c._order).sort((b, a) => b - a);
|
|
482
|
+
const maxOrderedColumnIndex = dstSortedColumnOrders.length - 1;
|
|
483
|
+
const orderedColumnIndex = dstSortedColumnOrders.indexOf(
|
|
484
|
+
dstSortedColumnOrders
|
|
485
|
+
.slice(0)
|
|
486
|
+
.sort((b, a) => Math.abs(b - this._focusedColumnOrder) - Math.abs(a - this._focusedColumnOrder))[0]
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Index of the destination column order
|
|
490
|
+
const dstOrderedColumnIndex =
|
|
491
|
+
dy === 0 && isCurrentCellRowDetails
|
|
492
|
+
? orderedColumnIndex
|
|
493
|
+
: Math.max(0, Math.min(orderedColumnIndex + dx, maxOrderedColumnIndex));
|
|
494
|
+
|
|
495
|
+
if (dstOrderedColumnIndex !== orderedColumnIndex) {
|
|
496
|
+
// Horizontal movement invalidates stored _focusedColumnOrder
|
|
497
|
+
this._focusedColumnOrder = undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const columnIndexByOrder = dstColumns.reduce((acc, col, i) => ((acc[col._order] = i), acc), {});
|
|
501
|
+
const dstColumnIndex = columnIndexByOrder[dstSortedColumnOrders[dstOrderedColumnIndex]];
|
|
502
|
+
const dstCell = dstRow.children[dstColumnIndex];
|
|
503
|
+
|
|
504
|
+
this._scrollHorizontallyToCell(dstCell);
|
|
505
|
+
dstCell.focus();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** @private */
|
|
510
|
+
_onInteractionKeyDown(e, key) {
|
|
511
|
+
const localTarget = e.composedPath()[0];
|
|
512
|
+
const localTargetIsTextInput =
|
|
513
|
+
localTarget.localName === 'input' &&
|
|
514
|
+
!/^(button|checkbox|color|file|image|radio|range|reset|submit)$/i.test(localTarget.type);
|
|
515
|
+
|
|
516
|
+
let wantInteracting;
|
|
517
|
+
switch (key) {
|
|
518
|
+
case 'Enter':
|
|
519
|
+
wantInteracting = this.interacting ? !localTargetIsTextInput : true;
|
|
520
|
+
break;
|
|
521
|
+
case 'Escape':
|
|
522
|
+
wantInteracting = false;
|
|
523
|
+
break;
|
|
524
|
+
case 'F2':
|
|
525
|
+
wantInteracting = !this.interacting;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const { cell } = this._getGridEventLocation(e);
|
|
530
|
+
|
|
531
|
+
if (this.interacting !== wantInteracting && cell !== null) {
|
|
532
|
+
if (wantInteracting) {
|
|
533
|
+
const focusTarget = cell._content.querySelector('[focus-target]') || cell._content.firstElementChild;
|
|
534
|
+
if (focusTarget) {
|
|
535
|
+
e.preventDefault();
|
|
536
|
+
focusTarget.focus();
|
|
537
|
+
this._setInteracting(true);
|
|
538
|
+
this.toggleAttribute('navigating', false);
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
this._focusedColumnOrder = undefined;
|
|
543
|
+
cell.focus();
|
|
544
|
+
this._setInteracting(false);
|
|
545
|
+
this.toggleAttribute('navigating', true);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** @private */
|
|
551
|
+
_predictFocusStepTarget(srcElement, step) {
|
|
552
|
+
const tabOrder = [
|
|
553
|
+
this.$.table,
|
|
554
|
+
this._headerFocusable,
|
|
555
|
+
this._itemsFocusable,
|
|
556
|
+
this._footerFocusable,
|
|
557
|
+
this.$.focusexit
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
let index = tabOrder.indexOf(srcElement);
|
|
561
|
+
|
|
562
|
+
index += step;
|
|
563
|
+
while (index >= 0 && index <= tabOrder.length - 1) {
|
|
564
|
+
const rowElement = this.__rowFocusMode ? tabOrder[index] : tabOrder[index].parentNode;
|
|
565
|
+
|
|
566
|
+
if (!rowElement || rowElement.hidden) {
|
|
567
|
+
index += step;
|
|
568
|
+
} else {
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return tabOrder[index];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** @private */
|
|
577
|
+
_onTabKeyDown(e) {
|
|
578
|
+
const focusTarget = this._predictFocusStepTarget(e.composedPath()[0], e.shiftKey ? -1 : 1);
|
|
579
|
+
|
|
580
|
+
if (focusTarget === this.$.table) {
|
|
581
|
+
// The focus is about to exit the grid to the top.
|
|
582
|
+
this.$.table.focus();
|
|
583
|
+
} else if (focusTarget === this.$.focusexit) {
|
|
584
|
+
// The focus is about to exit the grid to the bottom.
|
|
585
|
+
this.$.focusexit.focus();
|
|
586
|
+
} else if (focusTarget === this._itemsFocusable) {
|
|
587
|
+
let itemsFocusTarget = focusTarget;
|
|
588
|
+
const targetRow = this.__isRow(focusTarget) ? focusTarget : focusTarget.parentNode;
|
|
589
|
+
this._ensureScrolledToIndex(this._focusedItemIndex);
|
|
590
|
+
if (targetRow.index !== this._focusedItemIndex && this.__isCell(focusTarget)) {
|
|
591
|
+
// The target row, which is about to be focused next, has been
|
|
592
|
+
// assigned with a new index since last focus, probably because of
|
|
593
|
+
// scrolling. Focus the row for the stored focused item index instead.
|
|
594
|
+
const columnIndex = Array.from(targetRow.children).indexOf(this._itemsFocusable);
|
|
595
|
+
const focusedItemRow = Array.from(this.$.items.children).filter(
|
|
596
|
+
(row) => row.index === this._focusedItemIndex
|
|
597
|
+
)[0];
|
|
598
|
+
if (focusedItemRow) {
|
|
599
|
+
itemsFocusTarget = focusedItemRow.children[columnIndex];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
itemsFocusTarget.focus();
|
|
604
|
+
} else {
|
|
605
|
+
e.preventDefault();
|
|
606
|
+
focusTarget.focus();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
this.toggleAttribute('navigating', true);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** @private */
|
|
613
|
+
_onSpaceKeyDown(e) {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
|
|
616
|
+
const element = e.composedPath()[0];
|
|
617
|
+
const isRow = this.__isRow(element);
|
|
618
|
+
if (isRow || !element._content || !element._content.firstElementChild) {
|
|
619
|
+
this.dispatchEvent(
|
|
620
|
+
new CustomEvent(isRow ? 'row-activate' : 'cell-activate', {
|
|
621
|
+
detail: {
|
|
622
|
+
model: this.__getRowModel(isRow ? element : element.parentElement)
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** @private */
|
|
630
|
+
_onKeyUp(e) {
|
|
631
|
+
if (!/^( |SpaceBar)$/.test(e.key) || this.interacting) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
|
|
637
|
+
const cell = e.composedPath()[0];
|
|
638
|
+
if (cell._content && cell._content.firstElementChild) {
|
|
639
|
+
const wasNavigating = this.hasAttribute('navigating');
|
|
640
|
+
cell._content.firstElementChild.click();
|
|
641
|
+
this.toggleAttribute('navigating', wasNavigating);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* @param {!FocusEvent} e
|
|
647
|
+
* @protected
|
|
648
|
+
*/
|
|
649
|
+
_onFocusIn(e) {
|
|
650
|
+
if (!this._isMousedown) {
|
|
651
|
+
this.toggleAttribute('navigating', true);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const rootTarget = e.composedPath()[0];
|
|
655
|
+
|
|
656
|
+
if (rootTarget === this.$.table || rootTarget === this.$.focusexit) {
|
|
657
|
+
// The focus enters the top (bottom) of the grid, meaning that user has
|
|
658
|
+
// tabbed (shift-tabbed) into the grid. Move the focus to
|
|
659
|
+
// the first (the last) focusable.
|
|
660
|
+
this._predictFocusStepTarget(rootTarget, rootTarget === this.$.table ? 1 : -1).focus();
|
|
661
|
+
this._setInteracting(false);
|
|
662
|
+
} else {
|
|
663
|
+
this._detectInteracting(e);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* @param {!FocusEvent} e
|
|
669
|
+
* @protected
|
|
670
|
+
*/
|
|
671
|
+
_onFocusOut(e) {
|
|
672
|
+
this.toggleAttribute('navigating', false);
|
|
673
|
+
this._detectInteracting(e);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** @private */
|
|
677
|
+
_onContentFocusIn(e) {
|
|
678
|
+
const { section, cell, row } = this._getGridEventLocation(e);
|
|
679
|
+
this._detectInteracting(e);
|
|
680
|
+
|
|
681
|
+
if (section && (cell || row)) {
|
|
682
|
+
this._activeRowGroup = section;
|
|
683
|
+
if (this.$.header === section) {
|
|
684
|
+
this._headerFocusable = this.__rowFocusMode ? row : cell;
|
|
685
|
+
} else if (this.$.items === section) {
|
|
686
|
+
this._itemsFocusable = this.__rowFocusMode ? row : cell;
|
|
687
|
+
} else if (this.$.footer === section) {
|
|
688
|
+
this._footerFocusable = this.__rowFocusMode ? row : cell;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (cell) {
|
|
692
|
+
// Fire a public event for cell.
|
|
693
|
+
const context = this.getEventContext(e);
|
|
694
|
+
cell.dispatchEvent(
|
|
695
|
+
new CustomEvent('cell-focus', { bubbles: true, composed: true, detail: { context: context } })
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
this._detectFocusedItemIndex(e);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** @private
|
|
704
|
+
* Enables interaction mode if a cells descendant receives focus or keyboard
|
|
705
|
+
* input. Disables it if the event is not related to cell content.
|
|
706
|
+
* @param {!KeyboardEvent|!FocusEvent} e
|
|
707
|
+
*/
|
|
708
|
+
_detectInteracting(e) {
|
|
709
|
+
const isInteracting = e.composedPath().some((el) => el.localName === 'vaadin-grid-cell-content');
|
|
710
|
+
this._setInteracting(isInteracting);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** @private */
|
|
714
|
+
_detectFocusedItemIndex(e) {
|
|
715
|
+
const { section, row } = this._getGridEventLocation(e);
|
|
716
|
+
if (section === this.$.items) {
|
|
717
|
+
this._focusedItemIndex = row.index;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** @private
|
|
722
|
+
* Enables or disables the focus target of the containing section of the
|
|
723
|
+
* grid from receiving focus, based on whether the user is interacting with
|
|
724
|
+
* that section of the grid.
|
|
725
|
+
* @param {HTMLElement} focusTarget
|
|
726
|
+
*/
|
|
727
|
+
_updateGridSectionFocusTarget(focusTarget) {
|
|
728
|
+
if (!focusTarget) return;
|
|
729
|
+
|
|
730
|
+
const section = this._getGridSectionFromFocusTarget(focusTarget);
|
|
731
|
+
const isInteractingWithinActiveSection = this.interacting && section === this._activeRowGroup;
|
|
732
|
+
|
|
733
|
+
focusTarget.tabIndex = isInteractingWithinActiveSection ? -1 : 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* @param {!HTMLTableRowElement} row
|
|
738
|
+
* @param {number} index
|
|
739
|
+
* @protected
|
|
740
|
+
*/
|
|
741
|
+
_preventScrollerRotatingCellFocus(row, index) {
|
|
742
|
+
if (
|
|
743
|
+
row.index === this._focusedItemIndex &&
|
|
744
|
+
this.hasAttribute('navigating') &&
|
|
745
|
+
this._activeRowGroup === this.$.items
|
|
746
|
+
) {
|
|
747
|
+
// Focused item has went, hide navigation mode
|
|
748
|
+
this._navigatingIsHidden = true;
|
|
749
|
+
this.toggleAttribute('navigating', false);
|
|
750
|
+
}
|
|
751
|
+
if (index === this._focusedItemIndex && this._navigatingIsHidden) {
|
|
752
|
+
// Focused item is back, restore navigation mode
|
|
753
|
+
this._navigatingIsHidden = false;
|
|
754
|
+
this.toggleAttribute('navigating', true);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* @param {HTMLTableSectionElement=} rowGroup
|
|
760
|
+
* @param {number=} rowIndex
|
|
761
|
+
* @return {!Array<!GridColumn>}
|
|
762
|
+
* @protected
|
|
763
|
+
*/
|
|
764
|
+
_getColumns(rowGroup, rowIndex) {
|
|
765
|
+
let columnTreeLevel = this._columnTree.length - 1;
|
|
766
|
+
if (rowGroup === this.$.header) {
|
|
767
|
+
columnTreeLevel = rowIndex;
|
|
768
|
+
} else if (rowGroup === this.$.footer) {
|
|
769
|
+
columnTreeLevel = this._columnTree.length - 1 - rowIndex;
|
|
770
|
+
}
|
|
771
|
+
return this._columnTree[columnTreeLevel];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** @private */
|
|
775
|
+
__isValidFocusable(element) {
|
|
776
|
+
return this.$.table.contains(element) && element.offsetHeight;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** @protected */
|
|
780
|
+
_resetKeyboardNavigation() {
|
|
781
|
+
if (!this.__isValidFocusable(this._headerFocusable) && this.$.header.firstElementChild) {
|
|
782
|
+
this._headerFocusable = Array.from(this.$.header.firstElementChild.children).filter((el) => !el.hidden)[0];
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!this.__isValidFocusable(this._itemsFocusable) && this.$.items.firstElementChild) {
|
|
786
|
+
const firstVisibleIndexRow = this.__getFirstVisibleItem();
|
|
787
|
+
if (firstVisibleIndexRow) {
|
|
788
|
+
this._itemsFocusable = Array.from(firstVisibleIndexRow.children).filter((el) => !el.hidden)[0];
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (!this.__isValidFocusable(this._footerFocusable) && this.$.footer.firstElementChild) {
|
|
793
|
+
this._footerFocusable = Array.from(this.$.footer.firstElementChild.children).filter((el) => !el.hidden)[0];
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @param {!HTMLElement} dstCell
|
|
799
|
+
* @protected
|
|
800
|
+
*/
|
|
801
|
+
_scrollHorizontallyToCell(dstCell) {
|
|
802
|
+
if (dstCell.hasAttribute('frozen') || this.__isDetailsCell(dstCell)) {
|
|
803
|
+
// These cells are, by design, always visible, no need to scroll.
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const dstCellRect = dstCell.getBoundingClientRect();
|
|
808
|
+
const dstRow = dstCell.parentNode;
|
|
809
|
+
const dstCellIndex = Array.from(dstRow.children).indexOf(dstCell);
|
|
810
|
+
const tableRect = this.$.table.getBoundingClientRect();
|
|
811
|
+
let leftBoundary = tableRect.left,
|
|
812
|
+
rightBoundary = tableRect.right;
|
|
813
|
+
for (let i = dstCellIndex - 1; i >= 0; i--) {
|
|
814
|
+
const cell = dstRow.children[i];
|
|
815
|
+
if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (cell.hasAttribute('frozen')) {
|
|
819
|
+
leftBoundary = cell.getBoundingClientRect().right;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
for (let i = dstCellIndex + 1; i < dstRow.children.length; i++) {
|
|
824
|
+
const cell = dstRow.children[i];
|
|
825
|
+
if (cell.hasAttribute('hidden') || this.__isDetailsCell(cell)) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (cell.hasAttribute('frozen')) {
|
|
829
|
+
rightBoundary = cell.getBoundingClientRect().left;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (dstCellRect.left < leftBoundary) {
|
|
835
|
+
this.$.table.scrollLeft += Math.round(dstCellRect.left - leftBoundary);
|
|
836
|
+
}
|
|
837
|
+
if (dstCellRect.right > rightBoundary) {
|
|
838
|
+
this.$.table.scrollLeft += Math.round(dstCellRect.right - rightBoundary);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* @typedef {Object} GridEventLocation
|
|
844
|
+
* @property {HTMLTableSectionElement | null} section - The table section element that the event occurred in (header, body, or footer), or null if the event did not occur in a section
|
|
845
|
+
* @property {HTMLTableRowElement | null} row - The row element that the event occurred in, or null if the event did not occur in a row
|
|
846
|
+
* @property {HTMLTableCellElement | null} cell - The cell element that the event occurred in, or null if the event did not occur in a cell
|
|
847
|
+
* @private
|
|
848
|
+
*/
|
|
849
|
+
/**
|
|
850
|
+
* Takes an event and returns a location object describing in which part of the grid the event occurred.
|
|
851
|
+
* The event may either target table section, a row, a cell or contents of a cell.
|
|
852
|
+
* @param {Event} e
|
|
853
|
+
* @returns {GridEventLocation}
|
|
854
|
+
* @private
|
|
855
|
+
*/
|
|
856
|
+
_getGridEventLocation(e) {
|
|
857
|
+
const path = e.composedPath();
|
|
858
|
+
const tableIndex = path.indexOf(this.$.table);
|
|
859
|
+
// Assuming ascending path to table is: [...,] th|td, tr, thead|tbody, table [,...]
|
|
860
|
+
const section = tableIndex >= 1 ? path[tableIndex - 1] : null;
|
|
861
|
+
const row = tableIndex >= 2 ? path[tableIndex - 2] : null;
|
|
862
|
+
const cell = tableIndex >= 3 ? path[tableIndex - 3] : null;
|
|
863
|
+
|
|
864
|
+
return {
|
|
865
|
+
section,
|
|
866
|
+
row,
|
|
867
|
+
cell
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Helper method that maps a focus target cell to the containing grid section
|
|
873
|
+
* @param {HTMLElement} focusTarget
|
|
874
|
+
* @returns {HTMLTableSectionElement | null}
|
|
875
|
+
* @private
|
|
876
|
+
*/
|
|
877
|
+
_getGridSectionFromFocusTarget(focusTarget) {
|
|
878
|
+
if (focusTarget === this._headerFocusable) return this.$.header;
|
|
879
|
+
if (focusTarget === this._itemsFocusable) return this.$.items;
|
|
880
|
+
if (focusTarget === this._footerFocusable) return this.$.footer;
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Fired when a cell is focused with click or keyboard navigation.
|
|
886
|
+
*
|
|
887
|
+
* Use context property of @see {@link GridCellFocusEvent} to get detail information about the event.
|
|
888
|
+
*
|
|
889
|
+
* @event cell-focus
|
|
890
|
+
*/
|
|
891
|
+
};
|