@juspay/svelte-ui-components 2.80.10 → 2.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/Table/BuiltinCell.svelte +607 -0
- package/dist/Table/BuiltinCell.svelte.d.ts +9 -0
- package/dist/Table/Table.svelte +466 -39
- package/dist/Table/cellData.d.ts +48 -0
- package/dist/Table/cellData.js +292 -0
- package/dist/Table/normalizeColumns.d.ts +24 -0
- package/dist/Table/normalizeColumns.js +23 -0
- package/dist/Table/properties.d.ts +282 -0
- package/dist/Table/sortEngine.d.ts +29 -0
- package/dist/Table/sortEngine.js +71 -0
- package/dist/assets/dots-vertical.svg +5 -0
- package/dist/assets/trend-down.svg +4 -0
- package/dist/assets/trend-up.svg +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/Table/Table.svelte
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { TableProperties, TableCheckboxSelectionConfig } from './properties';
|
|
2
|
+
import type { TableProperties, TableCheckboxSelectionConfig, TableRow } from './properties';
|
|
3
|
+
import { normalizeColumns } from './normalizeColumns';
|
|
4
|
+
import BuiltinCell from './BuiltinCell.svelte';
|
|
3
5
|
import type { JSONValue } from 'type-decoder';
|
|
4
6
|
import { SvelteSet } from 'svelte/reactivity';
|
|
5
7
|
import Button from '../Button/Button.svelte';
|
|
8
|
+
import Menu from '../Menu/Menu.svelte';
|
|
9
|
+
import Tooltip from '../Tooltip/Tooltip.svelte';
|
|
10
|
+
import Pagination from '../Pagination/Pagination.svelte';
|
|
11
|
+
import Select from '../Select/Select.svelte';
|
|
12
|
+
import chevronDownSmSvg from '../assets/chevron-down-sm.svg?raw';
|
|
6
13
|
import chevronUpSvg from '../assets/chevron-up.svg?raw';
|
|
7
14
|
import chevronDownSvg from '../assets/chevron-down.svg?raw';
|
|
8
15
|
import sortDefaultSvg from '../assets/sort-default.svg?raw';
|
|
@@ -15,8 +22,11 @@
|
|
|
15
22
|
tableTitle = '',
|
|
16
23
|
tableHeaders = [],
|
|
17
24
|
tableData = [],
|
|
25
|
+
columns,
|
|
26
|
+
rows,
|
|
18
27
|
sortable = true,
|
|
19
28
|
sortableColumns,
|
|
29
|
+
sortMode = 'client',
|
|
20
30
|
stickyHeader = false,
|
|
21
31
|
isTableScrollable = false,
|
|
22
32
|
isContentScrollable = false,
|
|
@@ -36,9 +46,38 @@
|
|
|
36
46
|
getCellTestId,
|
|
37
47
|
checkboxSelection,
|
|
38
48
|
searchConfig,
|
|
39
|
-
onSearchChange
|
|
49
|
+
onSearchChange,
|
|
50
|
+
pagination,
|
|
51
|
+
toolbarSlot,
|
|
52
|
+
rowNumberColumn = false
|
|
40
53
|
}: TableProperties = $props();
|
|
41
54
|
|
|
55
|
+
// ─── Keyed column model → positional projection ─────────────────────────────
|
|
56
|
+
// When `columns` is provided, the keyed model is normalized once and the
|
|
57
|
+
// positional engine below runs on the projection; when absent, the positional
|
|
58
|
+
// props pass through untouched, preserving existing behavior exactly.
|
|
59
|
+
let normalized = $derived(columns ? normalizeColumns(columns, rows ?? []) : null);
|
|
60
|
+
let effectiveHeaders = $derived(normalized ? normalized.tableHeaders : tableHeaders);
|
|
61
|
+
let effectiveData = $derived(normalized ? normalized.tableData : tableData);
|
|
62
|
+
let effectiveSortableColumns = $derived(
|
|
63
|
+
normalized ? normalized.sortableColumns : sortableColumns
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recovers the original keyed row from a projected positional row so
|
|
68
|
+
* column-scoped `cell` snippets receive the keyed shape. Sorting and
|
|
69
|
+
* filtering copy the outer array but keep row references, so reference
|
|
70
|
+
* identity survives the whole pipeline.
|
|
71
|
+
*/
|
|
72
|
+
let keyedRowByProjected = $derived.by((): Map<JSONValue[], TableRow> | null => {
|
|
73
|
+
if (!normalized || !rows) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return new Map(
|
|
77
|
+
normalized.tableData.map((projectedRow, rowIndex) => [projectedRow, rows[rowIndex]])
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
42
81
|
// ─── Sort state ──────────────────────────────────────────────────────────────
|
|
43
82
|
let sortColumn = $state<number | null>(null);
|
|
44
83
|
let sortDirection = $state<'asc' | 'desc'>('asc');
|
|
@@ -47,8 +86,8 @@
|
|
|
47
86
|
if (!sortable) {
|
|
48
87
|
return false;
|
|
49
88
|
}
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
89
|
+
if (effectiveSortableColumns) {
|
|
90
|
+
return effectiveSortableColumns.includes(colIndex);
|
|
52
91
|
}
|
|
53
92
|
return true;
|
|
54
93
|
};
|
|
@@ -93,6 +132,7 @@
|
|
|
93
132
|
return;
|
|
94
133
|
}
|
|
95
134
|
searchTerm = searchInputRef.value;
|
|
135
|
+
pageOverride = 1;
|
|
96
136
|
if (isServerSearch) {
|
|
97
137
|
onSearchChange?.(searchTerm);
|
|
98
138
|
}
|
|
@@ -100,6 +140,7 @@
|
|
|
100
140
|
|
|
101
141
|
const clearSearch = (): void => {
|
|
102
142
|
searchTerm = '';
|
|
143
|
+
pageOverride = 1;
|
|
103
144
|
if (isServerSearch) {
|
|
104
145
|
onSearchChange?.('');
|
|
105
146
|
}
|
|
@@ -107,14 +148,41 @@
|
|
|
107
148
|
|
|
108
149
|
// ─── Sort + Search pipeline ──────────────────────────────────────────────────
|
|
109
150
|
let sortedTableData = $derived.by(() => {
|
|
110
|
-
if (sortColumn === null) {
|
|
111
|
-
return [...
|
|
151
|
+
if (sortColumn === null || sortMode === 'server') {
|
|
152
|
+
return [...effectiveData];
|
|
112
153
|
}
|
|
113
154
|
|
|
114
155
|
const colIndex = sortColumn;
|
|
115
156
|
const direction = sortDirection;
|
|
116
157
|
|
|
117
|
-
|
|
158
|
+
// Keyed-mode per-column sort-value extraction: the consumer's getSortValue
|
|
159
|
+
// supplies the comparable (currency/date parsing stays app-side), the
|
|
160
|
+
// built-in scalar comparator below does the ordering.
|
|
161
|
+
const getSortValue = columns?.[colIndex]?.getSortValue;
|
|
162
|
+
const keyedLookup = keyedRowByProjected;
|
|
163
|
+
if (typeof getSortValue === 'function' && keyedLookup) {
|
|
164
|
+
const emptyRow: TableRow = {};
|
|
165
|
+
const decorated = effectiveData.map((row, rowIndex) => ({
|
|
166
|
+
row,
|
|
167
|
+
sortValue: getSortValue(keyedLookup.get(row) ?? emptyRow, rowIndex)
|
|
168
|
+
}));
|
|
169
|
+
decorated.sort((entryA, entryB) => {
|
|
170
|
+
const valueA = entryA.sortValue;
|
|
171
|
+
const valueB = entryB.sortValue;
|
|
172
|
+
let comparison: number;
|
|
173
|
+
if (typeof valueA === 'number' && typeof valueB === 'number') {
|
|
174
|
+
comparison = valueA - valueB;
|
|
175
|
+
} else if (typeof valueA === 'boolean' && typeof valueB === 'boolean') {
|
|
176
|
+
comparison = valueA === valueB ? 0 : valueA ? -1 : 1;
|
|
177
|
+
} else {
|
|
178
|
+
comparison = String(valueA).localeCompare(String(valueB));
|
|
179
|
+
}
|
|
180
|
+
return direction === 'asc' ? comparison : -comparison;
|
|
181
|
+
});
|
|
182
|
+
return decorated.map((entry) => entry.row);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [...effectiveData].sort((rowA, rowB) => {
|
|
118
186
|
const valueA = rowA[colIndex];
|
|
119
187
|
const valueB = rowB[colIndex];
|
|
120
188
|
|
|
@@ -170,6 +238,89 @@
|
|
|
170
238
|
});
|
|
171
239
|
});
|
|
172
240
|
|
|
241
|
+
// ─── Built-in pagination ─────────────────────────────────────────────────
|
|
242
|
+
// Client mode slices the filtered rows internally; server mode leaves the
|
|
243
|
+
// supplied rows untouched (they ARE the current page) and only drives the
|
|
244
|
+
// paginator chrome. Search input and page-size changes snap back to page 1.
|
|
245
|
+
let pageOverride = $state<number | null>(null);
|
|
246
|
+
let pageSizeOverride = $state<number | null>(null);
|
|
247
|
+
let paginationMode = $derived(pagination?.mode ?? 'client');
|
|
248
|
+
let effectivePageSize = $derived(pageSizeOverride ?? pagination?.pageSize ?? 10);
|
|
249
|
+
let effectivePage = $derived.by(() => {
|
|
250
|
+
if (!pagination) {
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
if (paginationMode === 'server') {
|
|
254
|
+
return pagination.page ?? 1;
|
|
255
|
+
}
|
|
256
|
+
return pageOverride ?? pagination.page ?? 1;
|
|
257
|
+
});
|
|
258
|
+
let paginationTotalItems = $derived.by(() => {
|
|
259
|
+
if (!pagination) {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
if (paginationMode === 'server') {
|
|
263
|
+
return pagination.totalItems ?? 0;
|
|
264
|
+
}
|
|
265
|
+
return filteredTableData.length;
|
|
266
|
+
});
|
|
267
|
+
let paginationTotalPages = $derived(
|
|
268
|
+
Math.max(1, Math.ceil(paginationTotalItems / Math.max(1, effectivePageSize)))
|
|
269
|
+
);
|
|
270
|
+
let paginatedTableData = $derived.by(() => {
|
|
271
|
+
if (!pagination || paginationMode === 'server') {
|
|
272
|
+
return filteredTableData;
|
|
273
|
+
}
|
|
274
|
+
const startIndex = (effectivePage - 1) * effectivePageSize;
|
|
275
|
+
return filteredTableData.slice(startIndex, startIndex + effectivePageSize);
|
|
276
|
+
});
|
|
277
|
+
let paginationRangeText = $derived.by(() => {
|
|
278
|
+
if (!pagination) {
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
const total = paginationTotalItems;
|
|
282
|
+
const from = total === 0 ? 0 : (effectivePage - 1) * effectivePageSize + 1;
|
|
283
|
+
const to = Math.min(effectivePage * effectivePageSize, total);
|
|
284
|
+
if (pagination.rangeLabel) {
|
|
285
|
+
return pagination.rangeLabel(from, to, total);
|
|
286
|
+
}
|
|
287
|
+
return total > 0 ? `${from}-${to} of ${total}` : '';
|
|
288
|
+
});
|
|
289
|
+
let pageSizeOptions = $derived(pagination?.pageSizeOptions ?? [10, 25, 50, 100]);
|
|
290
|
+
|
|
291
|
+
const handlePageChange = (page: number): void => {
|
|
292
|
+
pageOverride = page;
|
|
293
|
+
pagination?.onPageChange?.(page);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const handlePageSizeChange = (nextSize: number): void => {
|
|
297
|
+
pageSizeOverride = nextSize;
|
|
298
|
+
pageOverride = 1;
|
|
299
|
+
pagination?.onPageSizeChange?.(nextSize);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** 1-based, pagination-aware sequence number for the row-number column. */
|
|
303
|
+
const rowNumberFor = (rowIndex: number): number => {
|
|
304
|
+
if (pagination) {
|
|
305
|
+
return (effectivePage - 1) * effectivePageSize + rowIndex + 1;
|
|
306
|
+
}
|
|
307
|
+
return rowIndex + 1;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Offset that converts the render loop's page-local index back into an index
|
|
312
|
+
* into the consumer-supplied rows. Client-mode pagination slices internally,
|
|
313
|
+
* so page 2+ would otherwise hand handlers (onRowClick, column handlers,
|
|
314
|
+
* cell snippets, test-id callbacks) an index that mis-addresses the
|
|
315
|
+
* consumer's full array. Server mode supplies the current page as the whole
|
|
316
|
+
* array, so no offset applies.
|
|
317
|
+
*/
|
|
318
|
+
let rowIndexOffset = $derived(
|
|
319
|
+
pagination && (pagination.mode ?? 'client') === 'client'
|
|
320
|
+
? (effectivePage - 1) * effectivePageSize
|
|
321
|
+
: 0
|
|
322
|
+
);
|
|
323
|
+
|
|
173
324
|
// ─── C2-1: Checkbox selection ─────────────────────────────────────────────
|
|
174
325
|
const resolveRowId = (
|
|
175
326
|
cfg: TableCheckboxSelectionConfig,
|
|
@@ -179,14 +330,23 @@
|
|
|
179
330
|
return cfg.getRowId ? cfg.getRowId(row, rowIndex) : String(rowIndex);
|
|
180
331
|
};
|
|
181
332
|
|
|
182
|
-
let
|
|
333
|
+
let internalSelectedIds = new SvelteSet<string>();
|
|
334
|
+
|
|
335
|
+
// Controlled-selection overlay: when the consumer supplies selectedIds,
|
|
336
|
+
// Table renders FROM that set and never mutates it — onSelectionChange
|
|
337
|
+
// reports the would-be next set instead. Absent (every pre-existing
|
|
338
|
+
// consumer), the internal reactive set behaves exactly as before.
|
|
339
|
+
let controlledSelectedIds = $derived(checkboxSelection?.selectedIds ?? null);
|
|
340
|
+
let effectiveSelectedIds = $derived<ReadonlySet<string>>(
|
|
341
|
+
controlledSelectedIds ?? internalSelectedIds
|
|
342
|
+
);
|
|
183
343
|
|
|
184
344
|
const isRowDisabled = (rowId: string): boolean => {
|
|
185
345
|
return checkboxSelection?.disabledRowIds?.has(rowId) ?? false;
|
|
186
346
|
};
|
|
187
347
|
|
|
188
348
|
const isRowSelected = (rowId: string): boolean => {
|
|
189
|
-
return
|
|
349
|
+
return effectiveSelectedIds.has(rowId);
|
|
190
350
|
};
|
|
191
351
|
|
|
192
352
|
/**
|
|
@@ -217,20 +377,31 @@
|
|
|
217
377
|
});
|
|
218
378
|
|
|
219
379
|
/**
|
|
220
|
-
*
|
|
380
|
+
* Row-reference → stable-ID lookup so the paginated view resolves the same
|
|
381
|
+
* IDs as the full filtered view (slicing preserves row references).
|
|
382
|
+
*/
|
|
383
|
+
let rowIdByRow = $derived(
|
|
384
|
+
new Map(filteredTableData.map((row, index) => [row, filteredRowIds[index]]))
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* IDs of all selectable (non-disabled) rows in the CURRENT VIEW — the page
|
|
389
|
+
* the user is looking at under client pagination, the whole filtered set
|
|
390
|
+
* otherwise (no pagination, and server mode, where the consumer's rows array
|
|
391
|
+
* already is the page).
|
|
221
392
|
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* default numeric ID (`String(originalIndex)`) unchanged regardless of which
|
|
226
|
-
* rows the current search term hides, preventing ID collisions as the visible
|
|
227
|
-
* set changes.
|
|
393
|
+
* The header select-all and its tri-state operate on this set, so checking
|
|
394
|
+
* the header selects exactly the rows the user can see. Selections made on
|
|
395
|
+
* other pages are left untouched by a header toggle on this page.
|
|
228
396
|
*/
|
|
229
397
|
let selectableRowIds = $derived.by((): string[] => {
|
|
230
398
|
if (!checkboxSelection || checkboxSelection.enabled === false) {
|
|
231
399
|
return [];
|
|
232
400
|
}
|
|
233
|
-
return
|
|
401
|
+
return paginatedTableData.flatMap((row) => {
|
|
402
|
+
const rowId = rowIdByRow.get(row) ?? null;
|
|
403
|
+
return rowId !== null && !isRowDisabled(rowId) ? [rowId] : [];
|
|
404
|
+
});
|
|
234
405
|
});
|
|
235
406
|
|
|
236
407
|
/**
|
|
@@ -247,7 +418,9 @@
|
|
|
247
418
|
) {
|
|
248
419
|
return 'none';
|
|
249
420
|
}
|
|
250
|
-
const selectedCount = selectableRowIds.filter((rowId) =>
|
|
421
|
+
const selectedCount = selectableRowIds.filter((rowId) =>
|
|
422
|
+
effectiveSelectedIds.has(rowId)
|
|
423
|
+
).length;
|
|
251
424
|
if (selectedCount === 0) {
|
|
252
425
|
return 'none';
|
|
253
426
|
}
|
|
@@ -262,39 +435,64 @@
|
|
|
262
435
|
return;
|
|
263
436
|
}
|
|
264
437
|
|
|
438
|
+
if (controlledSelectedIds) {
|
|
439
|
+
const wasSelected = controlledSelectedIds.has(rowId);
|
|
440
|
+
const nextSelection =
|
|
441
|
+
checkboxSelection.selectionMode === 'single'
|
|
442
|
+
? new Set(wasSelected ? [] : [rowId])
|
|
443
|
+
: wasSelected
|
|
444
|
+
? new Set([...controlledSelectedIds].filter((selectedId) => selectedId !== rowId))
|
|
445
|
+
: new Set([...controlledSelectedIds, rowId]);
|
|
446
|
+
checkboxSelection.onSelectionChange?.(nextSelection);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
265
450
|
if (checkboxSelection.selectionMode === 'single') {
|
|
266
|
-
const wasSelected =
|
|
267
|
-
|
|
451
|
+
const wasSelected = internalSelectedIds.has(rowId);
|
|
452
|
+
internalSelectedIds.clear();
|
|
268
453
|
if (!wasSelected) {
|
|
269
|
-
|
|
454
|
+
internalSelectedIds.add(rowId);
|
|
270
455
|
}
|
|
271
456
|
} else {
|
|
272
|
-
if (
|
|
273
|
-
|
|
457
|
+
if (internalSelectedIds.has(rowId)) {
|
|
458
|
+
internalSelectedIds.delete(rowId);
|
|
274
459
|
} else {
|
|
275
|
-
|
|
460
|
+
internalSelectedIds.add(rowId);
|
|
276
461
|
}
|
|
277
462
|
}
|
|
278
|
-
checkboxSelection.onSelectionChange?.(new Set(
|
|
463
|
+
checkboxSelection.onSelectionChange?.(new Set(internalSelectedIds));
|
|
279
464
|
};
|
|
280
465
|
|
|
281
466
|
const toggleAllSelection = (): void => {
|
|
282
467
|
if (!checkboxSelection || checkboxSelection.enabled === false) {
|
|
283
468
|
return;
|
|
284
469
|
}
|
|
470
|
+
|
|
471
|
+
if (controlledSelectedIds) {
|
|
472
|
+
const selectableSet = new Set(selectableRowIds);
|
|
473
|
+
const nextSelection =
|
|
474
|
+
headerCheckboxState === 'all'
|
|
475
|
+
? new Set(
|
|
476
|
+
[...controlledSelectedIds].filter((selectedId) => !selectableSet.has(selectedId))
|
|
477
|
+
)
|
|
478
|
+
: new Set([...controlledSelectedIds, ...selectableRowIds]);
|
|
479
|
+
checkboxSelection.onSelectionChange?.(nextSelection);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
285
483
|
// selectableRowIds already excludes disabled rows
|
|
286
484
|
if (headerCheckboxState === 'all') {
|
|
287
485
|
// deselect all selectable rows in the current view
|
|
288
486
|
for (const rowId of selectableRowIds) {
|
|
289
|
-
|
|
487
|
+
internalSelectedIds.delete(rowId);
|
|
290
488
|
}
|
|
291
489
|
} else {
|
|
292
490
|
// select all selectable rows in the current view
|
|
293
491
|
for (const rowId of selectableRowIds) {
|
|
294
|
-
|
|
492
|
+
internalSelectedIds.add(rowId);
|
|
295
493
|
}
|
|
296
494
|
}
|
|
297
|
-
checkboxSelection.onSelectionChange?.(new Set(
|
|
495
|
+
checkboxSelection.onSelectionChange?.(new Set(internalSelectedIds));
|
|
298
496
|
};
|
|
299
497
|
|
|
300
498
|
const handleCheckboxKeydown = (event: KeyboardEvent, action: () => void): void => {
|
|
@@ -345,7 +543,13 @@
|
|
|
345
543
|
</div>
|
|
346
544
|
{/if}
|
|
347
545
|
|
|
348
|
-
{#if
|
|
546
|
+
{#if typeof toolbarSlot === 'function' && isCheckboxMode && effectiveSelectedIds.size > 0}
|
|
547
|
+
<div class="table-toolbar">
|
|
548
|
+
{@render toolbarSlot({ selectedIds: new Set(effectiveSelectedIds) })}
|
|
549
|
+
</div>
|
|
550
|
+
{/if}
|
|
551
|
+
|
|
552
|
+
{#if effectiveHeaders.length !== 0 || effectiveData.length !== 0}
|
|
349
553
|
<div
|
|
350
554
|
class="table-container {isTableScrollable ? 'scrollable-table' : ''} {classes ?? ''}"
|
|
351
555
|
data-pw={testId}
|
|
@@ -369,6 +573,9 @@
|
|
|
369
573
|
? 'mixed'
|
|
370
574
|
: headerCheckboxState === 'all'}
|
|
371
575
|
aria-label="Select all rows"
|
|
576
|
+
{...checkboxSelection?.getRowAttributes
|
|
577
|
+
? checkboxSelection.getRowAttributes('__header__', -1)
|
|
578
|
+
: {}}
|
|
372
579
|
aria-controls={selectableRowIds.map((rowId) => `row-checkbox-${rowId}`).join(' ')}
|
|
373
580
|
onclick={toggleAllSelection}
|
|
374
581
|
onkeydown={(keyboardEvent) =>
|
|
@@ -388,10 +595,71 @@
|
|
|
388
595
|
<th class="table-header table-checkbox-col" class:table-header-sticky={isStickyHeader}>
|
|
389
596
|
</th>
|
|
390
597
|
{/if}
|
|
391
|
-
{#
|
|
392
|
-
<th class="table-header" class:table-header-sticky={isStickyHeader}
|
|
393
|
-
|
|
394
|
-
|
|
598
|
+
{#if rowNumberColumn}
|
|
599
|
+
<th class="table-header table-row-number-col" class:table-header-sticky={isStickyHeader}
|
|
600
|
+
>#</th
|
|
601
|
+
>
|
|
602
|
+
{/if}
|
|
603
|
+
{#each effectiveHeaders as header, colIndex (colIndex)}
|
|
604
|
+
{@const headerColumn = columns?.[colIndex]}
|
|
605
|
+
<th
|
|
606
|
+
class="table-header"
|
|
607
|
+
class:table-header-sticky={isStickyHeader}
|
|
608
|
+
data-pw={headerColumn?.testId ?? null}
|
|
609
|
+
style:text-align={headerColumn?.align ?? null}
|
|
610
|
+
style:max-width={headerColumn?.maxWidth ?? null}
|
|
611
|
+
>
|
|
612
|
+
<span
|
|
613
|
+
class="table-header-content"
|
|
614
|
+
style:justify-content={headerColumn?.align === 'right'
|
|
615
|
+
? 'flex-end'
|
|
616
|
+
: headerColumn?.align === 'center'
|
|
617
|
+
? 'center'
|
|
618
|
+
: null}
|
|
619
|
+
>
|
|
620
|
+
{#if headerColumn?.tooltip}
|
|
621
|
+
<Tooltip text={headerColumn.tooltip}>
|
|
622
|
+
<span class="table-header-label">{header}</span>
|
|
623
|
+
</Tooltip>
|
|
624
|
+
{:else}
|
|
625
|
+
{header}
|
|
626
|
+
{/if}
|
|
627
|
+
{#if headerColumn?.filter}
|
|
628
|
+
{@const filter = headerColumn.filter}
|
|
629
|
+
<span class="table-header-filter">
|
|
630
|
+
<Menu
|
|
631
|
+
items={filter.options.map((option) => ({
|
|
632
|
+
value: option.value,
|
|
633
|
+
label: option.label
|
|
634
|
+
}))}
|
|
635
|
+
selectedValue={filter.selectedValue ?? null}
|
|
636
|
+
role="listbox"
|
|
637
|
+
testId={headerColumn.testId && `${headerColumn.testId}-filter`}
|
|
638
|
+
onselect={(menuItem) =>
|
|
639
|
+
filter.onFilterChange?.(
|
|
640
|
+
menuItem.value === filter.selectedValue ? null : menuItem.value
|
|
641
|
+
)}
|
|
642
|
+
>
|
|
643
|
+
{#snippet trigger()}
|
|
644
|
+
<span
|
|
645
|
+
class="table-header-filter-trigger"
|
|
646
|
+
class:table-header-filter-active={typeof filter.selectedValue ===
|
|
647
|
+
'string'}
|
|
648
|
+
>
|
|
649
|
+
<Button
|
|
650
|
+
ariaLabel="Filter by {header}"
|
|
651
|
+
testId={headerColumn.testId && `${headerColumn.testId}-filter-trigger`}
|
|
652
|
+
>
|
|
653
|
+
{#snippet icon()}
|
|
654
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
655
|
+
<span class="table-header-filter-icon">{@html chevronDownSmSvg}</span>
|
|
656
|
+
{/snippet}
|
|
657
|
+
</Button>
|
|
658
|
+
</span>
|
|
659
|
+
{/snippet}
|
|
660
|
+
</Menu>
|
|
661
|
+
</span>
|
|
662
|
+
{/if}
|
|
395
663
|
{#if isColumnSortable(colIndex)}
|
|
396
664
|
<div class="sort-button">
|
|
397
665
|
<Button onclick={() => handleSort(colIndex)} ariaLabel="Sort by {header}">
|
|
@@ -434,14 +702,17 @@
|
|
|
434
702
|
<tr>
|
|
435
703
|
<td
|
|
436
704
|
class="table-empty"
|
|
437
|
-
colspan={
|
|
705
|
+
colspan={effectiveHeaders.length +
|
|
706
|
+
(isCheckboxMode ? 1 : 0) +
|
|
707
|
+
(rowNumberColumn ? 1 : 0)}
|
|
438
708
|
>
|
|
439
709
|
{@render empty()}
|
|
440
710
|
</td>
|
|
441
711
|
</tr>
|
|
442
712
|
{:else}
|
|
443
|
-
{#each
|
|
444
|
-
{@const
|
|
713
|
+
{#each paginatedTableData as row, pageRowIndex (rowIdByRow.get(row) ?? pageRowIndex)}
|
|
714
|
+
{@const rowIndex = pageRowIndex + rowIndexOffset}
|
|
715
|
+
{@const rowId = rowIdByRow.get(row) ?? String(rowIndex)}
|
|
445
716
|
{@const rowDisabled = isCheckboxMode && isRowDisabled(rowId)}
|
|
446
717
|
{@const rowSelected = isCheckboxMode && isRowSelected(rowId)}
|
|
447
718
|
<tr
|
|
@@ -466,6 +737,10 @@
|
|
|
466
737
|
tabindex={rowDisabled ? -1 : 0}
|
|
467
738
|
aria-checked={rowSelected}
|
|
468
739
|
aria-disabled={rowDisabled}
|
|
740
|
+
aria-label={`Select row ${rowId || 'non-selectable'}`}
|
|
741
|
+
{...checkboxSelection?.getRowAttributes
|
|
742
|
+
? checkboxSelection.getRowAttributes(rowId, rowIndex)
|
|
743
|
+
: {}}
|
|
469
744
|
onclick={(mouseEvent) => {
|
|
470
745
|
mouseEvent.stopPropagation();
|
|
471
746
|
toggleRowSelection(rowId);
|
|
@@ -482,15 +757,34 @@
|
|
|
482
757
|
</span>
|
|
483
758
|
</td>
|
|
484
759
|
{/if}
|
|
760
|
+
{#if rowNumberColumn}
|
|
761
|
+
<td class="table-content table-row-number-col">{rowNumberFor(pageRowIndex)}</td>
|
|
762
|
+
{/if}
|
|
485
763
|
{#each row as cellValue, colIndex (colIndex)}
|
|
764
|
+
{@const keyedColumn = columns?.[colIndex]}
|
|
765
|
+
{@const keyedRow = keyedRowByProjected?.get(row)}
|
|
766
|
+
{@const isScalarCell =
|
|
767
|
+
typeof cellValue === 'string' ||
|
|
768
|
+
typeof cellValue === 'number' ||
|
|
769
|
+
typeof cellValue === 'boolean'}
|
|
486
770
|
<td
|
|
487
771
|
class="table-content"
|
|
488
772
|
data-pw={typeof getCellTestId === 'function'
|
|
489
773
|
? getCellTestId(row, cellValue, rowIndex)
|
|
490
774
|
: null}
|
|
775
|
+
style:text-align={keyedColumn?.align ?? null}
|
|
776
|
+
style:max-width={keyedColumn?.maxWidth ?? null}
|
|
777
|
+
title={keyedColumn?.maxWidth && isScalarCell ? String(cellValue) : null}
|
|
491
778
|
>
|
|
492
|
-
<div
|
|
493
|
-
{
|
|
779
|
+
<div
|
|
780
|
+
class={isContentScrollable ? 'scrollable-content' : ''}
|
|
781
|
+
class:table-cell-clamp={keyedColumn?.maxWidth && isScalarCell}
|
|
782
|
+
>
|
|
783
|
+
{#if keyedColumn && typeof keyedColumn.cell === 'function' && keyedRow}
|
|
784
|
+
{@render keyedColumn.cell(keyedRow, rowIndex)}
|
|
785
|
+
{:else if keyedColumn?.type && keyedColumn.type !== 'text' && keyedColumn.type !== 'custom'}
|
|
786
|
+
<BuiltinCell column={keyedColumn} value={cellValue} {rowIndex} />
|
|
787
|
+
{:else if typeof cell === 'function'}
|
|
494
788
|
{@render cell(cellValue, rowIndex, colIndex)}
|
|
495
789
|
{:else}
|
|
496
790
|
{cellValue}
|
|
@@ -507,6 +801,42 @@
|
|
|
507
801
|
<div class="table-footer">
|
|
508
802
|
{@render paginatorSlot()}
|
|
509
803
|
</div>
|
|
804
|
+
{:else if pagination && (paginationTotalPages > 1 || pagination.hasMore)}
|
|
805
|
+
<!-- DataGrid parity: pagination chrome only renders when the data spans
|
|
806
|
+
more than one page (or the server reports more chunks); a
|
|
807
|
+
single-page table shows no footer. -->
|
|
808
|
+
<div class="table-footer table-paginator" data-pw={pagination.testId ?? null}>
|
|
809
|
+
<span class="table-paginator-range">{paginationRangeText}</span>
|
|
810
|
+
<span class="table-paginator-controls">
|
|
811
|
+
{#if pageSizeOptions.length > 0}
|
|
812
|
+
<span class="table-paginator-size">
|
|
813
|
+
<Select
|
|
814
|
+
items={pageSizeOptions.map((sizeOption) => ({
|
|
815
|
+
id: String(sizeOption),
|
|
816
|
+
label: String(sizeOption)
|
|
817
|
+
}))}
|
|
818
|
+
value={[String(effectivePageSize)]}
|
|
819
|
+
disabled={pagination.isLoading ?? false}
|
|
820
|
+
testId={pagination.testId && `${pagination.testId}-page-size`}
|
|
821
|
+
onchange={(selectedSizes) => {
|
|
822
|
+
if (selectedSizes.length > 0) {
|
|
823
|
+
handlePageSizeChange(Number(selectedSizes[0]));
|
|
824
|
+
}
|
|
825
|
+
}}
|
|
826
|
+
/>
|
|
827
|
+
</span>
|
|
828
|
+
{/if}
|
|
829
|
+
<Pagination
|
|
830
|
+
totalPages={paginationTotalPages}
|
|
831
|
+
currentPage={effectivePage}
|
|
832
|
+
hasMore={pagination.hasMore ?? false}
|
|
833
|
+
disabled={pagination.isLoading ?? false}
|
|
834
|
+
testId={pagination.testId && `${pagination.testId}-pages`}
|
|
835
|
+
onchange={handlePageChange}
|
|
836
|
+
onLoadMore={pagination.onLoadMore}
|
|
837
|
+
/>
|
|
838
|
+
</span>
|
|
839
|
+
</div>
|
|
510
840
|
{/if}
|
|
511
841
|
</div>
|
|
512
842
|
{/if}
|
|
@@ -617,7 +947,13 @@
|
|
|
617
947
|
padding: var(--table-padding, 12px 16px);
|
|
618
948
|
text-align: var(--table-text-align, left);
|
|
619
949
|
width: var(--table-column-width);
|
|
620
|
-
word
|
|
950
|
+
/* Wrap at word boundaries; only a single word wider than the column may
|
|
951
|
+
split as a last resort. break-word (not anywhere) keeps each column's
|
|
952
|
+
min-content width at its longest word, so ordinary labels never split
|
|
953
|
+
mid-word — the old break-all default did. Opt back in per consumer via
|
|
954
|
+
--table-word-break / --table-overflow-wrap. */
|
|
955
|
+
word-break: var(--table-word-break, normal);
|
|
956
|
+
overflow-wrap: var(--table-overflow-wrap, break-word);
|
|
621
957
|
}
|
|
622
958
|
|
|
623
959
|
.scrollable-content {
|
|
@@ -641,6 +977,53 @@
|
|
|
641
977
|
gap: 4px;
|
|
642
978
|
}
|
|
643
979
|
|
|
980
|
+
.table-header-label {
|
|
981
|
+
text-decoration: var(--table-header-tooltip-underline, underline dotted);
|
|
982
|
+
text-underline-offset: 2px;
|
|
983
|
+
cursor: help;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.table-header-filter {
|
|
987
|
+
display: inline-flex;
|
|
988
|
+
align-items: center;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.table-header-filter-trigger {
|
|
992
|
+
--button-color: transparent;
|
|
993
|
+
--button-border: none;
|
|
994
|
+
--button-padding: 2px;
|
|
995
|
+
--button-margin: 0;
|
|
996
|
+
--button-width: fit-content;
|
|
997
|
+
--button-height: fit-content;
|
|
998
|
+
--button-text-color: var(--table-sort-button-color, inherit);
|
|
999
|
+
--button-border-radius: 4px;
|
|
1000
|
+
--button-hover-color: var(--table-sort-button-hover-background, rgba(0, 0, 0, 0.05));
|
|
1001
|
+
display: inline-flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
opacity: var(--table-sort-idle-opacity, 0.5);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.table-header-filter-trigger:hover {
|
|
1007
|
+
opacity: var(--table-sort-idle-hover-opacity, 0.85);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.table-header-filter-active {
|
|
1011
|
+
opacity: 1;
|
|
1012
|
+
color: var(--table-filter-active-color, #2563eb);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.table-header-filter-icon :global(svg) {
|
|
1016
|
+
width: var(--table-sort-icon-size, 14px);
|
|
1017
|
+
height: var(--table-sort-icon-size, 14px);
|
|
1018
|
+
display: block;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.table-cell-clamp {
|
|
1022
|
+
overflow: hidden;
|
|
1023
|
+
text-overflow: ellipsis;
|
|
1024
|
+
white-space: nowrap;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
644
1027
|
.table-header-sticky {
|
|
645
1028
|
position: sticky;
|
|
646
1029
|
top: var(--table-header-sticky-top, 0);
|
|
@@ -810,6 +1193,50 @@
|
|
|
810
1193
|
background-color: var(--table-footer-background, transparent);
|
|
811
1194
|
}
|
|
812
1195
|
|
|
1196
|
+
.table-paginator {
|
|
1197
|
+
display: flex;
|
|
1198
|
+
align-items: center;
|
|
1199
|
+
justify-content: space-between;
|
|
1200
|
+
gap: var(--table-paginator-gap, 12px);
|
|
1201
|
+
flex-wrap: wrap;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.table-paginator-range {
|
|
1205
|
+
font-size: var(--table-paginator-range-font-size, 13px);
|
|
1206
|
+
color: var(--table-paginator-range-color, #6b7280);
|
|
1207
|
+
font-variant-numeric: tabular-nums;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.table-paginator-controls {
|
|
1211
|
+
display: flex;
|
|
1212
|
+
align-items: center;
|
|
1213
|
+
gap: var(--table-paginator-gap, 12px);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.table-paginator-size {
|
|
1217
|
+
display: inline-flex;
|
|
1218
|
+
min-width: var(--table-paginator-size-width, 72px);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/* ── Toolbar (bulk actions) ─────────────────────────────────────────────── */
|
|
1222
|
+
.table-toolbar {
|
|
1223
|
+
display: flex;
|
|
1224
|
+
align-items: center;
|
|
1225
|
+
gap: var(--table-toolbar-gap, 12px);
|
|
1226
|
+
padding: var(--table-toolbar-padding, 8px 12px);
|
|
1227
|
+
border: var(--table-toolbar-border, 1px solid #e5e7eb);
|
|
1228
|
+
border-radius: var(--table-toolbar-border-radius, var(--radius, 4px));
|
|
1229
|
+
background-color: var(--table-toolbar-background, #f9fafb);
|
|
1230
|
+
margin-bottom: var(--table-toolbar-margin-bottom, 8px);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/* ── Row-number column ──────────────────────────────────────────────────── */
|
|
1234
|
+
.table-row-number-col {
|
|
1235
|
+
width: var(--table-row-number-col-width, 48px);
|
|
1236
|
+
color: var(--table-row-number-color, #6b7280);
|
|
1237
|
+
font-variant-numeric: tabular-nums;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
813
1240
|
/* ── Accessibility ──────────────────────────────────────────────────────── */
|
|
814
1241
|
.sr-only {
|
|
815
1242
|
position: absolute;
|