@marianmeres/stuic 3.161.0 → 3.162.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.
|
@@ -98,6 +98,54 @@
|
|
|
98
98
|
/** Callback when a row is clicked */
|
|
99
99
|
onRowClick?: (row: T, index: number) => void;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
103
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
104
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
105
|
+
*
|
|
106
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
107
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
108
|
+
*
|
|
109
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
110
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
111
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
112
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
113
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
114
|
+
*/
|
|
115
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
118
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
119
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
120
|
+
*/
|
|
121
|
+
rowHrefColumn?: string;
|
|
122
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
123
|
+
classRowLink?: string;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
127
|
+
*
|
|
128
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
129
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
130
|
+
* link address", and an announcement of where it goes).
|
|
131
|
+
*
|
|
132
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
133
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
134
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
135
|
+
*
|
|
136
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
137
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
138
|
+
* worse trade than the one the card makes.
|
|
139
|
+
*
|
|
140
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
141
|
+
*/
|
|
142
|
+
rowActivatable?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
145
|
+
* Only applied when the row is actually activatable.
|
|
146
|
+
*/
|
|
147
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
148
|
+
|
|
101
149
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
102
150
|
loading?: boolean;
|
|
103
151
|
|
|
@@ -203,6 +251,11 @@
|
|
|
203
251
|
selectedAll = $bindable(false),
|
|
204
252
|
excluded = $bindable(new Set()),
|
|
205
253
|
onRowClick,
|
|
254
|
+
rowHref,
|
|
255
|
+
rowHrefColumn,
|
|
256
|
+
classRowLink,
|
|
257
|
+
rowActivatable = false,
|
|
258
|
+
rowLabel,
|
|
206
259
|
loading = false,
|
|
207
260
|
cell,
|
|
208
261
|
row,
|
|
@@ -342,22 +395,46 @@
|
|
|
342
395
|
return allOnPageSelected;
|
|
343
396
|
});
|
|
344
397
|
|
|
345
|
-
// --- Row click ---
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
398
|
+
// --- Row click / activation ---
|
|
399
|
+
|
|
400
|
+
// A row is a big target that legitimately contains its own controls. Anything that
|
|
401
|
+
// originates inside one of those is that control's business, not the row's - this
|
|
402
|
+
// guards the click handler AND both keydown handlers (Enter on a focused lead link
|
|
403
|
+
// must navigate, not fire onRowClick; Space on a focused checkbox must toggle it).
|
|
404
|
+
function isInteractiveTarget(e: Event): boolean {
|
|
405
|
+
const target = e.target as HTMLElement | null;
|
|
406
|
+
if (!target?.closest) return false;
|
|
407
|
+
return !!(
|
|
349
408
|
target.closest('input[type="checkbox"]') ||
|
|
350
409
|
target.closest("button") ||
|
|
351
410
|
target.closest("a")
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function activateRow(row: T, index: number) {
|
|
355
415
|
if (selectable && selectOnRowClick) {
|
|
356
416
|
toggleSelectRow(getRowId(row, index));
|
|
357
417
|
}
|
|
358
418
|
onRowClick?.(row, index);
|
|
359
419
|
}
|
|
360
420
|
|
|
421
|
+
function handleRowClick(row: T, index: number, e: MouseEvent) {
|
|
422
|
+
if (isInteractiveTarget(e)) return;
|
|
423
|
+
activateRow(row, index);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Shared by the desktop `<tr>` (Enter only) and the mobile card (Enter + Space, since
|
|
428
|
+
* the card actually is a `role="button"`).
|
|
429
|
+
*/
|
|
430
|
+
function handleRowKeydown(row: T, index: number, e: KeyboardEvent, withSpace: boolean) {
|
|
431
|
+
if (!rowClickable) return;
|
|
432
|
+
if (e.key !== "Enter" && !(withSpace && e.key === " ")) return;
|
|
433
|
+
if (isInteractiveTarget(e)) return;
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
activateRow(row, index);
|
|
436
|
+
}
|
|
437
|
+
|
|
361
438
|
// --- Cell value helpers ---
|
|
362
439
|
function getCellValue(row: T, column: DataTableColumn<T>): any {
|
|
363
440
|
return column.key.split(".").reduce((obj: any, k) => obj?.[k], row);
|
|
@@ -373,8 +450,45 @@
|
|
|
373
450
|
let rootClass = $derived(unstyled ? classProp : twMerge("stuic-data-table", classProp));
|
|
374
451
|
|
|
375
452
|
let mobileColumns = $derived(columns.filter((col) => !col.hideOnMobile));
|
|
453
|
+
|
|
454
|
+
// --- Row link / activation derivations ---
|
|
455
|
+
let rowClickable = $derived(!!(onRowClick || selectOnRowClick));
|
|
456
|
+
let rowIsActivatable = $derived(rowActivatable && rowClickable);
|
|
457
|
+
|
|
458
|
+
// The lead cell is the one whose content gets wrapped in the `rowHref` anchor.
|
|
459
|
+
let leadColumnKey = $derived(rowHrefColumn ?? columns[0]?.key);
|
|
460
|
+
// A `hideOnMobile` lead column would leave the card with no link at all, so there we
|
|
461
|
+
// fall back to the first visible field rather than silently dropping the anchor.
|
|
462
|
+
let mobileLeadColumnKey = $derived(
|
|
463
|
+
mobileColumns.some((col) => col.key === leadColumnKey)
|
|
464
|
+
? leadColumnKey
|
|
465
|
+
: mobileColumns[0]?.key
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
let rowLinkClass = $derived(
|
|
469
|
+
unstyled ? classRowLink : twMerge("stuic-data-table-row-link", classRowLink)
|
|
470
|
+
);
|
|
376
471
|
</script>
|
|
377
472
|
|
|
473
|
+
<!--
|
|
474
|
+
The rendered content of a single cell, shared by both layouts so that a `rowHref`
|
|
475
|
+
anchor wraps exactly what the cell would have rendered anyway (a consumer `cell`
|
|
476
|
+
snippet included).
|
|
477
|
+
-->
|
|
478
|
+
{#snippet cellBody(
|
|
479
|
+
col: DataTableColumn<T>,
|
|
480
|
+
rowData: T,
|
|
481
|
+
value: any,
|
|
482
|
+
rowIndex: number,
|
|
483
|
+
variant: "desktop" | "mobile"
|
|
484
|
+
)}
|
|
485
|
+
{#if cell}
|
|
486
|
+
{@render cell({ column: col, row: rowData, value, rowIndex, variant })}
|
|
487
|
+
{:else}
|
|
488
|
+
{getCellDisplay(rowData, col)}
|
|
489
|
+
{/if}
|
|
490
|
+
{/snippet}
|
|
491
|
+
|
|
378
492
|
<!-- Batch action bar -->
|
|
379
493
|
{#if selectable && effectiveCount > 0 && batchActions}
|
|
380
494
|
<div class={!unstyled ? "stuic-data-table-batch" : undefined}>
|
|
@@ -461,16 +575,20 @@
|
|
|
461
575
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
462
576
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
463
577
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
578
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
464
579
|
{#if row}
|
|
465
580
|
{@render row({ row: rowData, columns, rowIndex, isSelected })}
|
|
466
581
|
{:else}
|
|
467
582
|
<tr
|
|
468
583
|
data-hoverable={!unstyled ? "true" : undefined}
|
|
469
|
-
data-clickable={!unstyled &&
|
|
470
|
-
? "true"
|
|
471
|
-
: undefined}
|
|
584
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
472
585
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
586
|
+
tabindex={rowIsActivatable ? 0 : undefined}
|
|
587
|
+
aria-label={rowIsActivatable ? rowLabel?.(rowData, rowIndex) : undefined}
|
|
473
588
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
589
|
+
onkeydown={(e) => {
|
|
590
|
+
if (rowActivatable) handleRowKeydown(rowData, rowIndex, e, false);
|
|
591
|
+
}}
|
|
474
592
|
>
|
|
475
593
|
{#if selectable}
|
|
476
594
|
<td data-checkbox class="stuic-checkbox">
|
|
@@ -485,20 +603,17 @@
|
|
|
485
603
|
{/if}
|
|
486
604
|
{#each columns as col (col.key)}
|
|
487
605
|
{@const value = getCellValue(rowData, col)}
|
|
606
|
+
{@const href = col.key === leadColumnKey ? rowLink : undefined}
|
|
488
607
|
<td
|
|
489
608
|
class={col.class}
|
|
490
609
|
data-align={!unstyled && col.align ? col.align : undefined}
|
|
491
610
|
>
|
|
492
|
-
{#if
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
value,
|
|
497
|
-
rowIndex,
|
|
498
|
-
variant: "desktop",
|
|
499
|
-
})}
|
|
611
|
+
{#if href}
|
|
612
|
+
<a class={rowLinkClass} {href}>
|
|
613
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
614
|
+
</a>
|
|
500
615
|
{:else}
|
|
501
|
-
{
|
|
616
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
502
617
|
{/if}
|
|
503
618
|
</td>
|
|
504
619
|
{/each}
|
|
@@ -531,6 +646,7 @@
|
|
|
531
646
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
532
647
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
533
648
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
649
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
534
650
|
{#if mobileRow}
|
|
535
651
|
{@render mobileRow({
|
|
536
652
|
row: rowData,
|
|
@@ -542,25 +658,12 @@
|
|
|
542
658
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
543
659
|
<div
|
|
544
660
|
class={!unstyled ? "stuic-data-table-card" : undefined}
|
|
545
|
-
data-clickable={!unstyled &&
|
|
546
|
-
? "true"
|
|
547
|
-
: undefined}
|
|
661
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
548
662
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
549
|
-
role={
|
|
550
|
-
tabindex={
|
|
663
|
+
role={rowClickable ? "button" : undefined}
|
|
664
|
+
tabindex={rowClickable ? 0 : undefined}
|
|
551
665
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
552
|
-
onkeydown={(e) =>
|
|
553
|
-
if (
|
|
554
|
-
(onRowClick || selectOnRowClick) &&
|
|
555
|
-
(e.key === "Enter" || e.key === " ")
|
|
556
|
-
) {
|
|
557
|
-
e.preventDefault();
|
|
558
|
-
if (selectable && selectOnRowClick) {
|
|
559
|
-
toggleSelectRow(rowId);
|
|
560
|
-
}
|
|
561
|
-
onRowClick?.(rowData, rowIndex);
|
|
562
|
-
}
|
|
563
|
-
}}
|
|
666
|
+
onkeydown={(e) => handleRowKeydown(rowData, rowIndex, e, true)}
|
|
564
667
|
>
|
|
565
668
|
{#if selectable}
|
|
566
669
|
<div
|
|
@@ -579,6 +682,7 @@
|
|
|
579
682
|
{/if}
|
|
580
683
|
{#each mobileColumns as col (col.key)}
|
|
581
684
|
{@const value = getCellValue(rowData, col)}
|
|
685
|
+
{@const href = col.key === mobileLeadColumnKey ? rowLink : undefined}
|
|
582
686
|
<div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
|
|
583
687
|
<span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
|
|
584
688
|
{#if isTHCNotEmpty(col.label)}
|
|
@@ -588,16 +692,12 @@
|
|
|
588
692
|
{/if}
|
|
589
693
|
</span>
|
|
590
694
|
<span class={!unstyled ? "stuic-data-table-card-value" : undefined}>
|
|
591
|
-
{#if
|
|
592
|
-
{
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
value,
|
|
596
|
-
rowIndex,
|
|
597
|
-
variant: "mobile",
|
|
598
|
-
})}
|
|
695
|
+
{#if href}
|
|
696
|
+
<a class={rowLinkClass} {href}>
|
|
697
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
698
|
+
</a>
|
|
599
699
|
{:else}
|
|
600
|
-
{
|
|
700
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
601
701
|
{/if}
|
|
602
702
|
</span>
|
|
603
703
|
</div>
|
|
@@ -58,6 +58,52 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
58
58
|
excluded?: Set<string | number>;
|
|
59
59
|
/** Callback when a row is clicked */
|
|
60
60
|
onRowClick?: (row: T, index: number) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
63
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
64
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
65
|
+
*
|
|
66
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
67
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
68
|
+
*
|
|
69
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
70
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
71
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
72
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
73
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
74
|
+
*/
|
|
75
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
78
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
79
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
80
|
+
*/
|
|
81
|
+
rowHrefColumn?: string;
|
|
82
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
83
|
+
classRowLink?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
86
|
+
*
|
|
87
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
88
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
89
|
+
* link address", and an announcement of where it goes).
|
|
90
|
+
*
|
|
91
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
92
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
93
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
94
|
+
*
|
|
95
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
96
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
97
|
+
* worse trade than the one the card makes.
|
|
98
|
+
*
|
|
99
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
100
|
+
*/
|
|
101
|
+
rowActivatable?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
104
|
+
* Only applied when the row is actually activatable.
|
|
105
|
+
*/
|
|
106
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
61
107
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
62
108
|
loading?: boolean;
|
|
63
109
|
/** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
|
|
@@ -143,6 +143,89 @@ New records inserted while all-pages mode is active are implicitly selected (the
|
|
|
143
143
|
|
|
144
144
|
**Filter changes:** when filters change in the consumer, reset the bound selection stores (`selected`, `selectedAll`, `excluded`) explicitly — DataTable doesn't track which filter produced the current state.
|
|
145
145
|
|
|
146
|
+
### Clickable Rows That Work Without a Mouse
|
|
147
|
+
|
|
148
|
+
`onRowClick` alone makes a row **mouse-only** on the desktop table layout: a `<tr>` is not
|
|
149
|
+
focusable, so nothing about the row is reachable by keyboard. (The mobile card layout has
|
|
150
|
+
always been fine — it is a `<div role="button">`.) Two opt-in props close that gap, and
|
|
151
|
+
they are not alternatives:
|
|
152
|
+
|
|
153
|
+
| Your row action is… | Use |
|
|
154
|
+
| ------------------------------- | ---------------- |
|
|
155
|
+
| a navigation (it has a URL) | `rowHref` |
|
|
156
|
+
| anything else (drawer, expand…) | `rowActivatable` |
|
|
157
|
+
|
|
158
|
+
#### `rowHref` — the lead cell becomes a real link
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<DataTable
|
|
162
|
+
{columns}
|
|
163
|
+
{data}
|
|
164
|
+
getRowId={(row) => row.id}
|
|
165
|
+
rowHref={(row) => `/users/${row.id}`}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The content of the row's **lead cell** (the first column by default, or `rowHrefColumn`)
|
|
170
|
+
is wrapped in an `<a href>` — including whatever your `cell` snippet renders for it, and
|
|
171
|
+
respecting the column's `renderValue`. A link gets, for free and correctly: keyboard focus
|
|
172
|
+
in the right tab order, `Enter`, ⌘/middle-click and "open in new tab", "copy link address",
|
|
173
|
+
and a screen-reader announcement of _where it goes_ rather than an anonymous "button".
|
|
174
|
+
|
|
175
|
+
Return `undefined` for a row with no destination (a placeholder, a row still minting its
|
|
176
|
+
id) — that row's lead cell renders exactly as it would without `rowHref`.
|
|
177
|
+
|
|
178
|
+
`rowHref` composes with `onRowClick`: the row click handler ignores clicks originating
|
|
179
|
+
inside an anchor, so the two never double-fire. The consequence is worth stating — a plain
|
|
180
|
+
click **on the lead cell** navigates via the href and does _not_ call `onRowClick`, so point
|
|
181
|
+
both at the same destination.
|
|
182
|
+
|
|
183
|
+
On mobile the same field is linked. If the lead column is `hideOnMobile`, the card links its
|
|
184
|
+
first visible field instead, rather than silently dropping the anchor.
|
|
185
|
+
|
|
186
|
+
By default the link looks like the text it replaced (`color: inherit`, no underline until
|
|
187
|
+
hover) — a table where every lead cell is blue-and-underlined is a worse table. Three CSS
|
|
188
|
+
variables and `classRowLink` are there if you disagree.
|
|
189
|
+
|
|
190
|
+
#### `rowActivatable` — the `<tr>` itself
|
|
191
|
+
|
|
192
|
+
For a row whose action has no address:
|
|
193
|
+
|
|
194
|
+
```svelte
|
|
195
|
+
<DataTable
|
|
196
|
+
{columns}
|
|
197
|
+
{data}
|
|
198
|
+
onRowClick={(row) => openDrawer(row)}
|
|
199
|
+
rowActivatable
|
|
200
|
+
rowLabel={(row) => `Open ${row.name}`}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `<tr>` gets `tabindex="0"`, an optional `aria-label` from `rowLabel`, and a
|
|
205
|
+
`:focus-visible` ring. It is a no-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
206
|
+
|
|
207
|
+
Two deliberate choices, so nobody "fixes" them later:
|
|
208
|
+
|
|
209
|
+
- **No `role="button"` on the `<tr>`.** A `<tr>` carries an implicit `row` role that the
|
|
210
|
+
table's own structure depends on. Overriding it detaches the row from the table for
|
|
211
|
+
assistive technology — row/column counts stop making sense and cell-by-cell navigation
|
|
212
|
+
breaks. The row stays a row; it merely becomes focusable. (The mobile card is a `<div>`,
|
|
213
|
+
so `role="button"` is correct _there_.)
|
|
214
|
+
- **`Enter` only, no `Space`.** `Space` on a focused row scrolls the page, and a row is not
|
|
215
|
+
a button — taking `Space` away from a table is a worse trade than the one the card makes.
|
|
216
|
+
|
|
217
|
+
With `selectable` + `selectOnRowClick`, `Enter` toggles selection _and_ fires `onRowClick`,
|
|
218
|
+
matching the mobile card.
|
|
219
|
+
|
|
220
|
+
#### Both are opt-in
|
|
221
|
+
|
|
222
|
+
With neither prop set, the rendered markup is exactly what it was before they existed. A
|
|
223
|
+
consumer who replaces the whole `<tr>` with the `row` snippet opts out of both and owns the
|
|
224
|
+
keyboard story themselves.
|
|
225
|
+
|
|
226
|
+
Interactive descendants always win: a click, `Enter` or `Space` that originates inside a
|
|
227
|
+
`<button>`, an `<a>` or a checkbox in the row is that control's business, not the row's.
|
|
228
|
+
|
|
146
229
|
### Custom Cell Rendering
|
|
147
230
|
|
|
148
231
|
The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
|
|
@@ -204,33 +287,38 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
204
287
|
|
|
205
288
|
## Props
|
|
206
289
|
|
|
207
|
-
| Prop | Type
|
|
208
|
-
| --------------------- |
|
|
209
|
-
| `columns` | `DataTableColumn<T>[]`
|
|
210
|
-
| `data` | `T[]`
|
|
211
|
-
| `getRowId` | `(row, index) => string \| number`
|
|
212
|
-
| `paging` | `PagingCalcResult`
|
|
213
|
-
| `onPageChange` | `(offset: number) => void`
|
|
214
|
-
| `selectable` | `boolean`
|
|
215
|
-
| `selected` | `Set<string \| number>`
|
|
216
|
-
| `selectOnRowClick` | `boolean`
|
|
217
|
-
| `selectDisabledBy` | `(row, index) => boolean`
|
|
218
|
-
| `allowSelectAllPages` | `boolean`
|
|
219
|
-
| `selectedAll` | `boolean`
|
|
220
|
-
| `excluded` | `Set<string \| number>`
|
|
221
|
-
| `onRowClick` | `(row, index) => void`
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
290
|
+
| Prop | Type | Default | Description |
|
|
291
|
+
| --------------------- | ------------------------------------- | ------------- | -------------------------------------------------------------------------- |
|
|
292
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
293
|
+
| `data` | `T[]` | required | Array of row data |
|
|
294
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
295
|
+
| `paging` | `PagingCalcResult` | - | Paging state (from `@marianmeres/paging-store`) |
|
|
296
|
+
| `onPageChange` | `(offset: number) => void` | - | Called with the new offset when the user navigates pages |
|
|
297
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
298
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
299
|
+
| `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
|
|
300
|
+
| `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
|
|
301
|
+
| `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
|
|
302
|
+
| `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
|
|
303
|
+
| `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
|
|
304
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
305
|
+
| `rowHref` | `(row, index) => string \| undefined` | - | Wrap the lead cell's content in an `<a href>` (keyboard/⌘-click reachable) |
|
|
306
|
+
| `rowHrefColumn` | `string` | first column | Which column is the lead cell for `rowHref` |
|
|
307
|
+
| `classRowLink` | `string` | - | Extra classes for the `rowHref` anchor |
|
|
308
|
+
| `rowActivatable` | `boolean` | `false` | Make the desktop `<tr>` focusable + `Enter`-activatable (no `role`) |
|
|
309
|
+
| `rowLabel` | `(row, index) => string \| undefined` | - | Accessible name for an activatable row |
|
|
310
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
311
|
+
| `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
|
|
312
|
+
| `t` | `TranslateFn` | built-in | Optional translation function |
|
|
313
|
+
| `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
|
|
314
|
+
| `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
|
|
315
|
+
| `mobileRow` | `Snippet` | - | Custom mobile card renderer |
|
|
316
|
+
| `batchActions` | `Snippet` | - | Batch action bar content |
|
|
317
|
+
| `selectAllBanner` | `Snippet` | - | Override default "select all across pages" banner |
|
|
318
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
319
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
320
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
321
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
234
322
|
|
|
235
323
|
### Snippet signatures
|
|
236
324
|
|
|
@@ -262,29 +350,34 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
262
350
|
|
|
263
351
|
## CSS Variables
|
|
264
352
|
|
|
265
|
-
| Variable
|
|
266
|
-
|
|
|
267
|
-
| `--stuic-data-table-radius`
|
|
268
|
-
| `--stuic-data-table-border-color`
|
|
269
|
-
| `--stuic-data-table-header-bg`
|
|
270
|
-
| `--stuic-data-table-header-color`
|
|
271
|
-
| `--stuic-data-table-header-font-size`
|
|
272
|
-
| `--stuic-data-table-header-font-weight`
|
|
273
|
-
| `--stuic-data-table-header-padding-x`
|
|
274
|
-
| `--stuic-data-table-header-padding-y`
|
|
275
|
-
| `--stuic-data-table-row-bg`
|
|
276
|
-
| `--stuic-data-table-row-bg-hover`
|
|
277
|
-
| `--stuic-data-table-row-bg-selected`
|
|
278
|
-
| `--stuic-data-table-row-border-color`
|
|
279
|
-
| `--stuic-data-table-
|
|
280
|
-
| `--stuic-data-table-
|
|
281
|
-
| `--stuic-data-table-
|
|
282
|
-
| `--stuic-data-table-
|
|
283
|
-
| `--stuic-data-table-
|
|
284
|
-
| `--stuic-data-table-
|
|
285
|
-
| `--stuic-data-table-
|
|
286
|
-
| `--stuic-data-table-
|
|
287
|
-
| `--stuic-data-table-
|
|
288
|
-
| `--stuic-data-table-
|
|
289
|
-
| `--stuic-data-table-
|
|
290
|
-
| `--stuic-data-table-
|
|
353
|
+
| Variable | Default | Description |
|
|
354
|
+
| ---------------------------------------------- | ------------------------------------- | ---------------------------- |
|
|
355
|
+
| `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
|
|
356
|
+
| `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
|
|
357
|
+
| `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
|
|
358
|
+
| `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
|
|
359
|
+
| `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
|
|
360
|
+
| `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
|
|
361
|
+
| `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
|
|
362
|
+
| `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
|
|
363
|
+
| `--stuic-data-table-row-bg` | `transparent` | Row background |
|
|
364
|
+
| `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
|
|
365
|
+
| `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
|
|
366
|
+
| `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
|
|
367
|
+
| `--stuic-data-table-row-link-color` | `inherit` | `rowHref` anchor color |
|
|
368
|
+
| `--stuic-data-table-row-link-decoration` | `none` | `rowHref` anchor decoration |
|
|
369
|
+
| `--stuic-data-table-row-link-decoration-hover` | `underline` | …on hover |
|
|
370
|
+
| `--stuic-data-table-row-ring-width` | `3px` | Activatable row focus ring |
|
|
371
|
+
| `--stuic-data-table-row-ring-color` | `var(--stuic-color-ring)` | Activatable row ring color |
|
|
372
|
+
| `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
|
|
373
|
+
| `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
|
|
374
|
+
| `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
|
|
375
|
+
| `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
|
|
376
|
+
| `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
|
|
377
|
+
| `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
|
|
378
|
+
| `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
|
|
379
|
+
| `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
|
|
380
|
+
| `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
|
|
381
|
+
| `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | Select-all banner background |
|
|
382
|
+
| `--stuic-data-table-select-all-padding-x` | `0.75rem` | Banner horizontal padding |
|
|
383
|
+
| `--stuic-data-table-select-all-padding-y` | `0.5rem` | Banner vertical padding |
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
--stuic-data-table-row-bg-selected: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
25
25
|
--stuic-data-table-row-border-color: var(--stuic-color-border);
|
|
26
26
|
|
|
27
|
+
/* Row link (rowHref) — defaults to "looks like the text it replaced" */
|
|
28
|
+
--stuic-data-table-row-link-color: inherit;
|
|
29
|
+
--stuic-data-table-row-link-decoration: none;
|
|
30
|
+
--stuic-data-table-row-link-decoration-hover: underline;
|
|
31
|
+
|
|
32
|
+
/* Focus ring for an activatable row (rowActivatable) */
|
|
33
|
+
--stuic-data-table-row-ring-width: 3px;
|
|
34
|
+
--stuic-data-table-row-ring-color: var(--stuic-color-ring);
|
|
35
|
+
|
|
27
36
|
/* Cell */
|
|
28
37
|
--stuic-data-table-cell-padding-x: 0.75rem;
|
|
29
38
|
--stuic-data-table-cell-padding-y: 0.75rem;
|
|
@@ -152,6 +161,31 @@
|
|
|
152
161
|
);
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
/*
|
|
165
|
+
`rowActivatable` puts tabindex on the <tr>, and a focusable element with no
|
|
166
|
+
visible ring is worse than no tabindex at all. The negative offset keeps the
|
|
167
|
+
ring inside the row box — an outset ring on a <tr> is clipped unpredictably
|
|
168
|
+
by border-collapse.
|
|
169
|
+
*/
|
|
170
|
+
.stuic-data-table tbody tr:focus-visible {
|
|
171
|
+
outline: var(--stuic-data-table-row-ring-width) solid
|
|
172
|
+
var(--stuic-data-table-row-ring-color);
|
|
173
|
+
outline-offset: calc(var(--stuic-data-table-row-ring-width) * -1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ============================================================================
|
|
177
|
+
ROW LINK (rowHref)
|
|
178
|
+
============================================================================ */
|
|
179
|
+
|
|
180
|
+
.stuic-data-table-row-link {
|
|
181
|
+
color: var(--stuic-data-table-row-link-color);
|
|
182
|
+
text-decoration: var(--stuic-data-table-row-link-decoration);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.stuic-data-table-row-link:hover {
|
|
186
|
+
text-decoration: var(--stuic-data-table-row-link-decoration-hover);
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
/* ============================================================================
|
|
156
190
|
CELL
|
|
157
191
|
============================================================================ */
|