@happyvertical/smrt-ui 0.42.4 → 0.42.6
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 +157 -3
- package/dist/components/data/DataTable.svelte +1097 -111
- package/dist/components/data/DataTable.svelte.d.ts +1 -1
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +45 -5
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +153 -11
- package/dist/components/data/DataTableLayout.d.ts +37 -0
- package/dist/components/data/DataTableLayout.d.ts.map +1 -0
- package/dist/components/data/DataTableLayout.js +135 -0
- package/dist/components/data/DataTablePerformance.d.ts +31 -0
- package/dist/components/data/DataTablePerformance.d.ts.map +1 -0
- package/dist/components/data/DataTablePerformance.js +46 -0
- package/dist/components/data/DataTableVirtualization.d.ts +71 -0
- package/dist/components/data/DataTableVirtualization.d.ts.map +1 -0
- package/dist/components/data/DataTableVirtualization.js +100 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts +2 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.d.ts.map +1 -0
- package/dist/components/data/__benchmarks__/DataTable.bench.js +53 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts +27 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTableConformanceFixture.js +141 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts +10 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.d.ts.map +1 -0
- package/dist/components/data/__fixtures__/DataTablePerformanceFixture.js +23 -0
- package/dist/components/data/__tests__/DataTable.test.js +393 -8
- package/dist/components/data/__tests__/DataTableConformance.test.js +212 -0
- package/dist/components/data/__tests__/DataTableController.test.js +124 -2
- package/dist/components/data/__tests__/DataTableLayout.test.js +195 -0
- package/dist/components/data/__tests__/DataTablePerformance.test.js +21 -0
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +80 -0
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +255 -0
- package/dist/components/data/__tests__/data-surface.test.js +675 -0
- package/dist/components/data/data-surface.d.ts +246 -0
- package/dist/components/data/data-surface.d.ts.map +1 -0
- package/dist/components/data/data-surface.js +1102 -0
- package/dist/components/data/index.d.ts +3 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +3 -0
- package/dist/components/data/types.d.ts +83 -1
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/i18n/strings.d.ts +25 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +27 -2
- package/dist/svelte/playground/DataTablePreview.svelte +374 -5
- package/dist/svelte/playground/DataTablePreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +2 -2
- package/package.json +3 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Material 3 styling with theme token support
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { type Snippet, tick } from 'svelte';
|
|
15
15
|
import { M } from '../../i18n/strings.js';
|
|
16
16
|
import Trans from '../../i18n/Trans.svelte';
|
|
17
17
|
import { useI18n } from '../../i18n/use-i18n.js';
|
|
@@ -27,9 +27,24 @@ import {
|
|
|
27
27
|
type DataTableSnapshot,
|
|
28
28
|
type DataTableViewState,
|
|
29
29
|
type DataTableViewStateInput,
|
|
30
|
+
dataTableRowIdKey,
|
|
30
31
|
} from './DataTableController.js';
|
|
31
32
|
import { resolveDataTableRows } from './DataTableIdentity.js';
|
|
32
|
-
import
|
|
33
|
+
import {
|
|
34
|
+
type DataTableResolvedColumn,
|
|
35
|
+
resolveDataTableLayout,
|
|
36
|
+
} from './DataTableLayout.js';
|
|
37
|
+
import {
|
|
38
|
+
maximumDataTableVirtualScrollTop,
|
|
39
|
+
resolveDataTableVirtualWindow,
|
|
40
|
+
scrollTopForDataTableRow,
|
|
41
|
+
} from './DataTableVirtualization.js';
|
|
42
|
+
import type {
|
|
43
|
+
DataTableColumn,
|
|
44
|
+
DataTableProps,
|
|
45
|
+
DataTableStructuralRow,
|
|
46
|
+
SortState,
|
|
47
|
+
} from './types.js';
|
|
33
48
|
import { defaultSort, getNestedValue } from './types.js';
|
|
34
49
|
|
|
35
50
|
const { t } = useI18n();
|
|
@@ -50,6 +65,7 @@ let {
|
|
|
50
65
|
selected = $bindable(new Set<string | number>()),
|
|
51
66
|
onSelectionChange,
|
|
52
67
|
onRowClick,
|
|
68
|
+
rowLabel,
|
|
53
69
|
sortable = false,
|
|
54
70
|
sort = $bindable({ columnId: null, direction: null }),
|
|
55
71
|
onSortChange,
|
|
@@ -58,6 +74,7 @@ let {
|
|
|
58
74
|
page = $bindable(1),
|
|
59
75
|
pageSize,
|
|
60
76
|
manualPagination = false,
|
|
77
|
+
virtualization,
|
|
61
78
|
totalRows,
|
|
62
79
|
onPageChange,
|
|
63
80
|
expanded = $bindable(new Set<string | number>()),
|
|
@@ -66,8 +83,14 @@ let {
|
|
|
66
83
|
expandedContent,
|
|
67
84
|
toolbar,
|
|
68
85
|
footer,
|
|
86
|
+
structuralRows = [],
|
|
69
87
|
visibleColumnIds,
|
|
70
88
|
loading = false,
|
|
89
|
+
refreshing = false,
|
|
90
|
+
stale = false,
|
|
91
|
+
partialResults = false,
|
|
92
|
+
error = null,
|
|
93
|
+
onRetry,
|
|
71
94
|
empty,
|
|
72
95
|
rowClass,
|
|
73
96
|
size = 'md',
|
|
@@ -84,6 +107,8 @@ let {
|
|
|
84
107
|
modes,
|
|
85
108
|
}: ExtendedProps<T> = $props();
|
|
86
109
|
|
|
110
|
+
const dataTableId = $props.id();
|
|
111
|
+
|
|
87
112
|
function legacyModes(): DataTableModes {
|
|
88
113
|
return {
|
|
89
114
|
filtering: modes?.filtering ?? 'local',
|
|
@@ -93,21 +118,23 @@ function legacyModes(): DataTableModes {
|
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
function legacyViewState(): Partial<DataTableViewState> {
|
|
96
|
-
|
|
121
|
+
const next: Partial<DataTableViewState> = {
|
|
97
122
|
sorting:
|
|
98
123
|
sort.columnId && sort.direction
|
|
99
124
|
? [{ columnId: sort.columnId, direction: sort.direction }]
|
|
100
125
|
: [],
|
|
101
126
|
page,
|
|
102
127
|
pageSize: pageSize ?? null,
|
|
103
|
-
columnOrder: columns.map((column) => column.id),
|
|
104
|
-
columnVisibility: columns.map((column) => ({
|
|
105
|
-
columnId: column.id,
|
|
106
|
-
visible: !visibleColumnIds || visibleColumnIds.has(column.id),
|
|
107
|
-
})),
|
|
108
128
|
selectedRowIds: [...selected],
|
|
109
129
|
expandedRowIds: [...expanded],
|
|
110
130
|
};
|
|
131
|
+
if (visibleColumnIds) {
|
|
132
|
+
next.columnVisibility = columns.map((column) => ({
|
|
133
|
+
columnId: column.id,
|
|
134
|
+
visible: visibleColumnIds.has(column.id),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
function mergedLegacyState(): Omit<DataTableViewState, 'selection'> {
|
|
@@ -121,8 +148,12 @@ const localController = createDataTableController({
|
|
|
121
148
|
});
|
|
122
149
|
|
|
123
150
|
let controllerSnapshot = $state<DataTableSnapshot>(localController.snapshot());
|
|
151
|
+
let measuredColumnWidths = $state<Record<string, number>>({});
|
|
124
152
|
let publishedLegacySignature: string | undefined;
|
|
125
153
|
let localControllerInitialized = false;
|
|
154
|
+
let hasHorizontalOverflow = $state(false);
|
|
155
|
+
let canScrollLeft = $state(false);
|
|
156
|
+
let canScrollRight = $state(false);
|
|
126
157
|
|
|
127
158
|
function activeController(): DataTableController {
|
|
128
159
|
return controller ?? localController;
|
|
@@ -187,7 +218,14 @@ $effect(() => {
|
|
|
187
218
|
|
|
188
219
|
localController.setControlled(controlledState !== undefined);
|
|
189
220
|
localController.setModes(legacyModes());
|
|
190
|
-
|
|
221
|
+
const columnIds = columns.map((column) => column.id);
|
|
222
|
+
const hiddenColumnIds = columns
|
|
223
|
+
.filter(
|
|
224
|
+
(column) =>
|
|
225
|
+
column.hidden ||
|
|
226
|
+
(visibleColumnIds !== undefined && !visibleColumnIds.has(column.id)),
|
|
227
|
+
)
|
|
228
|
+
.map((column) => column.id);
|
|
191
229
|
|
|
192
230
|
if (!localControllerInitialized) {
|
|
193
231
|
localControllerInitialized = true;
|
|
@@ -196,9 +234,12 @@ $effect(() => {
|
|
|
196
234
|
...initialState,
|
|
197
235
|
};
|
|
198
236
|
localController.replaceState(nextState);
|
|
237
|
+
localController.setColumnIds(columnIds, hiddenColumnIds);
|
|
199
238
|
return;
|
|
200
239
|
}
|
|
201
240
|
|
|
241
|
+
localController.setColumnIds(columnIds, hiddenColumnIds);
|
|
242
|
+
|
|
202
243
|
if (controlledState !== undefined) {
|
|
203
244
|
localController.replaceState(controlledState);
|
|
204
245
|
return;
|
|
@@ -214,7 +255,16 @@ $effect(() => {
|
|
|
214
255
|
|
|
215
256
|
$effect(() => {
|
|
216
257
|
const current = activeController();
|
|
217
|
-
current.setColumnIds(
|
|
258
|
+
current.setColumnIds(
|
|
259
|
+
columns.map((column) => column.id),
|
|
260
|
+
columns
|
|
261
|
+
.filter(
|
|
262
|
+
(column) =>
|
|
263
|
+
column.hidden ||
|
|
264
|
+
(visibleColumnIds !== undefined && !visibleColumnIds.has(column.id)),
|
|
265
|
+
)
|
|
266
|
+
.map((column) => column.id),
|
|
267
|
+
);
|
|
218
268
|
controllerSnapshot = current.snapshot();
|
|
219
269
|
return current.subscribe((transition) => {
|
|
220
270
|
controllerSnapshot = transition.next;
|
|
@@ -234,13 +284,23 @@ $effect(() => {
|
|
|
234
284
|
|
|
235
285
|
const tableState = $derived(controllerSnapshot.state);
|
|
236
286
|
const tableModes = $derived(controllerSnapshot.modes);
|
|
287
|
+
const constrainedLayoutState = $derived.by(() => ({
|
|
288
|
+
...tableState,
|
|
289
|
+
columnWidths: tableState.columnWidths.map((entry) => {
|
|
290
|
+
const column = columns.find((candidate) => candidate.id === entry.columnId);
|
|
291
|
+
return column
|
|
292
|
+
? { ...entry, width: clampedColumnWidth(column, entry.width) }
|
|
293
|
+
: entry;
|
|
294
|
+
}),
|
|
295
|
+
}));
|
|
237
296
|
const requiresStableRowIdentity = $derived(
|
|
238
297
|
selectable ||
|
|
239
298
|
Boolean(expandedContent) ||
|
|
240
299
|
agentAddressable ||
|
|
241
300
|
tableModes.filtering === 'manual' ||
|
|
242
301
|
tableModes.sorting === 'manual' ||
|
|
243
|
-
tableModes.pagination === 'manual'
|
|
302
|
+
tableModes.pagination === 'manual' ||
|
|
303
|
+
Boolean(virtualization),
|
|
244
304
|
);
|
|
245
305
|
const sourceRows = $derived.by(() =>
|
|
246
306
|
resolveDataTableRows(data, rowKey, {
|
|
@@ -261,6 +321,25 @@ function handleSort(column: DataTableColumn<T>, event: MouseEvent) {
|
|
|
261
321
|
});
|
|
262
322
|
}
|
|
263
323
|
|
|
324
|
+
function usesAutomaticSortButton(column: DataTableColumn<T>): boolean {
|
|
325
|
+
return (
|
|
326
|
+
sortable && column.sortable === true && column.headerSortMode !== 'manual'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function sortActionLabel(column: DataTableColumn<T>): string {
|
|
331
|
+
const columnSort = tableState.sorting.find(
|
|
332
|
+
(sort) => sort.columnId === column.id,
|
|
333
|
+
);
|
|
334
|
+
if (!columnSort) {
|
|
335
|
+
return t(M['ui.data_table.sort_ascending'], { column: column.label });
|
|
336
|
+
}
|
|
337
|
+
if (columnSort.direction === 'asc') {
|
|
338
|
+
return t(M['ui.data_table.sort_descending'], { column: column.label });
|
|
339
|
+
}
|
|
340
|
+
return t(M['ui.data_table.clear_sort'], { column: column.label });
|
|
341
|
+
}
|
|
342
|
+
|
|
264
343
|
function handleRowSelect(key: DataTableRowId, event: Event) {
|
|
265
344
|
event.stopPropagation();
|
|
266
345
|
dispatch({ type: 'toggleRowSelection', rowId: key });
|
|
@@ -305,6 +384,100 @@ function handleRowClick(row: T, index: number) {
|
|
|
305
384
|
onRowClick?.(row, index);
|
|
306
385
|
}
|
|
307
386
|
|
|
387
|
+
function getRowLabel(row: T, index: number): string {
|
|
388
|
+
return (
|
|
389
|
+
rowLabel?.(row, index) ??
|
|
390
|
+
t(M['ui.data_table.row_number'], {
|
|
391
|
+
number: virtualRowOffset + index + 1,
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function expansionContentId(key: DataTableRowId): string {
|
|
397
|
+
return `${dataTableId}-expanded-${encodeURIComponent(dataTableRowIdKey(key))}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isNestedInteractiveTarget(target: EventTarget | null): boolean {
|
|
401
|
+
return (
|
|
402
|
+
target instanceof Element &&
|
|
403
|
+
target.closest(
|
|
404
|
+
'a, button, input, select, textarea, label, summary, [contenteditable="true"], [role="button"], [role="link"], [role="menuitem"], [role="checkbox"], [role="switch"]',
|
|
405
|
+
) !== null
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function handleRowActivation(event: MouseEvent, row: T, index: number) {
|
|
410
|
+
if (isNestedInteractiveTarget(event.target)) return;
|
|
411
|
+
handleRowClick(row, index);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function handleRowKeydown(event: KeyboardEvent, row: T, index: number) {
|
|
415
|
+
if (
|
|
416
|
+
!onRowClick ||
|
|
417
|
+
isNestedInteractiveTarget(event.target) ||
|
|
418
|
+
(event.key !== 'Enter' && event.key !== ' ')
|
|
419
|
+
) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
event.preventDefault();
|
|
423
|
+
handleRowClick(row, index);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function updateOverflowState() {
|
|
427
|
+
const container = tableContainer;
|
|
428
|
+
if (!container) return;
|
|
429
|
+
|
|
430
|
+
const maxScrollLeft = Math.max(
|
|
431
|
+
0,
|
|
432
|
+
container.scrollWidth - container.clientWidth,
|
|
433
|
+
);
|
|
434
|
+
hasHorizontalOverflow = maxScrollLeft > 1;
|
|
435
|
+
canScrollLeft = hasHorizontalOverflow && container.scrollLeft > 1;
|
|
436
|
+
canScrollRight =
|
|
437
|
+
hasHorizontalOverflow && container.scrollLeft < maxScrollLeft - 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function handleOverflowKeydown(event: KeyboardEvent) {
|
|
441
|
+
const container = tableContainer;
|
|
442
|
+
if (!container || !hasHorizontalOverflow || event.target !== container) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const maxScrollLeft = Math.max(
|
|
447
|
+
0,
|
|
448
|
+
container.scrollWidth - container.clientWidth,
|
|
449
|
+
);
|
|
450
|
+
const scrollStep = Math.max(Math.floor(container.clientWidth * 0.8), 40);
|
|
451
|
+
let nextScrollLeft: number | undefined;
|
|
452
|
+
|
|
453
|
+
switch (event.key) {
|
|
454
|
+
case 'ArrowLeft':
|
|
455
|
+
nextScrollLeft = container.scrollLeft - scrollStep;
|
|
456
|
+
break;
|
|
457
|
+
case 'ArrowRight':
|
|
458
|
+
nextScrollLeft = container.scrollLeft + scrollStep;
|
|
459
|
+
break;
|
|
460
|
+
case 'Home':
|
|
461
|
+
nextScrollLeft = 0;
|
|
462
|
+
break;
|
|
463
|
+
case 'End':
|
|
464
|
+
nextScrollLeft = maxScrollLeft;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (nextScrollLeft === undefined) return;
|
|
469
|
+
|
|
470
|
+
const clampedScrollLeft = Math.max(
|
|
471
|
+
0,
|
|
472
|
+
Math.min(maxScrollLeft, nextScrollLeft),
|
|
473
|
+
);
|
|
474
|
+
if (clampedScrollLeft === container.scrollLeft) return;
|
|
475
|
+
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
container.scrollLeft = clampedScrollLeft;
|
|
478
|
+
updateOverflowState();
|
|
479
|
+
}
|
|
480
|
+
|
|
308
481
|
// Action to set indeterminate state (can't be set via HTML attribute)
|
|
309
482
|
function setIndeterminate(node: HTMLInputElement, value: boolean) {
|
|
310
483
|
node.indeterminate = value;
|
|
@@ -315,23 +488,22 @@ function setIndeterminate(node: HTMLInputElement, value: boolean) {
|
|
|
315
488
|
};
|
|
316
489
|
}
|
|
317
490
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
491
|
+
const dataTableLayout = $derived(
|
|
492
|
+
resolveDataTableLayout(columns, constrainedLayoutState, measuredColumnWidths),
|
|
493
|
+
);
|
|
494
|
+
const visibleColumns = $derived(dataTableLayout.columns);
|
|
495
|
+
const dataBodyStructuralRows = $derived(
|
|
496
|
+
structuralRows.filter((row) => row.kind !== 'footer'),
|
|
497
|
+
);
|
|
498
|
+
const footerStructuralRows = $derived(
|
|
499
|
+
structuralRows.filter((row) => row.kind === 'footer'),
|
|
500
|
+
);
|
|
501
|
+
const structuralRowCount = $derived(
|
|
502
|
+
dataBodyStructuralRows.length +
|
|
503
|
+
footerStructuralRows.length +
|
|
504
|
+
(footer ? 1 : 0),
|
|
505
|
+
);
|
|
506
|
+
const headerRowCount = $derived(dataTableLayout.headerRows.length);
|
|
335
507
|
|
|
336
508
|
function sameFilterValue(value: unknown, expected: unknown): boolean {
|
|
337
509
|
return Object.is(value, expected);
|
|
@@ -466,6 +638,169 @@ const displayRows = $derived.by(() => {
|
|
|
466
638
|
return sortedRows.slice(start, start + tableState.pageSize);
|
|
467
639
|
});
|
|
468
640
|
|
|
641
|
+
let tableContainer: HTMLDivElement | undefined = $state();
|
|
642
|
+
let tableCaption: HTMLTableCaptionElement | undefined = $state();
|
|
643
|
+
let tableHead: HTMLTableSectionElement | undefined = $state();
|
|
644
|
+
let tableBody: HTMLTableSectionElement | undefined = $state();
|
|
645
|
+
let tableStructuralBody: HTMLTableSectionElement | undefined = $state();
|
|
646
|
+
let tableFooter: HTMLTableSectionElement | undefined = $state();
|
|
647
|
+
let virtualScrollTop = $state(0);
|
|
648
|
+
let virtualStructuralHeight = $state(0);
|
|
649
|
+
let virtualCaptionHeight = $state(0);
|
|
650
|
+
let virtualFooterHeight = $state(0);
|
|
651
|
+
|
|
652
|
+
const virtualizedBody = $derived(Boolean(virtualization) && !expandedContent);
|
|
653
|
+
|
|
654
|
+
const virtualizationWindow = $derived.by(() =>
|
|
655
|
+
resolveDataTableVirtualWindow({
|
|
656
|
+
options: virtualization,
|
|
657
|
+
rowCount: displayRows.length,
|
|
658
|
+
scrollTop: virtualization?.scrollTop ?? virtualScrollTop,
|
|
659
|
+
hasVariableRowHeight: Boolean(expandedContent),
|
|
660
|
+
headerRowCount,
|
|
661
|
+
summaryRowCount: structuralRowCount,
|
|
662
|
+
}),
|
|
663
|
+
);
|
|
664
|
+
const renderedRows = $derived.by(() =>
|
|
665
|
+
displayRows.slice(
|
|
666
|
+
virtualizationWindow.startIndex,
|
|
667
|
+
virtualizationWindow.endIndex,
|
|
668
|
+
),
|
|
669
|
+
);
|
|
670
|
+
const virtualRowCount = $derived(totalRowCount ?? displayRows.length);
|
|
671
|
+
const virtualRowOffset = $derived(
|
|
672
|
+
tableModes.pagination === 'manual' && tableState.pageSize
|
|
673
|
+
? (tableState.page - 1) * tableState.pageSize
|
|
674
|
+
: 0,
|
|
675
|
+
);
|
|
676
|
+
const virtualContainerHeight = $derived(
|
|
677
|
+
virtualizedBody && virtualization
|
|
678
|
+
? virtualization.viewportHeight + virtualStructuralHeight
|
|
679
|
+
: undefined,
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
function clampVirtualScrollTop(nextScrollTop: number): number {
|
|
683
|
+
if (!virtualization) return 0;
|
|
684
|
+
const maximum = Math.max(
|
|
685
|
+
0,
|
|
686
|
+
maximumDataTableVirtualScrollTop(
|
|
687
|
+
displayRows.length,
|
|
688
|
+
virtualization,
|
|
689
|
+
virtualFooterHeight,
|
|
690
|
+
),
|
|
691
|
+
);
|
|
692
|
+
return Math.min(Math.max(0, nextScrollTop), maximum);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function setVirtualScrollTop(nextScrollTop: number, notify = false) {
|
|
696
|
+
if (!virtualization || !tableContainer || !virtualizedBody) return;
|
|
697
|
+
const clampedScrollTop = clampVirtualScrollTop(nextScrollTop);
|
|
698
|
+
tableContainer.scrollTop = clampedScrollTop;
|
|
699
|
+
virtualScrollTop = clampedScrollTop;
|
|
700
|
+
if (notify) virtualization.onScrollTopChange?.(clampedScrollTop);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function handleTableScroll(event: Event) {
|
|
704
|
+
if (!virtualizationWindow.enabled) return;
|
|
705
|
+
const nextScrollTop = (event.currentTarget as HTMLDivElement).scrollTop;
|
|
706
|
+
const clampedScrollTop = clampVirtualScrollTop(nextScrollTop);
|
|
707
|
+
virtualScrollTop = clampedScrollTop;
|
|
708
|
+
virtualization?.onScrollTopChange?.(clampedScrollTop);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function handleVirtualizationKeydown(event: KeyboardEvent) {
|
|
712
|
+
if (!virtualizationWindow.enabled || !virtualization || !tableContainer)
|
|
713
|
+
return;
|
|
714
|
+
const pageStep = Math.max(
|
|
715
|
+
virtualization.rowHeight,
|
|
716
|
+
virtualization.viewportHeight - virtualization.rowHeight,
|
|
717
|
+
);
|
|
718
|
+
const currentScrollTop = tableContainer.scrollTop;
|
|
719
|
+
const nextScrollTop =
|
|
720
|
+
event.key === 'ArrowDown'
|
|
721
|
+
? currentScrollTop + virtualization.rowHeight
|
|
722
|
+
: event.key === 'ArrowUp'
|
|
723
|
+
? currentScrollTop - virtualization.rowHeight
|
|
724
|
+
: event.key === 'PageDown'
|
|
725
|
+
? currentScrollTop + pageStep
|
|
726
|
+
: event.key === 'PageUp'
|
|
727
|
+
? currentScrollTop - pageStep
|
|
728
|
+
: event.key === 'Home'
|
|
729
|
+
? 0
|
|
730
|
+
: event.key === 'End'
|
|
731
|
+
? Number.POSITIVE_INFINITY
|
|
732
|
+
: undefined;
|
|
733
|
+
if (nextScrollTop === undefined) return;
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
setVirtualScrollTop(nextScrollTop, true);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function handleRowFocus(rowId: DataTableRowId) {
|
|
739
|
+
virtualization?.onFocusedRowIdChange?.(rowId);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
$effect(() => {
|
|
743
|
+
if (!virtualizedBody || !tableContainer) return;
|
|
744
|
+
const controlledScrollTop = virtualization?.scrollTop;
|
|
745
|
+
if (controlledScrollTop !== undefined) {
|
|
746
|
+
setVirtualScrollTop(controlledScrollTop);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
$effect(() => {
|
|
751
|
+
if (!virtualizedBody || !virtualization || !tableContainer) return;
|
|
752
|
+
const focusRowId = virtualization.focusedRowId;
|
|
753
|
+
const focusIndex =
|
|
754
|
+
focusRowId == null
|
|
755
|
+
? -1
|
|
756
|
+
: displayRows.findIndex(({ rowId }) => rowId === focusRowId);
|
|
757
|
+
if (focusIndex < 0 || focusRowId == null) return;
|
|
758
|
+
const nextScrollTop = scrollTopForDataTableRow(
|
|
759
|
+
focusIndex,
|
|
760
|
+
displayRows.length,
|
|
761
|
+
virtualization,
|
|
762
|
+
tableContainer.scrollTop,
|
|
763
|
+
);
|
|
764
|
+
setVirtualScrollTop(nextScrollTop, virtualization.scrollTop !== undefined);
|
|
765
|
+
const focusContainer = tableContainer;
|
|
766
|
+
void tick().then(() => {
|
|
767
|
+
const focusedRow = Array.from(
|
|
768
|
+
focusContainer.querySelectorAll<HTMLTableRowElement>('tr[data-row-id]'),
|
|
769
|
+
).find((row) => row.dataset.rowId === dataTableRowIdKey(focusRowId));
|
|
770
|
+
focusedRow?.focus({ preventScroll: true });
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
$effect(() => {
|
|
775
|
+
if (!virtualizedBody || !tableBody) {
|
|
776
|
+
virtualStructuralHeight = 0;
|
|
777
|
+
virtualCaptionHeight = 0;
|
|
778
|
+
virtualFooterHeight = 0;
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const body = tableBody;
|
|
783
|
+
const captionElement = tableCaption;
|
|
784
|
+
const head = tableHead;
|
|
785
|
+
const structuralBody = tableStructuralBody;
|
|
786
|
+
const footerElement = tableFooter;
|
|
787
|
+
const measureStructure = () => {
|
|
788
|
+
virtualStructuralHeight = body.offsetTop;
|
|
789
|
+
virtualCaptionHeight = captionElement?.offsetHeight ?? 0;
|
|
790
|
+
virtualFooterHeight =
|
|
791
|
+
(structuralBody?.offsetHeight ?? 0) + (footerElement?.offsetHeight ?? 0);
|
|
792
|
+
};
|
|
793
|
+
measureStructure();
|
|
794
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
795
|
+
|
|
796
|
+
const observer = new ResizeObserver(measureStructure);
|
|
797
|
+
if (captionElement) observer.observe(captionElement);
|
|
798
|
+
if (head) observer.observe(head);
|
|
799
|
+
if (structuralBody) observer.observe(structuralBody);
|
|
800
|
+
if (footerElement) observer.observe(footerElement);
|
|
801
|
+
return () => observer.disconnect();
|
|
802
|
+
});
|
|
803
|
+
|
|
469
804
|
$effect(() => {
|
|
470
805
|
const total = totalRows;
|
|
471
806
|
if (total === undefined) return;
|
|
@@ -502,100 +837,419 @@ const someSelected = $derived(
|
|
|
502
837
|
!allSelected,
|
|
503
838
|
);
|
|
504
839
|
|
|
840
|
+
const errorMessage = $derived.by(() => {
|
|
841
|
+
if (error == null) return undefined;
|
|
842
|
+
const message = typeof error === 'string' ? error : error.message;
|
|
843
|
+
return message.trim() || t(M['ui.data_table.load_error']);
|
|
844
|
+
});
|
|
845
|
+
const hasRenderedRows = $derived(displayRows.length > 0);
|
|
846
|
+
const isInitialLoading = $derived(
|
|
847
|
+
(loading || refreshing) && !hasRenderedRows && !errorMessage,
|
|
848
|
+
);
|
|
849
|
+
const isRefreshing = $derived(
|
|
850
|
+
(loading || refreshing) && hasRenderedRows && !errorMessage,
|
|
851
|
+
);
|
|
852
|
+
const isBusy = $derived((loading || refreshing) && !errorMessage);
|
|
853
|
+
|
|
505
854
|
const columnCount = $derived(
|
|
506
855
|
visibleColumns.length + (selectable ? 1 : 0) + (expandedContent ? 1 : 0),
|
|
507
856
|
);
|
|
857
|
+
const overflowRegionLabel = $derived(
|
|
858
|
+
caption
|
|
859
|
+
? t(M['ui.data_table.overflow_region'], { caption })
|
|
860
|
+
: t(M['ui.data_table.default_overflow_region']),
|
|
861
|
+
);
|
|
508
862
|
|
|
509
863
|
function getCellValue(row: T, column: DataTableColumn<T>): unknown {
|
|
510
864
|
const accessor = column.accessor ?? column.id;
|
|
511
865
|
return getNestedValue(row, String(accessor));
|
|
512
866
|
}
|
|
513
867
|
|
|
868
|
+
function widthForColumn(
|
|
869
|
+
column: DataTableResolvedColumn<T>,
|
|
870
|
+
): string | undefined {
|
|
871
|
+
return column.width === undefined ? column.column.width : `${column.width}px`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function observeColumnWidth(node: HTMLElement, initialColumnId: string) {
|
|
875
|
+
let columnId = initialColumnId;
|
|
876
|
+
const recordWidth = () => {
|
|
877
|
+
const width = Math.ceil(node.getBoundingClientRect().width);
|
|
878
|
+
if (width <= 0 || measuredColumnWidths[columnId] === width) return;
|
|
879
|
+
measuredColumnWidths = { ...measuredColumnWidths, [columnId]: width };
|
|
880
|
+
};
|
|
881
|
+
recordWidth();
|
|
882
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
883
|
+
const observer = new ResizeObserver(recordWidth);
|
|
884
|
+
observer.observe(node);
|
|
885
|
+
return {
|
|
886
|
+
update(nextColumnId: string) {
|
|
887
|
+
columnId = nextColumnId;
|
|
888
|
+
recordWidth();
|
|
889
|
+
},
|
|
890
|
+
destroy() {
|
|
891
|
+
observer.disconnect();
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function numberFromCssPixels(value: string | undefined): number | undefined {
|
|
897
|
+
if (!value) return undefined;
|
|
898
|
+
const match = /^(\d+(?:\.\d+)?)(px|rem)$/.exec(value.trim());
|
|
899
|
+
if (!match) return undefined;
|
|
900
|
+
const number = Number(match[1]);
|
|
901
|
+
if (match[2] === 'px') return number;
|
|
902
|
+
const rootFontSize =
|
|
903
|
+
typeof document === 'undefined'
|
|
904
|
+
? undefined
|
|
905
|
+
: /^\d+(?:\.\d+)?px$/.exec(
|
|
906
|
+
getComputedStyle(document.documentElement).fontSize.trim(),
|
|
907
|
+
);
|
|
908
|
+
return number * (rootFontSize ? Number(rootFontSize[0].slice(0, -2)) : 16);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function minimumWidth(column: DataTableColumn<T>): number {
|
|
912
|
+
return numberFromCssPixels(column.minWidth) ?? 40;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function maximumWidth(column: DataTableColumn<T>): number | undefined {
|
|
916
|
+
return numberFromCssPixels(column.maxWidth);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function clampedColumnWidth(column: DataTableColumn<T>, width: number): number {
|
|
920
|
+
const maximum = maximumWidth(column);
|
|
921
|
+
return Math.min(
|
|
922
|
+
Math.max(minimumWidth(column), Math.round(width)),
|
|
923
|
+
maximum ?? Infinity,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function currentColumnWidth(
|
|
928
|
+
resolved: DataTableResolvedColumn<T>,
|
|
929
|
+
target?: EventTarget | null,
|
|
930
|
+
): number {
|
|
931
|
+
const measuredWidth = measuredColumnWidths[resolved.column.id];
|
|
932
|
+
if (measuredWidth !== undefined && measuredWidth > 0) return measuredWidth;
|
|
933
|
+
if (resolved.width !== undefined) return resolved.width;
|
|
934
|
+
const staticWidth = numberFromCssPixels(resolved.column.width);
|
|
935
|
+
if (staticWidth !== undefined) return staticWidth;
|
|
936
|
+
const header = target instanceof Element ? target.closest('th') : undefined;
|
|
937
|
+
return header?.getBoundingClientRect().width ?? 160;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function setColumnWidth(column: DataTableColumn<T>, width: number) {
|
|
941
|
+
dispatch({
|
|
942
|
+
type: 'setColumnWidth',
|
|
943
|
+
columnId: column.id,
|
|
944
|
+
width: clampedColumnWidth(column, width),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function handleResizeKeydown(
|
|
949
|
+
event: KeyboardEvent,
|
|
950
|
+
resolved: DataTableResolvedColumn<T>,
|
|
951
|
+
) {
|
|
952
|
+
const step = event.shiftKey ? 32 : 8;
|
|
953
|
+
const current = currentColumnWidth(resolved, event.currentTarget);
|
|
954
|
+
const maximum = maximumWidth(resolved.column);
|
|
955
|
+
const next =
|
|
956
|
+
event.key === 'ArrowLeft'
|
|
957
|
+
? current - step
|
|
958
|
+
: event.key === 'ArrowRight'
|
|
959
|
+
? current + step
|
|
960
|
+
: event.key === 'Home'
|
|
961
|
+
? minimumWidth(resolved.column)
|
|
962
|
+
: event.key === 'End' && maximum !== undefined
|
|
963
|
+
? maximum
|
|
964
|
+
: undefined;
|
|
965
|
+
if (next === undefined) return;
|
|
966
|
+
event.preventDefault();
|
|
967
|
+
setColumnWidth(resolved.column, next);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function handleResizePointerDown(
|
|
971
|
+
event: PointerEvent,
|
|
972
|
+
resolved: DataTableResolvedColumn<T>,
|
|
973
|
+
) {
|
|
974
|
+
if (event.button !== 0) return;
|
|
975
|
+
event.preventDefault();
|
|
976
|
+
event.stopPropagation();
|
|
977
|
+
const handle = event.currentTarget as HTMLElement;
|
|
978
|
+
const startX = event.clientX;
|
|
979
|
+
const startWidth = currentColumnWidth(resolved, handle);
|
|
980
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
981
|
+
const update = (nextEvent: PointerEvent) =>
|
|
982
|
+
setColumnWidth(resolved.column, startWidth + nextEvent.clientX - startX);
|
|
983
|
+
const finish = () => {
|
|
984
|
+
handle.removeEventListener('pointermove', update);
|
|
985
|
+
handle.removeEventListener('pointerup', finish);
|
|
986
|
+
handle.removeEventListener('pointercancel', finish);
|
|
987
|
+
};
|
|
988
|
+
handle.addEventListener('pointermove', update);
|
|
989
|
+
handle.addEventListener('pointerup', finish, { once: true });
|
|
990
|
+
handle.addEventListener('pointercancel', finish, { once: true });
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function structuralLabelColumnId(
|
|
994
|
+
row: DataTableStructuralRow<T>,
|
|
995
|
+
): string | undefined {
|
|
996
|
+
if (
|
|
997
|
+
row.labelColumnId &&
|
|
998
|
+
visibleColumns.some((column) => column.column.id === row.labelColumnId)
|
|
999
|
+
) {
|
|
1000
|
+
return row.labelColumnId;
|
|
1001
|
+
}
|
|
1002
|
+
return visibleColumns[0]?.column.id;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function structuralRowLabel(row: DataTableStructuralRow<T>): string {
|
|
1006
|
+
const kind =
|
|
1007
|
+
row.kind === 'summary'
|
|
1008
|
+
? t(M['ui.data_table.summary'])
|
|
1009
|
+
: row.kind === 'subtotal'
|
|
1010
|
+
? t(M['ui.data_table.subtotal'])
|
|
1011
|
+
: row.kind === 'aggregate'
|
|
1012
|
+
? t(M['ui.data_table.aggregate'])
|
|
1013
|
+
: t(M['ui.data_table.footer_row']);
|
|
1014
|
+
return t(M['ui.data_table.structural_row'], { kind, label: row.label });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function structuralCellValue(
|
|
1018
|
+
row: DataTableStructuralRow<T>,
|
|
1019
|
+
column: DataTableColumn<T>,
|
|
1020
|
+
): unknown {
|
|
1021
|
+
return (
|
|
1022
|
+
row.values?.[column.id] ??
|
|
1023
|
+
(column.id === structuralLabelColumnId(row) ? row.label : '')
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
514
1027
|
const sizeClasses = {
|
|
515
1028
|
sm: 'data-table--sm',
|
|
516
1029
|
md: 'data-table--md',
|
|
517
1030
|
lg: 'data-table--lg',
|
|
518
1031
|
};
|
|
1032
|
+
|
|
1033
|
+
$effect(() => {
|
|
1034
|
+
const container = tableContainer;
|
|
1035
|
+
void columnCount;
|
|
1036
|
+
void displayRows.length;
|
|
1037
|
+
if (!container) return;
|
|
1038
|
+
|
|
1039
|
+
const resizeObserver =
|
|
1040
|
+
typeof ResizeObserver === 'undefined'
|
|
1041
|
+
? undefined
|
|
1042
|
+
: new ResizeObserver(updateOverflowState);
|
|
1043
|
+
resizeObserver?.observe(container);
|
|
1044
|
+
const table = container.querySelector('table');
|
|
1045
|
+
if (table) resizeObserver?.observe(table);
|
|
1046
|
+
window.addEventListener('resize', updateOverflowState);
|
|
1047
|
+
updateOverflowState();
|
|
1048
|
+
|
|
1049
|
+
return () => {
|
|
1050
|
+
resizeObserver?.disconnect();
|
|
1051
|
+
window.removeEventListener('resize', updateOverflowState);
|
|
1052
|
+
};
|
|
1053
|
+
});
|
|
519
1054
|
</script>
|
|
520
1055
|
|
|
521
1056
|
{#if toolbar}<div class="data-table-toolbar">{@render toolbar()}</div>{/if}
|
|
522
|
-
|
|
1057
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex: the region provides horizontal or virtual keyboard scrolling. -->
|
|
1058
|
+
<div
|
|
1059
|
+
bind:this={tableContainer}
|
|
1060
|
+
class="data-table-container"
|
|
1061
|
+
class:data-table-container--sticky={stickyHeader || virtualizedBody}
|
|
1062
|
+
class:data-table-container--overflowing={hasHorizontalOverflow}
|
|
1063
|
+
class:data-table-container--virtualized={virtualizationWindow.enabled}
|
|
1064
|
+
style:height={virtualContainerHeight === undefined ? undefined : `${virtualContainerHeight}px`}
|
|
1065
|
+
tabindex={virtualizationWindow.enabled || hasHorizontalOverflow ? 0 : undefined}
|
|
1066
|
+
role={virtualizationWindow.enabled || hasHorizontalOverflow ? 'region' : undefined}
|
|
1067
|
+
aria-label={virtualizationWindow.enabled
|
|
1068
|
+
? caption
|
|
1069
|
+
? t(M['ui.data_table.virtual_region'], { caption })
|
|
1070
|
+
: t(M['ui.data_table.default_virtual_region'])
|
|
1071
|
+
: hasHorizontalOverflow
|
|
1072
|
+
? overflowRegionLabel
|
|
1073
|
+
: undefined}
|
|
1074
|
+
onscroll={(event) => {
|
|
1075
|
+
updateOverflowState();
|
|
1076
|
+
handleTableScroll(event);
|
|
1077
|
+
}}
|
|
1078
|
+
onkeydown={(event) => {
|
|
1079
|
+
handleVirtualizationKeydown(event);
|
|
1080
|
+
if (!event.defaultPrevented) handleOverflowKeydown(event);
|
|
1081
|
+
}}
|
|
1082
|
+
>
|
|
523
1083
|
<table
|
|
524
1084
|
class="data-table {sizeClasses[size]}"
|
|
525
1085
|
class:data-table--striped={striped}
|
|
1086
|
+
class:data-table--virtualized={virtualizationWindow.enabled}
|
|
526
1087
|
class:data-table--hoverable={hoverable}
|
|
527
1088
|
class:data-table--dense={dense}
|
|
528
|
-
class:data-table--loading={
|
|
1089
|
+
class:data-table--loading={isInitialLoading}
|
|
1090
|
+
aria-busy={isBusy}
|
|
1091
|
+
aria-rowcount={virtualizationWindow.enabled
|
|
1092
|
+
? virtualRowCount + headerRowCount + structuralRowCount
|
|
1093
|
+
: undefined}
|
|
1094
|
+
style={`--data-table-caption-height: ${virtualCaptionHeight}px`}
|
|
529
1095
|
>
|
|
530
1096
|
{#if caption}
|
|
531
|
-
<caption class="data-table__caption">{caption}</caption>
|
|
1097
|
+
<caption bind:this={tableCaption} class="data-table__caption">{caption}</caption>
|
|
532
1098
|
{/if}
|
|
533
1099
|
|
|
534
|
-
<thead class="data-table__head">
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
1100
|
+
<thead bind:this={tableHead} class="data-table__head">
|
|
1101
|
+
{#each dataTableLayout.headerRows as headerRow, headerIndex (headerIndex)}
|
|
1102
|
+
<tr class="data-table__row data-table__row--header">
|
|
1103
|
+
{#if headerIndex === 0 && expandedContent}
|
|
1104
|
+
<th class="data-table__cell data-table__cell--expand" scope="col" rowspan={headerRowCount}>
|
|
1105
|
+
<span class="sr-only"><Trans key={M['ui.data_table.expand']} /></span>
|
|
1106
|
+
</th>
|
|
1107
|
+
{/if}
|
|
1108
|
+
{#if headerIndex === 0 && selectable}
|
|
1109
|
+
<th class="data-table__cell data-table__cell--checkbox" scope="col" rowspan={headerRowCount}>
|
|
1110
|
+
<input
|
|
1111
|
+
type="checkbox"
|
|
1112
|
+
checked={allSelected}
|
|
1113
|
+
use:setIndeterminate={someSelected}
|
|
1114
|
+
onchange={handleSelectAll}
|
|
1115
|
+
aria-label={t(M['ui.data_table.select_current_page'])}
|
|
1116
|
+
class="data-table__checkbox"
|
|
1117
|
+
/>
|
|
1118
|
+
</th>
|
|
1119
|
+
{/if}
|
|
549
1120
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
class
|
|
572
|
-
|
|
1121
|
+
{#each headerRow as headerCell, cellIndex (`${headerIndex}:${cellIndex}`)}
|
|
1122
|
+
{#if headerCell.kind === 'group'}
|
|
1123
|
+
<th
|
|
1124
|
+
class="data-table__cell data-table__cell--header data-table__cell--header-group"
|
|
1125
|
+
class:data-table__cell--pinned-start={headerCell.pin === 'start'}
|
|
1126
|
+
class:data-table__cell--pinned-end={headerCell.pin === 'end'}
|
|
1127
|
+
scope="colgroup"
|
|
1128
|
+
colspan={headerCell.colspan}
|
|
1129
|
+
rowspan={headerCell.rowspan}
|
|
1130
|
+
style:left={headerCell.pin === 'start' ? headerCell.stickyOffset : undefined}
|
|
1131
|
+
style:right={headerCell.pin === 'end' ? headerCell.stickyOffset : undefined}
|
|
1132
|
+
>
|
|
1133
|
+
{headerCell.label}
|
|
1134
|
+
</th>
|
|
1135
|
+
{:else}
|
|
1136
|
+
{@const resolvedColumn = headerCell.column}
|
|
1137
|
+
{@const column = resolvedColumn.column}
|
|
1138
|
+
<th
|
|
1139
|
+
class="data-table__cell data-table__cell--header"
|
|
1140
|
+
class:data-table__cell--sortable={usesAutomaticSortButton(column)}
|
|
1141
|
+
class:data-table__cell--sorted={currentSort.columnId === column.id}
|
|
1142
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1143
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1144
|
+
data-column-id={column.id}
|
|
1145
|
+
data-column-role={column.role}
|
|
1146
|
+
data-responsive-priority={column.responsive?.priority}
|
|
1147
|
+
data-keep-visible={column.responsive?.keepVisible ? 'true' : undefined}
|
|
1148
|
+
use:observeColumnWidth={column.id}
|
|
1149
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1150
|
+
style:min-width={column.minWidth}
|
|
1151
|
+
style:max-width={column.maxWidth}
|
|
1152
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1153
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
1154
|
+
style:text-align={column.align}
|
|
1155
|
+
scope="col"
|
|
1156
|
+
rowspan={headerCell.rowspan}
|
|
1157
|
+
aria-label={column.resizable && !column.header
|
|
1158
|
+
? column.label
|
|
1159
|
+
: undefined}
|
|
1160
|
+
aria-sort={currentSort.columnId === column.id
|
|
1161
|
+
? currentSort.direction === 'asc'
|
|
1162
|
+
? 'ascending'
|
|
1163
|
+
: 'descending'
|
|
1164
|
+
: undefined}
|
|
573
1165
|
>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1166
|
+
{#if usesAutomaticSortButton(column)}
|
|
1167
|
+
{#if column.header}
|
|
1168
|
+
<div class="data-table__header-content">
|
|
1169
|
+
{@render column.header({ column })}
|
|
1170
|
+
<button
|
|
1171
|
+
type="button"
|
|
1172
|
+
class="data-table__sort-button"
|
|
1173
|
+
onclick={(event) => handleSort(column, event)}
|
|
1174
|
+
aria-label={sortActionLabel(column)}
|
|
1175
|
+
>
|
|
1176
|
+
<span class="data-table__sort-icon" aria-hidden="true">
|
|
1177
|
+
{#if currentSort.columnId === column.id}
|
|
1178
|
+
{currentSort.direction === 'asc' ? '↑' : '↓'}
|
|
1179
|
+
{:else}
|
|
1180
|
+
↕
|
|
1181
|
+
{/if}
|
|
1182
|
+
</span>
|
|
1183
|
+
</button>
|
|
1184
|
+
</div>
|
|
578
1185
|
{:else}
|
|
579
|
-
|
|
1186
|
+
<button
|
|
1187
|
+
type="button"
|
|
1188
|
+
class="data-table__sort-button"
|
|
1189
|
+
onclick={(event) => handleSort(column, event)}
|
|
1190
|
+
aria-label={sortActionLabel(column)}
|
|
1191
|
+
>
|
|
1192
|
+
<span>{column.label}</span>
|
|
1193
|
+
<span class="data-table__sort-icon" aria-hidden="true">
|
|
1194
|
+
{#if currentSort.columnId === column.id}
|
|
1195
|
+
{currentSort.direction === 'asc' ? '↑' : '↓'}
|
|
1196
|
+
{:else}
|
|
1197
|
+
↕
|
|
1198
|
+
{/if}
|
|
1199
|
+
</span>
|
|
1200
|
+
</button>
|
|
580
1201
|
{/if}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1202
|
+
{:else if column.header}
|
|
1203
|
+
{@render column.header({ column })}
|
|
1204
|
+
{:else}
|
|
1205
|
+
{column.label}
|
|
1206
|
+
{/if}
|
|
1207
|
+
{#if column.resizable}
|
|
1208
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions: a focusable ARIA separator is the resize control. -->
|
|
1209
|
+
<span
|
|
1210
|
+
class="data-table__resize-handle"
|
|
1211
|
+
role="separator"
|
|
1212
|
+
aria-orientation="vertical"
|
|
1213
|
+
aria-label={t(M['ui.data_table.resize_column'], { column: column.label })}
|
|
1214
|
+
aria-valuemin={minimumWidth(column)}
|
|
1215
|
+
aria-valuemax={maximumWidth(column)}
|
|
1216
|
+
aria-valuenow={Math.round(currentColumnWidth(resolvedColumn))}
|
|
1217
|
+
tabindex="0"
|
|
1218
|
+
onpointerdown={(event) => handleResizePointerDown(event, resolvedColumn)}
|
|
1219
|
+
onkeydown={(event) => handleResizeKeydown(event, resolvedColumn)}
|
|
1220
|
+
></span>
|
|
1221
|
+
{/if}
|
|
1222
|
+
</th>
|
|
585
1223
|
{/if}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1224
|
+
{/each}
|
|
1225
|
+
</tr>
|
|
1226
|
+
{/each}
|
|
589
1227
|
</thead>
|
|
590
1228
|
|
|
591
|
-
<tbody class="data-table__body">
|
|
592
|
-
{#if
|
|
1229
|
+
<tbody bind:this={tableBody} class="data-table__body">
|
|
1230
|
+
{#if errorMessage && !hasRenderedRows}
|
|
1231
|
+
<tr class="data-table__row data-table__row--error">
|
|
1232
|
+
<td
|
|
1233
|
+
class="data-table__cell data-table__cell--error"
|
|
1234
|
+
colspan={columnCount}
|
|
1235
|
+
>
|
|
1236
|
+
<div class="data-table__error-state" role="alert">
|
|
1237
|
+
<span>{errorMessage}</span>
|
|
1238
|
+
{#if onRetry}
|
|
1239
|
+
<button type="button" class="data-table__retry-button" onclick={onRetry}>
|
|
1240
|
+
{t(M['ui.data_table.retry'])}
|
|
1241
|
+
</button>
|
|
1242
|
+
{/if}
|
|
1243
|
+
</div>
|
|
1244
|
+
</td>
|
|
1245
|
+
</tr>
|
|
1246
|
+
{:else if isInitialLoading}
|
|
593
1247
|
<tr class="data-table__row data-table__row--loading">
|
|
594
1248
|
<td
|
|
595
1249
|
class="data-table__cell data-table__cell--loading"
|
|
596
1250
|
colspan={columnCount}
|
|
597
1251
|
>
|
|
598
|
-
<div class="data-table__loading-indicator">
|
|
1252
|
+
<div class="data-table__loading-indicator" role="status" aria-live="polite">
|
|
599
1253
|
<span class="data-table__spinner"></span>
|
|
600
1254
|
<span><Trans key={M['ui.data_table.loading']} /></span>
|
|
601
1255
|
</div>
|
|
@@ -617,28 +1271,37 @@ const sizeClasses = {
|
|
|
617
1271
|
</td>
|
|
618
1272
|
</tr>
|
|
619
1273
|
{:else}
|
|
620
|
-
{#
|
|
1274
|
+
{#if virtualizationWindow.topSpacerHeight > 0}
|
|
1275
|
+
<tr class="data-table__virtual-spacer" aria-hidden="true">
|
|
1276
|
+
<td colspan={columnCount} style:height={`${virtualizationWindow.topSpacerHeight}px`}></td>
|
|
1277
|
+
</tr>
|
|
1278
|
+
{/if}
|
|
1279
|
+
{#each renderedRows as entry, index (entry.rowId)}
|
|
621
1280
|
{@const row = entry.row}
|
|
622
1281
|
{@const key = entry.rowId}
|
|
1282
|
+
{@const displayIndex = virtualizationWindow.startIndex + index}
|
|
623
1283
|
{@const isSelected = tableState.selection.scope === 'allMatching' || selectedIds.has(key)}
|
|
624
1284
|
{@const isExpanded = expandedIds.has(key)}
|
|
625
|
-
{@const rowCanExpand = canExpand?.(row,
|
|
1285
|
+
{@const rowCanExpand = canExpand?.(row, displayIndex) ?? true}
|
|
626
1286
|
<tr
|
|
627
|
-
class="data-table__row {rowClass?.(row,
|
|
1287
|
+
class="data-table__row {rowClass?.(row, displayIndex) ?? ''}"
|
|
628
1288
|
class:data-table__row--selected={isSelected}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
}
|
|
1289
|
+
class:data-table__row--striped={
|
|
1290
|
+
virtualizationWindow.enabled && striped && displayIndex % 2 === 1
|
|
1291
|
+
}
|
|
1292
|
+
class:data-table__row--interactive={Boolean(onRowClick)}
|
|
1293
|
+
data-row-id={dataTableRowIdKey(key)}
|
|
1294
|
+
aria-rowindex={virtualizationWindow.enabled
|
|
1295
|
+
? virtualRowOffset + displayIndex + headerRowCount + 1
|
|
1296
|
+
: undefined}
|
|
1297
|
+
onclick={(event) => handleRowActivation(event, row, displayIndex)}
|
|
1298
|
+
tabindex={onRowClick || virtualizationWindow.enabled ? 0 : undefined}
|
|
1299
|
+
onfocusin={() => handleRowFocus(key)}
|
|
1300
|
+
onkeydown={(event) => handleRowKeydown(event, row, displayIndex)}
|
|
638
1301
|
>
|
|
639
1302
|
{#if expandedContent}
|
|
640
1303
|
<td class="data-table__cell data-table__cell--expand">
|
|
641
|
-
{#if rowCanExpand}<button type="button" class="data-table__expand-button" aria-label={isExpanded ? '
|
|
1304
|
+
{#if rowCanExpand}<button type="button" class="data-table__expand-button" aria-label={isExpanded ? t(M['ui.data_table.collapse_row'], { row: getRowLabel(row, displayIndex) }) : t(M['ui.data_table.expand_row'], { row: getRowLabel(row, displayIndex) })} aria-expanded={isExpanded} aria-controls={expansionContentId(key)} onclick={(event) => handleExpanded(key, event)}>{isExpanded ? '−' : '+'}</button>{/if}
|
|
642
1305
|
</td>
|
|
643
1306
|
{/if}
|
|
644
1307
|
{#if selectable}
|
|
@@ -648,22 +1311,34 @@ const sizeClasses = {
|
|
|
648
1311
|
checked={isSelected}
|
|
649
1312
|
disabled={tableState.selection.scope === 'allMatching'}
|
|
650
1313
|
onchange={(e) => handleRowSelect(key, e)}
|
|
651
|
-
aria-label={t(M['ui.data_table.select_row'])}
|
|
1314
|
+
aria-label={isSelected ? t(M['ui.data_table.deselect_row'], { row: getRowLabel(row, displayIndex) }) : t(M['ui.data_table.select_row'], { row: getRowLabel(row, displayIndex) })}
|
|
652
1315
|
class="data-table__checkbox"
|
|
653
1316
|
/>
|
|
654
1317
|
</td>
|
|
655
1318
|
{/if}
|
|
656
1319
|
|
|
657
|
-
{#each visibleColumns as
|
|
1320
|
+
{#each visibleColumns as resolvedColumn (resolvedColumn.column.id)}
|
|
1321
|
+
{@const column = resolvedColumn.column}
|
|
658
1322
|
{@const value = getCellValue(row, column)}
|
|
659
1323
|
<td
|
|
660
1324
|
class="data-table__cell {column.className ?? ''}"
|
|
1325
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1326
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1327
|
+
data-column-id={column.id}
|
|
1328
|
+
data-column-role={column.role}
|
|
1329
|
+
data-responsive-priority={column.responsive?.priority}
|
|
1330
|
+
data-keep-visible={column.responsive?.keepVisible ? 'true' : undefined}
|
|
1331
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1332
|
+
style:min-width={column.minWidth}
|
|
1333
|
+
style:max-width={column.maxWidth}
|
|
1334
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1335
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
661
1336
|
style:text-align={column.align}
|
|
662
1337
|
>
|
|
663
1338
|
{#if cell}
|
|
664
|
-
{@render cell({ column, row, value, index })}
|
|
1339
|
+
{@render cell({ column, row, value, index: displayIndex })}
|
|
665
1340
|
{:else if column.cell}
|
|
666
|
-
{@render column.cell({ row, value, index })}
|
|
1341
|
+
{@render column.cell({ row, value, index: displayIndex })}
|
|
667
1342
|
{:else}
|
|
668
1343
|
{value ?? ''}
|
|
669
1344
|
{/if}
|
|
@@ -671,26 +1346,206 @@ const sizeClasses = {
|
|
|
671
1346
|
{/each}
|
|
672
1347
|
</tr>
|
|
673
1348
|
{#if expandedContent && isExpanded}
|
|
674
|
-
<tr class="data-table__row data-table__row--expanded">
|
|
675
|
-
<td class="data-table__cell data-table__cell--expanded" colspan={columnCount}>{@render expandedContent({ row, index })}</td>
|
|
1349
|
+
<tr id={expansionContentId(key)} class="data-table__row data-table__row--expanded">
|
|
1350
|
+
<td class="data-table__cell data-table__cell--expanded" colspan={columnCount}>{@render expandedContent({ row, index: displayIndex })}</td>
|
|
676
1351
|
</tr>
|
|
677
1352
|
{/if}
|
|
678
1353
|
{/each}
|
|
1354
|
+
{#if virtualizationWindow.bottomSpacerHeight > 0}
|
|
1355
|
+
<tr class="data-table__virtual-spacer" aria-hidden="true">
|
|
1356
|
+
<td colspan={columnCount} style:height={`${virtualizationWindow.bottomSpacerHeight}px`}></td>
|
|
1357
|
+
</tr>
|
|
1358
|
+
{/if}
|
|
679
1359
|
{/if}
|
|
680
1360
|
</tbody>
|
|
681
|
-
{#if
|
|
682
|
-
<
|
|
1361
|
+
{#if dataBodyStructuralRows.length > 0}
|
|
1362
|
+
<tbody
|
|
1363
|
+
bind:this={tableStructuralBody}
|
|
1364
|
+
class="data-table__body data-table__body--structural"
|
|
1365
|
+
>
|
|
1366
|
+
{#each dataBodyStructuralRows as structuralRow, structuralIndex (structuralRow.id)}
|
|
1367
|
+
{@const labelColumnId = structuralLabelColumnId(structuralRow)}
|
|
1368
|
+
<tr
|
|
1369
|
+
class="data-table__row data-table__row--structural"
|
|
1370
|
+
data-row-kind={structuralRow.kind}
|
|
1371
|
+
aria-label={structuralRowLabel(structuralRow)}
|
|
1372
|
+
aria-rowindex={virtualizationWindow.enabled
|
|
1373
|
+
? virtualRowOffset + displayRows.length + headerRowCount + structuralIndex + 1
|
|
1374
|
+
: undefined}
|
|
1375
|
+
>
|
|
1376
|
+
{#if expandedContent}<td class="data-table__cell data-table__cell--expand"></td>{/if}
|
|
1377
|
+
{#if selectable}<td class="data-table__cell data-table__cell--checkbox"></td>{/if}
|
|
1378
|
+
{#each visibleColumns as resolvedColumn (resolvedColumn.column.id)}
|
|
1379
|
+
{@const column = resolvedColumn.column}
|
|
1380
|
+
{@const value = structuralCellValue(structuralRow, column)}
|
|
1381
|
+
{#if column.id === labelColumnId}
|
|
1382
|
+
<th
|
|
1383
|
+
class="data-table__cell data-table__cell--structural-label {column.className ?? ''}"
|
|
1384
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1385
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1386
|
+
scope="row"
|
|
1387
|
+
data-column-id={column.id}
|
|
1388
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1389
|
+
style:min-width={column.minWidth}
|
|
1390
|
+
style:max-width={column.maxWidth}
|
|
1391
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1392
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
1393
|
+
style:text-align={column.align}
|
|
1394
|
+
>
|
|
1395
|
+
<span class="sr-only">{structuralRowLabel(structuralRow)}: </span>
|
|
1396
|
+
{#if structuralRow.cell}
|
|
1397
|
+
{@render structuralRow.cell({ row: structuralRow, column, value })}
|
|
1398
|
+
{:else}
|
|
1399
|
+
{value ?? ''}
|
|
1400
|
+
{/if}
|
|
1401
|
+
</th>
|
|
1402
|
+
{:else}
|
|
1403
|
+
<td
|
|
1404
|
+
class="data-table__cell {column.className ?? ''}"
|
|
1405
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1406
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1407
|
+
data-column-id={column.id}
|
|
1408
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1409
|
+
style:min-width={column.minWidth}
|
|
1410
|
+
style:max-width={column.maxWidth}
|
|
1411
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1412
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
1413
|
+
style:text-align={column.align}
|
|
1414
|
+
>
|
|
1415
|
+
{#if structuralRow.cell}
|
|
1416
|
+
{@render structuralRow.cell({ row: structuralRow, column, value })}
|
|
1417
|
+
{:else}
|
|
1418
|
+
{value ?? ''}
|
|
1419
|
+
{/if}
|
|
1420
|
+
</td>
|
|
1421
|
+
{/if}
|
|
1422
|
+
{/each}
|
|
1423
|
+
</tr>
|
|
1424
|
+
{/each}
|
|
1425
|
+
</tbody>
|
|
1426
|
+
{/if}
|
|
1427
|
+
{#if footer || footerStructuralRows.length > 0}
|
|
1428
|
+
<tfoot bind:this={tableFooter}>
|
|
1429
|
+
{#each footerStructuralRows as structuralRow, structuralIndex (structuralRow.id)}
|
|
1430
|
+
{@const labelColumnId = structuralLabelColumnId(structuralRow)}
|
|
1431
|
+
<tr
|
|
1432
|
+
class="data-table__row data-table__row--structural data-table__row--footer"
|
|
1433
|
+
data-row-kind={structuralRow.kind}
|
|
1434
|
+
aria-label={structuralRowLabel(structuralRow)}
|
|
1435
|
+
aria-rowindex={virtualizationWindow.enabled
|
|
1436
|
+
? virtualRowCount + headerRowCount + dataBodyStructuralRows.length + structuralIndex + 1
|
|
1437
|
+
: undefined}
|
|
1438
|
+
>
|
|
1439
|
+
{#if expandedContent}<td class="data-table__cell data-table__cell--expand"></td>{/if}
|
|
1440
|
+
{#if selectable}<td class="data-table__cell data-table__cell--checkbox"></td>{/if}
|
|
1441
|
+
{#each visibleColumns as resolvedColumn (resolvedColumn.column.id)}
|
|
1442
|
+
{@const column = resolvedColumn.column}
|
|
1443
|
+
{@const value = structuralCellValue(structuralRow, column)}
|
|
1444
|
+
{#if column.id === labelColumnId}
|
|
1445
|
+
<th
|
|
1446
|
+
class="data-table__cell data-table__cell--structural-label {column.className ?? ''}"
|
|
1447
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1448
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1449
|
+
scope="row"
|
|
1450
|
+
data-column-id={column.id}
|
|
1451
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1452
|
+
style:min-width={column.minWidth}
|
|
1453
|
+
style:max-width={column.maxWidth}
|
|
1454
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1455
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
1456
|
+
style:text-align={column.align}
|
|
1457
|
+
>
|
|
1458
|
+
<span class="sr-only">{structuralRowLabel(structuralRow)}: </span>
|
|
1459
|
+
{#if structuralRow.cell}
|
|
1460
|
+
{@render structuralRow.cell({ row: structuralRow, column, value })}
|
|
1461
|
+
{:else}
|
|
1462
|
+
{value ?? ''}
|
|
1463
|
+
{/if}
|
|
1464
|
+
</th>
|
|
1465
|
+
{:else}
|
|
1466
|
+
<td
|
|
1467
|
+
class="data-table__cell {column.className ?? ''}"
|
|
1468
|
+
class:data-table__cell--pinned-start={resolvedColumn.pin === 'start'}
|
|
1469
|
+
class:data-table__cell--pinned-end={resolvedColumn.pin === 'end'}
|
|
1470
|
+
data-column-id={column.id}
|
|
1471
|
+
style:width={widthForColumn(resolvedColumn)}
|
|
1472
|
+
style:min-width={column.minWidth}
|
|
1473
|
+
style:max-width={column.maxWidth}
|
|
1474
|
+
style:left={resolvedColumn.pin === 'start' ? resolvedColumn.stickyOffset : undefined}
|
|
1475
|
+
style:right={resolvedColumn.pin === 'end' ? resolvedColumn.stickyOffset : undefined}
|
|
1476
|
+
style:text-align={column.align}
|
|
1477
|
+
>
|
|
1478
|
+
{#if structuralRow.cell}
|
|
1479
|
+
{@render structuralRow.cell({ row: structuralRow, column, value })}
|
|
1480
|
+
{:else}
|
|
1481
|
+
{value ?? ''}
|
|
1482
|
+
{/if}
|
|
1483
|
+
</td>
|
|
1484
|
+
{/if}
|
|
1485
|
+
{/each}
|
|
1486
|
+
</tr>
|
|
1487
|
+
{/each}
|
|
1488
|
+
{#if footer}
|
|
1489
|
+
<tr aria-rowindex={virtualizationWindow.enabled
|
|
1490
|
+
? virtualRowCount + headerRowCount + structuralRowCount
|
|
1491
|
+
: undefined}>
|
|
1492
|
+
<td class="data-table__cell data-table__footer" colspan={columnCount}>
|
|
1493
|
+
{@render footer({ rows: displayRows.map(({ row }) => row) })}
|
|
1494
|
+
</td>
|
|
1495
|
+
</tr>
|
|
1496
|
+
{/if}
|
|
1497
|
+
</tfoot>
|
|
683
1498
|
{/if}
|
|
684
1499
|
</table>
|
|
1500
|
+
{#if errorMessage && hasRenderedRows}
|
|
1501
|
+
<div class="data-table__async-status data-table__async-status--error" role="alert">
|
|
1502
|
+
<span>{errorMessage}</span>
|
|
1503
|
+
{#if onRetry}
|
|
1504
|
+
<button type="button" class="data-table__retry-button" onclick={onRetry}>
|
|
1505
|
+
{t(M['ui.data_table.retry'])}
|
|
1506
|
+
</button>
|
|
1507
|
+
{/if}
|
|
1508
|
+
</div>
|
|
1509
|
+
{:else if isRefreshing || stale || partialResults}
|
|
1510
|
+
<div class="data-table__async-status" role="status" aria-live="polite" aria-atomic="true">
|
|
1511
|
+
{#if isRefreshing}<span>{t(M['ui.data_table.refreshing'])}</span>{/if}
|
|
1512
|
+
{#if stale}<span>{t(M['ui.data_table.stale'])}</span>{/if}
|
|
1513
|
+
{#if partialResults}<span>{t(M['ui.data_table.partial_results'])}</span>{/if}
|
|
1514
|
+
</div>
|
|
1515
|
+
{/if}
|
|
685
1516
|
</div>
|
|
686
|
-
{#if
|
|
1517
|
+
{#if hasHorizontalOverflow}
|
|
1518
|
+
<div class="data-table__overflow-cue" aria-hidden="true">
|
|
1519
|
+
{#if canScrollLeft}<span>← {t(M['ui.data_table.more_columns'])}</span>{/if}
|
|
1520
|
+
{#if canScrollRight}<span>{t(M['ui.data_table.more_columns'])} →</span>{/if}
|
|
1521
|
+
</div>
|
|
1522
|
+
{/if}
|
|
1523
|
+
{#if tableState.pageSize && totalPages && totalPages > 1}<Pagination currentPage={tableState.page} {totalPages} onPageChange={handlePageChange} aria-label={t(M['ui.data_table.pagination'])} />{/if}
|
|
687
1524
|
|
|
688
1525
|
<style>
|
|
689
1526
|
.data-table-container {
|
|
1527
|
+
position: relative;
|
|
690
1528
|
width: 100%;
|
|
691
1529
|
overflow-x: auto;
|
|
692
1530
|
}
|
|
693
1531
|
|
|
1532
|
+
.data-table-container--overflowing:focus-visible {
|
|
1533
|
+
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
1534
|
+
outline-offset: 2px;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.data-table__overflow-cue {
|
|
1538
|
+
display: flex;
|
|
1539
|
+
justify-content: space-between;
|
|
1540
|
+
padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
|
|
1541
|
+
border-top: 1px solid var(--smrt-color-outline-variant, #e5e7eb);
|
|
1542
|
+
background: var(--smrt-color-surface-container-low, #f9fafb);
|
|
1543
|
+
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
1544
|
+
font-size: var(--smrt-typography-label-small-size, 0.75rem);
|
|
1545
|
+
font-weight: var(--smrt-typography-weight-medium, 500);
|
|
1546
|
+
pointer-events: none;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
694
1549
|
.data-table-toolbar { margin-bottom: var(--smrt-spacing-2); }
|
|
695
1550
|
|
|
696
1551
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
@@ -700,6 +1555,23 @@ const sizeClasses = {
|
|
|
700
1555
|
overflow-y: auto;
|
|
701
1556
|
}
|
|
702
1557
|
|
|
1558
|
+
.data-table-container--virtualized {
|
|
1559
|
+
overflow: auto;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/* Keep structural context fixed so scrollTop is measured from data row zero. */
|
|
1563
|
+
.data-table-container--virtualized .data-table__caption {
|
|
1564
|
+
position: sticky;
|
|
1565
|
+
top: 0;
|
|
1566
|
+
z-index: 2;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.data-table-container--sticky.data-table-container--virtualized .data-table__head {
|
|
1570
|
+
position: sticky;
|
|
1571
|
+
top: var(--data-table-caption-height, 0px);
|
|
1572
|
+
z-index: 1;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
703
1575
|
.data-table {
|
|
704
1576
|
width: 100%;
|
|
705
1577
|
border-collapse: collapse;
|
|
@@ -734,6 +1606,7 @@ const sizeClasses = {
|
|
|
734
1606
|
}
|
|
735
1607
|
|
|
736
1608
|
.data-table__cell--header {
|
|
1609
|
+
position: relative;
|
|
737
1610
|
padding: var(--smrt-spacing-3, 0.75rem) var(--smrt-spacing-4, 1rem);
|
|
738
1611
|
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
739
1612
|
text-align: left;
|
|
@@ -759,6 +1632,45 @@ const sizeClasses = {
|
|
|
759
1632
|
cursor: pointer;
|
|
760
1633
|
}
|
|
761
1634
|
|
|
1635
|
+
.data-table__header-content {
|
|
1636
|
+
display: inline-flex;
|
|
1637
|
+
align-items: center;
|
|
1638
|
+
gap: var(--smrt-spacing-1, 0.25rem);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.data-table__cell--header-group {
|
|
1642
|
+
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
1643
|
+
font: var(--smrt-typography-label-medium-font);
|
|
1644
|
+
letter-spacing: 0.02em;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
.data-table__resize-handle {
|
|
1648
|
+
position: absolute;
|
|
1649
|
+
top: 0;
|
|
1650
|
+
right: -0.35rem;
|
|
1651
|
+
z-index: 3;
|
|
1652
|
+
display: block;
|
|
1653
|
+
width: 0.7rem;
|
|
1654
|
+
height: 100%;
|
|
1655
|
+
cursor: col-resize;
|
|
1656
|
+
touch-action: none;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.data-table__resize-handle::after {
|
|
1660
|
+
position: absolute;
|
|
1661
|
+
top: 20%;
|
|
1662
|
+
right: 0.3rem;
|
|
1663
|
+
width: 1px;
|
|
1664
|
+
height: 60%;
|
|
1665
|
+
background: var(--smrt-color-outline, #9ca3af);
|
|
1666
|
+
content: '';
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
.data-table__resize-handle:focus-visible {
|
|
1670
|
+
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
1671
|
+
outline-offset: -2px;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
762
1674
|
.data-table__sort-button:hover {
|
|
763
1675
|
color: var(--smrt-color-primary, #3b82f6);
|
|
764
1676
|
}
|
|
@@ -783,6 +1695,11 @@ const sizeClasses = {
|
|
|
783
1695
|
transition: background-color var(--smrt-duration-fast, 150ms) var(--smrt-easing-standard, ease);
|
|
784
1696
|
}
|
|
785
1697
|
|
|
1698
|
+
.data-table__virtual-spacer td {
|
|
1699
|
+
padding: 0;
|
|
1700
|
+
border: 0;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
786
1703
|
.data-table--hoverable .data-table__row:hover:not(.data-table__row--loading):not(.data-table__row--empty) {
|
|
787
1704
|
background: var(--smrt-color-surface-container-low, #f9fafb);
|
|
788
1705
|
}
|
|
@@ -791,15 +1708,21 @@ const sizeClasses = {
|
|
|
791
1708
|
background: var(--smrt-color-primary-container, #dbeafe) !important;
|
|
792
1709
|
}
|
|
793
1710
|
|
|
794
|
-
.data-table--striped .data-table__row:nth-child(even)
|
|
1711
|
+
.data-table--striped:not(.data-table--virtualized) .data-table__row:nth-child(even),
|
|
1712
|
+
.data-table__row--striped {
|
|
795
1713
|
background: var(--smrt-color-surface-container-lowest, #fafafa);
|
|
796
1714
|
}
|
|
797
1715
|
|
|
798
|
-
.data-table__row
|
|
1716
|
+
.data-table__row--interactive {
|
|
799
1717
|
cursor: pointer;
|
|
800
1718
|
}
|
|
801
1719
|
|
|
802
|
-
.data-table__row
|
|
1720
|
+
.data-table__row--interactive:focus-visible {
|
|
1721
|
+
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
1722
|
+
outline-offset: -2px;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.data-table-container--virtualized .data-table__row:focus-visible {
|
|
803
1726
|
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
804
1727
|
outline-offset: -2px;
|
|
805
1728
|
}
|
|
@@ -809,6 +1732,24 @@ const sizeClasses = {
|
|
|
809
1732
|
vertical-align: middle;
|
|
810
1733
|
}
|
|
811
1734
|
|
|
1735
|
+
.data-table__cell--pinned-start,
|
|
1736
|
+
.data-table__cell--pinned-end {
|
|
1737
|
+
position: sticky;
|
|
1738
|
+
z-index: 1;
|
|
1739
|
+
background: var(--smrt-color-surface, #ffffff);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
.data-table__cell--header.data-table__cell--pinned-start,
|
|
1743
|
+
.data-table__cell--header.data-table__cell--pinned-end {
|
|
1744
|
+
z-index: 3;
|
|
1745
|
+
background: var(--smrt-color-surface-container, #f3f4f6);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
.data-table__row--selected .data-table__cell--pinned-start,
|
|
1749
|
+
.data-table__row--selected .data-table__cell--pinned-end {
|
|
1750
|
+
background: var(--smrt-color-primary-container, #dbeafe);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
812
1753
|
.data-table__cell--checkbox {
|
|
813
1754
|
width: 48px;
|
|
814
1755
|
text-align: center;
|
|
@@ -819,6 +1760,9 @@ const sizeClasses = {
|
|
|
819
1760
|
.data-table__expand-button:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
820
1761
|
.data-table__cell--expanded { padding: var(--smrt-spacing-4); background: var(--smrt-color-surface-container-low); }
|
|
821
1762
|
.data-table__footer { border-top: 2px solid var(--smrt-color-outline-variant); font-weight: var(--smrt-typography-weight-medium); }
|
|
1763
|
+
.data-table__row--structural { background: var(--smrt-color-surface-container-low, #f9fafb); }
|
|
1764
|
+
.data-table__row--footer { border-top: 2px solid var(--smrt-color-outline-variant); }
|
|
1765
|
+
.data-table__cell--structural-label { font-weight: var(--smrt-typography-weight-semibold, 600); }
|
|
822
1766
|
|
|
823
1767
|
.data-table__checkbox {
|
|
824
1768
|
width: 18px;
|
|
@@ -829,7 +1773,8 @@ const sizeClasses = {
|
|
|
829
1773
|
|
|
830
1774
|
/* Loading */
|
|
831
1775
|
.data-table__cell--loading,
|
|
832
|
-
.data-table__cell--empty
|
|
1776
|
+
.data-table__cell--empty,
|
|
1777
|
+
.data-table__cell--error {
|
|
833
1778
|
padding: var(--smrt-spacing-8, 2rem);
|
|
834
1779
|
text-align: center;
|
|
835
1780
|
}
|
|
@@ -861,6 +1806,42 @@ const sizeClasses = {
|
|
|
861
1806
|
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
862
1807
|
}
|
|
863
1808
|
|
|
1809
|
+
.data-table__error-state,
|
|
1810
|
+
.data-table__async-status {
|
|
1811
|
+
display: flex;
|
|
1812
|
+
align-items: center;
|
|
1813
|
+
justify-content: center;
|
|
1814
|
+
flex-wrap: wrap;
|
|
1815
|
+
gap: var(--smrt-spacing-3, 0.75rem);
|
|
1816
|
+
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.data-table__error-state,
|
|
1820
|
+
.data-table__async-status--error {
|
|
1821
|
+
color: var(--smrt-color-error, #b3261e);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.data-table__async-status {
|
|
1825
|
+
padding: var(--smrt-spacing-3, 0.75rem);
|
|
1826
|
+
border-top: 1px solid var(--smrt-color-outline-variant, #e5e7eb);
|
|
1827
|
+
font: var(--smrt-typography-body-small-font);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.data-table__retry-button {
|
|
1831
|
+
border: 1px solid currentColor;
|
|
1832
|
+
border-radius: var(--smrt-radius-small, 0.25rem);
|
|
1833
|
+
background: transparent;
|
|
1834
|
+
color: inherit;
|
|
1835
|
+
cursor: pointer;
|
|
1836
|
+
font: inherit;
|
|
1837
|
+
padding: var(--smrt-spacing-1, 0.25rem) var(--smrt-spacing-3, 0.75rem);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
.data-table__retry-button:focus-visible {
|
|
1841
|
+
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
1842
|
+
outline-offset: 2px;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
864
1845
|
/* Size variants */
|
|
865
1846
|
.data-table--sm .data-table__cell {
|
|
866
1847
|
padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
|
|
@@ -890,6 +1871,11 @@ const sizeClasses = {
|
|
|
890
1871
|
/* Loading overlay */
|
|
891
1872
|
.data-table--loading {
|
|
892
1873
|
opacity: 0.7;
|
|
893
|
-
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1877
|
+
.data-table__spinner {
|
|
1878
|
+
animation: none;
|
|
1879
|
+
}
|
|
894
1880
|
}
|
|
895
1881
|
</style>
|