@eifi1/ui-kit 0.48.0 → 0.48.2
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 +4 -3
- package/dist/components/data-table-fit.d.ts +8 -1
- package/dist/components/data-table-fit.js +7 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +3 -1
- package/dist/components/data-table-groups.js +11 -5
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +8 -1
- package/dist/components/data-table-head.js +11 -3
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +3 -1
- package/dist/components/data-table-leading-row.js +2 -1
- package/dist/components/data-table-leading-row.js.map +1 -1
- package/dist/components/data-table-row-actions.js +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +84 -0
- package/dist/components/data-table-row.js +261 -0
- package/dist/components/data-table-row.js.map +1 -0
- package/dist/components/data-table-selection.d.ts +3 -0
- package/dist/components/data-table-selection.js +12 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +98 -0
- package/dist/components/data-table-virtual.js +369 -0
- package/dist/components/data-table-virtual.js.map +1 -0
- package/dist/components/data-table.d.ts +58 -2
- package/dist/components/data-table.js +227 -230
- package/dist/components/data-table.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/shell/app-shell.js +7 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/package.json +1 -1
- package/src/components/data-table-fit.ts +17 -2
- package/src/components/data-table-groups.tsx +15 -4
- package/src/components/data-table-head.tsx +25 -2
- package/src/components/data-table-leading-row.tsx +4 -1
- package/src/components/data-table-row-actions.tsx +3 -2
- package/src/components/data-table-row.tsx +483 -0
- package/src/components/data-table-selection.ts +17 -1
- package/src/components/data-table-virtual.ts +547 -0
- package/src/components/data-table.tsx +326 -327
- package/src/data-table.ts +2 -0
- package/src/hooks/use-anchored-panel.ts +9 -1
- package/src/shell/app-shell.tsx +7 -1
|
@@ -5,17 +5,9 @@ import { Spinner } from "./spinner";
|
|
|
5
5
|
import { isTableOwnColumn, useRowActionColumn } from "./data-table-row-actions";
|
|
6
6
|
import { useColumnResize } from "./data-table-resize";
|
|
7
7
|
import { useRangeSelection } from "./data-table-selection";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
columnLabel,
|
|
12
|
-
fromOutside,
|
|
13
|
-
fromOwnControl,
|
|
14
|
-
logicalAlign,
|
|
15
|
-
resolveDataTableLabels,
|
|
16
|
-
splitCellProps,
|
|
17
|
-
} from "./data-table-shared";
|
|
18
|
-
import { MobileFilters, MobileRow, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
|
|
8
|
+
import { columnLabel, logicalAlign, resolveDataTableLabels } from "./data-table-shared";
|
|
9
|
+
import { MobileFilters, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
|
|
10
|
+
import { DataTableMobileRow, DataTableRow, type DataTableRowCtx } from "./data-table-row";
|
|
19
11
|
import { DataTableHead } from "./data-table-head";
|
|
20
12
|
import { DataTableLeadingRow } from "./data-table-leading-row";
|
|
21
13
|
import {
|
|
@@ -26,8 +18,11 @@ import {
|
|
|
26
18
|
useGroupBandTops,
|
|
27
19
|
type DataTableGroupBy,
|
|
28
20
|
type DataTableLoadMore,
|
|
21
|
+
type RowGroup,
|
|
29
22
|
} from "./data-table-groups";
|
|
30
23
|
export type { DataTableGroupBy, DataTableLoadMore } from "./data-table-groups";
|
|
24
|
+
import { VIRTUAL_SELECT_COL, rowNavTarget, useRowWindow, type DataTableVirtualize, type VirtualGroup } from "./data-table-virtual";
|
|
25
|
+
export type { DataTableVirtualize } from "./data-table-virtual";
|
|
31
26
|
import { ColumnSettingsPanel } from "./data-table-column-settings";
|
|
32
27
|
import { useLargeText } from "../hooks/use-large-text";
|
|
33
28
|
import { cn } from "../lib/cn";
|
|
@@ -352,6 +347,21 @@ export interface DataTableProps<T> {
|
|
|
352
347
|
* that produces `rows`.
|
|
353
348
|
*/
|
|
354
349
|
serverPagination?: ServerPagination;
|
|
350
|
+
/**
|
|
351
|
+
* 0.48.1 (keksdose K-052-1): the rows are already filtered and sorted by the server —
|
|
352
|
+
* render them as given. No client filter, no client sort, no client paging (every row
|
|
353
|
+
* shows, as with `paginated={false}`). The column filters and sort headers behave as
|
|
354
|
+
* under `serverPagination`: hidden unless the owner takes them over with
|
|
355
|
+
* `onFiltersChange` / `onSortsChange`, and then a click only REPORTS the new state (a
|
|
356
|
+
* header click calls `onSortsChange` with the next sort and the rows keep their order
|
|
357
|
+
* until the owner re-queries and passes new ones).
|
|
358
|
+
*
|
|
359
|
+
* Defaults to `true` when {@link loadMore} is set — the slices it loads come from the
|
|
360
|
+
* server in its order, and re-filtering them dropped rows (the other leg of each
|
|
361
|
+
* transfer). Pass `serverRows={false}` for a client table that loads more chunks but
|
|
362
|
+
* filters and sorts what it holds itself. `serverPagination` implies it regardless.
|
|
363
|
+
*/
|
|
364
|
+
serverRows?: boolean;
|
|
355
365
|
/**
|
|
356
366
|
* Controlled filter/sort state. When the `on*Change` callback is provided
|
|
357
367
|
* the table stops owning that piece of state: it renders `filters`/`sorts`
|
|
@@ -587,9 +597,48 @@ export interface DataTableProps<T> {
|
|
|
587
597
|
* IntersectionObserver, guarded against double calls while one is in flight) and with
|
|
588
598
|
* a button the keyboard reaches, named by the app ("Load August 2026"). With
|
|
589
599
|
* `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the
|
|
590
|
-
* card list's last item.
|
|
600
|
+
* card list's last item. Since 0.48.1 it implies {@link serverRows} — the rows are
|
|
601
|
+
* rendered in the order given, unfiltered — unless `serverRows={false}`. See
|
|
602
|
+
* {@link DataTableLoadMore}.
|
|
591
603
|
*/
|
|
592
604
|
loadMore?: DataTableLoadMore;
|
|
605
|
+
/**
|
|
606
|
+
* 0.48.2 (keksdose dev #632, Marcel: "The table is extremely slow. Is it virtualised…?"):
|
|
607
|
+
* render only the desktop rows near the visible part of the scroller, plus `overscan`
|
|
608
|
+
* rows each side ({@link DataTableVirtualize}), with spacer rows standing in for the
|
|
609
|
+
* rest. For long lists — keksdose's register under `loadMore` holds 1000+ rows. Opt-in;
|
|
610
|
+
* without it every row renders, as before.
|
|
611
|
+
*
|
|
612
|
+
* - **Scroller:** the bounded body (`fillHeight` or `maxBodyHeight`), or the page when the
|
|
613
|
+
* body is unbounded (`maxBodyHeight="none"`): the window is what of the table both the
|
|
614
|
+
* scroller and the browser window show.
|
|
615
|
+
* - **Kept rendered:** the header, the `leadingRow`, the group bands (sticky and stacked
|
|
616
|
+
* as before; a period's band while any of its rows is in view), every OPEN row (so an
|
|
617
|
+
* inline editor in `expandedRow` keeps its state), the roving tab stop and a row
|
|
618
|
+
* holding the focus. The `loadMore` footer follows the last spacer and loads once.
|
|
619
|
+
* - **Heights:** each rendered row is measured and remembered by its key; a row not yet
|
|
620
|
+
* seen counts as the measured average (`estimateRowHeight` before any). The scroll
|
|
621
|
+
* height settles as the rows are seen; the browser's scroll anchoring holds the rows in
|
|
622
|
+
* view while a spacer above is corrected (Safari has none: a jump of the difference
|
|
623
|
+
* is possible there on the first pass).
|
|
624
|
+
* - **Columns:** the header widths are measured once and locked in a `<colgroup>`, so the
|
|
625
|
+
* columns do not move as rows come and go; a wider cell scrolling in widens its column
|
|
626
|
+
* (once). Re-measured when the rows, the columns, a width or the scroller's width
|
|
627
|
+
* change. A `grow` column and a resized one are not locked.
|
|
628
|
+
* - **Keyboard and screen reader:** Arrow/Home/End move over every row — the target is
|
|
629
|
+
* rendered, then focused (End reaches the last row); Shift+click ranges and select-all
|
|
630
|
+
* work on the data, not the DOM. The table has `aria-rowcount`, every rendered row its
|
|
631
|
+
* `aria-rowindex` (bands, details and the totals row counted); spacers are
|
|
632
|
+
* `aria-hidden`.
|
|
633
|
+
* - **Trade-offs:** the browser's find-in-page sees only the rendered rows, and so does
|
|
634
|
+
* print; an `id` from `rowAttributes` (an anchor, an `aria-controls` target, a
|
|
635
|
+
* `scrollIntoView` on a fresh row) exists only while its row is rendered. Memoise
|
|
636
|
+
* `columns` (and `mobileCard`): the rows bail out of a render only then, and a scroll
|
|
637
|
+
* renders the rows that come in, not the whole window.
|
|
638
|
+
* - **Phone:** desktop only. The card list renders every row it holds (`loadMore`,
|
|
639
|
+
* `paginated={false}`) or reveals them in chunks as it is scrolled (client paging).
|
|
640
|
+
*/
|
|
641
|
+
virtualize?: boolean | DataTableVirtualize;
|
|
593
642
|
/**
|
|
594
643
|
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
595
644
|
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
@@ -845,35 +894,14 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
|
|
|
845
894
|
|
|
846
895
|
const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
|
|
847
896
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
Array.from(body.children).filter(
|
|
857
|
-
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute("data-row-nav"),
|
|
858
|
-
),
|
|
859
|
-
);
|
|
860
|
-
const i = rows.indexOf(from);
|
|
861
|
-
const target =
|
|
862
|
-
key === "ArrowDown"
|
|
863
|
-
? rows[i + 1]
|
|
864
|
-
: key === "ArrowUp"
|
|
865
|
-
? rows[i - 1]
|
|
866
|
-
: key === "Home"
|
|
867
|
-
? rows[0]
|
|
868
|
-
: key === "End"
|
|
869
|
-
? rows[rows.length - 1]
|
|
870
|
-
: undefined;
|
|
871
|
-
// At either end the key is still consumed, or ArrowDown on the last row would
|
|
872
|
-
// scroll the page out from under a focused row.
|
|
873
|
-
target?.focus();
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
const ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;
|
|
897
|
+
// `virtualize`'s constants (0.48.2): the band levels a group's `<tbody>` opens with.
|
|
898
|
+
const NO_BANDS: readonly (1 | 2)[] = [];
|
|
899
|
+
const BAND_1: readonly (1 | 2)[] = [1];
|
|
900
|
+
const BAND_2: readonly (1 | 2)[] = [2];
|
|
901
|
+
const BAND_1_2: readonly (1 | 2)[] = [1, 2];
|
|
902
|
+
const NO_VIRTUAL_OPTIONS: DataTableVirtualize = {};
|
|
903
|
+
|
|
904
|
+
// `moveRowFocus` (the roving rows' Arrow/Home/End) lives with the row in data-table-row.tsx.
|
|
877
905
|
|
|
878
906
|
// ---------- Main DataTable ----------
|
|
879
907
|
|
|
@@ -901,6 +929,7 @@ export function DataTable<T>({
|
|
|
901
929
|
fillHeight = false,
|
|
902
930
|
maxBodyHeight = "calc(100dvh - 12rem)",
|
|
903
931
|
serverPagination,
|
|
932
|
+
serverRows: serverRowsProp,
|
|
904
933
|
filters: filtersProp,
|
|
905
934
|
onFiltersChange,
|
|
906
935
|
sorts: sortsProp,
|
|
@@ -925,6 +954,7 @@ export function DataTable<T>({
|
|
|
925
954
|
leadingRowSticky = false,
|
|
926
955
|
groupBy,
|
|
927
956
|
loadMore,
|
|
957
|
+
virtualize = false,
|
|
928
958
|
density = "comfortable",
|
|
929
959
|
sortCycle = "tri",
|
|
930
960
|
chrome = "full",
|
|
@@ -946,6 +976,10 @@ export function DataTable<T>({
|
|
|
946
976
|
const resizable = resizableProp ?? !minimal;
|
|
947
977
|
const multiSort = multiSortProp ?? !minimal;
|
|
948
978
|
const isServer = !!serverPagination;
|
|
979
|
+
// The server filtered and sorted these rows (0.48.1, K-052-1): no client filter, sort
|
|
980
|
+
// or paging, and the sort/filter UI only reports to the owner. `isServer` keeps
|
|
981
|
+
// meaning "the server pages" (its pager, its page and page size).
|
|
982
|
+
const serverRows = isServer || (serverRowsProp ?? !!loadMore);
|
|
949
983
|
const isLoading = !!serverPagination?.isLoading;
|
|
950
984
|
// prop > provider > English, through the table's own resolver rather than
|
|
951
985
|
// `useKitLabels`: that resolver derives `columnsCount` from a translated `columns`,
|
|
@@ -1077,7 +1111,7 @@ export function DataTable<T>({
|
|
|
1077
1111
|
}, [columnSig, selectOptionsSig, rows]);
|
|
1078
1112
|
|
|
1079
1113
|
const filtered = useMemo(() => {
|
|
1080
|
-
if (
|
|
1114
|
+
if (serverRows) return rows;
|
|
1081
1115
|
let result = rows;
|
|
1082
1116
|
for (const col of columns) {
|
|
1083
1117
|
const state = filters[col.key];
|
|
@@ -1086,10 +1120,10 @@ export function DataTable<T>({
|
|
|
1086
1120
|
}
|
|
1087
1121
|
return result;
|
|
1088
1122
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1089
|
-
}, [rows, columnSig, filters,
|
|
1123
|
+
}, [rows, columnSig, filters, serverRows, locale]);
|
|
1090
1124
|
|
|
1091
1125
|
const sorted = useMemo(() => {
|
|
1092
|
-
if (
|
|
1126
|
+
if (serverRows) return filtered;
|
|
1093
1127
|
// Priority chain: the first sort key that distinguishes two rows wins;
|
|
1094
1128
|
// nulls sort last for that key regardless of direction (as before).
|
|
1095
1129
|
const chain = sorts
|
|
@@ -1114,12 +1148,12 @@ export function DataTable<T>({
|
|
|
1114
1148
|
});
|
|
1115
1149
|
return copy;
|
|
1116
1150
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1117
|
-
}, [filtered, sorts, columnSig,
|
|
1151
|
+
}, [filtered, sorts, columnSig, serverRows]);
|
|
1118
1152
|
|
|
1119
1153
|
// `paginated={false}` is expressed as "page size = everything" rather than as a
|
|
1120
1154
|
// second code path, so slicing, the page clamp and the range summary all keep
|
|
1121
1155
|
// exactly one definition.
|
|
1122
|
-
const unpaged = !isServer && (!paginated || !!loadMore);
|
|
1156
|
+
const unpaged = !isServer && (!paginated || !!loadMore || serverRows);
|
|
1123
1157
|
const effectivePageSize = isServer
|
|
1124
1158
|
? serverPagination!.pageSize
|
|
1125
1159
|
: unpaged || pageSize === Infinity
|
|
@@ -1202,7 +1236,7 @@ export function DataTable<T>({
|
|
|
1202
1236
|
// "clear all"). Only touches keys the table actually exposes as filters.
|
|
1203
1237
|
const clearAllFilters = () => {
|
|
1204
1238
|
const keys = columns
|
|
1205
|
-
.filter((col) => ((!
|
|
1239
|
+
.filter((col) => ((!serverRows || onFiltersChange) ? resolveFilter(col) : null))
|
|
1206
1240
|
.map((col) => col.key);
|
|
1207
1241
|
if (!keys.length) return;
|
|
1208
1242
|
if (onFiltersChange) {
|
|
@@ -1236,9 +1270,9 @@ export function DataTable<T>({
|
|
|
1236
1270
|
// some later render from the caller, and `serverPagination.total` is ALREADY the
|
|
1237
1271
|
// filtered total — so the "n of m" sentence has no second number to give here.
|
|
1238
1272
|
// Saying nothing is better than announcing the page size as a result count.
|
|
1239
|
-
if (
|
|
1273
|
+
if (serverRows) return;
|
|
1240
1274
|
announce(labels.filterResults(filtered.length, rows.length));
|
|
1241
|
-
}, [filterKey, filtered.length, rows.length,
|
|
1275
|
+
}, [filterKey, filtered.length, rows.length, serverRows, announce, labels]);
|
|
1242
1276
|
|
|
1243
1277
|
// 1-based, because that is what the page strip shows and what a person says.
|
|
1244
1278
|
const goToPage = (p: number) => {
|
|
@@ -1249,12 +1283,16 @@ export function DataTable<T>({
|
|
|
1249
1283
|
|
|
1250
1284
|
// 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out
|
|
1251
1285
|
// (data-table-fit.ts). Desktop only; the scroller is read after render.
|
|
1286
|
+
// Large and Extra large: read here, as the fit re-measures when it changes.
|
|
1287
|
+
const large = useLargeText();
|
|
1252
1288
|
const { autoHidden, keep: keepColumn } = useColumnFit({
|
|
1253
1289
|
columns,
|
|
1254
1290
|
hiddenCols,
|
|
1255
1291
|
headRefs,
|
|
1256
1292
|
scroller: () => desktopScroller.current,
|
|
1257
1293
|
enabled: !usePhoneLayout(),
|
|
1294
|
+
// Measured only when one of these changed (0.48.2): not on a ticked box.
|
|
1295
|
+
content: [slice, columns, hiddenCols, widths, density, large],
|
|
1258
1296
|
});
|
|
1259
1297
|
const visibleColumns = useMemo(
|
|
1260
1298
|
() => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),
|
|
@@ -1348,17 +1386,20 @@ export function DataTable<T>({
|
|
|
1348
1386
|
// of a horizontally-scrolling table. Column settings stay desktop-only; the
|
|
1349
1387
|
// filters come through a bottom sheet (feedback #299) and the sort through the
|
|
1350
1388
|
// opt-in `mobileSort` row (keksdose K10), both on the table's own state.
|
|
1351
|
-
|
|
1389
|
+
// Memoised on `visibleColumns`: the cards compare their columns by identity (0.48.2).
|
|
1390
|
+
const mobileColumns = useMemo(() => visibleColumns.filter((c) => !c.mobileHidden), [visibleColumns]);
|
|
1352
1391
|
const mobilePrimaryCol =
|
|
1353
1392
|
mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;
|
|
1354
|
-
const mobileSecondaryColumns =
|
|
1393
|
+
const mobileSecondaryColumns = useMemo(
|
|
1394
|
+
() => mobileColumns.filter((c) => c !== mobilePrimaryCol),
|
|
1395
|
+
[mobileColumns, mobilePrimaryCol],
|
|
1396
|
+
);
|
|
1355
1397
|
// The columns the card leaves out, for the opened row (`mobileDetailsInRow`).
|
|
1356
1398
|
const mobileDetailColumns = mobileDetailsInRow ? visibleColumns.filter((c) => c.mobileHidden) : [];
|
|
1357
1399
|
// Large and Extra large (§4 "DataTable phone card"): the card's labels go ABOVE their
|
|
1358
1400
|
// values, nothing in it truncates, and the rows get more room. Most of it is a
|
|
1359
1401
|
// `large:` class; the label/value grid needs a different STRUCTURE (each pair in a
|
|
1360
|
-
// box of its own), which a class cannot write.
|
|
1361
|
-
const large = useLargeText();
|
|
1402
|
+
// box of its own), which a class cannot write. (`large` is read above, by the fit.)
|
|
1362
1403
|
// What an opened phone row shows: the card's left-out columns first (when asked
|
|
1363
1404
|
// for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.
|
|
1364
1405
|
const opensRow = !!expandedRow || mobileDetailColumns.length > 0;
|
|
@@ -1443,7 +1484,7 @@ export function DataTable<T>({
|
|
|
1443
1484
|
const phoneSelecting = !!selection && !isMdUp && (phoneSelectMode || anySelected);
|
|
1444
1485
|
// ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,
|
|
1445
1486
|
// anchored on the row's KEY so a sort, filter or page change cannot move it)
|
|
1446
|
-
const {
|
|
1487
|
+
const { canSelect, selectRange, setAnchor, markRangeClick, takeRangeClick } = useRangeSelection({
|
|
1447
1488
|
selection,
|
|
1448
1489
|
slice,
|
|
1449
1490
|
rowKey,
|
|
@@ -1476,6 +1517,42 @@ export function DataTable<T>({
|
|
|
1476
1517
|
? rowKey(slice[0])
|
|
1477
1518
|
: null;
|
|
1478
1519
|
|
|
1520
|
+
// What the memoised rows' handlers read at event time (0.48.2, K-052-2): the latest
|
|
1521
|
+
// props, refreshed every commit, so a row that bailed out of a render still toggles
|
|
1522
|
+
// through the current `selection` and opens through the current `onRowClick`.
|
|
1523
|
+
// `virtualize` (0.48.2): desktop only, and only with rows to window. Arrow/Home/End then
|
|
1524
|
+
// move over every row, rendered or not: the target is rendered, then focused.
|
|
1525
|
+
const virtualOn = !!virtualize && isMdUp && slice.length > 0;
|
|
1526
|
+
const focusRowRef = useRef<((index: number) => void) | undefined>(undefined);
|
|
1527
|
+
const latestRowCtx: DataTableRowCtx<T> = {
|
|
1528
|
+
moveFocus: virtualOn
|
|
1529
|
+
? (from, key) => {
|
|
1530
|
+
const target = rowNavTarget(from, key, slice.length);
|
|
1531
|
+
if (target === null) return false;
|
|
1532
|
+
focusRowRef.current?.(target);
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
: undefined,
|
|
1536
|
+
onRowClick,
|
|
1537
|
+
onToggle: selection?.onToggle,
|
|
1538
|
+
isSelected: (row) => !!selection?.isSelected(row),
|
|
1539
|
+
canSelect,
|
|
1540
|
+
selectRange,
|
|
1541
|
+
setAnchor,
|
|
1542
|
+
markRangeClick,
|
|
1543
|
+
takeRangeClick,
|
|
1544
|
+
setFocusRowKey,
|
|
1545
|
+
onLongPress: (row) => {
|
|
1546
|
+
setPhoneSelectMode(true);
|
|
1547
|
+
setAnchor(rowKey(row));
|
|
1548
|
+
if (selection && !selection.isSelected(row)) selection.onToggle(row, true);
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
const rowCtx = useRef(latestRowCtx);
|
|
1552
|
+
useLayoutEffect(() => {
|
|
1553
|
+
rowCtx.current = latestRowCtx;
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1479
1556
|
// When dialog-mode is on, the expanded row's detail goes into a bottom-sheet
|
|
1480
1557
|
// modal instead of unfolding inline. Only one row is ever expanded at a time.
|
|
1481
1558
|
const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;
|
|
@@ -1492,46 +1569,38 @@ export function DataTable<T>({
|
|
|
1492
1569
|
|
|
1493
1570
|
// One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list
|
|
1494
1571
|
// and the grouped list (below) share identical row markup.
|
|
1495
|
-
const renderMobileRow = (row: T) =>
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
selectionAnchor.current = rowKey(row);
|
|
1528
|
-
if (!selection.isSelected(row)) selection.onToggle(row, true);
|
|
1529
|
-
},
|
|
1530
|
-
}
|
|
1531
|
-
: null
|
|
1532
|
-
}
|
|
1533
|
-
/>
|
|
1534
|
-
);
|
|
1572
|
+
const renderMobileRow = (row: T) => {
|
|
1573
|
+
const key = rowKey(row);
|
|
1574
|
+
const expanded = isExpanded?.(row) ?? false;
|
|
1575
|
+
return (
|
|
1576
|
+
<DataTableMobileRow<T>
|
|
1577
|
+
key={key}
|
|
1578
|
+
ctx={rowCtx}
|
|
1579
|
+
rowKey={key}
|
|
1580
|
+
row={row}
|
|
1581
|
+
expanded={expanded}
|
|
1582
|
+
expansion={expanded ? openedRowContent(row) : null}
|
|
1583
|
+
opensRow={opensRow}
|
|
1584
|
+
className={rowClassName?.(row)}
|
|
1585
|
+
attrs={rowAttributes?.(row)}
|
|
1586
|
+
interactive={!!onRowClick}
|
|
1587
|
+
href={mobileCardLinkable ? rowHref?.(row) : undefined}
|
|
1588
|
+
swipeActionsFor={swipeActionsFor}
|
|
1589
|
+
mobileDir={mobileDir}
|
|
1590
|
+
mobileCardLinkable={mobileCardLinkable}
|
|
1591
|
+
mobileCard={mobileCard}
|
|
1592
|
+
mobilePrimaryCol={mobilePrimaryCol}
|
|
1593
|
+
mobileSecondaryColumns={mobileSecondaryColumns}
|
|
1594
|
+
mobileExpandAsDialog={mobileExpandAsDialog}
|
|
1595
|
+
compact={compact}
|
|
1596
|
+
large={large}
|
|
1597
|
+
selectionOn={!!selection}
|
|
1598
|
+
selectActive={phoneSelecting}
|
|
1599
|
+
selectable={canSelect(row)}
|
|
1600
|
+
selected={!!selection && selection.isSelected(row)}
|
|
1601
|
+
/>
|
|
1602
|
+
);
|
|
1603
|
+
};
|
|
1535
1604
|
|
|
1536
1605
|
// Consecutive grouping of the (already-sorted) mobile slice into labelled
|
|
1537
1606
|
// sections for the "assistance"-style list (feedback #317). Null unless the
|
|
@@ -1544,6 +1613,57 @@ export function DataTable<T>({
|
|
|
1544
1613
|
!!desktopGroups,
|
|
1545
1614
|
`${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? ""}`,
|
|
1546
1615
|
);
|
|
1616
|
+
// ---- Row windowing ---- (0.48.2, keksdose dev #632; data-table-virtual.ts)
|
|
1617
|
+
// One run per innermost group, with its bands (`virtualOn` is above, with the rows' context).
|
|
1618
|
+
const virtualGroups: VirtualGroup[] = [];
|
|
1619
|
+
const virtualBands: { g1: RowGroup<T>; g2: RowGroup<T> | null }[] = [];
|
|
1620
|
+
if (virtualOn) {
|
|
1621
|
+
if (!desktopGroups) virtualGroups.push({ start: 0, end: slice.length, bands: NO_BANDS, first: 0 });
|
|
1622
|
+
else
|
|
1623
|
+
for (const g1 of desktopGroups) {
|
|
1624
|
+
const first = virtualGroups.length;
|
|
1625
|
+
if (!g1.children) {
|
|
1626
|
+
virtualGroups.push({ start: g1.start, end: g1.start + g1.rows.length, bands: BAND_1, first });
|
|
1627
|
+
virtualBands.push({ g1, g2: null });
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
g1.children.forEach((g2, j) => {
|
|
1631
|
+
virtualGroups.push({ start: g2.start, end: g2.start + g2.rows.length, bands: j === 0 ? BAND_1_2 : BAND_2, first });
|
|
1632
|
+
virtualBands.push({ g1, g2 });
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const expandedIndices: number[] = [];
|
|
1637
|
+
if (virtualOn && isExpanded && expandedRow) slice.forEach((r, i) => isExpanded(r) && expandedIndices.push(i));
|
|
1638
|
+
const virtualColKeys = [...(selection ? [VIRTUAL_SELECT_COL] : []), ...visibleColumns.map((c) => c.key)];
|
|
1639
|
+
const virtualLockable = new Set(
|
|
1640
|
+
virtualColKeys.filter((k) => !widths[k] && !visibleColumns.some((c) => c.key === k && c.grow)),
|
|
1641
|
+
);
|
|
1642
|
+
const rowWindow = useRowWindow({
|
|
1643
|
+
enabled: virtualOn,
|
|
1644
|
+
rows: slice,
|
|
1645
|
+
rowKey,
|
|
1646
|
+
groups: virtualGroups,
|
|
1647
|
+
expanded: expandedIndices,
|
|
1648
|
+
pinnedKeys: [tabRowKey],
|
|
1649
|
+
scrollerRef: desktopScroller,
|
|
1650
|
+
tableRef,
|
|
1651
|
+
options: typeof virtualize === "object" ? virtualize : NO_VIRTUAL_OPTIONS,
|
|
1652
|
+
compact,
|
|
1653
|
+
rowsBefore: 1 + (leadingRow ? 1 : 0),
|
|
1654
|
+
rowsAfter: showFooter ? 1 : 0,
|
|
1655
|
+
colKeys: virtualColKeys,
|
|
1656
|
+
lockable: virtualLockable,
|
|
1657
|
+
resetKey: `${Object.entries(widths).join(";")}|${density}|${large}|${slice.length}:${
|
|
1658
|
+
slice.length ? `${rowKey(slice[0])}:${rowKey(slice[slice.length - 1])}` : ""
|
|
1659
|
+
}`,
|
|
1660
|
+
});
|
|
1661
|
+
// What `moveFocus` (in the rows' context, above) reaches: the window's focus mover.
|
|
1662
|
+
const focusRowAt = rowWindow?.focusRow;
|
|
1663
|
+
useLayoutEffect(() => {
|
|
1664
|
+
focusRowRef.current = focusRowAt;
|
|
1665
|
+
}, [focusRowAt]);
|
|
1666
|
+
|
|
1547
1667
|
// `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.
|
|
1548
1668
|
const mobileLevel = typeof mobileGroupBy === "function" ? null : (mobileGroupBy ?? null);
|
|
1549
1669
|
const mobileKey = typeof mobileGroupBy === "function" ? mobileGroupBy : mobileGroupBy?.key;
|
|
@@ -1612,12 +1732,12 @@ export function DataTable<T>({
|
|
|
1612
1732
|
{/* Mobile filter access (feedback #299): the per-column filter popovers
|
|
1613
1733
|
live in the desktop header, which the card list doesn't render — so
|
|
1614
1734
|
expose the same filters through a bottom sheet here. */}
|
|
1615
|
-
{(!
|
|
1735
|
+
{(!serverRows || onFiltersChange) && (
|
|
1616
1736
|
<MobileFilters
|
|
1617
1737
|
columns={columns}
|
|
1618
1738
|
filters={filters}
|
|
1619
1739
|
selectOptionsByKey={selectOptionsByKey}
|
|
1620
|
-
isServer={
|
|
1740
|
+
isServer={serverRows}
|
|
1621
1741
|
hasFilterCallback={!!onFiltersChange}
|
|
1622
1742
|
onSetFilter={setFilterValue}
|
|
1623
1743
|
onClearFilter={clearFilter}
|
|
@@ -1628,7 +1748,7 @@ export function DataTable<T>({
|
|
|
1628
1748
|
)}
|
|
1629
1749
|
{mobileSort && (
|
|
1630
1750
|
<MobileSort
|
|
1631
|
-
columns={visibleColumns.filter((c) => (!
|
|
1751
|
+
columns={visibleColumns.filter((c) => (!serverRows || !!onSortsChange) && !!c.sortBy)}
|
|
1632
1752
|
sorts={sorts}
|
|
1633
1753
|
sortCycle={sortCycle}
|
|
1634
1754
|
compact={compact}
|
|
@@ -1844,7 +1964,20 @@ export function DataTable<T>({
|
|
|
1844
1964
|
data-fit-grow={growDrawn ? "" : undefined}
|
|
1845
1965
|
aria-label={labels.table}
|
|
1846
1966
|
aria-busy={isLoading || undefined}
|
|
1967
|
+
// `virtualize` (0.48.2): every row counted, rendered or not.
|
|
1968
|
+
aria-rowcount={rowWindow?.rowCount}
|
|
1969
|
+
data-virtualized={rowWindow ? "" : undefined}
|
|
1847
1970
|
>
|
|
1971
|
+
{/* The locked column widths (0.48.2, `virtualize`): rows coming and going no
|
|
1972
|
+
longer move the columns. */}
|
|
1973
|
+
{rowWindow && (
|
|
1974
|
+
<colgroup>
|
|
1975
|
+
{virtualColKeys.map((key) => {
|
|
1976
|
+
const w = rowWindow.colWidths?.[key];
|
|
1977
|
+
return <col key={key} style={w !== undefined ? { width: w } : undefined} />;
|
|
1978
|
+
})}
|
|
1979
|
+
</colgroup>
|
|
1980
|
+
)}
|
|
1848
1981
|
{/* Sticky header. position:sticky pins to the nearest scroll-container
|
|
1849
1982
|
ancestor — so the header can only stick to THIS wrapper, never the
|
|
1850
1983
|
page. For that to actually hold the header in place, the wrapper
|
|
@@ -1860,7 +1993,7 @@ export function DataTable<T>({
|
|
|
1860
1993
|
compact={compact}
|
|
1861
1994
|
labels={labels}
|
|
1862
1995
|
locale={locale}
|
|
1863
|
-
isServer={
|
|
1996
|
+
isServer={serverRows}
|
|
1864
1997
|
hasSortCallback={!!onSortsChange}
|
|
1865
1998
|
hasFilterCallback={!!onFiltersChange}
|
|
1866
1999
|
sorts={sorts}
|
|
@@ -1877,235 +2010,57 @@ export function DataTable<T>({
|
|
|
1877
2010
|
startResize={startResize}
|
|
1878
2011
|
moveResize={moveResize}
|
|
1879
2012
|
finishResize={finishResize}
|
|
2013
|
+
ariaRowIndex={rowWindow ? 1 : undefined}
|
|
2014
|
+
lockedWidths={rowWindow?.colWidths}
|
|
2015
|
+
selectionLockKey={VIRTUAL_SELECT_COL}
|
|
1880
2016
|
/>
|
|
1881
2017
|
{(() => {
|
|
1882
2018
|
const bodyClass = cn(isLoading && slice.length > 0 && "opacity-60 transition-opacity");
|
|
1883
2019
|
const leading = leadingRow ? (
|
|
1884
|
-
<DataTableLeadingRow
|
|
2020
|
+
<DataTableLeadingRow
|
|
2021
|
+
colSpan={totalColSpan}
|
|
2022
|
+
sticky={leadingRowSticky}
|
|
2023
|
+
ariaRowIndex={rowWindow ? 2 : undefined}
|
|
2024
|
+
>
|
|
1885
2025
|
{leadingRow}
|
|
1886
2026
|
</DataTableLeadingRow>
|
|
1887
2027
|
) : null;
|
|
2028
|
+
// One memoised row each (0.48.2, K-052-2; data-table-row.tsx): the table
|
|
2029
|
+
// derives each row's own inputs here, as primitives, so a tick re-renders
|
|
2030
|
+
// only the rows whose inputs changed.
|
|
1888
2031
|
const renderRow = (row: T, rowIndex: number) => {
|
|
1889
2032
|
const expanded = isExpanded?.(row) ?? false;
|
|
1890
|
-
const expansion = expanded ? expandedRow?.(row) : null;
|
|
1891
|
-
const rowInteractive = !!onRowClick || !!selection;
|
|
1892
|
-
const href = rowHref?.(row);
|
|
1893
2033
|
const key = rowKey(row);
|
|
1894
|
-
const detailId = `${detailIdBase}-${rowIndex}`;
|
|
1895
|
-
// Whether the row is open, said where ARIA allows it (round 0.34, the
|
|
1896
|
-
// showcase's axe check): `aria-expanded` belongs to a treegrid's row, not a
|
|
1897
|
-
// table's, and the <tr> wore it. A disclosure button in the first cell carries
|
|
1898
|
-
// it now, with `aria-controls` on the detail row while that is there.
|
|
1899
|
-
// Visually hidden and not a tab stop: the row stays the one roving stop, with
|
|
1900
|
-
// Enter/Space and the whole-row click — this is the screen reader cursor's
|
|
1901
|
-
// way in, and where it hears the state. Named by `aria-label`, so the cell's
|
|
1902
|
-
// text stays the cell's own.
|
|
1903
|
-
const disclosure =
|
|
1904
|
-
rowsFocusable && expandedRow ? (
|
|
1905
|
-
<button
|
|
1906
|
-
type="button"
|
|
1907
|
-
tabIndex={-1}
|
|
1908
|
-
className="sr-only-fixed"
|
|
1909
|
-
aria-label={labels.rowDetails}
|
|
1910
|
-
aria-expanded={expanded}
|
|
1911
|
-
aria-controls={expansion ? detailId : undefined}
|
|
1912
|
-
// The row's own handler leaves a click on a control inside it alone.
|
|
1913
|
-
onClick={() => onRowClick!(row)}
|
|
1914
|
-
/>
|
|
1915
|
-
) : null;
|
|
1916
2034
|
return (
|
|
1917
|
-
<
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
: undefined
|
|
1947
|
-
}
|
|
1948
|
-
className={cn(
|
|
1949
|
-
"border-t border-subtle",
|
|
1950
|
-
rowInteractive && "cursor-pointer hover:bg-hover",
|
|
1951
|
-
// The kit's focus frame width (§5, `--focus-ring-width`), drawn as an
|
|
1952
|
-
// OUTLINE rather than FOCUS_RING's box-shadow: a shadow on a `<tr>`
|
|
1953
|
-
// is not painted by every engine, an outline is.
|
|
1954
|
-
rowsFocusable &&
|
|
1955
|
-
"focus-visible:bg-hover focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-brand",
|
|
1956
|
-
rowClassName?.(row),
|
|
1957
|
-
)}
|
|
1958
|
-
onClick={
|
|
1959
|
-
rowInteractive
|
|
1960
|
-
? (e) => {
|
|
1961
|
-
// A click on a control inside the row (a button, a link,
|
|
1962
|
-
// an input the cell renders) is that control's — it must
|
|
1963
|
-
// not also open the row. Cells no longer have to
|
|
1964
|
-
// `stopPropagation` to get that.
|
|
1965
|
-
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
1966
|
-
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1967
|
-
// Modifier-clicks anywhere in the row drive
|
|
1968
|
-
// selection instead of expanding it (feedback #289):
|
|
1969
|
-
// Ctrl/Cmd toggles a single row, Shift selects the
|
|
1970
|
-
// range from the last-toggled row (inclusive of this
|
|
1971
|
-
// one) — the same as the checkbox, but for the whole
|
|
1972
|
-
// row, which is what the checkbox-only version was
|
|
1973
|
-
// missing.
|
|
1974
|
-
if (selection && canSelect(row)) {
|
|
1975
|
-
if (e.shiftKey) {
|
|
1976
|
-
// Drop the text selection a shift-click would
|
|
1977
|
-
// otherwise smear across the rows.
|
|
1978
|
-
window.getSelection?.()?.removeAllRanges();
|
|
1979
|
-
if (selectRange(rowIndex)) return;
|
|
1980
|
-
// No anchor yet: treat it as the first pick.
|
|
1981
|
-
selectionAnchor.current = rowKey(row);
|
|
1982
|
-
selection.onToggle(row, !selection.isSelected(row));
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
if (e.metaKey || e.ctrlKey) {
|
|
1986
|
-
selectionAnchor.current = rowKey(row);
|
|
1987
|
-
selection.onToggle(row, !selection.isSelected(row));
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
onRowClick?.(row);
|
|
1992
|
-
}
|
|
1993
|
-
: undefined
|
|
1994
|
-
}
|
|
1995
|
-
>
|
|
1996
|
-
{selection && (
|
|
1997
|
-
<td
|
|
1998
|
-
className={cn("align-top", compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2")}
|
|
1999
|
-
// Don't let selecting a row also trigger the row click
|
|
2000
|
-
// (which expands/edits it).
|
|
2001
|
-
onClick={(e) => e.stopPropagation()}
|
|
2002
|
-
>
|
|
2003
|
-
{canSelect(row) && (
|
|
2004
|
-
<input
|
|
2005
|
-
type="checkbox"
|
|
2006
|
-
className={cn("accent-indigo-600", compact ? "size-3.5" : "size-4")}
|
|
2007
|
-
checked={selection.isSelected(row)}
|
|
2008
|
-
// Shift+click selects the range from the last toggled
|
|
2009
|
-
// row. `preventDefault` puts the box back, but it does NOT
|
|
2010
|
-
// stop React's `onChange`: React derives a checkbox's
|
|
2011
|
-
// change from the `click` itself, after the browser has
|
|
2012
|
-
// already flipped `checked` and before the cancellation
|
|
2013
|
-
// reverts it — so the same click also reported itself as
|
|
2014
|
-
// a single toggle, and the owner got `onToggleMany` and
|
|
2015
|
-
// then `onToggle` for the clicked row. The click is
|
|
2016
|
-
// remembered by identity and its change is dropped; a
|
|
2017
|
-
// boolean flag would outlive a click whose change never
|
|
2018
|
-
// came and swallow the user's next real one.
|
|
2019
|
-
onClick={(e) => {
|
|
2020
|
-
if (e.shiftKey && selectRange(rowIndex)) {
|
|
2021
|
-
e.preventDefault();
|
|
2022
|
-
rangeClick.current = e.nativeEvent;
|
|
2023
|
-
}
|
|
2024
|
-
}}
|
|
2025
|
-
onChange={(e) => {
|
|
2026
|
-
if (rangeClick.current && e.nativeEvent === rangeClick.current) {
|
|
2027
|
-
rangeClick.current = null;
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
selectionAnchor.current = rowKey(row);
|
|
2031
|
-
selection.onToggle(row, e.target.checked);
|
|
2032
|
-
}}
|
|
2033
|
-
aria-label={labels.selectRow}
|
|
2034
|
-
/>
|
|
2035
|
-
)}
|
|
2036
|
-
</td>
|
|
2037
|
-
)}
|
|
2038
|
-
{visibleColumns.map((col, colIndex) => {
|
|
2039
|
-
const width = widths[col.key];
|
|
2040
|
-
// The caller's first, the table's over it — see `cellProps`.
|
|
2041
|
-
const own = splitCellProps(col.cellProps?.(row));
|
|
2042
|
-
const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;
|
|
2043
|
-
return (
|
|
2044
|
-
<td
|
|
2045
|
-
{...own.attrs}
|
|
2046
|
-
key={col.key}
|
|
2047
|
-
data-col={col.key}
|
|
2048
|
-
style={own.style || pinned ? { ...own.style, ...pinned } : undefined}
|
|
2049
|
-
className={cn(
|
|
2050
|
-
"align-top",
|
|
2051
|
-
compact ? "px-2 py-1" : "px-3 py-2",
|
|
2052
|
-
width && "overflow-hidden text-ellipsis",
|
|
2053
|
-
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
2054
|
-
logicalAlign(own.className),
|
|
2055
|
-
logicalAlign(col.className),
|
|
2056
|
-
)}
|
|
2057
|
-
>
|
|
2058
|
-
{colIndex === 0 && disclosure}
|
|
2059
|
-
{col === linkColumn && href ? (
|
|
2060
|
-
// No `onActivate`: the click is left to bubble to the
|
|
2061
|
-
// <tr> handler above, which already owns expand-vs-select.
|
|
2062
|
-
// Calling it here as well would toggle the row twice.
|
|
2063
|
-
<RowLink href={href} className="block" data-row-link="">
|
|
2064
|
-
{col.cell(row)}
|
|
2065
|
-
</RowLink>
|
|
2066
|
-
) : (
|
|
2067
|
-
col.cell(row)
|
|
2068
|
-
)}
|
|
2069
|
-
</td>
|
|
2070
|
-
);
|
|
2071
|
-
})}
|
|
2072
|
-
</tr>
|
|
2073
|
-
{expansion && (
|
|
2074
|
-
<tr id={disclosure ? detailId : undefined} className="bg-surface-2 border-t border-subtle">
|
|
2075
|
-
{/* The detail panel spans every column. In an `auto` table
|
|
2076
|
-
layout (the default here — columns only get fixed style
|
|
2077
|
-
widths once the user resizes/auto-sizes them) the cell's
|
|
2078
|
-
content preferred-width is redistributed across the
|
|
2079
|
-
spanned columns, so a long expansion visibly re-sizes the
|
|
2080
|
-
columns above it — and one-line content does not, since
|
|
2081
|
-
its preferred width is small (feedback #104). Wrapping the
|
|
2082
|
-
content in `w-0 min-w-full` pins the cell's preferred
|
|
2083
|
-
width to ~0 (the explicit width:0 child), so it can never
|
|
2084
|
-
perturb the columns; the div then fills to the full row
|
|
2085
|
-
width only at paint time, and long text wraps within it.
|
|
2086
|
-
Inside it the content is pinned to what the scroller SHOWS
|
|
2087
|
-
(0.47, keksdose S4): on a table wider than its scroller a
|
|
2088
|
-
table-wide panel put an inline editor's Save/Cancel off-screen.
|
|
2089
|
-
`sticky start-0` holds it at the scroller's start edge (the
|
|
2090
|
-
right one in RTL) as the row scrolls sideways, and
|
|
2091
|
-
`--dt-visible-w` (the scroller's clientWidth, measured above)
|
|
2092
|
-
sizes it; before the first measure, and in a table no wider
|
|
2093
|
-
than its scroller, it is the full width as before. */}
|
|
2094
|
-
<td colSpan={totalColSpan} className="p-0">
|
|
2095
|
-
<div className="w-0 min-w-full">
|
|
2096
|
-
<div
|
|
2097
|
-
className={cn(
|
|
2098
|
-
"sticky start-0 w-[var(--dt-visible-w,100%)] max-w-full",
|
|
2099
|
-
compact ? "px-2 py-2" : "px-3 py-3",
|
|
2100
|
-
)}
|
|
2101
|
-
>
|
|
2102
|
-
{expansion}
|
|
2103
|
-
</div>
|
|
2104
|
-
</div>
|
|
2105
|
-
</td>
|
|
2106
|
-
</tr>
|
|
2107
|
-
)}
|
|
2108
|
-
</Fragment>
|
|
2035
|
+
<DataTableRow<T>
|
|
2036
|
+
key={key}
|
|
2037
|
+
row={row}
|
|
2038
|
+
rowIndex={rowIndex}
|
|
2039
|
+
rowKey={key}
|
|
2040
|
+
ctx={rowCtx}
|
|
2041
|
+
columns={visibleColumns}
|
|
2042
|
+
widths={widths}
|
|
2043
|
+
growDrawn={growDrawn}
|
|
2044
|
+
linkColumn={linkColumn}
|
|
2045
|
+
href={rowHref?.(row)}
|
|
2046
|
+
selectionColumn={!!selection}
|
|
2047
|
+
selectable={canSelect(row)}
|
|
2048
|
+
selected={!!selection && selection.isSelected(row)}
|
|
2049
|
+
expanded={expanded}
|
|
2050
|
+
expansion={expanded ? expandedRow?.(row) : null}
|
|
2051
|
+
disclosure={rowsFocusable && !!expandedRow}
|
|
2052
|
+
detailId={`${detailIdBase}-${rowIndex}`}
|
|
2053
|
+
rowsFocusable={rowsFocusable}
|
|
2054
|
+
rowInteractive={!!onRowClick || !!selection}
|
|
2055
|
+
tabbable={key === tabRowKey}
|
|
2056
|
+
className={rowClassName?.(row)}
|
|
2057
|
+
attrs={rowAttributes?.(row)}
|
|
2058
|
+
compact={compact}
|
|
2059
|
+
totalColSpan={totalColSpan}
|
|
2060
|
+
rowDetailsLabel={labels.rowDetails}
|
|
2061
|
+
selectRowLabel={labels.selectRow}
|
|
2062
|
+
ariaRowIndex={rowWindow?.ariaRow(rowIndex)}
|
|
2063
|
+
/>
|
|
2109
2064
|
);
|
|
2110
2065
|
};
|
|
2111
2066
|
const emptyRow =
|
|
@@ -2121,6 +2076,60 @@ export function DataTable<T>({
|
|
|
2121
2076
|
</td>
|
|
2122
2077
|
</tr>
|
|
2123
2078
|
) : null;
|
|
2079
|
+
const band = (
|
|
2080
|
+
level: 1 | 2,
|
|
2081
|
+
def: DataTableGroupBy<T>,
|
|
2082
|
+
g: { key: string; rows: T[] },
|
|
2083
|
+
ariaRowIndex?: number,
|
|
2084
|
+
) => (
|
|
2085
|
+
<DataTableGroupBand
|
|
2086
|
+
level={level}
|
|
2087
|
+
colSpan={totalColSpan}
|
|
2088
|
+
top={level === 1 ? bandTops[0] : bandTops[1]}
|
|
2089
|
+
compact={compact}
|
|
2090
|
+
label={def.label ? def.label(g.key, g.rows) : g.key}
|
|
2091
|
+
end={def.end?.(g.key, g.rows)}
|
|
2092
|
+
ariaRowIndex={ariaRowIndex}
|
|
2093
|
+
/>
|
|
2094
|
+
);
|
|
2095
|
+
if (rowWindow) {
|
|
2096
|
+
// Windowed (0.48.2): each group's own `<tbody>` with its bands and the
|
|
2097
|
+
// rendered rows, spacer rows between; one spacer `<tbody>` per run of groups
|
|
2098
|
+
// with nothing rendered. Spacers are hidden from the accessibility tree.
|
|
2099
|
+
const spacer = (key: string, height: number) => (
|
|
2100
|
+
<tr key={key} aria-hidden="true" data-vt-spacer="">
|
|
2101
|
+
<td colSpan={totalColSpan} style={{ height, padding: 0, border: 0 }} />
|
|
2102
|
+
</tr>
|
|
2103
|
+
);
|
|
2104
|
+
return (
|
|
2105
|
+
<>
|
|
2106
|
+
{leading && <tbody className={bodyClass}>{leading}</tbody>}
|
|
2107
|
+
{rowWindow.bodies.map((b) => {
|
|
2108
|
+
if (b.kind === "spacer")
|
|
2109
|
+
return (
|
|
2110
|
+
<tbody key={b.key} data-vt-body="" aria-hidden="true">
|
|
2111
|
+
{spacer("s", b.height)}
|
|
2112
|
+
</tbody>
|
|
2113
|
+
);
|
|
2114
|
+
const g = virtualGroups[b.group];
|
|
2115
|
+
const vb = virtualBands[b.group];
|
|
2116
|
+
return (
|
|
2117
|
+
<tbody
|
|
2118
|
+
key={vb ? `vg:${vb.g1.key}:${vb.g2?.key ?? ""}:${g.start}` : "vg"}
|
|
2119
|
+
data-vt-body=""
|
|
2120
|
+
className={bodyClass}
|
|
2121
|
+
>
|
|
2122
|
+
{vb && g.bands.includes(1) && band(1, levels[0], vb.g1, rowWindow.ariaBand(b.group, 1))}
|
|
2123
|
+
{vb?.g2 && band(2, levels[1], vb.g2, rowWindow.ariaBand(b.group, 2))}
|
|
2124
|
+
{b.items.map((it) =>
|
|
2125
|
+
it.kind === "gap" ? spacer(it.key, it.height) : renderRow(slice[it.index], it.index),
|
|
2126
|
+
)}
|
|
2127
|
+
</tbody>
|
|
2128
|
+
);
|
|
2129
|
+
})}
|
|
2130
|
+
</>
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2124
2133
|
if (!desktopGroups) {
|
|
2125
2134
|
return (
|
|
2126
2135
|
<tbody className={bodyClass}>
|
|
@@ -2133,16 +2142,6 @@ export function DataTable<T>({
|
|
|
2133
2142
|
// Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its
|
|
2134
2143
|
// band the row header; the leading row (and the empty state) in their own
|
|
2135
2144
|
// before the first group.
|
|
2136
|
-
const band = (level: 1 | 2, def: DataTableGroupBy<T>, g: { key: string; rows: T[] }) => (
|
|
2137
|
-
<DataTableGroupBand
|
|
2138
|
-
level={level}
|
|
2139
|
-
colSpan={totalColSpan}
|
|
2140
|
-
top={level === 1 ? bandTops[0] : bandTops[1]}
|
|
2141
|
-
compact={compact}
|
|
2142
|
-
label={def.label ? def.label(g.key, g.rows) : g.key}
|
|
2143
|
-
end={def.end?.(g.key, g.rows)}
|
|
2144
|
-
/>
|
|
2145
|
-
);
|
|
2146
2145
|
const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));
|
|
2147
2146
|
return (
|
|
2148
2147
|
<>
|
|
@@ -2184,7 +2183,7 @@ export function DataTable<T>({
|
|
|
2184
2183
|
isLoading && "opacity-60 transition-opacity",
|
|
2185
2184
|
)}
|
|
2186
2185
|
>
|
|
2187
|
-
<tr>
|
|
2186
|
+
<tr aria-rowindex={rowWindow?.rowCount}>
|
|
2188
2187
|
{/* The selection column has nothing to total: an empty cell keeps the
|
|
2189
2188
|
values under their own headers. */}
|
|
2190
2189
|
{selection && (
|