@happyvertical/smrt-ui 0.42.3 → 0.42.5
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 +275 -0
- package/dist/components/data/DataTable.svelte +1444 -172
- 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 +235 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -0
- package/dist/components/data/DataTableController.js +792 -0
- package/dist/components/data/DataTableIdentity.d.ts +21 -0
- package/dist/components/data/DataTableIdentity.d.ts.map +1 -0
- package/dist/components/data/DataTableIdentity.js +28 -0
- 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 +644 -21
- package/dist/components/data/__tests__/DataTableConformance.test.js +212 -0
- package/dist/components/data/__tests__/DataTableController.test.js +336 -0
- package/dist/components/data/__tests__/DataTableIdentity.test.js +29 -0
- 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 +5 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +5 -0
- package/dist/components/data/types.d.ts +110 -2
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/i18n/strings.d.ts +26 -0
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +28 -2
- package/dist/svelte/playground/DataTablePreview.svelte +400 -10
- package/dist/svelte/playground/DataTablePreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +2 -2
- package/package.json +3 -2
|
@@ -11,15 +11,38 @@
|
|
|
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';
|
|
18
18
|
import Pagination from '../ui/Pagination.svelte';
|
|
19
|
+
import {
|
|
20
|
+
compareDataTableRowIds,
|
|
21
|
+
createDataTableController,
|
|
22
|
+
type DataTableCommand,
|
|
23
|
+
type DataTableController,
|
|
24
|
+
type DataTableFilter,
|
|
25
|
+
type DataTableModes,
|
|
26
|
+
type DataTableRowId,
|
|
27
|
+
type DataTableSnapshot,
|
|
28
|
+
type DataTableViewState,
|
|
29
|
+
type DataTableViewStateInput,
|
|
30
|
+
dataTableRowIdKey,
|
|
31
|
+
} from './DataTableController.js';
|
|
32
|
+
import { resolveDataTableRows } from './DataTableIdentity.js';
|
|
33
|
+
import {
|
|
34
|
+
type DataTableResolvedColumn,
|
|
35
|
+
resolveDataTableLayout,
|
|
36
|
+
} from './DataTableLayout.js';
|
|
37
|
+
import {
|
|
38
|
+
maximumDataTableVirtualScrollTop,
|
|
39
|
+
resolveDataTableVirtualWindow,
|
|
40
|
+
scrollTopForDataTableRow,
|
|
41
|
+
} from './DataTableVirtualization.js';
|
|
19
42
|
import type {
|
|
20
43
|
DataTableColumn,
|
|
21
44
|
DataTableProps,
|
|
22
|
-
|
|
45
|
+
DataTableStructuralRow,
|
|
23
46
|
SortState,
|
|
24
47
|
} from './types.js';
|
|
25
48
|
import { defaultSort, getNestedValue } from './types.js';
|
|
@@ -37,10 +60,12 @@ let {
|
|
|
37
60
|
data = [],
|
|
38
61
|
columns = [],
|
|
39
62
|
rowKey,
|
|
63
|
+
agentAddressable = false,
|
|
40
64
|
selectable = false,
|
|
41
65
|
selected = $bindable(new Set<string | number>()),
|
|
42
66
|
onSelectionChange,
|
|
43
67
|
onRowClick,
|
|
68
|
+
rowLabel,
|
|
44
69
|
sortable = false,
|
|
45
70
|
sort = $bindable({ columnId: null, direction: null }),
|
|
46
71
|
onSortChange,
|
|
@@ -49,6 +74,7 @@ let {
|
|
|
49
74
|
page = $bindable(1),
|
|
50
75
|
pageSize,
|
|
51
76
|
manualPagination = false,
|
|
77
|
+
virtualization,
|
|
52
78
|
totalRows,
|
|
53
79
|
onPageChange,
|
|
54
80
|
expanded = $bindable(new Set<string | number>()),
|
|
@@ -57,8 +83,14 @@ let {
|
|
|
57
83
|
expandedContent,
|
|
58
84
|
toolbar,
|
|
59
85
|
footer,
|
|
86
|
+
structuralRows = [],
|
|
60
87
|
visibleColumnIds,
|
|
61
88
|
loading = false,
|
|
89
|
+
refreshing = false,
|
|
90
|
+
stale = false,
|
|
91
|
+
partialResults = false,
|
|
92
|
+
error = null,
|
|
93
|
+
onRetry,
|
|
62
94
|
empty,
|
|
63
95
|
rowClass,
|
|
64
96
|
size = 'md',
|
|
@@ -68,85 +100,283 @@ let {
|
|
|
68
100
|
caption,
|
|
69
101
|
dense = false,
|
|
70
102
|
cell,
|
|
103
|
+
controller,
|
|
104
|
+
state: controlledState,
|
|
105
|
+
initialState,
|
|
106
|
+
onStateChange,
|
|
107
|
+
modes,
|
|
71
108
|
}: ExtendedProps<T> = $props();
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
const dataTableId = $props.id();
|
|
111
|
+
|
|
112
|
+
function legacyModes(): DataTableModes {
|
|
113
|
+
return {
|
|
114
|
+
filtering: modes?.filtering ?? 'local',
|
|
115
|
+
sorting: modes?.sorting ?? (manualSorting ? 'manual' : 'local'),
|
|
116
|
+
pagination: modes?.pagination ?? (manualPagination ? 'manual' : 'local'),
|
|
117
|
+
};
|
|
78
118
|
}
|
|
79
119
|
|
|
80
|
-
function
|
|
81
|
-
|
|
120
|
+
function legacyViewState(): Partial<DataTableViewState> {
|
|
121
|
+
const next: Partial<DataTableViewState> = {
|
|
122
|
+
sorting:
|
|
123
|
+
sort.columnId && sort.direction
|
|
124
|
+
? [{ columnId: sort.columnId, direction: sort.direction }]
|
|
125
|
+
: [],
|
|
126
|
+
page,
|
|
127
|
+
pageSize: pageSize ?? null,
|
|
128
|
+
selectedRowIds: [...selected],
|
|
129
|
+
expandedRowIds: [...expanded],
|
|
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;
|
|
82
138
|
}
|
|
83
139
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
function mergedLegacyState(): Omit<DataTableViewState, 'selection'> {
|
|
141
|
+
const { selection: _selection, ...current } = localController.getState();
|
|
142
|
+
return { ...current, ...legacyViewState() };
|
|
143
|
+
}
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
145
|
+
const localController = createDataTableController({
|
|
146
|
+
modes: legacyModes(),
|
|
147
|
+
onStateChange: (next, command) => onStateChange?.(next, command),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
let controllerSnapshot = $state<DataTableSnapshot>(localController.snapshot());
|
|
151
|
+
let measuredColumnWidths = $state<Record<string, number>>({});
|
|
152
|
+
let publishedLegacySignature: string | undefined;
|
|
153
|
+
let localControllerInitialized = false;
|
|
154
|
+
let hasHorizontalOverflow = $state(false);
|
|
155
|
+
let canScrollLeft = $state(false);
|
|
156
|
+
let canScrollRight = $state(false);
|
|
157
|
+
|
|
158
|
+
function activeController(): DataTableController {
|
|
159
|
+
return controller ?? localController;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function legacySignature(): string {
|
|
163
|
+
const legacy = legacyViewState();
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
sorting: legacy.sorting,
|
|
166
|
+
page: legacy.page,
|
|
167
|
+
pageSize: legacy.pageSize,
|
|
168
|
+
columnVisibility: legacy.columnVisibility,
|
|
169
|
+
selectedRowIds: [...(legacy.selectedRowIds ?? [])].sort(),
|
|
170
|
+
expandedRowIds: [...(legacy.expandedRowIds ?? [])].sort(),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function legacySort(next: DataTableViewState): SortState {
|
|
175
|
+
const rule = next.sorting[0];
|
|
176
|
+
return rule
|
|
177
|
+
? { columnId: rule.columnId, direction: rule.direction }
|
|
178
|
+
: { columnId: null, direction: null };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function publishLegacyState(
|
|
182
|
+
next: DataTableViewState,
|
|
183
|
+
command: DataTableCommand,
|
|
184
|
+
previous: DataTableViewState,
|
|
185
|
+
) {
|
|
186
|
+
const nextSort = legacySort(next);
|
|
187
|
+
const previousSort = legacySort(previous);
|
|
188
|
+
sort = nextSort;
|
|
189
|
+
page = next.page;
|
|
190
|
+
selected = new Set(next.selectedRowIds);
|
|
191
|
+
expanded = new Set(next.expandedRowIds);
|
|
192
|
+
publishedLegacySignature = legacySignature();
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
nextSort.columnId !== previousSort.columnId ||
|
|
196
|
+
nextSort.direction !== previousSort.direction
|
|
197
|
+
) {
|
|
198
|
+
onSortChange?.(nextSort);
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
JSON.stringify(next.selectedRowIds) !==
|
|
202
|
+
JSON.stringify(previous.selectedRowIds)
|
|
203
|
+
) {
|
|
204
|
+
onSelectionChange?.(new Set(next.selectedRowIds));
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
JSON.stringify(next.expandedRowIds) !==
|
|
208
|
+
JSON.stringify(previous.expandedRowIds)
|
|
209
|
+
) {
|
|
210
|
+
onExpandedChange?.(new Set(next.expandedRowIds));
|
|
211
|
+
}
|
|
212
|
+
if (next.page !== previous.page) onPageChange?.(next.page);
|
|
213
|
+
void command;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
$effect(() => {
|
|
217
|
+
if (controller) return;
|
|
218
|
+
|
|
219
|
+
localController.setControlled(controlledState !== undefined);
|
|
220
|
+
localController.setModes(legacyModes());
|
|
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);
|
|
229
|
+
|
|
230
|
+
if (!localControllerInitialized) {
|
|
231
|
+
localControllerInitialized = true;
|
|
232
|
+
const nextState: DataTableViewStateInput = controlledState ?? {
|
|
233
|
+
...mergedLegacyState(),
|
|
234
|
+
...initialState,
|
|
235
|
+
};
|
|
236
|
+
localController.replaceState(nextState);
|
|
237
|
+
localController.setColumnIds(columnIds, hiddenColumnIds);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
localController.setColumnIds(columnIds, hiddenColumnIds);
|
|
242
|
+
|
|
243
|
+
if (controlledState !== undefined) {
|
|
244
|
+
localController.replaceState(controlledState);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
99
247
|
|
|
100
|
-
|
|
101
|
-
|
|
248
|
+
const signature = legacySignature();
|
|
249
|
+
if (signature === publishedLegacySignature) {
|
|
250
|
+
publishedLegacySignature = undefined;
|
|
251
|
+
return;
|
|
102
252
|
}
|
|
253
|
+
localController.replaceState(mergedLegacyState());
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
$effect(() => {
|
|
257
|
+
const current = activeController();
|
|
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
|
+
);
|
|
268
|
+
controllerSnapshot = current.snapshot();
|
|
269
|
+
return current.subscribe((transition) => {
|
|
270
|
+
controllerSnapshot = transition.next;
|
|
271
|
+
if (
|
|
272
|
+
current === localController &&
|
|
273
|
+
controlledState === undefined &&
|
|
274
|
+
transition.command
|
|
275
|
+
) {
|
|
276
|
+
publishLegacyState(
|
|
277
|
+
transition.next.state,
|
|
278
|
+
transition.command,
|
|
279
|
+
transition.previous.state,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const tableState = $derived(controllerSnapshot.state);
|
|
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
|
+
}));
|
|
296
|
+
const requiresStableRowIdentity = $derived(
|
|
297
|
+
selectable ||
|
|
298
|
+
Boolean(expandedContent) ||
|
|
299
|
+
agentAddressable ||
|
|
300
|
+
tableModes.filtering === 'manual' ||
|
|
301
|
+
tableModes.sorting === 'manual' ||
|
|
302
|
+
tableModes.pagination === 'manual' ||
|
|
303
|
+
Boolean(virtualization),
|
|
304
|
+
);
|
|
305
|
+
const sourceRows = $derived.by(() =>
|
|
306
|
+
resolveDataTableRows(data, rowKey, {
|
|
307
|
+
requireStableIdentity: requiresStableRowIdentity,
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
103
310
|
|
|
104
|
-
|
|
105
|
-
|
|
311
|
+
function dispatch(command: DataTableCommand) {
|
|
312
|
+
activeController().dispatch(command);
|
|
106
313
|
}
|
|
107
314
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
315
|
+
function handleSort(column: DataTableColumn<T>, event: MouseEvent) {
|
|
316
|
+
if (!sortable || !column.sortable) return;
|
|
317
|
+
dispatch({
|
|
318
|
+
type: 'toggleSorting',
|
|
319
|
+
columnId: column.id,
|
|
320
|
+
multi: event.shiftKey,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
112
323
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 });
|
|
117
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
|
+
}
|
|
118
342
|
|
|
119
|
-
|
|
120
|
-
|
|
343
|
+
function handleRowSelect(key: DataTableRowId, event: Event) {
|
|
344
|
+
event.stopPropagation();
|
|
345
|
+
dispatch({ type: 'toggleRowSelection', rowId: key });
|
|
121
346
|
}
|
|
122
347
|
|
|
123
|
-
// Handle select all
|
|
124
348
|
function handleSelectAll() {
|
|
349
|
+
if (tableState.selection.scope === 'allMatching') {
|
|
350
|
+
dispatch({
|
|
351
|
+
type: 'setSelection',
|
|
352
|
+
selection: { scope: 'explicit', rowIds: [] },
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const selectedIds = new Set(tableState.selectedRowIds);
|
|
357
|
+
const visibleIds = displayRows.map(({ rowId }) => rowId);
|
|
358
|
+
if (tableState.selection.scope === 'page') {
|
|
359
|
+
dispatch({
|
|
360
|
+
type: 'setPageSelection',
|
|
361
|
+
rowIds: allSelected ? [] : visibleIds,
|
|
362
|
+
});
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
125
365
|
if (allSelected) {
|
|
126
|
-
const
|
|
127
|
-
displayData.map((row, i) => getDisplayRowKey(row, i)),
|
|
128
|
-
);
|
|
129
|
-
selected = new Set([...selected].filter((key) => !visibleKeys.has(key)));
|
|
366
|
+
for (const key of visibleIds) selectedIds.delete(key);
|
|
130
367
|
} else {
|
|
131
|
-
|
|
132
|
-
...selected,
|
|
133
|
-
...displayData.map((row, i) => getDisplayRowKey(row, i)),
|
|
134
|
-
]);
|
|
368
|
+
for (const key of visibleIds) selectedIds.add(key);
|
|
135
369
|
}
|
|
136
|
-
|
|
370
|
+
dispatch({ type: 'setSelectedRows', rowIds: [...selectedIds] });
|
|
137
371
|
}
|
|
138
372
|
|
|
139
|
-
function handleExpanded(key:
|
|
373
|
+
function handleExpanded(key: DataTableRowId, event: Event) {
|
|
140
374
|
event.stopPropagation();
|
|
141
|
-
|
|
142
|
-
next.has(key) ? next.delete(key) : next.add(key);
|
|
143
|
-
expanded = next;
|
|
144
|
-
onExpandedChange?.(next);
|
|
375
|
+
dispatch({ type: 'toggleRowExpansion', rowId: key });
|
|
145
376
|
}
|
|
146
377
|
|
|
147
378
|
function handlePageChange(next: number) {
|
|
148
|
-
|
|
149
|
-
onPageChange?.(page);
|
|
379
|
+
dispatch({ type: 'setPage', page: next });
|
|
150
380
|
}
|
|
151
381
|
|
|
152
382
|
// Handle row click
|
|
@@ -154,6 +384,100 @@ function handleRowClick(row: T, index: number) {
|
|
|
154
384
|
onRowClick?.(row, index);
|
|
155
385
|
}
|
|
156
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
|
+
|
|
157
481
|
// Action to set indeterminate state (can't be set via HTML attribute)
|
|
158
482
|
function setIndeterminate(node: HTMLInputElement, value: boolean) {
|
|
159
483
|
node.indeterminate = value;
|
|
@@ -164,160 +488,774 @@ function setIndeterminate(node: HTMLInputElement, value: boolean) {
|
|
|
164
488
|
};
|
|
165
489
|
}
|
|
166
490
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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'),
|
|
172
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);
|
|
173
507
|
|
|
174
|
-
|
|
508
|
+
function sameFilterValue(value: unknown, expected: unknown): boolean {
|
|
509
|
+
return Object.is(value, expected);
|
|
510
|
+
}
|
|
175
511
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
512
|
+
function textValue(value: unknown): string {
|
|
513
|
+
return String(value ?? '').toLowerCase();
|
|
514
|
+
}
|
|
179
515
|
|
|
180
|
-
|
|
181
|
-
|
|
516
|
+
function compareFilterValues(left: unknown, right: unknown): number {
|
|
517
|
+
const leftValue = left instanceof Date ? left.getTime() : left;
|
|
518
|
+
const rightValue = right instanceof Date ? right.getTime() : right;
|
|
519
|
+
if (typeof leftValue === 'number' && typeof rightValue === 'number')
|
|
520
|
+
return leftValue - rightValue;
|
|
521
|
+
const a = String(leftValue ?? '');
|
|
522
|
+
const b = String(rightValue ?? '');
|
|
523
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
524
|
+
}
|
|
182
525
|
|
|
183
|
-
|
|
184
|
-
|
|
526
|
+
function matchesFilter(
|
|
527
|
+
row: T,
|
|
528
|
+
column: DataTableColumn<T>,
|
|
529
|
+
filter: DataTableFilter,
|
|
530
|
+
): boolean {
|
|
531
|
+
if (column.filterable === false) return true;
|
|
532
|
+
const value = getCellValue(row, column);
|
|
533
|
+
if (column.filterFn) return column.filterFn(row, value, filter);
|
|
534
|
+
const expected = filter.value;
|
|
535
|
+
const valueText = textValue(value);
|
|
536
|
+
const expectedText = textValue(expected);
|
|
537
|
+
switch (filter.operator) {
|
|
538
|
+
case 'equals':
|
|
539
|
+
return sameFilterValue(value, expected);
|
|
540
|
+
case 'notEquals':
|
|
541
|
+
return !sameFilterValue(value, expected);
|
|
542
|
+
case 'contains':
|
|
543
|
+
return valueText.includes(expectedText);
|
|
544
|
+
case 'notContains':
|
|
545
|
+
return !valueText.includes(expectedText);
|
|
546
|
+
case 'startsWith':
|
|
547
|
+
return valueText.startsWith(expectedText);
|
|
548
|
+
case 'endsWith':
|
|
549
|
+
return valueText.endsWith(expectedText);
|
|
550
|
+
case 'in':
|
|
551
|
+
return (
|
|
552
|
+
Array.isArray(expected) &&
|
|
553
|
+
expected.some((entry) => sameFilterValue(value, entry))
|
|
554
|
+
);
|
|
555
|
+
case 'notIn':
|
|
556
|
+
return (
|
|
557
|
+
Array.isArray(expected) &&
|
|
558
|
+
!expected.some((entry) => sameFilterValue(value, entry))
|
|
559
|
+
);
|
|
560
|
+
case 'gt':
|
|
561
|
+
return compareFilterValues(value, expected) > 0;
|
|
562
|
+
case 'gte':
|
|
563
|
+
return compareFilterValues(value, expected) >= 0;
|
|
564
|
+
case 'lt':
|
|
565
|
+
return compareFilterValues(value, expected) < 0;
|
|
566
|
+
case 'lte':
|
|
567
|
+
return compareFilterValues(value, expected) <= 0;
|
|
568
|
+
case 'isNull':
|
|
569
|
+
return value == null;
|
|
570
|
+
case 'isNotNull':
|
|
571
|
+
return value != null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
185
574
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
575
|
+
const filteredRows = $derived.by(() => {
|
|
576
|
+
if (tableModes.filtering === 'manual') return sourceRows;
|
|
577
|
+
const search = tableState.search.toLowerCase();
|
|
578
|
+
return sourceRows.filter(({ row, sourceIndex }) => {
|
|
579
|
+
if (filterFn && !filterFn(row, sourceIndex)) return false;
|
|
580
|
+
if (
|
|
581
|
+
search &&
|
|
582
|
+
!columns.some(
|
|
583
|
+
(column) =>
|
|
584
|
+
column.searchable !== false &&
|
|
585
|
+
textValue(getCellValue(row, column)).includes(search),
|
|
586
|
+
)
|
|
587
|
+
) {
|
|
588
|
+
return false;
|
|
189
589
|
}
|
|
190
|
-
return
|
|
590
|
+
return tableState.filters.every((filter) => {
|
|
591
|
+
const column = columns.find(
|
|
592
|
+
(candidate) => candidate.id === filter.columnId,
|
|
593
|
+
);
|
|
594
|
+
return Boolean(column && matchesFilter(row, column, filter));
|
|
595
|
+
});
|
|
191
596
|
});
|
|
192
597
|
});
|
|
193
598
|
|
|
194
|
-
const
|
|
599
|
+
const sortedRows = $derived.by(() => {
|
|
600
|
+
if (tableModes.sorting === 'manual' || tableState.sorting.length === 0)
|
|
601
|
+
return filteredRows;
|
|
602
|
+
return filteredRows.slice().sort((left, right) => {
|
|
603
|
+
for (const rule of tableState.sorting) {
|
|
604
|
+
const column = columns.find(
|
|
605
|
+
(candidate) => candidate.id === rule.columnId,
|
|
606
|
+
);
|
|
607
|
+
if (!column) continue;
|
|
608
|
+
const result = column.sortFn
|
|
609
|
+
? column.sortFn(left.row, right.row, rule.direction)
|
|
610
|
+
: defaultSort(
|
|
611
|
+
left.row,
|
|
612
|
+
right.row,
|
|
613
|
+
String(column.accessor ?? column.id),
|
|
614
|
+
rule.direction,
|
|
615
|
+
);
|
|
616
|
+
if (result !== 0) return result;
|
|
617
|
+
}
|
|
618
|
+
return rowKey
|
|
619
|
+
? compareDataTableRowIds(left.rowId, right.rowId)
|
|
620
|
+
: left.sourceIndex - right.sourceIndex;
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const totalRowCount = $derived(
|
|
625
|
+
tableModes.pagination === 'manual' ? totalRows : sortedRows.length,
|
|
626
|
+
);
|
|
195
627
|
const totalPages = $derived(
|
|
196
|
-
|
|
628
|
+
tableState.pageSize
|
|
629
|
+
? totalRowCount === undefined
|
|
630
|
+
? null
|
|
631
|
+
: Math.max(1, Math.ceil(totalRowCount / tableState.pageSize))
|
|
632
|
+
: 1,
|
|
633
|
+
);
|
|
634
|
+
const displayRows = $derived.by(() => {
|
|
635
|
+
if (!tableState.pageSize || tableModes.pagination === 'manual')
|
|
636
|
+
return sortedRows;
|
|
637
|
+
const start = (tableState.page - 1) * tableState.pageSize;
|
|
638
|
+
return sortedRows.slice(start, start + tableState.pageSize);
|
|
639
|
+
});
|
|
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,
|
|
197
675
|
);
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
});
|
|
202
772
|
});
|
|
203
|
-
const displayIndexOffset = $derived(pageSize ? (page - 1) * pageSize : 0);
|
|
204
773
|
|
|
205
774
|
$effect(() => {
|
|
206
|
-
if (
|
|
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
|
+
|
|
804
|
+
$effect(() => {
|
|
805
|
+
const total = totalRows;
|
|
806
|
+
if (total === undefined) return;
|
|
807
|
+
if (tableModes.pagination !== 'manual') {
|
|
808
|
+
throw new TypeError(
|
|
809
|
+
'DataTable totalRows is only valid when pagination mode is manual',
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (!Number.isFinite(total) || !Number.isInteger(total) || total < 0) {
|
|
813
|
+
throw new TypeError(
|
|
814
|
+
'DataTable totalRows must be a non-negative integer when supplied',
|
|
815
|
+
);
|
|
816
|
+
}
|
|
207
817
|
});
|
|
208
818
|
|
|
209
|
-
|
|
819
|
+
$effect(() => {
|
|
820
|
+
void tableState.page;
|
|
821
|
+
void tableState.pageSize;
|
|
822
|
+
activeController().clampPage(totalRowCount);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const selectedIds = $derived(new Set(tableState.selectedRowIds));
|
|
826
|
+
const expandedIds = $derived(new Set(tableState.expandedRowIds));
|
|
827
|
+
const currentSort = $derived(legacySort(tableState));
|
|
828
|
+
|
|
210
829
|
const allSelected = $derived(
|
|
211
|
-
|
|
212
|
-
|
|
830
|
+
tableState.selection.scope === 'allMatching' ||
|
|
831
|
+
(displayRows.length > 0 &&
|
|
832
|
+
displayRows.every(({ rowId }) => selectedIds.has(rowId))),
|
|
213
833
|
);
|
|
214
834
|
const someSelected = $derived(
|
|
215
|
-
|
|
835
|
+
tableState.selection.scope !== 'allMatching' &&
|
|
836
|
+
displayRows.some(({ rowId }) => selectedIds.has(rowId)) &&
|
|
216
837
|
!allSelected,
|
|
217
838
|
);
|
|
218
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
|
+
|
|
219
854
|
const columnCount = $derived(
|
|
220
855
|
visibleColumns.length + (selectable ? 1 : 0) + (expandedContent ? 1 : 0),
|
|
221
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
|
+
);
|
|
222
862
|
|
|
223
|
-
// Get cell value
|
|
224
863
|
function getCellValue(row: T, column: DataTableColumn<T>): unknown {
|
|
225
864
|
const accessor = column.accessor ?? column.id;
|
|
226
865
|
return getNestedValue(row, String(accessor));
|
|
227
866
|
}
|
|
228
867
|
|
|
229
|
-
|
|
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
|
+
|
|
230
1027
|
const sizeClasses = {
|
|
231
1028
|
sm: 'data-table--sm',
|
|
232
1029
|
md: 'data-table--md',
|
|
233
1030
|
lg: 'data-table--lg',
|
|
234
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
|
+
});
|
|
235
1054
|
</script>
|
|
236
1055
|
|
|
237
1056
|
{#if toolbar}<div class="data-table-toolbar">{@render toolbar()}</div>{/if}
|
|
238
|
-
|
|
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
|
+
>
|
|
239
1083
|
<table
|
|
240
1084
|
class="data-table {sizeClasses[size]}"
|
|
241
1085
|
class:data-table--striped={striped}
|
|
1086
|
+
class:data-table--virtualized={virtualizationWindow.enabled}
|
|
242
1087
|
class:data-table--hoverable={hoverable}
|
|
243
1088
|
class:data-table--dense={dense}
|
|
244
|
-
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`}
|
|
245
1095
|
>
|
|
246
1096
|
{#if caption}
|
|
247
|
-
<caption class="data-table__caption">{caption}</caption>
|
|
1097
|
+
<caption bind:this={tableCaption} class="data-table__caption">{caption}</caption>
|
|
248
1098
|
{/if}
|
|
249
1099
|
|
|
250
|
-
<thead class="data-table__head">
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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}
|
|
265
1120
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
? sort.direction === 'asc'
|
|
278
|
-
? 'ascending'
|
|
279
|
-
: 'descending'
|
|
280
|
-
: undefined}
|
|
281
|
-
>
|
|
282
|
-
{#if column.header}
|
|
283
|
-
{@render column.header({ column })}
|
|
284
|
-
{:else if sortable && column.sortable}
|
|
285
|
-
<button
|
|
286
|
-
type="button"
|
|
287
|
-
class="data-table__sort-button"
|
|
288
|
-
onclick={() => handleSort(column)}
|
|
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}
|
|
289
1132
|
>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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}
|
|
1165
|
+
>
|
|
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>
|
|
294
1185
|
{:else}
|
|
295
|
-
|
|
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>
|
|
296
1201
|
{/if}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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>
|
|
301
1223
|
{/if}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
1224
|
+
{/each}
|
|
1225
|
+
</tr>
|
|
1226
|
+
{/each}
|
|
305
1227
|
</thead>
|
|
306
1228
|
|
|
307
|
-
<tbody class="data-table__body">
|
|
308
|
-
{#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}
|
|
309
1247
|
<tr class="data-table__row data-table__row--loading">
|
|
310
1248
|
<td
|
|
311
1249
|
class="data-table__cell data-table__cell--loading"
|
|
312
1250
|
colspan={columnCount}
|
|
313
1251
|
>
|
|
314
|
-
<div class="data-table__loading-indicator">
|
|
1252
|
+
<div class="data-table__loading-indicator" role="status" aria-live="polite">
|
|
315
1253
|
<span class="data-table__spinner"></span>
|
|
316
1254
|
<span><Trans key={M['ui.data_table.loading']} /></span>
|
|
317
1255
|
</div>
|
|
318
1256
|
</td>
|
|
319
1257
|
</tr>
|
|
320
|
-
{:else if
|
|
1258
|
+
{:else if displayRows.length === 0}
|
|
321
1259
|
<tr class="data-table__row data-table__row--empty">
|
|
322
1260
|
<td
|
|
323
1261
|
class="data-table__cell data-table__cell--empty"
|
|
@@ -333,27 +1271,37 @@ const sizeClasses = {
|
|
|
333
1271
|
</td>
|
|
334
1272
|
</tr>
|
|
335
1273
|
{:else}
|
|
336
|
-
{#
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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)}
|
|
1280
|
+
{@const row = entry.row}
|
|
1281
|
+
{@const key = entry.rowId}
|
|
1282
|
+
{@const displayIndex = virtualizationWindow.startIndex + index}
|
|
1283
|
+
{@const isSelected = tableState.selection.scope === 'allMatching' || selectedIds.has(key)}
|
|
1284
|
+
{@const isExpanded = expandedIds.has(key)}
|
|
1285
|
+
{@const rowCanExpand = canExpand?.(row, displayIndex) ?? true}
|
|
341
1286
|
<tr
|
|
342
|
-
class="data-table__row {rowClass?.(row,
|
|
1287
|
+
class="data-table__row {rowClass?.(row, displayIndex) ?? ''}"
|
|
343
1288
|
class:data-table__row--selected={isSelected}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
}
|
|
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)}
|
|
353
1301
|
>
|
|
354
1302
|
{#if expandedContent}
|
|
355
1303
|
<td class="data-table__cell data-table__cell--expand">
|
|
356
|
-
{#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}
|
|
357
1305
|
</td>
|
|
358
1306
|
{/if}
|
|
359
1307
|
{#if selectable}
|
|
@@ -361,23 +1309,36 @@ const sizeClasses = {
|
|
|
361
1309
|
<input
|
|
362
1310
|
type="checkbox"
|
|
363
1311
|
checked={isSelected}
|
|
1312
|
+
disabled={tableState.selection.scope === 'allMatching'}
|
|
364
1313
|
onchange={(e) => handleRowSelect(key, e)}
|
|
365
|
-
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) })}
|
|
366
1315
|
class="data-table__checkbox"
|
|
367
1316
|
/>
|
|
368
1317
|
</td>
|
|
369
1318
|
{/if}
|
|
370
1319
|
|
|
371
|
-
{#each visibleColumns as
|
|
1320
|
+
{#each visibleColumns as resolvedColumn (resolvedColumn.column.id)}
|
|
1321
|
+
{@const column = resolvedColumn.column}
|
|
372
1322
|
{@const value = getCellValue(row, column)}
|
|
373
1323
|
<td
|
|
374
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}
|
|
375
1336
|
style:text-align={column.align}
|
|
376
1337
|
>
|
|
377
1338
|
{#if cell}
|
|
378
|
-
{@render cell({ column, row, value, index })}
|
|
1339
|
+
{@render cell({ column, row, value, index: displayIndex })}
|
|
379
1340
|
{:else if column.cell}
|
|
380
|
-
{@render column.cell({ row, value, index })}
|
|
1341
|
+
{@render column.cell({ row, value, index: displayIndex })}
|
|
381
1342
|
{:else}
|
|
382
1343
|
{value ?? ''}
|
|
383
1344
|
{/if}
|
|
@@ -385,26 +1346,206 @@ const sizeClasses = {
|
|
|
385
1346
|
{/each}
|
|
386
1347
|
</tr>
|
|
387
1348
|
{#if expandedContent && isExpanded}
|
|
388
|
-
<tr class="data-table__row data-table__row--expanded">
|
|
389
|
-
<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>
|
|
390
1351
|
</tr>
|
|
391
1352
|
{/if}
|
|
392
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}
|
|
393
1359
|
{/if}
|
|
394
1360
|
</tbody>
|
|
395
|
-
{#if
|
|
396
|
-
<
|
|
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>
|
|
397
1498
|
{/if}
|
|
398
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}
|
|
399
1516
|
</div>
|
|
400
|
-
{#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}
|
|
401
1524
|
|
|
402
1525
|
<style>
|
|
403
1526
|
.data-table-container {
|
|
1527
|
+
position: relative;
|
|
404
1528
|
width: 100%;
|
|
405
1529
|
overflow-x: auto;
|
|
406
1530
|
}
|
|
407
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
|
+
|
|
408
1549
|
.data-table-toolbar { margin-bottom: var(--smrt-spacing-2); }
|
|
409
1550
|
|
|
410
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; }
|
|
@@ -414,6 +1555,23 @@ const sizeClasses = {
|
|
|
414
1555
|
overflow-y: auto;
|
|
415
1556
|
}
|
|
416
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
|
+
|
|
417
1575
|
.data-table {
|
|
418
1576
|
width: 100%;
|
|
419
1577
|
border-collapse: collapse;
|
|
@@ -448,6 +1606,7 @@ const sizeClasses = {
|
|
|
448
1606
|
}
|
|
449
1607
|
|
|
450
1608
|
.data-table__cell--header {
|
|
1609
|
+
position: relative;
|
|
451
1610
|
padding: var(--smrt-spacing-3, 0.75rem) var(--smrt-spacing-4, 1rem);
|
|
452
1611
|
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
453
1612
|
text-align: left;
|
|
@@ -473,6 +1632,45 @@ const sizeClasses = {
|
|
|
473
1632
|
cursor: pointer;
|
|
474
1633
|
}
|
|
475
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
|
+
|
|
476
1674
|
.data-table__sort-button:hover {
|
|
477
1675
|
color: var(--smrt-color-primary, #3b82f6);
|
|
478
1676
|
}
|
|
@@ -497,6 +1695,11 @@ const sizeClasses = {
|
|
|
497
1695
|
transition: background-color var(--smrt-duration-fast, 150ms) var(--smrt-easing-standard, ease);
|
|
498
1696
|
}
|
|
499
1697
|
|
|
1698
|
+
.data-table__virtual-spacer td {
|
|
1699
|
+
padding: 0;
|
|
1700
|
+
border: 0;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
500
1703
|
.data-table--hoverable .data-table__row:hover:not(.data-table__row--loading):not(.data-table__row--empty) {
|
|
501
1704
|
background: var(--smrt-color-surface-container-low, #f9fafb);
|
|
502
1705
|
}
|
|
@@ -505,15 +1708,21 @@ const sizeClasses = {
|
|
|
505
1708
|
background: var(--smrt-color-primary-container, #dbeafe) !important;
|
|
506
1709
|
}
|
|
507
1710
|
|
|
508
|
-
.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 {
|
|
509
1713
|
background: var(--smrt-color-surface-container-lowest, #fafafa);
|
|
510
1714
|
}
|
|
511
1715
|
|
|
512
|
-
.data-table__row
|
|
1716
|
+
.data-table__row--interactive {
|
|
513
1717
|
cursor: pointer;
|
|
514
1718
|
}
|
|
515
1719
|
|
|
516
|
-
.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 {
|
|
517
1726
|
outline: 2px solid var(--smrt-color-primary, #3b82f6);
|
|
518
1727
|
outline-offset: -2px;
|
|
519
1728
|
}
|
|
@@ -523,6 +1732,24 @@ const sizeClasses = {
|
|
|
523
1732
|
vertical-align: middle;
|
|
524
1733
|
}
|
|
525
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
|
+
|
|
526
1753
|
.data-table__cell--checkbox {
|
|
527
1754
|
width: 48px;
|
|
528
1755
|
text-align: center;
|
|
@@ -533,6 +1760,9 @@ const sizeClasses = {
|
|
|
533
1760
|
.data-table__expand-button:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
534
1761
|
.data-table__cell--expanded { padding: var(--smrt-spacing-4); background: var(--smrt-color-surface-container-low); }
|
|
535
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); }
|
|
536
1766
|
|
|
537
1767
|
.data-table__checkbox {
|
|
538
1768
|
width: 18px;
|
|
@@ -543,7 +1773,8 @@ const sizeClasses = {
|
|
|
543
1773
|
|
|
544
1774
|
/* Loading */
|
|
545
1775
|
.data-table__cell--loading,
|
|
546
|
-
.data-table__cell--empty
|
|
1776
|
+
.data-table__cell--empty,
|
|
1777
|
+
.data-table__cell--error {
|
|
547
1778
|
padding: var(--smrt-spacing-8, 2rem);
|
|
548
1779
|
text-align: center;
|
|
549
1780
|
}
|
|
@@ -575,6 +1806,42 @@ const sizeClasses = {
|
|
|
575
1806
|
color: var(--smrt-color-on-surface-variant, #6b7280);
|
|
576
1807
|
}
|
|
577
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
|
+
|
|
578
1845
|
/* Size variants */
|
|
579
1846
|
.data-table--sm .data-table__cell {
|
|
580
1847
|
padding: var(--smrt-spacing-2, 0.5rem) var(--smrt-spacing-3, 0.75rem);
|
|
@@ -604,6 +1871,11 @@ const sizeClasses = {
|
|
|
604
1871
|
/* Loading overlay */
|
|
605
1872
|
.data-table--loading {
|
|
606
1873
|
opacity: 0.7;
|
|
607
|
-
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1877
|
+
.data-table__spinner {
|
|
1878
|
+
animation: none;
|
|
1879
|
+
}
|
|
608
1880
|
}
|
|
609
1881
|
</style>
|