@eifi1/ui-kit 0.7.1 → 0.8.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 +32 -17
- package/dist/chart.d.ts +3 -2
- package/dist/components/alert-banner.d.ts +59 -10
- package/dist/components/alert-banner.js +114 -8
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button-group.js +49 -0
- package/dist/components/button-group.js.map +1 -0
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/chart-zoom.d.ts +63 -14
- package/dist/components/chart-zoom.js +82 -16
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip.d.ts +67 -2
- package/dist/components/chip.js +73 -8
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +120 -0
- package/dist/components/confirm-dialog.js +96 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/copy-button.d.ts +62 -0
- package/dist/components/copy-button.js +95 -0
- package/dist/components/copy-button.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +5 -1
- package/dist/components/data-table-pagination.js +71 -58
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +28 -5
- package/dist/components/data-table-sort.js +11 -7
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +544 -423
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +51 -5
- package/dist/components/date-picker.js +152 -40
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +60 -0
- package/dist/components/description-list.js +112 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/disclosure.d.ts +37 -2
- package/dist/components/disclosure.js +14 -5
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -0
- package/dist/components/file-button.d.ts +17 -1
- package/dist/components/file-button.js +8 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +54 -9
- package/dist/components/file-dropzone.js +100 -70
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +107 -0
- package/dist/components/floating-panel.js +206 -0
- package/dist/components/floating-panel.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +12 -1
- package/dist/components/full-bleed-dialog.js +26 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.d.ts +6 -2
- package/dist/components/modal.js +2 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/progress-bar.d.ts +54 -0
- package/dist/components/progress-bar.js +94 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/scroll-area.d.ts +53 -0
- package/dist/components/scroll-area.js +86 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/separator.d.ts +23 -0
- package/dist/components/separator.js +24 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/series-chart-ticks.d.ts +34 -1
- package/dist/components/series-chart-ticks.js +83 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +314 -19
- package/dist/components/series-chart.js +510 -120
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/skeleton.d.ts +36 -0
- package/dist/components/skeleton.js +35 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/stat-tile.d.ts +60 -6
- package/dist/components/stat-tile.js +60 -26
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +83 -0
- package/dist/components/table.js +166 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/toggle-legend.d.ts +3 -2
- package/dist/components/toggle-legend.js +1 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tree-view.d.ts +129 -0
- package/dist/components/tree-view.js +376 -0
- package/dist/components/tree-view.js.map +1 -0
- package/dist/components/treemap.d.ts +9 -2
- package/dist/components/treemap.js +2 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +87 -13
- package/dist/components/ui.js +111 -29
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
- package/dist/data-table.d.ts +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/dist/hooks/use-copy-to-clipboard.js +81 -0
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +43 -0
- package/dist/hooks/use-debounce.js +56 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +72 -0
- package/dist/hooks/use-file-drop.js +57 -0
- package/dist/hooks/use-file-drop.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -0
- package/dist/i18n/kit-labels.js +6 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
- package/dist/i18n/locales/de-CH-informal.js +8 -0
- package/dist/i18n/locales/de-CH-informal.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +3 -0
- package/dist/i18n/locales/de-CH.js +1 -14
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/de-informal.d.ts +64 -0
- package/dist/i18n/locales/de-informal.js +37 -0
- package/dist/i18n/locales/de-informal.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +3 -0
- package/dist/i18n/locales/de.js +25 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -0
- package/dist/i18n/locales/es.js +25 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -0
- package/dist/i18n/locales/fr.js +25 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -0
- package/dist/i18n/locales/hu.js +25 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -0
- package/dist/i18n/locales/it.js +25 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -0
- package/dist/i18n/locales/zh.js +25 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +13 -0
- package/dist/i18n/swiss.js +19 -0
- package/dist/i18n/swiss.js.map +1 -0
- package/dist/index.d.ts +25 -10
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +37 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/search/command-palette.d.ts +15 -1
- package/dist/search/command-palette.js +10 -2
- package/dist/search/command-palette.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +35 -2
- package/dist/wizard/stepper-nav.js +54 -22
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +107 -5
- package/dist/wizard/types.js +2 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +141 -21
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.d.ts +10 -2
- package/dist/wizard/wizard-summary.js +3 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +211 -20
- package/src/components/button-group.tsx +75 -0
- package/src/components/chart-zoom.tsx +158 -20
- package/src/components/chip.tsx +160 -9
- package/src/components/confirm-dialog.tsx +242 -0
- package/src/components/copy-button.tsx +158 -0
- package/src/components/data-table-pagination.tsx +19 -6
- package/src/components/data-table-sort.ts +49 -10
- package/src/components/data-table.tsx +293 -24
- package/src/components/date-picker.tsx +262 -57
- package/src/components/description-list.tsx +174 -0
- package/src/components/disclosure.tsx +58 -5
- package/src/components/file-button.tsx +22 -2
- package/src/components/file-dropzone.tsx +198 -100
- package/src/components/floating-panel.tsx +341 -0
- package/src/components/full-bleed-dialog.tsx +48 -5
- package/src/components/modal.tsx +9 -2
- package/src/components/progress-bar.tsx +163 -0
- package/src/components/scroll-area.tsx +129 -0
- package/src/components/separator.tsx +37 -0
- package/src/components/series-chart-ticks.ts +135 -0
- package/src/components/series-chart.tsx +934 -67
- package/src/components/skeleton.tsx +65 -0
- package/src/components/stat-tile.tsx +160 -27
- package/src/components/table.tsx +263 -0
- package/src/components/toggle-legend.tsx +4 -3
- package/src/components/tree-view.tsx +589 -0
- package/src/components/treemap.tsx +10 -2
- package/src/components/ui.tsx +263 -39
- package/src/data-table.ts +2 -0
- package/src/hooks/use-copy-to-clipboard.ts +122 -0
- package/src/hooks/use-debounce.ts +105 -0
- package/src/hooks/use-file-drop.ts +123 -0
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/kit-labels.tsx +19 -0
- package/src/i18n/locales/de-CH-informal.ts +10 -0
- package/src/i18n/locales/de-CH.ts +1 -16
- package/src/i18n/locales/de-informal.ts +61 -0
- package/src/i18n/locales/de.ts +25 -0
- package/src/i18n/locales/es.ts +25 -0
- package/src/i18n/locales/fr.ts +25 -0
- package/src/i18n/locales/hu.ts +25 -0
- package/src/i18n/locales/it.ts +25 -0
- package/src/i18n/locales/zh.ts +25 -0
- package/src/i18n/swiss.ts +25 -0
- package/src/index.ts +29 -0
- package/src/lib/dates.ts +48 -0
- package/src/search/command-palette.tsx +29 -3
- package/src/wizard/stepper-nav.tsx +102 -28
- package/src/wizard/types.ts +106 -4
- package/src/wizard/use-wizard.ts +211 -31
- package/src/wizard/wizard-summary.tsx +24 -12
- package/src/wizard.ts +3 -2
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./data-table-filters";
|
|
21
21
|
import type { ColumnFilter, FilterValue } from "./data-table-filters";
|
|
22
22
|
import { nextSorts } from "./data-table-sort";
|
|
23
|
-
import type { SortState } from "./data-table-sort";
|
|
23
|
+
import type { SortCycle, SortDir, SortState } from "./data-table-sort";
|
|
24
24
|
import { FilterPopover } from "./data-table-filter-popover";
|
|
25
25
|
import { Pagination } from "./data-table-pagination";
|
|
26
26
|
import { SwipeableRow, type SwipeAction } from "./swipeable-row";
|
|
@@ -68,6 +68,14 @@ export interface DataTableColumn<T> {
|
|
|
68
68
|
* see without reaching into React elements it deliberately treats as opaque.
|
|
69
69
|
*/
|
|
70
70
|
noRowLink?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The direction this column sorts in on its FIRST click. `"desc"` for money and
|
|
73
|
+
* count columns, where the rows worth reading are the large ones and an ascending
|
|
74
|
+
* first click opens on the zero rows (keksdose's aggregated report tables). The
|
|
75
|
+
* rest of the cycle follows from it — see {@link DataTableProps.sortCycle}.
|
|
76
|
+
* Default `"asc"`.
|
|
77
|
+
*/
|
|
78
|
+
firstSort?: SortDir;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
/** A column resize in flight. Held in a ref rather than in state: the live width is
|
|
@@ -306,8 +314,72 @@ export interface DataTableProps<T> {
|
|
|
306
314
|
* "hide upcoming/scheduled" toggle).
|
|
307
315
|
*/
|
|
308
316
|
leadingRow?: ReactNode;
|
|
317
|
+
/**
|
|
318
|
+
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
319
|
+
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
320
|
+
* twice their height and pushed the card's own content below the fold — the whole
|
|
321
|
+
* reason they were still hand-rolled `<table>`s. It tightens the header, the cells,
|
|
322
|
+
* the selection checkboxes, the pager and the phone cards together, so a compact
|
|
323
|
+
* table is compact everywhere rather than a dense body under a roomy header.
|
|
324
|
+
*
|
|
325
|
+
* Defaults to `"comfortable"`, which is the table every caller has today.
|
|
326
|
+
*/
|
|
327
|
+
density?: DataTableDensity;
|
|
328
|
+
/**
|
|
329
|
+
* What a repeated click on a sorted header does. `"tri"` (the default, and the only
|
|
330
|
+
* behaviour before 0.8.0): first direction → the other → unsorted. `"toggle"`: first
|
|
331
|
+
* direction ⇄ the other, never unsorted — for a report table that is always ranked
|
|
332
|
+
* by something. Each column's first direction is its own `firstSort`. `aria-sort`
|
|
333
|
+
* and the spoken "Sorted by …" follow whatever the click produced, so they are
|
|
334
|
+
* right under either cycle.
|
|
335
|
+
*/
|
|
336
|
+
sortCycle?: SortCycle;
|
|
337
|
+
/**
|
|
338
|
+
* The desktop table's power-user chrome, as one switch. `"full"` (default) is the
|
|
339
|
+
* table as it has always been. `"minimal"` is a compact report table in a half-width
|
|
340
|
+
* card: no column-settings rail, no resize handles, no multi-sort (and so no
|
|
341
|
+
* "Shift-click to add a sort" tooltip) — the three pieces that cost width and
|
|
342
|
+
* attention and that a ten-row summary never needs. Each can still be set on its
|
|
343
|
+
* own below; an explicit boolean wins over the preset.
|
|
344
|
+
*/
|
|
345
|
+
chrome?: DataTableChrome;
|
|
346
|
+
/**
|
|
347
|
+
* The vertical "Columns (n/m)" rail and its show/hide + auto-size panel. Default:
|
|
348
|
+
* on, unless `chrome="minimal"`. With it off, every column is shown and a hidden
|
|
349
|
+
* set persisted under `storageKey` is ignored — there would be no way to get a
|
|
350
|
+
* column back.
|
|
351
|
+
*/
|
|
352
|
+
columnSettings?: boolean;
|
|
353
|
+
/**
|
|
354
|
+
* The drag handles on the header's trailing edges. Default: on, unless
|
|
355
|
+
* `chrome="minimal"`. With it off, widths persisted under `storageKey` are ignored
|
|
356
|
+
* for the same reason as above: nothing could reset them.
|
|
357
|
+
*/
|
|
358
|
+
resizable?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Shift-click adds a secondary sort criterion. Default: on, unless
|
|
361
|
+
* `chrome="minimal"`. With it off, a shift-click sorts like a plain click and the
|
|
362
|
+
* header drops the tooltip that advertises the gesture.
|
|
363
|
+
*/
|
|
364
|
+
multiSort?: boolean;
|
|
365
|
+
/**
|
|
366
|
+
* The card around the table — border, surface, radius, shadow. Set false to sit the
|
|
367
|
+
* table inside the app's own card, under its own caption and CSV button, without a
|
|
368
|
+
* frame inside a frame. The table's layout is unchanged; only the chrome goes.
|
|
369
|
+
* Default true.
|
|
370
|
+
*/
|
|
371
|
+
frame?: boolean;
|
|
372
|
+
/** Extra classes for the table's root element (the card, or with `frame={false}`
|
|
373
|
+
* the plain wrapper). Merged last, so a caller's margin or width wins. */
|
|
374
|
+
className?: string;
|
|
309
375
|
}
|
|
310
376
|
|
|
377
|
+
/** See {@link DataTableProps.chrome}. */
|
|
378
|
+
export type DataTableChrome = "full" | "minimal";
|
|
379
|
+
|
|
380
|
+
/** See {@link DataTableProps.density}. */
|
|
381
|
+
export type DataTableDensity = "comfortable" | "compact";
|
|
382
|
+
|
|
311
383
|
export type FilterState = Record<string, FilterValue>;
|
|
312
384
|
export type { SortState };
|
|
313
385
|
|
|
@@ -389,6 +461,48 @@ function columnLabel<T>(col: DataTableColumn<T>): string {
|
|
|
389
461
|
return typeof col.header === "string" ? col.header : col.key;
|
|
390
462
|
}
|
|
391
463
|
|
|
464
|
+
/**
|
|
465
|
+
* What counts as a control of its own inside a clickable row: a click or key press
|
|
466
|
+
* that lands on one of these belongs to it, not to the row's `onRowClick`. The row's
|
|
467
|
+
* own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.
|
|
468
|
+
*/
|
|
469
|
+
const OWN_CONTROL =
|
|
470
|
+
'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
471
|
+
|
|
472
|
+
/** Whether an event on `row` started on a control nested inside it. */
|
|
473
|
+
function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
|
|
474
|
+
if (!(target instanceof Element)) return false;
|
|
475
|
+
const hit = target.closest(OWN_CONTROL);
|
|
476
|
+
return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute("data-row-link");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Arrow/Home/End between the focusable rows of one `<tbody>` — the roving half of
|
|
480
|
+
* the keyboard-activatable rows (see the row's `onKeyDown`). */
|
|
481
|
+
function moveRowFocus(from: HTMLElement, key: string): boolean {
|
|
482
|
+
if (!(key in ROW_NAV_KEYS)) return false;
|
|
483
|
+
const body = from.parentElement;
|
|
484
|
+
if (!body) return false;
|
|
485
|
+
const rows = Array.from(body.children).filter(
|
|
486
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute("data-row-nav"),
|
|
487
|
+
);
|
|
488
|
+
const i = rows.indexOf(from);
|
|
489
|
+
const target =
|
|
490
|
+
key === "ArrowDown"
|
|
491
|
+
? rows[i + 1]
|
|
492
|
+
: key === "ArrowUp"
|
|
493
|
+
? rows[i - 1]
|
|
494
|
+
: key === "Home"
|
|
495
|
+
? rows[0]
|
|
496
|
+
: key === "End"
|
|
497
|
+
? rows[rows.length - 1]
|
|
498
|
+
: undefined;
|
|
499
|
+
// At either end the key is still consumed, or ArrowDown on the last row would
|
|
500
|
+
// scroll the page out from under a focused row.
|
|
501
|
+
target?.focus();
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
const ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;
|
|
505
|
+
|
|
392
506
|
/** A click the APP owns. Anything else — middle, ⌘/Ctrl, Shift, Alt — belongs to
|
|
393
507
|
* the browser, and the whole feature is not touching it. */
|
|
394
508
|
const isPlainLeftClick = (e: React.MouseEvent) =>
|
|
@@ -501,7 +615,20 @@ export function DataTable<T>({
|
|
|
501
615
|
mobileCard,
|
|
502
616
|
mobileSwipeActions,
|
|
503
617
|
leadingRow,
|
|
618
|
+
density = "comfortable",
|
|
619
|
+
sortCycle = "tri",
|
|
620
|
+
chrome = "full",
|
|
621
|
+
columnSettings: columnSettingsProp,
|
|
622
|
+
resizable: resizableProp,
|
|
623
|
+
multiSort: multiSortProp,
|
|
624
|
+
frame = true,
|
|
625
|
+
className,
|
|
504
626
|
}: DataTableProps<T>) {
|
|
627
|
+
const compact = density === "compact";
|
|
628
|
+
const minimal = chrome === "minimal";
|
|
629
|
+
const columnSettings = columnSettingsProp ?? !minimal;
|
|
630
|
+
const resizable = resizableProp ?? !minimal;
|
|
631
|
+
const multiSort = multiSortProp ?? !minimal;
|
|
505
632
|
const isServer = !!serverPagination;
|
|
506
633
|
const isLoading = !!serverPagination?.isLoading;
|
|
507
634
|
// prop > provider > English, through the table's own resolver rather than
|
|
@@ -538,9 +665,9 @@ export function DataTable<T>({
|
|
|
538
665
|
setPage,
|
|
539
666
|
pageSize,
|
|
540
667
|
setPageSize,
|
|
541
|
-
widths,
|
|
668
|
+
widths: storedWidths,
|
|
542
669
|
setWidths,
|
|
543
|
-
hiddenCols,
|
|
670
|
+
hiddenCols: storedHiddenCols,
|
|
544
671
|
setHiddenCols,
|
|
545
672
|
showSettings,
|
|
546
673
|
setShowSettings,
|
|
@@ -555,6 +682,16 @@ export function DataTable<T>({
|
|
|
555
682
|
filtersProp,
|
|
556
683
|
});
|
|
557
684
|
|
|
685
|
+
// With the controls that change them switched off, persisted widths and hidden
|
|
686
|
+
// columns are ignored rather than applied: a table cannot be left in a state the
|
|
687
|
+
// user has no control to get out of. They stay in storage untouched, so turning
|
|
688
|
+
// the control back on brings them back.
|
|
689
|
+
const widths = useMemo(() => (resizable ? storedWidths : {}), [resizable, storedWidths]);
|
|
690
|
+
const hiddenCols = useMemo(
|
|
691
|
+
() => (columnSettings ? storedHiddenCols : new Set<string>()),
|
|
692
|
+
[columnSettings, storedHiddenCols],
|
|
693
|
+
);
|
|
694
|
+
|
|
558
695
|
// Every call site builds `columns` inline — `columns={[{ key: "name", … }]}` right
|
|
559
696
|
// there in the JSX — so the array is a new object on every render of the page around
|
|
560
697
|
// the table, and a `useMemo` keyed on its identity memoises nothing. The three below
|
|
@@ -664,8 +801,14 @@ export function DataTable<T>({
|
|
|
664
801
|
? sorted
|
|
665
802
|
: sorted.slice(safePage * effectivePageSize, safePage * effectivePageSize + effectivePageSize);
|
|
666
803
|
|
|
667
|
-
const toggleSort = (key: string,
|
|
668
|
-
const
|
|
804
|
+
const toggleSort = (key: string, shiftKey: boolean) => {
|
|
805
|
+
const col = columns.find((c) => c.key === key);
|
|
806
|
+
// The cycle is the table's, the first direction the column's. The announcement
|
|
807
|
+
// below reads the RESULT, so it can never describe a step the cycle did not take.
|
|
808
|
+
const next = nextSorts(sorts, key, multiSort && shiftKey, {
|
|
809
|
+
firstDir: col?.firstSort,
|
|
810
|
+
cycle: sortCycle,
|
|
811
|
+
});
|
|
669
812
|
if (onSortsChange) onSortsChange(next);
|
|
670
813
|
else setInternalSorts(next);
|
|
671
814
|
|
|
@@ -673,7 +816,6 @@ export function DataTable<T>({
|
|
|
673
816
|
// criterion BEHIND the existing one, and answering "Sorted by Date" to a click on
|
|
674
817
|
// Name misreports what the click did. The third click drops the column entirely,
|
|
675
818
|
// which is the state a user is most likely to have reached by accident.
|
|
676
|
-
const col = columns.find((c) => c.key === key);
|
|
677
819
|
const name = col ? columnLabel(col) : key;
|
|
678
820
|
const entry = next.find((s) => s.key === key);
|
|
679
821
|
announce(
|
|
@@ -970,6 +1112,33 @@ export function DataTable<T>({
|
|
|
970
1112
|
// header was clicked addresses a different row afterwards — and the range then ran
|
|
971
1113
|
// from a row the user had never touched.
|
|
972
1114
|
const selectionAnchor = useRef<string | number | null>(null);
|
|
1115
|
+
// ---- Keyboard-activatable rows ----
|
|
1116
|
+
//
|
|
1117
|
+
// A desktop row with `onRowClick` opened on a mouse click and on nothing else: no
|
|
1118
|
+
// tab stop, no Enter, no focus ring (keksdose's report tables hand-rolled all three,
|
|
1119
|
+
// which is half of why they could not move onto this table). The row now takes
|
|
1120
|
+
// focus itself, as a ROVING tab stop: one row of the table is in the tab order
|
|
1121
|
+
// (the last one focused, else the first), ArrowUp/ArrowDown/Home/End move between
|
|
1122
|
+
// rows, Enter and Space activate.
|
|
1123
|
+
//
|
|
1124
|
+
// Why the `<tr>` rather than the two alternatives:
|
|
1125
|
+
// * `role="button"` on the row would replace its `row` role, and the cells with
|
|
1126
|
+
// it — a screen reader would stop reading the table as a table.
|
|
1127
|
+
// * A button or link in one cell is what `rowHref` already is where the row HAS a
|
|
1128
|
+
// URL. Without one, a synthetic button would have to wrap caller-rendered cell
|
|
1129
|
+
// content, which nests the controls those cells carry (status toggles, links)
|
|
1130
|
+
// inside it — illegal markup, and the reason the mobile card is a div.
|
|
1131
|
+
// A focused `<tr>` keeps its role and reads out its cells. Roving rather than a tab
|
|
1132
|
+
// stop per row, so a 100-row page does not put 100 stops between the header and
|
|
1133
|
+
// the pager.
|
|
1134
|
+
const rowsFocusable = !!onRowClick;
|
|
1135
|
+
const [focusRowKey, setFocusRowKey] = useState<string | number | null>(null);
|
|
1136
|
+
const tabRowKey =
|
|
1137
|
+
focusRowKey !== null && slice.some((r) => rowKey(r) === focusRowKey)
|
|
1138
|
+
? focusRowKey
|
|
1139
|
+
: slice.length
|
|
1140
|
+
? rowKey(slice[0])
|
|
1141
|
+
: null;
|
|
973
1142
|
/** The checkbox click that drew a range, so its `onChange` can be told apart. */
|
|
974
1143
|
const rangeClick = useRef<Event | null>(null);
|
|
975
1144
|
const canSelect = (row: T) => !!selection && (selection.isSelectable?.(row) ?? true);
|
|
@@ -1031,7 +1200,8 @@ export function DataTable<T>({
|
|
|
1031
1200
|
const swipeEnabled = !!swipe && !expanded;
|
|
1032
1201
|
const href = mobileCardLinkable ? rowHref?.(row) : undefined;
|
|
1033
1202
|
const cardClass = cn(
|
|
1034
|
-
"w-full
|
|
1203
|
+
"w-full text-start flex items-center",
|
|
1204
|
+
compact ? "px-3 py-2 gap-2 text-sm" : "px-4 py-3 gap-3",
|
|
1035
1205
|
// Live #320's other half: the card acknowledges the touch before the sheet
|
|
1036
1206
|
// arrives. On a cold route the data can take a beat, and an unacknowledged tap
|
|
1037
1207
|
// reads as "did that register?" — which is most of what "abrupt" means here.
|
|
@@ -1051,14 +1221,19 @@ export function DataTable<T>({
|
|
|
1051
1221
|
<>
|
|
1052
1222
|
{/* The card's own content keeps the column it always had; the chevron sits
|
|
1053
1223
|
beside it rather than inside, so a caller's `mobileCard` is untouched. */}
|
|
1054
|
-
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
1224
|
+
<div className={cn("flex min-w-0 flex-1 flex-col", compact ? "gap-1" : "gap-2")}>
|
|
1055
1225
|
{mobileCard ? (
|
|
1056
1226
|
mobileCard(row)
|
|
1057
1227
|
) : (
|
|
1058
1228
|
<>
|
|
1059
1229
|
{mobilePrimaryCol && <div className="font-medium">{mobilePrimaryCol.cell(row)}</div>}
|
|
1060
1230
|
{mobileSecondaryColumns.length > 0 && (
|
|
1061
|
-
<dl
|
|
1231
|
+
<dl
|
|
1232
|
+
className={cn(
|
|
1233
|
+
"grid grid-cols-[auto_1fr] gap-x-3",
|
|
1234
|
+
compact ? "gap-y-0.5 text-xs" : "gap-y-1 text-sm",
|
|
1235
|
+
)}
|
|
1236
|
+
>
|
|
1062
1237
|
{mobileSecondaryColumns.map((col) => (
|
|
1063
1238
|
<Fragment key={col.key}>
|
|
1064
1239
|
<dt className="text-xs uppercase tracking-wide text-[var(--text-muted)] self-center">
|
|
@@ -1124,6 +1299,8 @@ export function DataTable<T>({
|
|
|
1124
1299
|
onKeyDown={
|
|
1125
1300
|
interactive
|
|
1126
1301
|
? (e) => {
|
|
1302
|
+
// A key press inside a nested control is that control's.
|
|
1303
|
+
if (e.target !== e.currentTarget) return;
|
|
1127
1304
|
if (e.key === "Enter" || e.key === " ") {
|
|
1128
1305
|
e.preventDefault();
|
|
1129
1306
|
onRowClick!(row);
|
|
@@ -1147,7 +1324,12 @@ export function DataTable<T>({
|
|
|
1147
1324
|
body
|
|
1148
1325
|
)}
|
|
1149
1326
|
{expansion && !mobileExpandAsDialog && (
|
|
1150
|
-
<div
|
|
1327
|
+
<div
|
|
1328
|
+
className={cn(
|
|
1329
|
+
"bg-[var(--bg-surface-2)] border-t border-[var(--border)]",
|
|
1330
|
+
compact ? "px-3 py-2" : "px-4 py-3",
|
|
1331
|
+
)}
|
|
1332
|
+
>
|
|
1151
1333
|
{expansion}
|
|
1152
1334
|
</div>
|
|
1153
1335
|
)}
|
|
@@ -1168,8 +1350,23 @@ export function DataTable<T>({
|
|
|
1168
1350
|
}, [])
|
|
1169
1351
|
: null;
|
|
1170
1352
|
|
|
1353
|
+
// `frame={false}` swaps the Card for a bare wrapper carrying the same hooks and
|
|
1354
|
+
// layout classes, so everything inside renders identically either way.
|
|
1355
|
+
const Root = frame ? FramedRoot : PlainRoot;
|
|
1356
|
+
|
|
1171
1357
|
return (
|
|
1172
|
-
<
|
|
1358
|
+
<Root
|
|
1359
|
+
// A styling and testing hook: a host's own cell content can follow the table's
|
|
1360
|
+
// density (`group-data-[density=compact]/table:…`) without being told twice.
|
|
1361
|
+
data-density={density}
|
|
1362
|
+
data-frame={frame ? undefined : "none"}
|
|
1363
|
+
className={cn(
|
|
1364
|
+
"group/table",
|
|
1365
|
+
frame ? "overflow-clip" : "min-w-0",
|
|
1366
|
+
fillHeight && "md:flex md:flex-1 md:flex-col md:min-h-0",
|
|
1367
|
+
className,
|
|
1368
|
+
)}
|
|
1369
|
+
>
|
|
1173
1370
|
{/* Outside both viewport branches on purpose. A screen reader subscribes to a
|
|
1174
1371
|
live region when it encounters it, so one that appears together with its
|
|
1175
1372
|
first message is usually missed — and crossing the md breakpoint swaps the
|
|
@@ -1214,7 +1411,12 @@ export function DataTable<T>({
|
|
|
1214
1411
|
))
|
|
1215
1412
|
: mobileSlice.map(renderMobileRow)}
|
|
1216
1413
|
{mobileSlice.length === 0 && (
|
|
1217
|
-
<li
|
|
1414
|
+
<li
|
|
1415
|
+
className={cn(
|
|
1416
|
+
"text-center text-[var(--text-muted)]",
|
|
1417
|
+
compact ? "px-3 py-3 text-xs" : "px-4 py-6 text-sm",
|
|
1418
|
+
)}
|
|
1419
|
+
>
|
|
1218
1420
|
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? "—")}
|
|
1219
1421
|
</li>
|
|
1220
1422
|
)}
|
|
@@ -1241,6 +1443,7 @@ export function DataTable<T>({
|
|
|
1241
1443
|
onPageSize={serverPagination!.onPageSizeChange}
|
|
1242
1444
|
labels={labels}
|
|
1243
1445
|
locale={locale}
|
|
1446
|
+
density={density}
|
|
1244
1447
|
/>
|
|
1245
1448
|
)}
|
|
1246
1449
|
{/* The row's own first column is this dialog's title — the same cell the card
|
|
@@ -1274,7 +1477,7 @@ export function DataTable<T>({
|
|
|
1274
1477
|
the rows that are about to be replaced, and the dimmed body says the same
|
|
1275
1478
|
to the eye. The pager stays live — see `ServerPagination.isLoading`. */}
|
|
1276
1479
|
<table
|
|
1277
|
-
className="w-full text-sm"
|
|
1480
|
+
className={cn("w-full", compact ? "text-xs" : "text-sm")}
|
|
1278
1481
|
aria-label={labels.table}
|
|
1279
1482
|
aria-busy={isLoading || undefined}
|
|
1280
1483
|
>
|
|
@@ -1292,11 +1495,14 @@ export function DataTable<T>({
|
|
|
1292
1495
|
{selection && (
|
|
1293
1496
|
<th
|
|
1294
1497
|
scope="col"
|
|
1295
|
-
className=
|
|
1498
|
+
className={cn(
|
|
1499
|
+
"sticky top-0 z-10 bg-[var(--bg-surface-2)] align-middle backdrop-blur-sm",
|
|
1500
|
+
compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2",
|
|
1501
|
+
)}
|
|
1296
1502
|
>
|
|
1297
1503
|
<input
|
|
1298
1504
|
type="checkbox"
|
|
1299
|
-
className="
|
|
1505
|
+
className={cn("align-middle accent-indigo-600", compact ? "size-3.5" : "size-4")}
|
|
1300
1506
|
checked={selection.allSelected}
|
|
1301
1507
|
ref={(el) => {
|
|
1302
1508
|
if (el) el.indeterminate = selection.someSelected && !selection.allSelected;
|
|
@@ -1350,7 +1556,8 @@ export function DataTable<T>({
|
|
|
1350
1556
|
}
|
|
1351
1557
|
style={width ? { width, minWidth: width, maxWidth: width } : undefined}
|
|
1352
1558
|
className={cn(
|
|
1353
|
-
"relative
|
|
1559
|
+
"relative font-medium align-middle whitespace-nowrap",
|
|
1560
|
+
compact ? "px-2 py-1" : "px-3 py-2",
|
|
1354
1561
|
// Keep the header visible while the user scrolls the page.
|
|
1355
1562
|
// Each <th> carries its own bg so the row doesn't render
|
|
1356
1563
|
// transparent over the data rows underneath.
|
|
@@ -1364,7 +1571,7 @@ export function DataTable<T>({
|
|
|
1364
1571
|
isEndAligned && "flex-row-reverse",
|
|
1365
1572
|
)}
|
|
1366
1573
|
>
|
|
1367
|
-
<Tooltip label={sortable ? labels.sortHint : undefined} portal>
|
|
1574
|
+
<Tooltip label={sortable && multiSort ? labels.sortHint : undefined} portal>
|
|
1368
1575
|
<button
|
|
1369
1576
|
type="button"
|
|
1370
1577
|
onClick={(e) => sortable && toggleSort(col.key, e.shiftKey)}
|
|
@@ -1423,6 +1630,7 @@ export function DataTable<T>({
|
|
|
1423
1630
|
</Popover>
|
|
1424
1631
|
)}
|
|
1425
1632
|
</div>
|
|
1633
|
+
{resizable && (
|
|
1426
1634
|
<span
|
|
1427
1635
|
role="separator"
|
|
1428
1636
|
aria-orientation="vertical"
|
|
@@ -1444,6 +1652,7 @@ export function DataTable<T>({
|
|
|
1444
1652
|
// trailing edge, which is its left one in a right-to-left table.
|
|
1445
1653
|
className="absolute end-0 top-0 z-10 h-full w-1.5 -translate-x-1/2 rtl:translate-x-1/2 cursor-col-resize select-none touch-none hover:bg-[var(--brand-bg-hover)]"
|
|
1446
1654
|
/>
|
|
1655
|
+
)}
|
|
1447
1656
|
</th>
|
|
1448
1657
|
);
|
|
1449
1658
|
})}
|
|
@@ -1464,18 +1673,55 @@ export function DataTable<T>({
|
|
|
1464
1673
|
const expansion = expanded ? expandedRow?.(row) : null;
|
|
1465
1674
|
const rowInteractive = !!onRowClick || !!selection;
|
|
1466
1675
|
const href = rowHref?.(row);
|
|
1676
|
+
const key = rowKey(row);
|
|
1467
1677
|
return (
|
|
1468
|
-
<Fragment key={
|
|
1678
|
+
<Fragment key={key}>
|
|
1469
1679
|
<tr
|
|
1470
1680
|
{...cleanAttrs(rowAttributes?.(row))}
|
|
1681
|
+
data-row-nav={rowsFocusable ? "" : undefined}
|
|
1682
|
+
tabIndex={rowsFocusable ? (key === tabRowKey ? 0 : -1) : undefined}
|
|
1683
|
+
aria-expanded={rowsFocusable && expandedRow ? expanded : undefined}
|
|
1684
|
+
onFocus={
|
|
1685
|
+
rowsFocusable
|
|
1686
|
+
? (e) => {
|
|
1687
|
+
if (e.target === e.currentTarget) setFocusRowKey(key);
|
|
1688
|
+
}
|
|
1689
|
+
: undefined
|
|
1690
|
+
}
|
|
1691
|
+
onKeyDown={
|
|
1692
|
+
rowsFocusable
|
|
1693
|
+
? (e) => {
|
|
1694
|
+
// Only the row's own key presses: Enter on a nested button
|
|
1695
|
+
// or link is that control's, and Space in a nested input
|
|
1696
|
+
// is a space.
|
|
1697
|
+
if (e.target !== e.currentTarget) return;
|
|
1698
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
1699
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1700
|
+
// Space would otherwise scroll the table.
|
|
1701
|
+
e.preventDefault();
|
|
1702
|
+
onRowClick!(row);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
if (e.shiftKey) return;
|
|
1706
|
+
if (moveRowFocus(e.currentTarget, e.key)) e.preventDefault();
|
|
1707
|
+
}
|
|
1708
|
+
: undefined
|
|
1709
|
+
}
|
|
1471
1710
|
className={cn(
|
|
1472
1711
|
"border-t border-[var(--border)]",
|
|
1473
1712
|
rowInteractive && "cursor-pointer hover:bg-[var(--bg-hover)]",
|
|
1713
|
+
rowsFocusable &&
|
|
1714
|
+
"focus-visible:bg-[var(--bg-hover)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
1474
1715
|
rowClassName?.(row),
|
|
1475
1716
|
)}
|
|
1476
1717
|
onClick={
|
|
1477
1718
|
rowInteractive
|
|
1478
1719
|
? (e) => {
|
|
1720
|
+
// A click on a control inside the row (a button, a link,
|
|
1721
|
+
// an input the cell renders) is that control's — it must
|
|
1722
|
+
// not also open the row. Cells no longer have to
|
|
1723
|
+
// `stopPropagation` to get that.
|
|
1724
|
+
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
1479
1725
|
// Modifier-clicks anywhere in the row drive
|
|
1480
1726
|
// selection instead of expanding it (feedback #289):
|
|
1481
1727
|
// Ctrl/Cmd toggles a single row, Shift selects the
|
|
@@ -1507,7 +1753,7 @@ export function DataTable<T>({
|
|
|
1507
1753
|
>
|
|
1508
1754
|
{selection && (
|
|
1509
1755
|
<td
|
|
1510
|
-
className="w-10 px-3 py-2
|
|
1756
|
+
className={cn("align-top", compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2")}
|
|
1511
1757
|
// Don't let selecting a row also trigger the row click
|
|
1512
1758
|
// (which expands/edits it).
|
|
1513
1759
|
onClick={(e) => e.stopPropagation()}
|
|
@@ -1515,7 +1761,7 @@ export function DataTable<T>({
|
|
|
1515
1761
|
{canSelect(row) && (
|
|
1516
1762
|
<input
|
|
1517
1763
|
type="checkbox"
|
|
1518
|
-
className="
|
|
1764
|
+
className={cn("accent-indigo-600", compact ? "size-3.5" : "size-4")}
|
|
1519
1765
|
checked={selection.isSelected(row)}
|
|
1520
1766
|
// Shift+click selects the range from the last toggled
|
|
1521
1767
|
// row. `preventDefault` puts the box back, but it does NOT
|
|
@@ -1555,7 +1801,8 @@ export function DataTable<T>({
|
|
|
1555
1801
|
data-col={col.key}
|
|
1556
1802
|
style={width ? { width, minWidth: width, maxWidth: width } : undefined}
|
|
1557
1803
|
className={cn(
|
|
1558
|
-
"
|
|
1804
|
+
"align-top",
|
|
1805
|
+
compact ? "px-2 py-1" : "px-3 py-2",
|
|
1559
1806
|
width && "overflow-hidden text-ellipsis",
|
|
1560
1807
|
logicalAlign(col.className),
|
|
1561
1808
|
)}
|
|
@@ -1564,7 +1811,7 @@ export function DataTable<T>({
|
|
|
1564
1811
|
// No `onActivate`: the click is left to bubble to the
|
|
1565
1812
|
// <tr> handler above, which already owns expand-vs-select.
|
|
1566
1813
|
// Calling it here as well would toggle the row twice.
|
|
1567
|
-
<RowLink href={href} className="block">
|
|
1814
|
+
<RowLink href={href} className="block" data-row-link="">
|
|
1568
1815
|
{col.cell(row)}
|
|
1569
1816
|
</RowLink>
|
|
1570
1817
|
) : (
|
|
@@ -1588,7 +1835,9 @@ export function DataTable<T>({
|
|
|
1588
1835
|
perturb the columns; the div then fills to the full row
|
|
1589
1836
|
width only at paint time, and long text wraps within it. */}
|
|
1590
1837
|
<td colSpan={totalColSpan} className="p-0">
|
|
1591
|
-
<div className="w-0 min-w-full px-3 py-3"
|
|
1838
|
+
<div className={cn("w-0 min-w-full", compact ? "px-2 py-2" : "px-3 py-3")}>
|
|
1839
|
+
{expansion}
|
|
1840
|
+
</div>
|
|
1592
1841
|
</td>
|
|
1593
1842
|
</tr>
|
|
1594
1843
|
)}
|
|
@@ -1599,7 +1848,7 @@ export function DataTable<T>({
|
|
|
1599
1848
|
<tr>
|
|
1600
1849
|
<td
|
|
1601
1850
|
colSpan={totalColSpan}
|
|
1602
|
-
className="
|
|
1851
|
+
className={cn("text-center text-[var(--text-muted)]", compact ? "px-2 py-2" : "px-3 py-4")}
|
|
1603
1852
|
>
|
|
1604
1853
|
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
1605
1854
|
page is still in flight is a claim the table cannot make. */}
|
|
@@ -1627,9 +1876,12 @@ export function DataTable<T>({
|
|
|
1627
1876
|
}
|
|
1628
1877
|
labels={labels}
|
|
1629
1878
|
locale={locale}
|
|
1879
|
+
density={density}
|
|
1630
1880
|
/>
|
|
1631
1881
|
)}
|
|
1632
1882
|
</div>
|
|
1883
|
+
{columnSettings && (
|
|
1884
|
+
<>
|
|
1633
1885
|
<Tooltip label={columnsCountLabel} portal>
|
|
1634
1886
|
<button
|
|
1635
1887
|
type="button"
|
|
@@ -1710,12 +1962,29 @@ export function DataTable<T>({
|
|
|
1710
1962
|
</div>
|
|
1711
1963
|
</div>
|
|
1712
1964
|
</div>
|
|
1965
|
+
</>
|
|
1966
|
+
)}
|
|
1713
1967
|
</div>
|
|
1714
1968
|
)}
|
|
1969
|
+
</Root>
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
/** The framed root: the kit's flush Card, as every table had before `frame`. */
|
|
1974
|
+
function FramedRoot({ children, ...rest }: React.HTMLAttributes<HTMLDivElement>) {
|
|
1975
|
+
return (
|
|
1976
|
+
<Card flush {...rest}>
|
|
1977
|
+
{children}
|
|
1715
1978
|
</Card>
|
|
1716
1979
|
);
|
|
1717
1980
|
}
|
|
1718
1981
|
|
|
1982
|
+
/** The unframed root (`frame={false}`): no border, surface, radius or shadow — the
|
|
1983
|
+
* host's own card supplies them. */
|
|
1984
|
+
function PlainRoot(props: React.HTMLAttributes<HTMLDivElement>) {
|
|
1985
|
+
return <div {...props} />;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1719
1988
|
/** The loading row's content: a spinner beside the word, the word for everyone. */
|
|
1720
1989
|
function LoadingText({ label }: { label: string }) {
|
|
1721
1990
|
return (
|