@tipolox/litgrid-web 1.0.2 → 1.1.0
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/README.md +1 -0
- package/dist/components/DataGrid.d.ts +17 -0
- package/dist/components/DataGrid.d.ts.map +1 -1
- package/dist/components/DataGrid.js +379 -93
- package/dist/components/DataGrid.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/DataGrid.ts +1 -0
- package/src/clipboard/serializeSelectedCells.ts +62 -0
- package/src/clipboard/serializeSelectedRows.ts +42 -0
- package/src/columnState/columnState.ts +102 -0
- package/src/components/DataGrid.ts +3188 -0
- package/src/index.ts +16 -0
- package/src/sizing/columnSizing.ts +25 -0
- package/src/sizing/constants.ts +3 -0
- package/src/sizing/rowSizing.ts +5 -0
- package/src/theme/darkTheme.ts +34 -0
- package/src/theme/lightTheme.ts +35 -0
- package/src/types.ts +108 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/DataGrid.ts
|
|
2
|
-
import { LitElement, css, html, render as litRender } from 'lit';
|
|
2
|
+
import { LitElement, css, html, nothing, render as litRender } from 'lit';
|
|
3
3
|
import { cache } from 'lit/directives/cache.js';
|
|
4
4
|
import { createGridEngine } from '@tipolox/litgrid-core';
|
|
5
5
|
import { calculateDisplayLayout, createVariableVirtualizer, createVirtualizer, mapDisplayScrollOffset } from '@tipolox/litgrid-renderer';
|
|
@@ -11,6 +11,7 @@ import { normalizeRowHeight } from '../sizing/rowSizing.js';
|
|
|
11
11
|
import { serializeSelectedCells } from '../clipboard/serializeSelectedCells.js';
|
|
12
12
|
import { serializeSelectedRows } from '../clipboard/serializeSelectedRows.js';
|
|
13
13
|
import { createColumnState, parseColumnState, resolveColumnState } from '../columnState/columnState.js';
|
|
14
|
+
const CELL_CONTROL_SELECTOR = 'a[href], area[href], button, input, select, textarea, summary, audio[controls], video[controls], [tabindex], [contenteditable]:not([contenteditable="false"])';
|
|
14
15
|
const FILTER_OPERATORS = [
|
|
15
16
|
{ value: 'contains', label: 'Contains' },
|
|
16
17
|
{ value: 'equals', label: 'Equals' },
|
|
@@ -60,6 +61,7 @@ export class DataGrid extends LitElement {
|
|
|
60
61
|
this.scrollbarWidth = 0;
|
|
61
62
|
this.columnDefs = [];
|
|
62
63
|
this.sourceColumnDefs = [];
|
|
64
|
+
this._inferColumns = true;
|
|
63
65
|
this.columnWidths = new Map();
|
|
64
66
|
this.hiddenColumnKeys = new Set();
|
|
65
67
|
this._columnStateStorageKey = null;
|
|
@@ -88,6 +90,11 @@ export class DataGrid extends LitElement {
|
|
|
88
90
|
this.headerRowElement = null;
|
|
89
91
|
this.activeCell = null;
|
|
90
92
|
this.shouldFocusActiveCell = false;
|
|
93
|
+
this.gridCellHasFocus = false;
|
|
94
|
+
this.shouldFocusViewport = false;
|
|
95
|
+
this.renderedActiveCellId = null;
|
|
96
|
+
this.renderedRowStart = 0;
|
|
97
|
+
this.renderedColumnStart = 0;
|
|
91
98
|
this.headerMenuTrigger = null;
|
|
92
99
|
this.shouldFocusHeaderMenu = false;
|
|
93
100
|
this.shouldRestoreHeaderMenuFocus = false;
|
|
@@ -143,22 +150,7 @@ export class DataGrid extends LitElement {
|
|
|
143
150
|
this.requestUpdate();
|
|
144
151
|
};
|
|
145
152
|
this.stopColumnReorder = () => {
|
|
146
|
-
|
|
147
|
-
if (!state)
|
|
148
|
-
return;
|
|
149
|
-
this.columnReorderState = null;
|
|
150
|
-
window.removeEventListener('pointermove', this.handleColumnReorder);
|
|
151
|
-
window.removeEventListener('pointerup', this.stopColumnReorder);
|
|
152
|
-
window.removeEventListener('pointercancel', this.stopColumnReorder);
|
|
153
|
-
this.removeColumnDragPreview();
|
|
154
|
-
if (state.hasMoved)
|
|
155
|
-
this.stopBodyResize();
|
|
156
|
-
if (state.hasMoved && state.sourceIndex !== state.targetIndex) {
|
|
157
|
-
this.moveColumn(state.columnKey, state.targetIndex);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
this.requestUpdate();
|
|
161
|
-
}
|
|
153
|
+
this.finishColumnReorder();
|
|
162
154
|
};
|
|
163
155
|
this.handleColumnResize = (event) => {
|
|
164
156
|
if (!this.columnResizeState) {
|
|
@@ -169,14 +161,7 @@ export class DataGrid extends LitElement {
|
|
|
169
161
|
this.requestUpdate();
|
|
170
162
|
};
|
|
171
163
|
this.stopColumnResize = () => {
|
|
172
|
-
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
this.columnResizeState = null;
|
|
176
|
-
this.commitColumnState('resize');
|
|
177
|
-
this.stopBodyResize();
|
|
178
|
-
window.removeEventListener('pointermove', this.handleColumnResize);
|
|
179
|
-
window.removeEventListener('pointerup', this.stopColumnResize);
|
|
164
|
+
this.finishColumnResize();
|
|
180
165
|
};
|
|
181
166
|
this.handleRowResize = (event) => {
|
|
182
167
|
if (!this.rowResizeState) {
|
|
@@ -241,6 +226,8 @@ export class DataGrid extends LitElement {
|
|
|
241
226
|
static { this.styles = [lightThemeStyles, darkThemeStyles, css `
|
|
242
227
|
:host {
|
|
243
228
|
display: block;
|
|
229
|
+
min-width: 0;
|
|
230
|
+
max-width: 100%;
|
|
244
231
|
color: var(--litgrid-color-text);
|
|
245
232
|
font: 14px/1.4 system-ui, sans-serif;
|
|
246
233
|
}
|
|
@@ -368,6 +355,8 @@ export class DataGrid extends LitElement {
|
|
|
368
355
|
|
|
369
356
|
.header-row-header {
|
|
370
357
|
position: relative;
|
|
358
|
+
/* Cover translated headers without clipping menus below the header. */
|
|
359
|
+
z-index: 4;
|
|
371
360
|
height: 36px;
|
|
372
361
|
border-right: 1px solid var(--litgrid-color-border);
|
|
373
362
|
}
|
|
@@ -706,6 +695,7 @@ export class DataGrid extends LitElement {
|
|
|
706
695
|
|
|
707
696
|
.pagination {
|
|
708
697
|
display: flex;
|
|
698
|
+
flex-wrap: wrap;
|
|
709
699
|
align-items: center;
|
|
710
700
|
justify-content: flex-end;
|
|
711
701
|
gap: 10px;
|
|
@@ -716,8 +706,29 @@ export class DataGrid extends LitElement {
|
|
|
716
706
|
font-size: 13px;
|
|
717
707
|
}
|
|
718
708
|
|
|
709
|
+
.pagination-summary,
|
|
710
|
+
.pagination-navigation {
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-wrap: wrap;
|
|
713
|
+
align-items: center;
|
|
714
|
+
gap: 10px;
|
|
715
|
+
min-width: 0;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.pagination span,
|
|
719
|
+
.pagination label {
|
|
720
|
+
white-space: nowrap;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.pagination label {
|
|
724
|
+
display: inline-flex;
|
|
725
|
+
align-items: center;
|
|
726
|
+
gap: 4px;
|
|
727
|
+
}
|
|
728
|
+
|
|
719
729
|
.pagination button,
|
|
720
730
|
.pagination select {
|
|
731
|
+
flex-shrink: 0;
|
|
721
732
|
border: 1px solid var(--litgrid-color-border);
|
|
722
733
|
border-radius: 6px;
|
|
723
734
|
background: var(--litgrid-color-surface);
|
|
@@ -740,6 +751,10 @@ export class DataGrid extends LitElement {
|
|
|
740
751
|
}
|
|
741
752
|
updated() {
|
|
742
753
|
this.updateScrollbarWidth();
|
|
754
|
+
if (this.shouldFocusViewport) {
|
|
755
|
+
this.shouldFocusViewport = false;
|
|
756
|
+
this.viewportElement?.focus({ preventScroll: true });
|
|
757
|
+
}
|
|
743
758
|
this.focusActiveCell();
|
|
744
759
|
this.focusHeaderMenu();
|
|
745
760
|
// Detect actual DOM spacer height after render. Some browsers clamp
|
|
@@ -775,6 +790,7 @@ export class DataGrid extends LitElement {
|
|
|
775
790
|
viewportElement.removeEventListener('scroll', this.handleScroll);
|
|
776
791
|
}
|
|
777
792
|
this.headerRowElement = null;
|
|
793
|
+
this.viewportElement = null;
|
|
778
794
|
if (this.measureContainer && this.measureContainer.parentNode) {
|
|
779
795
|
this.measureContainer.parentNode.removeChild(this.measureContainer);
|
|
780
796
|
}
|
|
@@ -805,6 +821,7 @@ export class DataGrid extends LitElement {
|
|
|
805
821
|
if (!(viewport instanceof HTMLElement)) {
|
|
806
822
|
return;
|
|
807
823
|
}
|
|
824
|
+
this.viewportElement = viewport;
|
|
808
825
|
const nextWidth = viewport.offsetWidth - viewport.clientWidth;
|
|
809
826
|
const nextViewportWidth = viewport.clientWidth;
|
|
810
827
|
if (nextWidth !== this.scrollbarWidth || nextViewportWidth !== this.viewportWidth) {
|
|
@@ -814,7 +831,7 @@ export class DataGrid extends LitElement {
|
|
|
814
831
|
}
|
|
815
832
|
}
|
|
816
833
|
getResolvedColumns() {
|
|
817
|
-
if (this.columnDefs.length > 0) {
|
|
834
|
+
if (this.columnDefs.length > 0 || !this._inferColumns) {
|
|
818
835
|
return this.columnDefs;
|
|
819
836
|
}
|
|
820
837
|
const firstRow = this.engine.getRows()[0];
|
|
@@ -1046,7 +1063,7 @@ export class DataGrid extends LitElement {
|
|
|
1046
1063
|
return null;
|
|
1047
1064
|
}
|
|
1048
1065
|
}
|
|
1049
|
-
restorePersistedColumnState() {
|
|
1066
|
+
restorePersistedColumnState(notify = true) {
|
|
1050
1067
|
if (!this._columnStateStorageKey || this.sourceColumnDefs.length === 0)
|
|
1051
1068
|
return;
|
|
1052
1069
|
const storage = this.getColumnStateStorage();
|
|
@@ -1062,7 +1079,8 @@ export class DataGrid extends LitElement {
|
|
|
1062
1079
|
this.hiddenColumnKeys = resolvedState.hiddenKeys;
|
|
1063
1080
|
this.activeCell = null;
|
|
1064
1081
|
this.requestUpdate();
|
|
1065
|
-
|
|
1082
|
+
if (notify)
|
|
1083
|
+
this.dispatchColumnStateChange('restore');
|
|
1066
1084
|
}
|
|
1067
1085
|
catch {
|
|
1068
1086
|
// Storage may be unavailable or blocked by browser privacy settings.
|
|
@@ -1138,7 +1156,7 @@ export class DataGrid extends LitElement {
|
|
|
1138
1156
|
this.columnChooserOpen = false;
|
|
1139
1157
|
this.columnChooserPosition = null;
|
|
1140
1158
|
this.shouldFocusHeaderMenu = false;
|
|
1141
|
-
this.shouldRestoreHeaderMenuFocus = restoreFocus
|
|
1159
|
+
this.shouldRestoreHeaderMenuFocus = restoreFocus;
|
|
1142
1160
|
this.requestUpdate();
|
|
1143
1161
|
}
|
|
1144
1162
|
toggleColumnChooser(event) {
|
|
@@ -1147,6 +1165,9 @@ export class DataGrid extends LitElement {
|
|
|
1147
1165
|
return;
|
|
1148
1166
|
}
|
|
1149
1167
|
if (event?.currentTarget instanceof HTMLElement) {
|
|
1168
|
+
if (!this.activeHeaderMenu && event.currentTarget instanceof HTMLButtonElement) {
|
|
1169
|
+
this.headerMenuTrigger = event.currentTarget;
|
|
1170
|
+
}
|
|
1150
1171
|
const shell = this.renderRoot.querySelector('.shell');
|
|
1151
1172
|
const menu = event.currentTarget.closest('.header-menu');
|
|
1152
1173
|
const anchorBounds = (menu ?? event.currentTarget).getBoundingClientRect();
|
|
@@ -1172,13 +1193,17 @@ export class DataGrid extends LitElement {
|
|
|
1172
1193
|
this.columnChooserOpen = false;
|
|
1173
1194
|
this.columnChooserPosition = null;
|
|
1174
1195
|
this.shouldFocusHeaderMenu = false;
|
|
1175
|
-
this.shouldRestoreHeaderMenuFocus = restoreFocus
|
|
1196
|
+
this.shouldRestoreHeaderMenuFocus = restoreFocus;
|
|
1176
1197
|
this.requestUpdate();
|
|
1177
1198
|
}
|
|
1178
1199
|
focusHeaderMenu() {
|
|
1179
1200
|
if (this.shouldRestoreHeaderMenuFocus) {
|
|
1180
1201
|
this.shouldRestoreHeaderMenuFocus = false;
|
|
1181
|
-
this.headerMenuTrigger?.
|
|
1202
|
+
const target = this.headerMenuTrigger?.isConnected
|
|
1203
|
+
? this.headerMenuTrigger
|
|
1204
|
+
: this.renderRoot.querySelector('.header-action-button, .columns-button')
|
|
1205
|
+
?? this.viewportElement;
|
|
1206
|
+
target?.focus();
|
|
1182
1207
|
this.headerMenuTrigger = null;
|
|
1183
1208
|
return;
|
|
1184
1209
|
}
|
|
@@ -1199,12 +1224,16 @@ export class DataGrid extends LitElement {
|
|
|
1199
1224
|
this.closeHeaderMenu(true);
|
|
1200
1225
|
}
|
|
1201
1226
|
handleViewportFocus(event) {
|
|
1202
|
-
if (event.target !== event.currentTarget || this.activeCell) {
|
|
1227
|
+
if (event.target !== event.currentTarget || this.activeCell || this.engine.getRowCount() === 0 || this.getVisibleColumns().length === 0) {
|
|
1203
1228
|
return;
|
|
1204
1229
|
}
|
|
1205
1230
|
this.setActiveCell(0, 0);
|
|
1206
1231
|
this.requestUpdate();
|
|
1207
1232
|
}
|
|
1233
|
+
handleGridFocusIn(event) {
|
|
1234
|
+
const target = event.composedPath()[0];
|
|
1235
|
+
this.gridCellHasFocus = target instanceof HTMLElement && target.classList.contains('cell');
|
|
1236
|
+
}
|
|
1208
1237
|
announceScreenReader(message) {
|
|
1209
1238
|
if (!this.screenReaderAnnouncements || !message || message === this.lastScreenReaderAnnouncement) {
|
|
1210
1239
|
return;
|
|
@@ -1214,7 +1243,18 @@ export class DataGrid extends LitElement {
|
|
|
1214
1243
|
this.requestUpdate();
|
|
1215
1244
|
}
|
|
1216
1245
|
getScreenReaderDescription() {
|
|
1217
|
-
|
|
1246
|
+
if (this.ariaDescription)
|
|
1247
|
+
return this.ariaDescription;
|
|
1248
|
+
const mode = this.engine.getConfig().selection.mode;
|
|
1249
|
+
const navigation = 'Use arrow keys to move between cells. Press F2 to interact with controls in a cell and Escape to return to cell navigation.';
|
|
1250
|
+
if (mode === 'none')
|
|
1251
|
+
return navigation;
|
|
1252
|
+
const selection = mode === 'multi-row'
|
|
1253
|
+
? 'Press Enter or Space to select a row. Hold Control or Command to toggle a row, or Shift to select a range of rows.'
|
|
1254
|
+
: mode === 'multi-cell'
|
|
1255
|
+
? 'Press Enter or Space to toggle a cell.'
|
|
1256
|
+
: `Press Enter or Space to select a ${mode === 'row' ? 'row' : 'cell'}.`;
|
|
1257
|
+
return `${navigation} ${selection} Press Control or Command C to copy selected cells or rows.`;
|
|
1218
1258
|
}
|
|
1219
1259
|
getColumnLabel(columnKey) {
|
|
1220
1260
|
const column = this.getVisibleColumns().find((item) => item.key === columnKey)
|
|
@@ -1238,8 +1278,11 @@ export class DataGrid extends LitElement {
|
|
|
1238
1278
|
announceActiveCell() {
|
|
1239
1279
|
this.announceScreenReader(this.getActiveCellAnnouncement());
|
|
1240
1280
|
}
|
|
1241
|
-
announceRowSelection(rowIndex) {
|
|
1242
|
-
|
|
1281
|
+
announceRowSelection(rowIndex, intent = 'replace') {
|
|
1282
|
+
const anchor = this.engine.getSelection().anchorRowIndex ?? rowIndex;
|
|
1283
|
+
this.announceScreenReader(intent === 'range' && anchor !== rowIndex
|
|
1284
|
+
? `Rows ${Math.min(anchor, rowIndex) + 1} through ${Math.max(anchor, rowIndex) + 1} selected.`
|
|
1285
|
+
: `Row ${rowIndex + 1} ${this.engine.isRowSelected(rowIndex) ? 'selected' : 'deselected'}.`);
|
|
1243
1286
|
}
|
|
1244
1287
|
announceCellSelection(rowIndex, columnKey) {
|
|
1245
1288
|
this.announceScreenReader(`${this.getColumnLabel(columnKey)}, row ${rowIndex + 1} ${this.engine.isCellSelected(rowIndex, columnKey) ? 'selected' : 'deselected'}.`);
|
|
@@ -1252,27 +1295,27 @@ export class DataGrid extends LitElement {
|
|
|
1252
1295
|
this.engine.sortBy(columnKey, 'asc');
|
|
1253
1296
|
this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted ascending.`);
|
|
1254
1297
|
this.requestUpdate();
|
|
1255
|
-
this.closeHeaderMenu();
|
|
1298
|
+
this.closeHeaderMenu(true);
|
|
1256
1299
|
}
|
|
1257
1300
|
handleSortDescending(columnKey) {
|
|
1258
1301
|
this.engine.sortBy(columnKey, 'desc');
|
|
1259
1302
|
this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted descending.`);
|
|
1260
1303
|
this.requestUpdate();
|
|
1261
|
-
this.closeHeaderMenu();
|
|
1304
|
+
this.closeHeaderMenu(true);
|
|
1262
1305
|
}
|
|
1263
1306
|
handleClearSort() {
|
|
1264
1307
|
this.engine.clearSort();
|
|
1265
1308
|
this.announceScreenReader('Sorting cleared.');
|
|
1266
1309
|
this.requestUpdate();
|
|
1267
|
-
this.closeHeaderMenu();
|
|
1310
|
+
this.closeHeaderMenu(true);
|
|
1268
1311
|
}
|
|
1269
1312
|
handleBestFit(columnKey) {
|
|
1270
1313
|
this.bestFitColumn(columnKey);
|
|
1271
|
-
this.closeHeaderMenu();
|
|
1314
|
+
this.closeHeaderMenu(true);
|
|
1272
1315
|
}
|
|
1273
1316
|
handleBestFitAllColumns() {
|
|
1274
1317
|
this.bestFitAllColumns();
|
|
1275
|
-
this.closeHeaderMenu();
|
|
1318
|
+
this.closeHeaderMenu(true);
|
|
1276
1319
|
}
|
|
1277
1320
|
getTextMeasureContext() {
|
|
1278
1321
|
if (this.textMeasureContext) {
|
|
@@ -1428,7 +1471,7 @@ export class DataGrid extends LitElement {
|
|
|
1428
1471
|
}
|
|
1429
1472
|
toggleQuickSearch() {
|
|
1430
1473
|
this.quickSearchVisible = !this.quickSearchVisible;
|
|
1431
|
-
this.closeHeaderMenu();
|
|
1474
|
+
this.closeHeaderMenu(true);
|
|
1432
1475
|
}
|
|
1433
1476
|
handleQuickSearchInput(query) {
|
|
1434
1477
|
this.quickSearchDraft = query;
|
|
@@ -1488,18 +1531,35 @@ export class DataGrid extends LitElement {
|
|
|
1488
1531
|
return this.engine.getTotalRowCount();
|
|
1489
1532
|
}
|
|
1490
1533
|
selectRow(rowIndex, intent) {
|
|
1491
|
-
this.
|
|
1492
|
-
|
|
1534
|
+
if (!this.isRowSelectionMode() || !Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= this.engine.getRowCount())
|
|
1535
|
+
return;
|
|
1536
|
+
const effectiveIntent = this.engine.getConfig().selection.mode === 'row' ? 'replace' : intent ?? 'replace';
|
|
1537
|
+
const previous = this.engine.getSelection();
|
|
1538
|
+
// The engine's range operation always produces a contiguous anchored range.
|
|
1539
|
+
// Its endpoint and size identify a repeated operation without scanning rows.
|
|
1540
|
+
const unchanged = effectiveIntent === 'range'
|
|
1541
|
+
? previous.rowIndex === rowIndex && previous.rowIndexes.size === Math.abs((previous.anchorRowIndex ?? rowIndex) - rowIndex) + 1
|
|
1542
|
+
: effectiveIntent === 'replace' && previous.rowIndexes.size === 1 && previous.rowIndexes.has(rowIndex);
|
|
1543
|
+
this.engine.selectRow(rowIndex, effectiveIntent);
|
|
1544
|
+
if (!unchanged)
|
|
1545
|
+
this.announceRowSelection(rowIndex, effectiveIntent);
|
|
1493
1546
|
this.requestUpdate();
|
|
1494
1547
|
}
|
|
1495
1548
|
selectAllRows() {
|
|
1549
|
+
if (this.engine.getConfig().selection.mode !== 'multi-row' || this.engine.getRowCount() === 0)
|
|
1550
|
+
return;
|
|
1551
|
+
const unchanged = this.engine.getSelection().rowIndexes.size === this.engine.getRowCount();
|
|
1496
1552
|
this.engine.selectAllRows();
|
|
1497
|
-
|
|
1553
|
+
if (!unchanged)
|
|
1554
|
+
this.announceScreenReader(`All ${this.engine.getRowCount()} displayed rows selected.`);
|
|
1498
1555
|
this.requestUpdate();
|
|
1499
1556
|
}
|
|
1500
1557
|
clearSelection() {
|
|
1558
|
+
const previous = this.engine.getSelection();
|
|
1559
|
+
const hadSelection = previous.rowIndexes.size > 0 || previous.cells.size > 0;
|
|
1501
1560
|
this.engine.clearSelection();
|
|
1502
|
-
|
|
1561
|
+
if (hadSelection)
|
|
1562
|
+
this.announceScreenReader('Selection cleared.');
|
|
1503
1563
|
this.requestUpdate();
|
|
1504
1564
|
}
|
|
1505
1565
|
getSelection() {
|
|
@@ -1561,11 +1621,11 @@ export class DataGrid extends LitElement {
|
|
|
1561
1621
|
...filter,
|
|
1562
1622
|
value: this.isValueFreeFilter(filter.operator) ? undefined : filter.value
|
|
1563
1623
|
});
|
|
1564
|
-
this.closeHeaderMenu();
|
|
1624
|
+
this.closeHeaderMenu(true);
|
|
1565
1625
|
}
|
|
1566
1626
|
clearHeaderFilter(columnKey) {
|
|
1567
1627
|
this.clearFilter(columnKey);
|
|
1568
|
-
this.closeHeaderMenu();
|
|
1628
|
+
this.closeHeaderMenu(true);
|
|
1569
1629
|
}
|
|
1570
1630
|
isColumnFiltered(columnKey) {
|
|
1571
1631
|
return this.engine.getFilters().some((filter) => filter.columnKey === columnKey);
|
|
@@ -1663,6 +1723,24 @@ export class DataGrid extends LitElement {
|
|
|
1663
1723
|
this.columnDragPreview?.remove();
|
|
1664
1724
|
this.columnDragPreview = null;
|
|
1665
1725
|
}
|
|
1726
|
+
finishColumnReorder(commit = true) {
|
|
1727
|
+
const state = this.columnReorderState;
|
|
1728
|
+
if (!state)
|
|
1729
|
+
return;
|
|
1730
|
+
this.columnReorderState = null;
|
|
1731
|
+
window.removeEventListener('pointermove', this.handleColumnReorder);
|
|
1732
|
+
window.removeEventListener('pointerup', this.stopColumnReorder);
|
|
1733
|
+
window.removeEventListener('pointercancel', this.stopColumnReorder);
|
|
1734
|
+
this.removeColumnDragPreview();
|
|
1735
|
+
if (state.hasMoved)
|
|
1736
|
+
this.stopBodyResize();
|
|
1737
|
+
if (commit && state.hasMoved && state.sourceIndex !== state.targetIndex) {
|
|
1738
|
+
this.moveColumn(state.columnKey, state.targetIndex);
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
this.requestUpdate();
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1666
1744
|
getGridCellId(rowIndex, columnIndex) {
|
|
1667
1745
|
return `${this.gridId}-cell-${rowIndex}-${columnIndex}`;
|
|
1668
1746
|
}
|
|
@@ -1709,6 +1787,17 @@ export class DataGrid extends LitElement {
|
|
|
1709
1787
|
window.addEventListener('pointermove', this.handleColumnResize);
|
|
1710
1788
|
window.addEventListener('pointerup', this.stopColumnResize);
|
|
1711
1789
|
}
|
|
1790
|
+
finishColumnResize(commit = true) {
|
|
1791
|
+
if (!this.columnResizeState) {
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
this.columnResizeState = null;
|
|
1795
|
+
if (commit)
|
|
1796
|
+
this.commitColumnState('resize');
|
|
1797
|
+
this.stopBodyResize();
|
|
1798
|
+
window.removeEventListener('pointermove', this.handleColumnResize);
|
|
1799
|
+
window.removeEventListener('pointerup', this.stopColumnResize);
|
|
1800
|
+
}
|
|
1712
1801
|
startRowResize(event, rowIndex) {
|
|
1713
1802
|
event.preventDefault();
|
|
1714
1803
|
event.stopPropagation();
|
|
@@ -1787,8 +1876,23 @@ export class DataGrid extends LitElement {
|
|
|
1787
1876
|
this.shouldFocusActiveCell = shouldFocus;
|
|
1788
1877
|
this.announceActiveCell();
|
|
1789
1878
|
}
|
|
1879
|
+
reconcileActiveCell(rowCount, columnCount) {
|
|
1880
|
+
if (!this.activeCell)
|
|
1881
|
+
return;
|
|
1882
|
+
if (rowCount === 0 || columnCount === 0) {
|
|
1883
|
+
this.activeCell = null;
|
|
1884
|
+
this.shouldFocusActiveCell = false;
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const rowIndex = Math.min(this.activeCell.rowIndex, rowCount - 1);
|
|
1888
|
+
const columnIndex = Math.min(this.activeCell.columnIndex, columnCount - 1);
|
|
1889
|
+
if (rowIndex !== this.activeCell.rowIndex || columnIndex !== this.activeCell.columnIndex) {
|
|
1890
|
+
this.activeCell = { rowIndex, columnIndex };
|
|
1891
|
+
this.shouldFocusActiveCell = this.gridCellHasFocus;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1790
1894
|
focusActiveCell() {
|
|
1791
|
-
if (!this.shouldFocusActiveCell || !this.activeCell) {
|
|
1895
|
+
if (!this.shouldFocusActiveCell || !this.activeCell || !this.renderedActiveCellId) {
|
|
1792
1896
|
return;
|
|
1793
1897
|
}
|
|
1794
1898
|
const cell = this.renderRoot.querySelector(`.cell[data-row-index="${this.activeCell.rowIndex}"][data-column-index="${this.activeCell.columnIndex}"]`);
|
|
@@ -1892,32 +1996,115 @@ export class DataGrid extends LitElement {
|
|
|
1892
1996
|
viewport.scrollLeft = Math.max(0, columnRight - bodyViewportWidth);
|
|
1893
1997
|
}
|
|
1894
1998
|
}
|
|
1999
|
+
getCellControls(cell) {
|
|
2000
|
+
// Only inspect the cell explicitly entered by the user, never a render or
|
|
2001
|
+
// virtualization path. Custom focusable hosts can own shadow controls.
|
|
2002
|
+
return Array.from(cell.querySelectorAll(CELL_CONTROL_SELECTOR))
|
|
2003
|
+
.filter((control) => !control.matches(':disabled, input[type="hidden"]')
|
|
2004
|
+
&& !control.closest('[hidden], [inert]'));
|
|
2005
|
+
}
|
|
2006
|
+
focusCellControl(control) {
|
|
2007
|
+
control.focus({ preventScroll: true });
|
|
2008
|
+
return control.getRootNode().activeElement === control;
|
|
2009
|
+
}
|
|
2010
|
+
syncInteractionCell(cell) {
|
|
2011
|
+
const rowIndex = Number(cell.dataset.rowIndex);
|
|
2012
|
+
const columnIndex = Number(cell.dataset.columnIndex);
|
|
2013
|
+
if (this.activeCell?.rowIndex !== rowIndex || this.activeCell.columnIndex !== columnIndex) {
|
|
2014
|
+
this.setActiveCell(rowIndex, columnIndex);
|
|
2015
|
+
this.requestUpdate();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1895
2018
|
handleKeyDown(event) {
|
|
1896
|
-
|
|
1897
|
-
if (target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLButtonElement) {
|
|
2019
|
+
if (event.defaultPrevented)
|
|
1898
2020
|
return;
|
|
2021
|
+
let owningCell;
|
|
2022
|
+
let insideControl = false;
|
|
2023
|
+
// The composed path also protects nested markup and renderer shadow controls.
|
|
2024
|
+
for (const target of event.composedPath()) {
|
|
2025
|
+
if (!(target instanceof Element))
|
|
2026
|
+
continue;
|
|
2027
|
+
if (target.matches('.cell[role="gridcell"]')) {
|
|
2028
|
+
owningCell = target;
|
|
2029
|
+
break;
|
|
2030
|
+
}
|
|
2031
|
+
if (target === event.currentTarget)
|
|
2032
|
+
break;
|
|
2033
|
+
if (target.matches(CELL_CONTROL_SELECTOR))
|
|
2034
|
+
insideControl = true;
|
|
2035
|
+
}
|
|
2036
|
+
if (insideControl) {
|
|
2037
|
+
if (!owningCell)
|
|
2038
|
+
return;
|
|
2039
|
+
if (event.key === 'Escape') {
|
|
2040
|
+
event.preventDefault();
|
|
2041
|
+
event.stopPropagation();
|
|
2042
|
+
this.syncInteractionCell(owningCell);
|
|
2043
|
+
owningCell.focus({ preventScroll: true });
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const focused = this.renderRoot.activeElement;
|
|
2047
|
+
if (event.key !== 'Tab' || !focused || focused === owningCell || !owningCell.contains(focused))
|
|
2048
|
+
return;
|
|
2049
|
+
// An open shadow widget owns the Tab order of its internal controls.
|
|
2050
|
+
if (focused.shadowRoot?.activeElement)
|
|
2051
|
+
return;
|
|
2052
|
+
const controls = this.getCellControls(owningCell).filter((control) => control.tabIndex >= 0
|
|
2053
|
+
|| control === focused || control.matches('[contenteditable]:not([tabindex])'));
|
|
2054
|
+
const current = controls.indexOf(focused);
|
|
2055
|
+
// Shadow widgets own their internal Tab order; a focusable host is their entry point.
|
|
2056
|
+
if (current < 0)
|
|
2057
|
+
return;
|
|
2058
|
+
const direction = event.shiftKey ? -1 : 1;
|
|
2059
|
+
for (let index = current + direction; index >= 0 && index < controls.length; index += direction) {
|
|
2060
|
+
if (this.focusCellControl(controls[index])) {
|
|
2061
|
+
event.preventDefault();
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
this.syncInteractionCell(owningCell);
|
|
2066
|
+
if (event.shiftKey) {
|
|
2067
|
+
event.preventDefault();
|
|
2068
|
+
owningCell.focus({ preventScroll: true });
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
// At the last control, resume the existing forward cell Tab navigation.
|
|
1899
2072
|
}
|
|
1900
2073
|
const rowCount = this.engine.getRowCount();
|
|
1901
2074
|
const columnCount = this.getVisibleColumns().length;
|
|
2075
|
+
this.reconcileActiveCell(rowCount, columnCount);
|
|
1902
2076
|
if (rowCount === 0 || columnCount === 0) {
|
|
1903
2077
|
return;
|
|
1904
2078
|
}
|
|
1905
|
-
if (
|
|
2079
|
+
if (event.key === 'F2' && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
|
2080
|
+
const active = this.activeCell ?? { rowIndex: 0, columnIndex: 0 };
|
|
2081
|
+
const cell = owningCell ?? this.renderRoot.querySelector(`.cell[data-row-index="${active.rowIndex}"][data-column-index="${active.columnIndex}"]`);
|
|
2082
|
+
if (cell) {
|
|
2083
|
+
for (const control of this.getCellControls(cell)) {
|
|
2084
|
+
if (this.focusCellControl(control)) {
|
|
2085
|
+
event.preventDefault();
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if ((event.key === 'Enter' || event.key === ' ') && !event.altKey) {
|
|
1906
2093
|
const activeCell = this.activeCell ?? { rowIndex: 0, columnIndex: 0 };
|
|
1907
|
-
if (this.isCellSelectionMode()) {
|
|
2094
|
+
if (this.isCellSelectionMode() && !event.ctrlKey && !event.metaKey) {
|
|
1908
2095
|
const column = this.getVisibleColumns()[activeCell.columnIndex];
|
|
1909
2096
|
if (column) {
|
|
1910
2097
|
event.preventDefault();
|
|
2098
|
+
const unchanged = this.engine.getConfig().selection.mode === 'cell' && this.engine.isCellSelected(activeCell.rowIndex, column.key);
|
|
1911
2099
|
this.engine.selectCell(activeCell.rowIndex, column.key);
|
|
1912
|
-
|
|
2100
|
+
if (!unchanged)
|
|
2101
|
+
this.announceCellSelection(activeCell.rowIndex, column.key);
|
|
1913
2102
|
this.requestUpdate();
|
|
1914
2103
|
}
|
|
1915
2104
|
}
|
|
1916
2105
|
else if (this.isRowSelectionMode()) {
|
|
1917
2106
|
event.preventDefault();
|
|
1918
|
-
this.
|
|
1919
|
-
this.announceRowSelection(activeCell.rowIndex);
|
|
1920
|
-
this.requestUpdate();
|
|
2107
|
+
this.selectRow(activeCell.rowIndex, this.getRowSelectionIntent(event));
|
|
1921
2108
|
}
|
|
1922
2109
|
return;
|
|
1923
2110
|
}
|
|
@@ -1996,9 +2183,7 @@ export class DataGrid extends LitElement {
|
|
|
1996
2183
|
if (!this.isRowSelectionMode()) {
|
|
1997
2184
|
return;
|
|
1998
2185
|
}
|
|
1999
|
-
this.
|
|
2000
|
-
this.announceRowSelection(rowIndex);
|
|
2001
|
-
this.requestUpdate();
|
|
2186
|
+
this.selectRow(rowIndex, this.getRowSelectionIntent(event));
|
|
2002
2187
|
}
|
|
2003
2188
|
handleRowMouseDown(event) {
|
|
2004
2189
|
if (this.isRowSelectionMode() && event.shiftKey) {
|
|
@@ -2012,8 +2197,10 @@ export class DataGrid extends LitElement {
|
|
|
2012
2197
|
return;
|
|
2013
2198
|
}
|
|
2014
2199
|
event.stopPropagation();
|
|
2200
|
+
const unchanged = this.engine.getConfig().selection.mode === 'cell' && this.engine.isCellSelected(rowIndex, column.key);
|
|
2015
2201
|
this.engine.selectCell(rowIndex, column.key);
|
|
2016
|
-
|
|
2202
|
+
if (!unchanged)
|
|
2203
|
+
this.announceCellSelection(rowIndex, column.key);
|
|
2017
2204
|
this.requestUpdate();
|
|
2018
2205
|
}
|
|
2019
2206
|
renderCell(column, row, rowIndex) {
|
|
@@ -2066,7 +2253,6 @@ export class DataGrid extends LitElement {
|
|
|
2066
2253
|
`;
|
|
2067
2254
|
}
|
|
2068
2255
|
set data(value) {
|
|
2069
|
-
this.activeCell = null;
|
|
2070
2256
|
this.clearQuickSearchDebounce();
|
|
2071
2257
|
this.sourceRowCount = Array.isArray(value) ? value.length : 0;
|
|
2072
2258
|
this.engine.setData(value ?? []);
|
|
@@ -2076,9 +2262,7 @@ export class DataGrid extends LitElement {
|
|
|
2076
2262
|
this.requestUpdate();
|
|
2077
2263
|
}
|
|
2078
2264
|
handleRowCheckboxChange(rowIndex) {
|
|
2079
|
-
this.
|
|
2080
|
-
this.announceRowSelection(rowIndex);
|
|
2081
|
-
this.requestUpdate();
|
|
2265
|
+
this.selectRow(rowIndex, 'toggle');
|
|
2082
2266
|
}
|
|
2083
2267
|
getDisplayedSelectedRowCount() {
|
|
2084
2268
|
const rowCount = this.engine.getRowCount();
|
|
@@ -2109,13 +2293,89 @@ export class DataGrid extends LitElement {
|
|
|
2109
2293
|
}
|
|
2110
2294
|
this.requestUpdate();
|
|
2111
2295
|
}
|
|
2296
|
+
get inferColumns() {
|
|
2297
|
+
return this._inferColumns;
|
|
2298
|
+
}
|
|
2299
|
+
set inferColumns(value) {
|
|
2300
|
+
if (this._inferColumns === value)
|
|
2301
|
+
return;
|
|
2302
|
+
this._inferColumns = value;
|
|
2303
|
+
this.requestUpdate();
|
|
2304
|
+
}
|
|
2305
|
+
/** Reconcile an explicit schema, optionally switching its persistence namespace atomically. */
|
|
2306
|
+
updateColumnSchema(value, applyOrder = false, storageKey = this._columnStateStorageKey) {
|
|
2307
|
+
const columns = value.map(column => ({ ...column }));
|
|
2308
|
+
const nextByKey = new Map(columns.map(column => [column.key, column]));
|
|
2309
|
+
if (nextByKey.size !== columns.length)
|
|
2310
|
+
throw new Error('Duplicate column schema keys.');
|
|
2311
|
+
const previousByKey = new Map(this.sourceColumnDefs.map(column => [column.key, column]));
|
|
2312
|
+
const activeCell = this.activeCell;
|
|
2313
|
+
const activeColumnKey = activeCell
|
|
2314
|
+
? this.getVisibleColumns()[activeCell.columnIndex]?.key
|
|
2315
|
+
: undefined;
|
|
2316
|
+
const nextStorageKey = typeof storageKey === 'string' && storageKey.trim() ? storageKey : null;
|
|
2317
|
+
const storageChanged = nextStorageKey !== this._columnStateStorageKey;
|
|
2318
|
+
this.finishColumnResize(false);
|
|
2319
|
+
this.finishColumnReorder(false);
|
|
2320
|
+
if (storageChanged) {
|
|
2321
|
+
// Install the target schema and defaults before restoring its namespace.
|
|
2322
|
+
// Do not reconcile/persist the outgoing view, or carry its customization
|
|
2323
|
+
// into an unsaved target. A namespace switch itself performs no writes.
|
|
2324
|
+
this._columnStateStorageKey = nextStorageKey;
|
|
2325
|
+
this.sourceColumnDefs = columns;
|
|
2326
|
+
this.columnDefs = [...columns];
|
|
2327
|
+
this.columnWidths.clear();
|
|
2328
|
+
this.hiddenColumnKeys = new Set(columns.filter(column => column.hidden).map(column => column.key));
|
|
2329
|
+
this.restorePersistedColumnState(false);
|
|
2330
|
+
}
|
|
2331
|
+
else {
|
|
2332
|
+
for (const key of this.columnWidths.keys()) {
|
|
2333
|
+
if (!nextByKey.has(key))
|
|
2334
|
+
this.columnWidths.delete(key);
|
|
2335
|
+
}
|
|
2336
|
+
for (const key of this.hiddenColumnKeys) {
|
|
2337
|
+
if (!nextByKey.has(key))
|
|
2338
|
+
this.hiddenColumnKeys.delete(key);
|
|
2339
|
+
}
|
|
2340
|
+
for (const column of columns) {
|
|
2341
|
+
const previous = previousByKey.get(column.key);
|
|
2342
|
+
if (!previous || previous.width !== column.width)
|
|
2343
|
+
this.columnWidths.delete(column.key);
|
|
2344
|
+
if (!previous || previous.hidden !== column.hidden) {
|
|
2345
|
+
if (column.hidden)
|
|
2346
|
+
this.hiddenColumnKeys.add(column.key);
|
|
2347
|
+
else
|
|
2348
|
+
this.hiddenColumnKeys.delete(column.key);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
// Surviving columns keep user order; newly registered columns follow them.
|
|
2352
|
+
// An explicit Order change reapplies the complete deterministic schema order.
|
|
2353
|
+
const retained = this.columnDefs.filter(column => nextByKey.has(column.key))
|
|
2354
|
+
.map(column => nextByKey.get(column.key));
|
|
2355
|
+
const retainedKeys = new Set(retained.map(column => column.key));
|
|
2356
|
+
this.columnDefs = applyOrder ? columns : [
|
|
2357
|
+
...retained, ...columns.filter(column => !retainedKeys.has(column.key))
|
|
2358
|
+
];
|
|
2359
|
+
this.sourceColumnDefs = columns;
|
|
2360
|
+
this.persistColumnState();
|
|
2361
|
+
}
|
|
2362
|
+
this.clearBestFitWidths();
|
|
2363
|
+
if (activeCell) {
|
|
2364
|
+
const columnIndex = this.getVisibleColumns().findIndex(column => column.key === activeColumnKey);
|
|
2365
|
+
this.activeCell = columnIndex < 0 ? null : { ...activeCell, columnIndex };
|
|
2366
|
+
}
|
|
2367
|
+
this.requestUpdate();
|
|
2368
|
+
}
|
|
2112
2369
|
set columns(value) {
|
|
2113
|
-
|
|
2370
|
+
const activeCell = this.activeCell;
|
|
2114
2371
|
this.sourceColumnDefs = Array.isArray(value) ? [...value] : [];
|
|
2115
2372
|
this.columnDefs = [...this.sourceColumnDefs];
|
|
2116
2373
|
this.hiddenColumnKeys = new Set(this.sourceColumnDefs.filter((column) => column.hidden).map((column) => column.key));
|
|
2117
2374
|
this.clearBestFitWidths();
|
|
2118
2375
|
this.restorePersistedColumnState();
|
|
2376
|
+
// Structural synchronization must not discard navigation. This also
|
|
2377
|
+
// restores the position after persisted column state has been applied.
|
|
2378
|
+
this.activeCell = activeCell;
|
|
2119
2379
|
this.requestUpdate();
|
|
2120
2380
|
}
|
|
2121
2381
|
get columnStateStorageKey() {
|
|
@@ -2210,6 +2470,7 @@ export class DataGrid extends LitElement {
|
|
|
2210
2470
|
}
|
|
2211
2471
|
// Ensure start/end indices are within valid bounds to avoid empty renders
|
|
2212
2472
|
const safeStartIndex = Math.max(0, Math.min(range.startIndex, Math.max(0, count)));
|
|
2473
|
+
let renderedRowStart = safeStartIndex;
|
|
2213
2474
|
const safeEndIndex = Math.max(safeStartIndex, Math.min(range.endIndex, count));
|
|
2214
2475
|
let rows = this.engine.getVisibleRows(safeStartIndex, safeEndIndex);
|
|
2215
2476
|
if (rows.length === 0 && count > 0) {
|
|
@@ -2219,6 +2480,7 @@ export class DataGrid extends LitElement {
|
|
|
2219
2480
|
const fallbackStart = Math.max(0, count - visibleCountFallback);
|
|
2220
2481
|
const fallbackEnd = count;
|
|
2221
2482
|
rows = this.engine.getVisibleRows(fallbackStart, fallbackEnd);
|
|
2483
|
+
renderedRowStart = fallbackStart;
|
|
2222
2484
|
}
|
|
2223
2485
|
const columns = this.getVisibleColumns();
|
|
2224
2486
|
const columnWidths = columns.map((column) => this.getColumnWidth(column));
|
|
@@ -2230,6 +2492,23 @@ export class DataGrid extends LitElement {
|
|
|
2230
2492
|
});
|
|
2231
2493
|
const columnRange = this.columnVirtualizer.getState();
|
|
2232
2494
|
const visibleColumns = columns.slice(columnRange.startIndex, columnRange.endIndex);
|
|
2495
|
+
this.reconcileActiveCell(count, columns.length);
|
|
2496
|
+
const activeCellIsRendered = this.activeCell !== null
|
|
2497
|
+
&& this.activeCell.rowIndex >= renderedRowStart
|
|
2498
|
+
&& this.activeCell.rowIndex < renderedRowStart + rows.length
|
|
2499
|
+
&& this.activeCell.columnIndex >= columnRange.startIndex
|
|
2500
|
+
&& this.activeCell.columnIndex < columnRange.endIndex;
|
|
2501
|
+
this.renderedActiveCellId = activeCellIsRendered && this.activeCell
|
|
2502
|
+
? this.getGridCellId(this.activeCell.rowIndex, this.activeCell.columnIndex)
|
|
2503
|
+
: null;
|
|
2504
|
+
if (!activeCellIsRendered || this.renderedRowStart !== renderedRowStart || this.renderedColumnStart !== columnRange.startIndex) {
|
|
2505
|
+
// Preserve logical navigation when the cell leaves either rendered range.
|
|
2506
|
+
// A focused node can also be reused for another cell when the range moves.
|
|
2507
|
+
// Hand off once; later scroll renders don't touch focus on the viewport.
|
|
2508
|
+
this.shouldFocusViewport = this.gridCellHasFocus;
|
|
2509
|
+
}
|
|
2510
|
+
this.renderedRowStart = renderedRowStart;
|
|
2511
|
+
this.renderedColumnStart = columnRange.startIndex;
|
|
2233
2512
|
const visibleColumnWidths = columnWidths.slice(columnRange.startIndex, columnRange.endIndex);
|
|
2234
2513
|
const isFirstVisibleColumn = (column, index) => index === 0;
|
|
2235
2514
|
const gridTemplate = [
|
|
@@ -2439,22 +2718,25 @@ export class DataGrid extends LitElement {
|
|
|
2439
2718
|
</div>
|
|
2440
2719
|
${visibleColumns.length === 0
|
|
2441
2720
|
? html `<div class="column-chooser-empty">
|
|
2442
|
-
<button type="button" @click=${() => this.toggleColumnChooser()}>Columns</button>
|
|
2721
|
+
<button class="columns-button" type="button" @click=${(event) => this.toggleColumnChooser(event)}>Columns</button>
|
|
2443
2722
|
</div>`
|
|
2444
2723
|
: ''}
|
|
2445
2724
|
<div
|
|
2446
2725
|
class="viewport"
|
|
2447
2726
|
role="grid"
|
|
2727
|
+
aria-multiselectable=${['multi-row', 'multi-cell'].includes(this.engine.getConfig().selection.mode) ? 'true' : nothing}
|
|
2448
2728
|
aria-label=${this.ariaLabel || 'Data grid'}
|
|
2449
2729
|
aria-rowcount=${count + 1}
|
|
2450
2730
|
aria-colcount=${columns.length + (this.isRowHeaderVisible() ? 1 : 0)}
|
|
2451
|
-
aria-activedescendant=${this.
|
|
2731
|
+
aria-activedescendant=${this.renderedActiveCellId ?? nothing}
|
|
2452
2732
|
aria-describedby=${`${this.gridId}-description`}
|
|
2453
2733
|
aria-owns=${this.getHeaderRowId()}
|
|
2454
2734
|
style=${`--grid-height: ${this.height}px;`}
|
|
2455
2735
|
tabindex="0"
|
|
2456
2736
|
@scroll=${this.handleScroll}
|
|
2457
2737
|
@focus=${this.handleViewportFocus}
|
|
2738
|
+
@focusin=${this.handleGridFocusIn}
|
|
2739
|
+
@focusout=${() => { this.gridCellHasFocus = false; }}
|
|
2458
2740
|
|
|
2459
2741
|
@keydown=${this.handleKeyDown}
|
|
2460
2742
|
@click=${() => this.closeHeaderMenu()}
|
|
@@ -2466,7 +2748,7 @@ export class DataGrid extends LitElement {
|
|
|
2466
2748
|
style=${`--offset-top: ${displayOffsetTop}px; padding-bottom: ${displayBottomPadding}px;`}
|
|
2467
2749
|
>
|
|
2468
2750
|
${rows.map((row, index) => {
|
|
2469
|
-
const rowIndex =
|
|
2751
|
+
const rowIndex = renderedRowStart + index;
|
|
2470
2752
|
const height = this.getRowHeight(rowIndex);
|
|
2471
2753
|
return html `
|
|
2472
2754
|
<div
|
|
@@ -2491,31 +2773,35 @@ export class DataGrid extends LitElement {
|
|
|
2491
2773
|
${pagination.enabled
|
|
2492
2774
|
? html `
|
|
2493
2775
|
<div class="pagination" role="navigation" aria-label="Pagination">
|
|
2494
|
-
<
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2776
|
+
<div class="pagination-summary">
|
|
2777
|
+
<span>${pagination.totalRows} rows</span>
|
|
2778
|
+
<label>
|
|
2779
|
+
Rows per page
|
|
2780
|
+
<select
|
|
2781
|
+
aria-label="Rows per page"
|
|
2782
|
+
@change=${(event) => this.setPageSize(Number(event.target.value))}
|
|
2783
|
+
>
|
|
2784
|
+
${pageSizeOptions.map((pageSize) => html `
|
|
2785
|
+
<option value=${pageSize} ?selected=${pageSize === pagination.pageSize}>
|
|
2786
|
+
${pageSize}
|
|
2787
|
+
</option>
|
|
2788
|
+
`)}
|
|
2789
|
+
</select>
|
|
2790
|
+
</label>
|
|
2791
|
+
</div>
|
|
2792
|
+
<div class="pagination-navigation">
|
|
2793
|
+
<span>Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}</span>
|
|
2794
|
+
<button
|
|
2795
|
+
type="button"
|
|
2796
|
+
?disabled=${pagination.pageIndex === 0}
|
|
2797
|
+
@click=${() => this.setPage(pagination.pageIndex - 1)}
|
|
2798
|
+
>Previous</button>
|
|
2799
|
+
<button
|
|
2800
|
+
type="button"
|
|
2801
|
+
?disabled=${pagination.pageCount === 0 || pagination.pageIndex >= pagination.pageCount - 1}
|
|
2802
|
+
@click=${() => this.setPage(pagination.pageIndex + 1)}
|
|
2803
|
+
>Next</button>
|
|
2804
|
+
</div>
|
|
2519
2805
|
</div>
|
|
2520
2806
|
`
|
|
2521
2807
|
: ''}
|