@eifi1/ui-kit 0.48.1 → 0.49.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 +28 -13
- package/dist/admin/admin-action-confirm.js +1 -1
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.js +5 -1
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/user-roster.d.ts +11 -5
- package/dist/admin/user-roster.js +15 -4
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js +2 -2
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/not-found-page.js +1 -1
- package/dist/auth/not-found-page.js.map +1 -1
- package/dist/auth/reset-password-form.js +2 -2
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/verify-email.js +2 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/plan-card.js +2 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/components/breadcrumbs.d.ts +19 -1
- package/dist/components/breadcrumbs.js +11 -4
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-look.d.ts +37 -7
- package/dist/components/button-look.js +33 -6
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +4 -1
- package/dist/components/button.js +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +2 -1
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-edge.d.ts +10 -0
- package/dist/components/chart-edge.js +57 -0
- package/dist/components/chart-edge.js.map +1 -0
- package/dist/components/chart.js +2 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +8 -1
- package/dist/components/chip.js +4 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-context.d.ts +5 -0
- package/dist/components/confirm-context.js +7 -0
- package/dist/components/confirm-context.js.map +1 -0
- package/dist/components/confirm-dialog.js +3 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/danger-confirm.js +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +8 -1
- package/dist/components/data-table-fit.js +28 -4
- 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-mobile.d.ts +48 -2
- package/dist/components/data-table-mobile.js +87 -10
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -1
- package/dist/components/data-table-row-actions.js +9 -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 +74 -2
- package/dist/components/data-table.js +241 -221
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/floating-panel.d.ts +36 -13
- package/dist/components/floating-panel.js +27 -7
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/icon-button.d.ts +11 -0
- package/dist/components/icon-button.js +7 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/nav-pills.d.ts +8 -1
- package/dist/components/nav-pills.js +61 -38
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/page-header-context.d.ts +8 -0
- package/dist/components/page-header-context.js +7 -0
- package/dist/components/page-header-context.js.map +1 -0
- package/dist/components/page-header.d.ts +14 -2
- package/dist/components/page-header.js +10 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/progress-bar.js +5 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.js +1 -1
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions-labels.js +3 -1
- package/dist/components/row-actions-labels.js.map +1 -1
- package/dist/components/row-actions.d.ts +43 -9
- package/dist/components/row-actions.js +78 -15
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +37 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/series-chart.js +8 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +1 -1
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/sparkline.js +2 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -7
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +5 -1
- package/dist/components/status-dot.js +4 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +2 -2
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +26 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.js +37 -6
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-banner.js +1 -1
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/demo/demo-ended.js +1 -1
- package/dist/demo/demo-ended.js.map +1 -1
- package/dist/demo/demo-start.js +1 -1
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox.js +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +9 -1
- package/dist/hooks/use-dismiss.js +25 -5
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-document-title.d.ts +14 -0
- package/dist/hooks/use-document-title.js +22 -0
- package/dist/hooks/use-document-title.js.map +1 -0
- package/dist/i18n/german.js +6 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +31 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -2
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +2 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -0
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.js +2 -2
- package/dist/landing/landing-actions.js.map +1 -1
- package/dist/landing/public-header.js +2 -2
- package/dist/landing/public-header.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +27 -0
- package/dist/lib/clipboard-minus.js +63 -0
- package/dist/lib/clipboard-minus.js.map +1 -0
- package/dist/lib/document-title.d.ts +17 -0
- package/dist/lib/document-title.js +29 -0
- package/dist/lib/document-title.js.map +1 -0
- package/dist/lib/float-clearance.js +8 -2
- package/dist/lib/float-clearance.js.map +1 -1
- package/dist/lib/format.d.ts +35 -10
- package/dist/lib/format.js +55 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +22 -0
- package/dist/lib/minus.js +14 -0
- package/dist/lib/minus.js.map +1 -0
- package/dist/lib/status-tone.d.ts +30 -0
- package/dist/lib/status-tone.js +17 -0
- package/dist/lib/status-tone.js.map +1 -0
- package/dist/settings/settings-layout.js +3 -2
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +1 -1
- package/dist/shell/account-menu-language.js +9 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/app-shell-labels.js +2 -1
- package/dist/shell/app-shell-labels.js.map +1 -1
- package/dist/shell/app-shell.d.ts +58 -16
- package/dist/shell/app-shell.js +158 -44
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/language-code.d.ts +3 -0
- package/dist/shell/language-code.js +8 -0
- package/dist/shell/language-code.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +14 -6
- package/dist/shell/topbar-controls.js +14 -17
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +1 -1
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +82 -15
- package/dist/theme/chart-palette.js +51 -24
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +9 -0
- package/dist/theme/palette-presets.js +77 -62
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour.js +1 -1
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +3 -3
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +23 -6
- package/src/auth/accept-invitation.tsx +3 -3
- package/src/auth/not-found-page.tsx +1 -1
- package/src/auth/reset-password-form.tsx +3 -3
- package/src/auth/verify-email.tsx +3 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/breadcrumbs.tsx +32 -4
- package/src/components/button-look.ts +86 -15
- package/src/components/button.tsx +6 -3
- package/src/components/calendar-heatmap.tsx +2 -1
- package/src/components/chart-edge.ts +73 -0
- package/src/components/chart.tsx +3 -1
- package/src/components/chip.tsx +10 -1
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +58 -8
- 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-mobile.tsx +162 -11
- package/src/components/data-table-row-actions.tsx +12 -3
- package/src/components/data-table-row.tsx +485 -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 +367 -326
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/floating-panel.tsx +67 -16
- package/src/components/form-actions.tsx +2 -2
- package/src/components/icon-button.tsx +21 -8
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/progress-bar.tsx +6 -3
- package/src/components/reauth-dialog.tsx +1 -1
- package/src/components/row-actions-labels.ts +2 -0
- package/src/components/row-actions.tsx +154 -33
- package/src/components/section-nav.tsx +65 -0
- package/src/components/series-chart.tsx +12 -3
- package/src/components/share-card.tsx +1 -1
- package/src/components/signature-pad.tsx +1 -1
- package/src/components/sparkline.tsx +2 -1
- package/src/components/stat-tile.tsx +19 -7
- package/src/components/status-dot.tsx +7 -1
- package/src/components/tooltip.tsx +3 -1
- package/src/components/translation-review-editor.tsx +2 -2
- package/src/components/translation-review.tsx +2 -2
- package/src/components/treemap.tsx +34 -8
- package/src/components/use-table-state.ts +63 -8
- package/src/data-table.ts +5 -3
- package/src/demo/demo-banner.tsx +1 -1
- package/src/demo/demo-ended.tsx +1 -1
- package/src/demo/demo-start.tsx +1 -1
- package/src/feedback/feedback-inbox.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +9 -1
- package/src/hooks/use-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/german.ts +4 -0
- package/src/i18n/kit-labels.tsx +76 -1
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/i18n/review.ts +2 -0
- package/src/i18n/shared-labels.ts +1 -0
- package/src/index.ts +13 -0
- package/src/landing/landing-actions.tsx +2 -2
- package/src/landing/public-header.tsx +2 -2
- package/src/lib/clipboard-minus.ts +94 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +115 -30
- package/src/lib/minus.ts +40 -0
- package/src/lib/status-tone.ts +43 -0
- package/src/settings/settings-layout.tsx +3 -2
- package/src/shell/account-menu-language.tsx +14 -2
- package/src/shell/app-shell-labels.ts +1 -0
- package/src/shell/app-shell.tsx +298 -69
- package/src/shell/language-code.ts +5 -0
- package/src/shell/topbar-controls.tsx +35 -15
- package/src/testing/css-variables.generated.ts +6 -6
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour.tsx +1 -1
- package/src/wizard/stepper-nav.tsx +4 -4
- package/tokens.css +28 -18
|
@@ -5,17 +5,17 @@ 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 { columnLabel, logicalAlign, resolveDataTableLabels } from "./data-table-shared";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "./data-table-
|
|
18
|
-
import { MobileFilters, MobileRow, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
|
|
10
|
+
MobileFilters,
|
|
11
|
+
MobileSelectBar,
|
|
12
|
+
MobileSort,
|
|
13
|
+
StackedFields,
|
|
14
|
+
keepsOwnClicks,
|
|
15
|
+
mobileCardColumns,
|
|
16
|
+
useCardColumnWarnings,
|
|
17
|
+
} from "./data-table-mobile";
|
|
18
|
+
import { DataTableMobileRow, DataTableRow, type DataTableRowCtx } from "./data-table-row";
|
|
19
19
|
import { DataTableHead } from "./data-table-head";
|
|
20
20
|
import { DataTableLeadingRow } from "./data-table-leading-row";
|
|
21
21
|
import {
|
|
@@ -26,8 +26,11 @@ import {
|
|
|
26
26
|
useGroupBandTops,
|
|
27
27
|
type DataTableGroupBy,
|
|
28
28
|
type DataTableLoadMore,
|
|
29
|
+
type RowGroup,
|
|
29
30
|
} from "./data-table-groups";
|
|
30
31
|
export type { DataTableGroupBy, DataTableLoadMore } from "./data-table-groups";
|
|
32
|
+
import { VIRTUAL_SELECT_COL, rowNavTarget, useRowWindow, type DataTableVirtualize, type VirtualGroup } from "./data-table-virtual";
|
|
33
|
+
export type { DataTableVirtualize } from "./data-table-virtual";
|
|
31
34
|
import { ColumnSettingsPanel } from "./data-table-column-settings";
|
|
32
35
|
import { useLargeText } from "../hooks/use-large-text";
|
|
33
36
|
import { cn } from "../lib/cn";
|
|
@@ -66,6 +69,36 @@ export interface DataTableColumn<T> {
|
|
|
66
69
|
// bookkeeping) that would crowd a card without aiding scanning.
|
|
67
70
|
mobilePrimary?: boolean;
|
|
68
71
|
mobileHidden?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* This column on the phone's default card (0.49 KIT-7; ignored with `mobileCard`):
|
|
74
|
+
* `"primary"` — the card's title, as `mobilePrimary`; `"secondary"` — one of its
|
|
75
|
+
* fields; `"hidden"` — not on the card, as `mobileHidden`. The card shows at most
|
|
76
|
+
* three fields: the columns marked `"secondary"` when any
|
|
77
|
+
* is, else the first three visible columns after the title. A field whose value is
|
|
78
|
+
* empty — the cell returns `null`, `undefined`, `""` or the formatters' empty "—" —
|
|
79
|
+
* is left out of that card. The columns the card does not show go into the opened
|
|
80
|
+
* row with `mobileDetailsInRow`. A `noRowLink` column (the cell's own buttons or link)
|
|
81
|
+
* counts as a field: past the third it leaves the card with its action, and a
|
|
82
|
+
* development build says so once per table and column key. Give a column of row
|
|
83
|
+
* actions `role: "actions"` instead: it is then no field, and the card draws it after
|
|
84
|
+
* them.
|
|
85
|
+
*/
|
|
86
|
+
mobile?: "primary" | "secondary" | "hidden";
|
|
87
|
+
/**
|
|
88
|
+
* What the column is (0.49 KIT-7), for the phone's default card; the desktop table is
|
|
89
|
+
* unchanged.
|
|
90
|
+
*
|
|
91
|
+
* - `"status"`: its value sits as a chip beside the title instead of as a label/value
|
|
92
|
+
* line, and takes none of the card's three fields. A cell that returns text is drawn
|
|
93
|
+
* in a neutral `Chip`; one that returns an element (a `Chip` with its own tone, a
|
|
94
|
+
* `StatusDot`) is drawn as it is.
|
|
95
|
+
* - `"actions"`: the row's buttons (`rowActionsColumn` sets it). Never the card's title
|
|
96
|
+
* and never one of its three fields: the card draws it after them, at its end,
|
|
97
|
+
* whether or not other columns are marked `mobile: "secondary"`; only `mobileHidden`
|
|
98
|
+
* / `mobile: "hidden"` take it off. Its cell keeps its own clicks, as `noRowLink`:
|
|
99
|
+
* no row link wraps it, and a card that draws it is not a link.
|
|
100
|
+
*/
|
|
101
|
+
role?: "status" | "actions";
|
|
69
102
|
/**
|
|
70
103
|
* Set on a column whose cell renders its own `<a>` (or anything else that must
|
|
71
104
|
* keep its own click): the row anchor of `rowHref` then skips it and lands on
|
|
@@ -280,7 +313,10 @@ export interface DataTableProps<T> {
|
|
|
280
313
|
* When true, mirror filter/sort/page state into URL search params so views are
|
|
281
314
|
* shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
|
|
282
315
|
* URL state takes precedence over localStorage on initial load. Requires the
|
|
283
|
-
* consuming app to render inside a react-router Router.
|
|
316
|
+
* consuming app to render inside a react-router Router (0.49: only with `urlSync` — a
|
|
317
|
+
* table without it needs none). With `urlSync` and no Router, a development build
|
|
318
|
+
* throws "DataTable urlSync requires a Router"; a production build keeps the view in
|
|
319
|
+
* local state instead.
|
|
284
320
|
*
|
|
285
321
|
* `{ prefix }` namespaces them, for two URL-synced tables on one page:
|
|
286
322
|
* `urlSync={{ prefix: "inv." }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.
|
|
@@ -607,6 +643,43 @@ export interface DataTableProps<T> {
|
|
|
607
643
|
* {@link DataTableLoadMore}.
|
|
608
644
|
*/
|
|
609
645
|
loadMore?: DataTableLoadMore;
|
|
646
|
+
/**
|
|
647
|
+
* 0.48.2 (keksdose dev #632, Marcel: "The table is extremely slow. Is it virtualised…?"):
|
|
648
|
+
* render only the desktop rows near the visible part of the scroller, plus `overscan`
|
|
649
|
+
* rows each side ({@link DataTableVirtualize}), with spacer rows standing in for the
|
|
650
|
+
* rest. For long lists — keksdose's register under `loadMore` holds 1000+ rows. Opt-in;
|
|
651
|
+
* without it every row renders, as before.
|
|
652
|
+
*
|
|
653
|
+
* - **Scroller:** the bounded body (`fillHeight` or `maxBodyHeight`), or the page when the
|
|
654
|
+
* body is unbounded (`maxBodyHeight="none"`): the window is what of the table both the
|
|
655
|
+
* scroller and the browser window show.
|
|
656
|
+
* - **Kept rendered:** the header, the `leadingRow`, the group bands (sticky and stacked
|
|
657
|
+
* as before; a period's band while any of its rows is in view), every OPEN row (so an
|
|
658
|
+
* inline editor in `expandedRow` keeps its state), the roving tab stop and a row
|
|
659
|
+
* holding the focus. The `loadMore` footer follows the last spacer and loads once.
|
|
660
|
+
* - **Heights:** each rendered row is measured and remembered by its key; a row not yet
|
|
661
|
+
* seen counts as the measured average (`estimateRowHeight` before any). The scroll
|
|
662
|
+
* height settles as the rows are seen; the browser's scroll anchoring holds the rows in
|
|
663
|
+
* view while a spacer above is corrected (Safari has none: a jump of the difference
|
|
664
|
+
* is possible there on the first pass).
|
|
665
|
+
* - **Columns:** the header widths are measured once and locked in a `<colgroup>`, so the
|
|
666
|
+
* columns do not move as rows come and go; a wider cell scrolling in widens its column
|
|
667
|
+
* (once). Re-measured when the rows, the columns, a width or the scroller's width
|
|
668
|
+
* change. A `grow` column and a resized one are not locked.
|
|
669
|
+
* - **Keyboard and screen reader:** Arrow/Home/End move over every row — the target is
|
|
670
|
+
* rendered, then focused (End reaches the last row); Shift+click ranges and select-all
|
|
671
|
+
* work on the data, not the DOM. The table has `aria-rowcount`, every rendered row its
|
|
672
|
+
* `aria-rowindex` (bands, details and the totals row counted); spacers are
|
|
673
|
+
* `aria-hidden`.
|
|
674
|
+
* - **Trade-offs:** the browser's find-in-page sees only the rendered rows, and so does
|
|
675
|
+
* print; an `id` from `rowAttributes` (an anchor, an `aria-controls` target, a
|
|
676
|
+
* `scrollIntoView` on a fresh row) exists only while its row is rendered. Memoise
|
|
677
|
+
* `columns` (and `mobileCard`): the rows bail out of a render only then, and a scroll
|
|
678
|
+
* renders the rows that come in, not the whole window.
|
|
679
|
+
* - **Phone:** desktop only. The card list renders every row it holds (`loadMore`,
|
|
680
|
+
* `paginated={false}`) or reveals them in chunks as it is scrolled (client paging).
|
|
681
|
+
*/
|
|
682
|
+
virtualize?: boolean | DataTableVirtualize;
|
|
610
683
|
/**
|
|
611
684
|
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
612
685
|
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
@@ -862,35 +935,14 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
|
|
|
862
935
|
|
|
863
936
|
const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
|
|
864
937
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
Array.from(body.children).filter(
|
|
874
|
-
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute("data-row-nav"),
|
|
875
|
-
),
|
|
876
|
-
);
|
|
877
|
-
const i = rows.indexOf(from);
|
|
878
|
-
const target =
|
|
879
|
-
key === "ArrowDown"
|
|
880
|
-
? rows[i + 1]
|
|
881
|
-
: key === "ArrowUp"
|
|
882
|
-
? rows[i - 1]
|
|
883
|
-
: key === "Home"
|
|
884
|
-
? rows[0]
|
|
885
|
-
: key === "End"
|
|
886
|
-
? rows[rows.length - 1]
|
|
887
|
-
: undefined;
|
|
888
|
-
// At either end the key is still consumed, or ArrowDown on the last row would
|
|
889
|
-
// scroll the page out from under a focused row.
|
|
890
|
-
target?.focus();
|
|
891
|
-
return true;
|
|
892
|
-
}
|
|
893
|
-
const ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;
|
|
938
|
+
// `virtualize`'s constants (0.48.2): the band levels a group's `<tbody>` opens with.
|
|
939
|
+
const NO_BANDS: readonly (1 | 2)[] = [];
|
|
940
|
+
const BAND_1: readonly (1 | 2)[] = [1];
|
|
941
|
+
const BAND_2: readonly (1 | 2)[] = [2];
|
|
942
|
+
const BAND_1_2: readonly (1 | 2)[] = [1, 2];
|
|
943
|
+
const NO_VIRTUAL_OPTIONS: DataTableVirtualize = {};
|
|
944
|
+
|
|
945
|
+
// `moveRowFocus` (the roving rows' Arrow/Home/End) lives with the row in data-table-row.tsx.
|
|
894
946
|
|
|
895
947
|
// ---------- Main DataTable ----------
|
|
896
948
|
|
|
@@ -943,6 +995,7 @@ export function DataTable<T>({
|
|
|
943
995
|
leadingRowSticky = false,
|
|
944
996
|
groupBy,
|
|
945
997
|
loadMore,
|
|
998
|
+
virtualize = false,
|
|
946
999
|
density = "comfortable",
|
|
947
1000
|
sortCycle = "tri",
|
|
948
1001
|
chrome = "full",
|
|
@@ -1271,12 +1324,16 @@ export function DataTable<T>({
|
|
|
1271
1324
|
|
|
1272
1325
|
// 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out
|
|
1273
1326
|
// (data-table-fit.ts). Desktop only; the scroller is read after render.
|
|
1327
|
+
// Large and Extra large: read here, as the fit re-measures when it changes.
|
|
1328
|
+
const large = useLargeText();
|
|
1274
1329
|
const { autoHidden, keep: keepColumn } = useColumnFit({
|
|
1275
1330
|
columns,
|
|
1276
1331
|
hiddenCols,
|
|
1277
1332
|
headRefs,
|
|
1278
1333
|
scroller: () => desktopScroller.current,
|
|
1279
1334
|
enabled: !usePhoneLayout(),
|
|
1335
|
+
// Measured only when one of these changed (0.48.2): not on a ticked box.
|
|
1336
|
+
content: [slice, columns, hiddenCols, widths, density, large],
|
|
1280
1337
|
});
|
|
1281
1338
|
const visibleColumns = useMemo(
|
|
1282
1339
|
() => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),
|
|
@@ -1293,14 +1350,14 @@ export function DataTable<T>({
|
|
|
1293
1350
|
// column panel. Resolved from `visibleColumns` for exactly that reason — off a
|
|
1294
1351
|
// raw `columns` lookup a hidden headline would leave the row with no link at all.
|
|
1295
1352
|
//
|
|
1296
|
-
// `noRowLink` columns are skipped in BOTH steps: wrapping a
|
|
1297
|
-
// anchor nests two links and disables the inner one (see the prop's
|
|
1298
|
-
// nothing visible is left to wrap, the row gets NO anchor — an anchor
|
|
1299
|
-
// one cell there is would cancel that cell's own click and hand back
|
|
1300
|
-
// and a row without a link still behaves exactly as it did before #451.
|
|
1353
|
+
// `noRowLink` (and `role: "actions"`) columns are skipped in BOTH steps: wrapping a
|
|
1354
|
+
// cell that owns an anchor nests two links and disables the inner one (see the prop's
|
|
1355
|
+
// note). If nothing visible is left to wrap, the row gets NO anchor — an anchor
|
|
1356
|
+
// over the one cell there is would cancel that cell's own click and hand back
|
|
1357
|
+
// nothing, and a row without a link still behaves exactly as it did before #451.
|
|
1301
1358
|
const linkColumn = rowHref
|
|
1302
|
-
? (visibleColumns.find((c) => c.mobilePrimary && !c
|
|
1303
|
-
visibleColumns.find((c) => !c
|
|
1359
|
+
? (visibleColumns.find((c) => c.mobilePrimary && !keepsOwnClicks(c)) ??
|
|
1360
|
+
visibleColumns.find((c) => !keepsOwnClicks(c)))
|
|
1304
1361
|
: undefined;
|
|
1305
1362
|
|
|
1306
1363
|
// ---- Column resize ---- (data-table-resize.ts: pointer capture, one `setWidths` per
|
|
@@ -1370,17 +1427,41 @@ export function DataTable<T>({
|
|
|
1370
1427
|
// of a horizontally-scrolling table. Column settings stay desktop-only; the
|
|
1371
1428
|
// filters come through a bottom sheet (feedback #299) and the sort through the
|
|
1372
1429
|
// opt-in `mobileSort` row (keksdose K10), both on the table's own state.
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1430
|
+
// Memoised on `visibleColumns`: the cards compare their columns by identity (0.48.2).
|
|
1431
|
+
const mobileColumns = useMemo(
|
|
1432
|
+
() => visibleColumns.filter((c) => !c.mobileHidden && c.mobile !== "hidden"),
|
|
1433
|
+
[visibleColumns],
|
|
1434
|
+
);
|
|
1435
|
+
// The default card (0.49 KIT-7): the title, the status chips beside it, at most three
|
|
1436
|
+
// fields — the ones marked `mobile: "secondary"`, else the first after the title — and
|
|
1437
|
+
// the `role: "actions"` columns after them.
|
|
1438
|
+
const {
|
|
1439
|
+
primary: mobilePrimaryCol,
|
|
1440
|
+
status: mobileStatusColumns,
|
|
1441
|
+
fields: mobileSecondaryColumns,
|
|
1442
|
+
actions: mobileActionColumns,
|
|
1443
|
+
} = useMemo(() => mobileCardColumns(mobileColumns), [mobileColumns]);
|
|
1444
|
+
// A development build names a `noRowLink` column (a per-row delete) the card drops.
|
|
1445
|
+
useCardColumnWarnings(
|
|
1446
|
+
columns,
|
|
1447
|
+
!mobileCard && !mobileDetailsInRow,
|
|
1448
|
+
storageKey ?? (typeof urlSync === "object" ? urlSync.prefix : undefined),
|
|
1449
|
+
);
|
|
1450
|
+
// The columns the card leaves out, for the opened row (`mobileDetailsInRow`): the
|
|
1451
|
+
// hidden ones and, without a `mobileCard`, the ones past the card's three fields.
|
|
1452
|
+
const onCard = new Set<DataTableColumn<T>>([
|
|
1453
|
+
...(mobilePrimaryCol ? [mobilePrimaryCol] : []),
|
|
1454
|
+
...mobileStatusColumns,
|
|
1455
|
+
...mobileSecondaryColumns,
|
|
1456
|
+
...mobileActionColumns,
|
|
1457
|
+
]);
|
|
1458
|
+
const mobileDetailColumns = mobileDetailsInRow
|
|
1459
|
+
? visibleColumns.filter((c) => (mobileCard ? c.mobileHidden || c.mobile === "hidden" : !onCard.has(c)))
|
|
1460
|
+
: [];
|
|
1379
1461
|
// Large and Extra large (§4 "DataTable phone card"): the card's labels go ABOVE their
|
|
1380
1462
|
// values, nothing in it truncates, and the rows get more room. Most of it is a
|
|
1381
1463
|
// `large:` class; the label/value grid needs a different STRUCTURE (each pair in a
|
|
1382
|
-
// box of its own), which a class cannot write.
|
|
1383
|
-
const large = useLargeText();
|
|
1464
|
+
// box of its own), which a class cannot write. (`large` is read above, by the fit.)
|
|
1384
1465
|
// What an opened phone row shows: the card's left-out columns first (when asked
|
|
1385
1466
|
// for), then the app's own `expandedRow`. Desktop keeps `expandedRow` alone.
|
|
1386
1467
|
const opensRow = !!expandedRow || mobileDetailColumns.length > 0;
|
|
@@ -1401,7 +1482,7 @@ export function DataTable<T>({
|
|
|
1401
1482
|
// opens the row and leaves the cell's own link working. A caller's `mobileCard`
|
|
1402
1483
|
// renderer is opaque to us; a link inside one is the caller's to declare by
|
|
1403
1484
|
// marking the column it comes from.
|
|
1404
|
-
const mobileCardLinkable = mobileColumns.every((c) => !c
|
|
1485
|
+
const mobileCardLinkable = (mobileCard ? mobileColumns : [...onCard]).every((c) => !keepsOwnClicks(c));
|
|
1405
1486
|
// The card list's reading direction, for the one thing on it CSS cannot flip: which
|
|
1406
1487
|
// physical drag a logical `start`/`end` swipe action means. Read off the DOM through
|
|
1407
1488
|
// a ref callback (it runs at commit, like a layout effect) because `dir` is an
|
|
@@ -1465,7 +1546,7 @@ export function DataTable<T>({
|
|
|
1465
1546
|
const phoneSelecting = !!selection && !isMdUp && (phoneSelectMode || anySelected);
|
|
1466
1547
|
// ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,
|
|
1467
1548
|
// anchored on the row's KEY so a sort, filter or page change cannot move it)
|
|
1468
|
-
const {
|
|
1549
|
+
const { canSelect, selectRange, setAnchor, markRangeClick, takeRangeClick } = useRangeSelection({
|
|
1469
1550
|
selection,
|
|
1470
1551
|
slice,
|
|
1471
1552
|
rowKey,
|
|
@@ -1498,6 +1579,42 @@ export function DataTable<T>({
|
|
|
1498
1579
|
? rowKey(slice[0])
|
|
1499
1580
|
: null;
|
|
1500
1581
|
|
|
1582
|
+
// What the memoised rows' handlers read at event time (0.48.2, K-052-2): the latest
|
|
1583
|
+
// props, refreshed every commit, so a row that bailed out of a render still toggles
|
|
1584
|
+
// through the current `selection` and opens through the current `onRowClick`.
|
|
1585
|
+
// `virtualize` (0.48.2): desktop only, and only with rows to window. Arrow/Home/End then
|
|
1586
|
+
// move over every row, rendered or not: the target is rendered, then focused.
|
|
1587
|
+
const virtualOn = !!virtualize && isMdUp && slice.length > 0;
|
|
1588
|
+
const focusRowRef = useRef<((index: number) => void) | undefined>(undefined);
|
|
1589
|
+
const latestRowCtx: DataTableRowCtx<T> = {
|
|
1590
|
+
moveFocus: virtualOn
|
|
1591
|
+
? (from, key) => {
|
|
1592
|
+
const target = rowNavTarget(from, key, slice.length);
|
|
1593
|
+
if (target === null) return false;
|
|
1594
|
+
focusRowRef.current?.(target);
|
|
1595
|
+
return true;
|
|
1596
|
+
}
|
|
1597
|
+
: undefined,
|
|
1598
|
+
onRowClick,
|
|
1599
|
+
onToggle: selection?.onToggle,
|
|
1600
|
+
isSelected: (row) => !!selection?.isSelected(row),
|
|
1601
|
+
canSelect,
|
|
1602
|
+
selectRange,
|
|
1603
|
+
setAnchor,
|
|
1604
|
+
markRangeClick,
|
|
1605
|
+
takeRangeClick,
|
|
1606
|
+
setFocusRowKey,
|
|
1607
|
+
onLongPress: (row) => {
|
|
1608
|
+
setPhoneSelectMode(true);
|
|
1609
|
+
setAnchor(rowKey(row));
|
|
1610
|
+
if (selection && !selection.isSelected(row)) selection.onToggle(row, true);
|
|
1611
|
+
},
|
|
1612
|
+
};
|
|
1613
|
+
const rowCtx = useRef(latestRowCtx);
|
|
1614
|
+
useLayoutEffect(() => {
|
|
1615
|
+
rowCtx.current = latestRowCtx;
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1501
1618
|
// When dialog-mode is on, the expanded row's detail goes into a bottom-sheet
|
|
1502
1619
|
// modal instead of unfolding inline. Only one row is ever expanded at a time.
|
|
1503
1620
|
const mobileDialogRow = mobileExpandAsDialog && !isMdUp ? mobileSlice.find((r) => isExpanded?.(r)) : undefined;
|
|
@@ -1514,46 +1631,40 @@ export function DataTable<T>({
|
|
|
1514
1631
|
|
|
1515
1632
|
// One mobile card (`MobileRow`, data-table-mobile.tsx). A function so the flat list
|
|
1516
1633
|
// and the grouped list (below) share identical row markup.
|
|
1517
|
-
const renderMobileRow = (row: T) =>
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
},
|
|
1552
|
-
}
|
|
1553
|
-
: null
|
|
1554
|
-
}
|
|
1555
|
-
/>
|
|
1556
|
-
);
|
|
1634
|
+
const renderMobileRow = (row: T) => {
|
|
1635
|
+
const key = rowKey(row);
|
|
1636
|
+
const expanded = isExpanded?.(row) ?? false;
|
|
1637
|
+
return (
|
|
1638
|
+
<DataTableMobileRow<T>
|
|
1639
|
+
key={key}
|
|
1640
|
+
ctx={rowCtx}
|
|
1641
|
+
rowKey={key}
|
|
1642
|
+
row={row}
|
|
1643
|
+
expanded={expanded}
|
|
1644
|
+
expansion={expanded ? openedRowContent(row) : null}
|
|
1645
|
+
opensRow={opensRow}
|
|
1646
|
+
className={rowClassName?.(row)}
|
|
1647
|
+
attrs={rowAttributes?.(row)}
|
|
1648
|
+
interactive={!!onRowClick}
|
|
1649
|
+
href={mobileCardLinkable ? rowHref?.(row) : undefined}
|
|
1650
|
+
swipeActionsFor={swipeActionsFor}
|
|
1651
|
+
mobileDir={mobileDir}
|
|
1652
|
+
mobileCardLinkable={mobileCardLinkable}
|
|
1653
|
+
mobileCard={mobileCard}
|
|
1654
|
+
mobilePrimaryCol={mobilePrimaryCol}
|
|
1655
|
+
mobileStatusColumns={mobileStatusColumns}
|
|
1656
|
+
mobileSecondaryColumns={mobileSecondaryColumns}
|
|
1657
|
+
mobileActionColumns={mobileActionColumns}
|
|
1658
|
+
mobileExpandAsDialog={mobileExpandAsDialog}
|
|
1659
|
+
compact={compact}
|
|
1660
|
+
large={large}
|
|
1661
|
+
selectionOn={!!selection}
|
|
1662
|
+
selectActive={phoneSelecting}
|
|
1663
|
+
selectable={canSelect(row)}
|
|
1664
|
+
selected={!!selection && selection.isSelected(row)}
|
|
1665
|
+
/>
|
|
1666
|
+
);
|
|
1667
|
+
};
|
|
1557
1668
|
|
|
1558
1669
|
// Consecutive grouping of the (already-sorted) mobile slice into labelled
|
|
1559
1670
|
// sections for the "assistance"-style list (feedback #317). Null unless the
|
|
@@ -1566,6 +1677,57 @@ export function DataTable<T>({
|
|
|
1566
1677
|
!!desktopGroups,
|
|
1567
1678
|
`${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? ""}`,
|
|
1568
1679
|
);
|
|
1680
|
+
// ---- Row windowing ---- (0.48.2, keksdose dev #632; data-table-virtual.ts)
|
|
1681
|
+
// One run per innermost group, with its bands (`virtualOn` is above, with the rows' context).
|
|
1682
|
+
const virtualGroups: VirtualGroup[] = [];
|
|
1683
|
+
const virtualBands: { g1: RowGroup<T>; g2: RowGroup<T> | null }[] = [];
|
|
1684
|
+
if (virtualOn) {
|
|
1685
|
+
if (!desktopGroups) virtualGroups.push({ start: 0, end: slice.length, bands: NO_BANDS, first: 0 });
|
|
1686
|
+
else
|
|
1687
|
+
for (const g1 of desktopGroups) {
|
|
1688
|
+
const first = virtualGroups.length;
|
|
1689
|
+
if (!g1.children) {
|
|
1690
|
+
virtualGroups.push({ start: g1.start, end: g1.start + g1.rows.length, bands: BAND_1, first });
|
|
1691
|
+
virtualBands.push({ g1, g2: null });
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
g1.children.forEach((g2, j) => {
|
|
1695
|
+
virtualGroups.push({ start: g2.start, end: g2.start + g2.rows.length, bands: j === 0 ? BAND_1_2 : BAND_2, first });
|
|
1696
|
+
virtualBands.push({ g1, g2 });
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const expandedIndices: number[] = [];
|
|
1701
|
+
if (virtualOn && isExpanded && expandedRow) slice.forEach((r, i) => isExpanded(r) && expandedIndices.push(i));
|
|
1702
|
+
const virtualColKeys = [...(selection ? [VIRTUAL_SELECT_COL] : []), ...visibleColumns.map((c) => c.key)];
|
|
1703
|
+
const virtualLockable = new Set(
|
|
1704
|
+
virtualColKeys.filter((k) => !widths[k] && !visibleColumns.some((c) => c.key === k && c.grow)),
|
|
1705
|
+
);
|
|
1706
|
+
const rowWindow = useRowWindow({
|
|
1707
|
+
enabled: virtualOn,
|
|
1708
|
+
rows: slice,
|
|
1709
|
+
rowKey,
|
|
1710
|
+
groups: virtualGroups,
|
|
1711
|
+
expanded: expandedIndices,
|
|
1712
|
+
pinnedKeys: [tabRowKey],
|
|
1713
|
+
scrollerRef: desktopScroller,
|
|
1714
|
+
tableRef,
|
|
1715
|
+
options: typeof virtualize === "object" ? virtualize : NO_VIRTUAL_OPTIONS,
|
|
1716
|
+
compact,
|
|
1717
|
+
rowsBefore: 1 + (leadingRow ? 1 : 0),
|
|
1718
|
+
rowsAfter: showFooter ? 1 : 0,
|
|
1719
|
+
colKeys: virtualColKeys,
|
|
1720
|
+
lockable: virtualLockable,
|
|
1721
|
+
resetKey: `${Object.entries(widths).join(";")}|${density}|${large}|${slice.length}:${
|
|
1722
|
+
slice.length ? `${rowKey(slice[0])}:${rowKey(slice[slice.length - 1])}` : ""
|
|
1723
|
+
}`,
|
|
1724
|
+
});
|
|
1725
|
+
// What `moveFocus` (in the rows' context, above) reaches: the window's focus mover.
|
|
1726
|
+
const focusRowAt = rowWindow?.focusRow;
|
|
1727
|
+
useLayoutEffect(() => {
|
|
1728
|
+
focusRowRef.current = focusRowAt;
|
|
1729
|
+
}, [focusRowAt]);
|
|
1730
|
+
|
|
1569
1731
|
// `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.
|
|
1570
1732
|
const mobileLevel = typeof mobileGroupBy === "function" ? null : (mobileGroupBy ?? null);
|
|
1571
1733
|
const mobileKey = typeof mobileGroupBy === "function" ? mobileGroupBy : mobileGroupBy?.key;
|
|
@@ -1866,7 +2028,20 @@ export function DataTable<T>({
|
|
|
1866
2028
|
data-fit-grow={growDrawn ? "" : undefined}
|
|
1867
2029
|
aria-label={labels.table}
|
|
1868
2030
|
aria-busy={isLoading || undefined}
|
|
2031
|
+
// `virtualize` (0.48.2): every row counted, rendered or not.
|
|
2032
|
+
aria-rowcount={rowWindow?.rowCount}
|
|
2033
|
+
data-virtualized={rowWindow ? "" : undefined}
|
|
1869
2034
|
>
|
|
2035
|
+
{/* The locked column widths (0.48.2, `virtualize`): rows coming and going no
|
|
2036
|
+
longer move the columns. */}
|
|
2037
|
+
{rowWindow && (
|
|
2038
|
+
<colgroup>
|
|
2039
|
+
{virtualColKeys.map((key) => {
|
|
2040
|
+
const w = rowWindow.colWidths?.[key];
|
|
2041
|
+
return <col key={key} style={w !== undefined ? { width: w } : undefined} />;
|
|
2042
|
+
})}
|
|
2043
|
+
</colgroup>
|
|
2044
|
+
)}
|
|
1870
2045
|
{/* Sticky header. position:sticky pins to the nearest scroll-container
|
|
1871
2046
|
ancestor — so the header can only stick to THIS wrapper, never the
|
|
1872
2047
|
page. For that to actually hold the header in place, the wrapper
|
|
@@ -1899,235 +2074,57 @@ export function DataTable<T>({
|
|
|
1899
2074
|
startResize={startResize}
|
|
1900
2075
|
moveResize={moveResize}
|
|
1901
2076
|
finishResize={finishResize}
|
|
2077
|
+
ariaRowIndex={rowWindow ? 1 : undefined}
|
|
2078
|
+
lockedWidths={rowWindow?.colWidths}
|
|
2079
|
+
selectionLockKey={VIRTUAL_SELECT_COL}
|
|
1902
2080
|
/>
|
|
1903
2081
|
{(() => {
|
|
1904
2082
|
const bodyClass = cn(isLoading && slice.length > 0 && "opacity-60 transition-opacity");
|
|
1905
2083
|
const leading = leadingRow ? (
|
|
1906
|
-
<DataTableLeadingRow
|
|
2084
|
+
<DataTableLeadingRow
|
|
2085
|
+
colSpan={totalColSpan}
|
|
2086
|
+
sticky={leadingRowSticky}
|
|
2087
|
+
ariaRowIndex={rowWindow ? 2 : undefined}
|
|
2088
|
+
>
|
|
1907
2089
|
{leadingRow}
|
|
1908
2090
|
</DataTableLeadingRow>
|
|
1909
2091
|
) : null;
|
|
2092
|
+
// One memoised row each (0.48.2, K-052-2; data-table-row.tsx): the table
|
|
2093
|
+
// derives each row's own inputs here, as primitives, so a tick re-renders
|
|
2094
|
+
// only the rows whose inputs changed.
|
|
1910
2095
|
const renderRow = (row: T, rowIndex: number) => {
|
|
1911
2096
|
const expanded = isExpanded?.(row) ?? false;
|
|
1912
|
-
const expansion = expanded ? expandedRow?.(row) : null;
|
|
1913
|
-
const rowInteractive = !!onRowClick || !!selection;
|
|
1914
|
-
const href = rowHref?.(row);
|
|
1915
2097
|
const key = rowKey(row);
|
|
1916
|
-
const detailId = `${detailIdBase}-${rowIndex}`;
|
|
1917
|
-
// Whether the row is open, said where ARIA allows it (round 0.34, the
|
|
1918
|
-
// showcase's axe check): `aria-expanded` belongs to a treegrid's row, not a
|
|
1919
|
-
// table's, and the <tr> wore it. A disclosure button in the first cell carries
|
|
1920
|
-
// it now, with `aria-controls` on the detail row while that is there.
|
|
1921
|
-
// Visually hidden and not a tab stop: the row stays the one roving stop, with
|
|
1922
|
-
// Enter/Space and the whole-row click — this is the screen reader cursor's
|
|
1923
|
-
// way in, and where it hears the state. Named by `aria-label`, so the cell's
|
|
1924
|
-
// text stays the cell's own.
|
|
1925
|
-
const disclosure =
|
|
1926
|
-
rowsFocusable && expandedRow ? (
|
|
1927
|
-
<button
|
|
1928
|
-
type="button"
|
|
1929
|
-
tabIndex={-1}
|
|
1930
|
-
className="sr-only-fixed"
|
|
1931
|
-
aria-label={labels.rowDetails}
|
|
1932
|
-
aria-expanded={expanded}
|
|
1933
|
-
aria-controls={expansion ? detailId : undefined}
|
|
1934
|
-
// The row's own handler leaves a click on a control inside it alone.
|
|
1935
|
-
onClick={() => onRowClick!(row)}
|
|
1936
|
-
/>
|
|
1937
|
-
) : null;
|
|
1938
2098
|
return (
|
|
1939
|
-
<
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
: undefined
|
|
1969
|
-
}
|
|
1970
|
-
className={cn(
|
|
1971
|
-
"border-t border-subtle",
|
|
1972
|
-
rowInteractive && "cursor-pointer hover:bg-hover",
|
|
1973
|
-
// The kit's focus frame width (§5, `--focus-ring-width`), drawn as an
|
|
1974
|
-
// OUTLINE rather than FOCUS_RING's box-shadow: a shadow on a `<tr>`
|
|
1975
|
-
// is not painted by every engine, an outline is.
|
|
1976
|
-
rowsFocusable &&
|
|
1977
|
-
"focus-visible:bg-hover focus-visible:outline-focus focus-visible:-outline-offset-2 focus-visible:outline-brand",
|
|
1978
|
-
rowClassName?.(row),
|
|
1979
|
-
)}
|
|
1980
|
-
onClick={
|
|
1981
|
-
rowInteractive
|
|
1982
|
-
? (e) => {
|
|
1983
|
-
// A click on a control inside the row (a button, a link,
|
|
1984
|
-
// an input the cell renders) is that control's — it must
|
|
1985
|
-
// not also open the row. Cells no longer have to
|
|
1986
|
-
// `stopPropagation` to get that.
|
|
1987
|
-
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
1988
|
-
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1989
|
-
// Modifier-clicks anywhere in the row drive
|
|
1990
|
-
// selection instead of expanding it (feedback #289):
|
|
1991
|
-
// Ctrl/Cmd toggles a single row, Shift selects the
|
|
1992
|
-
// range from the last-toggled row (inclusive of this
|
|
1993
|
-
// one) — the same as the checkbox, but for the whole
|
|
1994
|
-
// row, which is what the checkbox-only version was
|
|
1995
|
-
// missing.
|
|
1996
|
-
if (selection && canSelect(row)) {
|
|
1997
|
-
if (e.shiftKey) {
|
|
1998
|
-
// Drop the text selection a shift-click would
|
|
1999
|
-
// otherwise smear across the rows.
|
|
2000
|
-
window.getSelection?.()?.removeAllRanges();
|
|
2001
|
-
if (selectRange(rowIndex)) return;
|
|
2002
|
-
// No anchor yet: treat it as the first pick.
|
|
2003
|
-
selectionAnchor.current = rowKey(row);
|
|
2004
|
-
selection.onToggle(row, !selection.isSelected(row));
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
if (e.metaKey || e.ctrlKey) {
|
|
2008
|
-
selectionAnchor.current = rowKey(row);
|
|
2009
|
-
selection.onToggle(row, !selection.isSelected(row));
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
onRowClick?.(row);
|
|
2014
|
-
}
|
|
2015
|
-
: undefined
|
|
2016
|
-
}
|
|
2017
|
-
>
|
|
2018
|
-
{selection && (
|
|
2019
|
-
<td
|
|
2020
|
-
className={cn("align-top", compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2")}
|
|
2021
|
-
// Don't let selecting a row also trigger the row click
|
|
2022
|
-
// (which expands/edits it).
|
|
2023
|
-
onClick={(e) => e.stopPropagation()}
|
|
2024
|
-
>
|
|
2025
|
-
{canSelect(row) && (
|
|
2026
|
-
<input
|
|
2027
|
-
type="checkbox"
|
|
2028
|
-
className={cn("accent-indigo-600", compact ? "size-3.5" : "size-4")}
|
|
2029
|
-
checked={selection.isSelected(row)}
|
|
2030
|
-
// Shift+click selects the range from the last toggled
|
|
2031
|
-
// row. `preventDefault` puts the box back, but it does NOT
|
|
2032
|
-
// stop React's `onChange`: React derives a checkbox's
|
|
2033
|
-
// change from the `click` itself, after the browser has
|
|
2034
|
-
// already flipped `checked` and before the cancellation
|
|
2035
|
-
// reverts it — so the same click also reported itself as
|
|
2036
|
-
// a single toggle, and the owner got `onToggleMany` and
|
|
2037
|
-
// then `onToggle` for the clicked row. The click is
|
|
2038
|
-
// remembered by identity and its change is dropped; a
|
|
2039
|
-
// boolean flag would outlive a click whose change never
|
|
2040
|
-
// came and swallow the user's next real one.
|
|
2041
|
-
onClick={(e) => {
|
|
2042
|
-
if (e.shiftKey && selectRange(rowIndex)) {
|
|
2043
|
-
e.preventDefault();
|
|
2044
|
-
rangeClick.current = e.nativeEvent;
|
|
2045
|
-
}
|
|
2046
|
-
}}
|
|
2047
|
-
onChange={(e) => {
|
|
2048
|
-
if (rangeClick.current && e.nativeEvent === rangeClick.current) {
|
|
2049
|
-
rangeClick.current = null;
|
|
2050
|
-
return;
|
|
2051
|
-
}
|
|
2052
|
-
selectionAnchor.current = rowKey(row);
|
|
2053
|
-
selection.onToggle(row, e.target.checked);
|
|
2054
|
-
}}
|
|
2055
|
-
aria-label={labels.selectRow}
|
|
2056
|
-
/>
|
|
2057
|
-
)}
|
|
2058
|
-
</td>
|
|
2059
|
-
)}
|
|
2060
|
-
{visibleColumns.map((col, colIndex) => {
|
|
2061
|
-
const width = widths[col.key];
|
|
2062
|
-
// The caller's first, the table's over it — see `cellProps`.
|
|
2063
|
-
const own = splitCellProps(col.cellProps?.(row));
|
|
2064
|
-
const pinned = width ? { width, minWidth: width, maxWidth: width } : undefined;
|
|
2065
|
-
return (
|
|
2066
|
-
<td
|
|
2067
|
-
{...own.attrs}
|
|
2068
|
-
key={col.key}
|
|
2069
|
-
data-col={col.key}
|
|
2070
|
-
style={own.style || pinned ? { ...own.style, ...pinned } : undefined}
|
|
2071
|
-
className={cn(
|
|
2072
|
-
"align-top",
|
|
2073
|
-
compact ? "px-2 py-1" : "px-3 py-2",
|
|
2074
|
-
width && "overflow-hidden text-ellipsis",
|
|
2075
|
-
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
2076
|
-
logicalAlign(own.className),
|
|
2077
|
-
logicalAlign(col.className),
|
|
2078
|
-
)}
|
|
2079
|
-
>
|
|
2080
|
-
{colIndex === 0 && disclosure}
|
|
2081
|
-
{col === linkColumn && href ? (
|
|
2082
|
-
// No `onActivate`: the click is left to bubble to the
|
|
2083
|
-
// <tr> handler above, which already owns expand-vs-select.
|
|
2084
|
-
// Calling it here as well would toggle the row twice.
|
|
2085
|
-
<RowLink href={href} className="block" data-row-link="">
|
|
2086
|
-
{col.cell(row)}
|
|
2087
|
-
</RowLink>
|
|
2088
|
-
) : (
|
|
2089
|
-
col.cell(row)
|
|
2090
|
-
)}
|
|
2091
|
-
</td>
|
|
2092
|
-
);
|
|
2093
|
-
})}
|
|
2094
|
-
</tr>
|
|
2095
|
-
{expansion && (
|
|
2096
|
-
<tr id={disclosure ? detailId : undefined} className="bg-surface-2 border-t border-subtle">
|
|
2097
|
-
{/* The detail panel spans every column. In an `auto` table
|
|
2098
|
-
layout (the default here — columns only get fixed style
|
|
2099
|
-
widths once the user resizes/auto-sizes them) the cell's
|
|
2100
|
-
content preferred-width is redistributed across the
|
|
2101
|
-
spanned columns, so a long expansion visibly re-sizes the
|
|
2102
|
-
columns above it — and one-line content does not, since
|
|
2103
|
-
its preferred width is small (feedback #104). Wrapping the
|
|
2104
|
-
content in `w-0 min-w-full` pins the cell's preferred
|
|
2105
|
-
width to ~0 (the explicit width:0 child), so it can never
|
|
2106
|
-
perturb the columns; the div then fills to the full row
|
|
2107
|
-
width only at paint time, and long text wraps within it.
|
|
2108
|
-
Inside it the content is pinned to what the scroller SHOWS
|
|
2109
|
-
(0.47, keksdose S4): on a table wider than its scroller a
|
|
2110
|
-
table-wide panel put an inline editor's Save/Cancel off-screen.
|
|
2111
|
-
`sticky start-0` holds it at the scroller's start edge (the
|
|
2112
|
-
right one in RTL) as the row scrolls sideways, and
|
|
2113
|
-
`--dt-visible-w` (the scroller's clientWidth, measured above)
|
|
2114
|
-
sizes it; before the first measure, and in a table no wider
|
|
2115
|
-
than its scroller, it is the full width as before. */}
|
|
2116
|
-
<td colSpan={totalColSpan} className="p-0">
|
|
2117
|
-
<div className="w-0 min-w-full">
|
|
2118
|
-
<div
|
|
2119
|
-
className={cn(
|
|
2120
|
-
"sticky start-0 w-[var(--dt-visible-w,100%)] max-w-full",
|
|
2121
|
-
compact ? "px-2 py-2" : "px-3 py-3",
|
|
2122
|
-
)}
|
|
2123
|
-
>
|
|
2124
|
-
{expansion}
|
|
2125
|
-
</div>
|
|
2126
|
-
</div>
|
|
2127
|
-
</td>
|
|
2128
|
-
</tr>
|
|
2129
|
-
)}
|
|
2130
|
-
</Fragment>
|
|
2099
|
+
<DataTableRow<T>
|
|
2100
|
+
key={key}
|
|
2101
|
+
row={row}
|
|
2102
|
+
rowIndex={rowIndex}
|
|
2103
|
+
rowKey={key}
|
|
2104
|
+
ctx={rowCtx}
|
|
2105
|
+
columns={visibleColumns}
|
|
2106
|
+
widths={widths}
|
|
2107
|
+
growDrawn={growDrawn}
|
|
2108
|
+
linkColumn={linkColumn}
|
|
2109
|
+
href={rowHref?.(row)}
|
|
2110
|
+
selectionColumn={!!selection}
|
|
2111
|
+
selectable={canSelect(row)}
|
|
2112
|
+
selected={!!selection && selection.isSelected(row)}
|
|
2113
|
+
expanded={expanded}
|
|
2114
|
+
expansion={expanded ? expandedRow?.(row) : null}
|
|
2115
|
+
disclosure={rowsFocusable && !!expandedRow}
|
|
2116
|
+
detailId={`${detailIdBase}-${rowIndex}`}
|
|
2117
|
+
rowsFocusable={rowsFocusable}
|
|
2118
|
+
rowInteractive={!!onRowClick || !!selection}
|
|
2119
|
+
tabbable={key === tabRowKey}
|
|
2120
|
+
className={rowClassName?.(row)}
|
|
2121
|
+
attrs={rowAttributes?.(row)}
|
|
2122
|
+
compact={compact}
|
|
2123
|
+
totalColSpan={totalColSpan}
|
|
2124
|
+
rowDetailsLabel={labels.rowDetails}
|
|
2125
|
+
selectRowLabel={labels.selectRow}
|
|
2126
|
+
ariaRowIndex={rowWindow?.ariaRow(rowIndex)}
|
|
2127
|
+
/>
|
|
2131
2128
|
);
|
|
2132
2129
|
};
|
|
2133
2130
|
const emptyRow =
|
|
@@ -2143,6 +2140,60 @@ export function DataTable<T>({
|
|
|
2143
2140
|
</td>
|
|
2144
2141
|
</tr>
|
|
2145
2142
|
) : null;
|
|
2143
|
+
const band = (
|
|
2144
|
+
level: 1 | 2,
|
|
2145
|
+
def: DataTableGroupBy<T>,
|
|
2146
|
+
g: { key: string; rows: T[] },
|
|
2147
|
+
ariaRowIndex?: number,
|
|
2148
|
+
) => (
|
|
2149
|
+
<DataTableGroupBand
|
|
2150
|
+
level={level}
|
|
2151
|
+
colSpan={totalColSpan}
|
|
2152
|
+
top={level === 1 ? bandTops[0] : bandTops[1]}
|
|
2153
|
+
compact={compact}
|
|
2154
|
+
label={def.label ? def.label(g.key, g.rows) : g.key}
|
|
2155
|
+
end={def.end?.(g.key, g.rows)}
|
|
2156
|
+
ariaRowIndex={ariaRowIndex}
|
|
2157
|
+
/>
|
|
2158
|
+
);
|
|
2159
|
+
if (rowWindow) {
|
|
2160
|
+
// Windowed (0.48.2): each group's own `<tbody>` with its bands and the
|
|
2161
|
+
// rendered rows, spacer rows between; one spacer `<tbody>` per run of groups
|
|
2162
|
+
// with nothing rendered. Spacers are hidden from the accessibility tree.
|
|
2163
|
+
const spacer = (key: string, height: number) => (
|
|
2164
|
+
<tr key={key} aria-hidden="true" data-vt-spacer="">
|
|
2165
|
+
<td colSpan={totalColSpan} style={{ height, padding: 0, border: 0 }} />
|
|
2166
|
+
</tr>
|
|
2167
|
+
);
|
|
2168
|
+
return (
|
|
2169
|
+
<>
|
|
2170
|
+
{leading && <tbody className={bodyClass}>{leading}</tbody>}
|
|
2171
|
+
{rowWindow.bodies.map((b) => {
|
|
2172
|
+
if (b.kind === "spacer")
|
|
2173
|
+
return (
|
|
2174
|
+
<tbody key={b.key} data-vt-body="" aria-hidden="true">
|
|
2175
|
+
{spacer("s", b.height)}
|
|
2176
|
+
</tbody>
|
|
2177
|
+
);
|
|
2178
|
+
const g = virtualGroups[b.group];
|
|
2179
|
+
const vb = virtualBands[b.group];
|
|
2180
|
+
return (
|
|
2181
|
+
<tbody
|
|
2182
|
+
key={vb ? `vg:${vb.g1.key}:${vb.g2?.key ?? ""}:${g.start}` : "vg"}
|
|
2183
|
+
data-vt-body=""
|
|
2184
|
+
className={bodyClass}
|
|
2185
|
+
>
|
|
2186
|
+
{vb && g.bands.includes(1) && band(1, levels[0], vb.g1, rowWindow.ariaBand(b.group, 1))}
|
|
2187
|
+
{vb?.g2 && band(2, levels[1], vb.g2, rowWindow.ariaBand(b.group, 2))}
|
|
2188
|
+
{b.items.map((it) =>
|
|
2189
|
+
it.kind === "gap" ? spacer(it.key, it.height) : renderRow(slice[it.index], it.index),
|
|
2190
|
+
)}
|
|
2191
|
+
</tbody>
|
|
2192
|
+
);
|
|
2193
|
+
})}
|
|
2194
|
+
</>
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2146
2197
|
if (!desktopGroups) {
|
|
2147
2198
|
return (
|
|
2148
2199
|
<tbody className={bodyClass}>
|
|
@@ -2155,16 +2206,6 @@ export function DataTable<T>({
|
|
|
2155
2206
|
// Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its
|
|
2156
2207
|
// band the row header; the leading row (and the empty state) in their own
|
|
2157
2208
|
// before the first group.
|
|
2158
|
-
const band = (level: 1 | 2, def: DataTableGroupBy<T>, g: { key: string; rows: T[] }) => (
|
|
2159
|
-
<DataTableGroupBand
|
|
2160
|
-
level={level}
|
|
2161
|
-
colSpan={totalColSpan}
|
|
2162
|
-
top={level === 1 ? bandTops[0] : bandTops[1]}
|
|
2163
|
-
compact={compact}
|
|
2164
|
-
label={def.label ? def.label(g.key, g.rows) : g.key}
|
|
2165
|
-
end={def.end?.(g.key, g.rows)}
|
|
2166
|
-
/>
|
|
2167
|
-
);
|
|
2168
2209
|
const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));
|
|
2169
2210
|
return (
|
|
2170
2211
|
<>
|
|
@@ -2206,7 +2247,7 @@ export function DataTable<T>({
|
|
|
2206
2247
|
isLoading && "opacity-60 transition-opacity",
|
|
2207
2248
|
)}
|
|
2208
2249
|
>
|
|
2209
|
-
<tr>
|
|
2250
|
+
<tr aria-rowindex={rowWindow?.rowCount}>
|
|
2210
2251
|
{/* The selection column has nothing to total: an empty cell keeps the
|
|
2211
2252
|
values under their own headers. */}
|
|
2212
2253
|
{selection && (
|