@eifi1/ui-kit 0.47.0 → 0.48.1
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 +5 -3
- package/dist/admin/admin-sections.d.ts +8 -0
- package/dist/admin/admin-sections.js +2 -1
- package/dist/admin/admin-sections.js.map +1 -1
- package/dist/auth/verify-email.d.ts +9 -1
- package/dist/auth/verify-email.js +22 -13
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/components/alert-banner.d.ts +29 -1
- package/dist/components/alert-banner.js +84 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/button-look.d.ts +4 -0
- package/dist/components/button-look.js +2 -0
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/combobox-core.js +5 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +6 -1
- package/dist/components/data-table-column-settings.js +12 -2
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +44 -0
- package/dist/components/data-table-fit.js +103 -0
- package/dist/components/data-table-fit.js.map +1 -0
- package/dist/components/data-table-groups.d.ts +95 -0
- package/dist/components/data-table-groups.js +165 -0
- package/dist/components/data-table-groups.js.map +1 -0
- package/dist/components/data-table-head.js +3 -1
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +7 -0
- package/dist/components/data-table-labels.js +3 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +17 -0
- package/dist/components/data-table-leading-row.js +41 -0
- package/dist/components/data-table-leading-row.js.map +1 -0
- package/dist/components/data-table-resize.js +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table.d.ts +95 -2
- package/dist/components/data-table.js +360 -267
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/disclosure.d.ts +3 -1
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.js +8 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/floating-panel.d.ts +14 -1
- package/dist/components/floating-panel.js +7 -2
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/hover-menu.js +5 -1
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +2 -1
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/popover.js +5 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/table-skeleton.d.ts +35 -0
- package/dist/components/table-skeleton.js +75 -0
- package/dist/components/table-skeleton.js.map +1 -0
- package/dist/components/tabs.d.ts +10 -12
- package/dist/components/tabs.js +20 -10
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/use-table-state.d.ts +8 -7
- package/dist/components/use-table-state.js +37 -3
- 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/eslint/index.js +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -0
- package/dist/feedback/feedback-table.js +30 -7
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +30 -1
- package/dist/hooks/use-dismiss.js +30 -2
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/i18n/german.js +6 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/float-clearance.d.ts +8 -0
- package/dist/lib/float-clearance.js +29 -0
- package/dist/lib/float-clearance.js.map +1 -0
- package/dist/settings/settings-catalogue.d.ts +10 -0
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.js +10 -3
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/app-shell.d.ts +5 -0
- package/dist/shell/app-shell.js +5 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/option-switcher-menu.d.ts +2 -0
- package/dist/shell/option-switcher-menu.js +6 -2
- package/dist/shell/option-switcher-menu.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.js +5 -3
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/tour/tour.js +1 -0
- package/dist/tour/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-sections.tsx +8 -0
- package/src/auth/verify-email.tsx +34 -9
- package/src/components/alert-banner.tsx +131 -7
- package/src/components/button-look.ts +6 -0
- package/src/components/combobox-core.tsx +7 -1
- package/src/components/data-table-column-settings.tsx +23 -2
- package/src/components/data-table-fit.ts +187 -0
- package/src/components/data-table-groups.tsx +275 -0
- package/src/components/data-table-head.tsx +4 -1
- package/src/components/data-table-labels.ts +13 -0
- package/src/components/data-table-leading-row.tsx +55 -0
- package/src/components/data-table-resize.ts +2 -1
- package/src/components/data-table.tsx +279 -54
- package/src/components/disclosure.tsx +22 -5
- package/src/components/dropdown.tsx +11 -1
- package/src/components/floating-panel.tsx +24 -1
- package/src/components/hover-menu.tsx +6 -1
- package/src/components/icon-button.tsx +3 -3
- package/src/components/popover.tsx +7 -1
- package/src/components/table-skeleton.tsx +133 -0
- package/src/components/tabs.tsx +67 -37
- package/src/components/use-table-state.ts +54 -3
- package/src/data-table.ts +2 -0
- package/src/feedback/feedback-table.tsx +33 -1
- package/src/hooks/use-dismiss.ts +65 -3
- package/src/i18n/german.ts +5 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +5 -0
- package/src/i18n/locales/fr.ts +5 -0
- package/src/i18n/locales/hu.ts +5 -0
- package/src/i18n/locales/it.ts +5 -0
- package/src/i18n/locales/zh.ts +5 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +3 -0
- package/src/lib/float-clearance.ts +54 -0
- package/src/settings/settings-catalogue.ts +10 -0
- package/src/settings/settings-layout.tsx +18 -4
- package/src/shell/app-shell.tsx +17 -3
- package/src/shell/option-switcher-menu.tsx +16 -4
- package/src/shell/topbar-action-menu.tsx +22 -10
- package/src/shell/topbar-controls.tsx +18 -12
- package/src/tour/tour.tsx +5 -0
|
@@ -17,6 +17,17 @@ import {
|
|
|
17
17
|
} from "./data-table-shared";
|
|
18
18
|
import { MobileFilters, MobileRow, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
|
|
19
19
|
import { DataTableHead } from "./data-table-head";
|
|
20
|
+
import { DataTableLeadingRow } from "./data-table-leading-row";
|
|
21
|
+
import {
|
|
22
|
+
DataTableGroupBand,
|
|
23
|
+
DataTableLoadMoreFooter,
|
|
24
|
+
groupLevels,
|
|
25
|
+
groupRows,
|
|
26
|
+
useGroupBandTops,
|
|
27
|
+
type DataTableGroupBy,
|
|
28
|
+
type DataTableLoadMore,
|
|
29
|
+
} from "./data-table-groups";
|
|
30
|
+
export type { DataTableGroupBy, DataTableLoadMore } from "./data-table-groups";
|
|
20
31
|
import { ColumnSettingsPanel } from "./data-table-column-settings";
|
|
21
32
|
import { useLargeText } from "../hooks/use-large-text";
|
|
22
33
|
import { cn } from "../lib/cn";
|
|
@@ -127,6 +138,44 @@ export interface DataTableColumn<T> {
|
|
|
127
138
|
* end-aligned), its resized width, and drops out with the column when it is hidden.
|
|
128
139
|
*/
|
|
129
140
|
footer?: (rows: T[], page: T[]) => ReactNode;
|
|
141
|
+
/**
|
|
142
|
+
* This column takes the width the others leave, and its text ellipsises (0.48,
|
|
143
|
+
* keksdose live #451): `w-full max-w-0` on its cells, so in the table's auto layout it
|
|
144
|
+
* is "the rest of the row" and one long title can no longer widen the table. One per
|
|
145
|
+
* table. It has no resize handle, and a width stored for it (before it grew) is
|
|
146
|
+
* ignored: it always takes the rest. Resize the others to change it. Opt-in: without it nothing
|
|
147
|
+
* changes. It keeps at least 10rem: with {@link fitPriority} columns the table leaves
|
|
148
|
+
* those out first, and scrolls sideways once none is left. Every other cell keeps one
|
|
149
|
+
* line at Normal text size (`whitespace-nowrap`; a column's own
|
|
150
|
+
* `not-large:whitespace-normal` lets its prose wrap), so a figure or a name is never
|
|
151
|
+
* broken in two to make room for it.
|
|
152
|
+
*/
|
|
153
|
+
grow?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Leave this column out while the TABLE is too narrow for it (0.48, keksdose live
|
|
156
|
+
* #451) — measured on the table's own scroller, so a sidebar, the window and Large
|
|
157
|
+
* text count alike. The lowest number goes first (ties: the later column first), and
|
|
158
|
+
* comes back last, once the scroller is as wide as the table needed with it. Columns
|
|
159
|
+
* without it never go. Desktop only: the phone's card list is not narrowed.
|
|
160
|
+
*
|
|
161
|
+
* The Columns panel counts only what is drawn ("Columns (8/10)") and marks a column
|
|
162
|
+
* left out this way "hidden for width". Ticking it there keeps it in for as long as
|
|
163
|
+
* the table is mounted (not persisted): the table makes room by leaving out the next
|
|
164
|
+
* one instead, and scrolls sideways once none is left. Unticking hides it as any column.
|
|
165
|
+
*/
|
|
166
|
+
fitPriority?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Start this column HIDDEN (0.48, keksdose dev #617) — for as long as the view saved
|
|
169
|
+
* under `storageKey` holds no entry for it: on a first visit, on a device whose saved
|
|
170
|
+
* view predates the column (or the flag), and for a column the app adds later. The
|
|
171
|
+
* user's own choice in the Columns panel always wins: ticking it is remembered
|
|
172
|
+
* (`shown` in the saved view), unticking it again stores it as any hidden column. The
|
|
173
|
+
* default itself is never written into the saved view, so dropping the flag brings the
|
|
174
|
+
* column back on every device where nobody chose. In the panel it is simply unticked —
|
|
175
|
+
* not "hidden for width", which `fitPriority` alone says. Ignored, like every hidden
|
|
176
|
+
* column, with `columnSettings={false}`: nothing could bring it back.
|
|
177
|
+
*/
|
|
178
|
+
defaultHidden?: boolean;
|
|
130
179
|
}
|
|
131
180
|
|
|
132
181
|
/** A `data-*` attribute, typed so `{ "data-private": "" }` needs no cast. */
|
|
@@ -303,6 +352,21 @@ export interface DataTableProps<T> {
|
|
|
303
352
|
* that produces `rows`.
|
|
304
353
|
*/
|
|
305
354
|
serverPagination?: ServerPagination;
|
|
355
|
+
/**
|
|
356
|
+
* 0.48.1 (keksdose K-052-1): the rows are already filtered and sorted by the server —
|
|
357
|
+
* render them as given. No client filter, no client sort, no client paging (every row
|
|
358
|
+
* shows, as with `paginated={false}`). The column filters and sort headers behave as
|
|
359
|
+
* under `serverPagination`: hidden unless the owner takes them over with
|
|
360
|
+
* `onFiltersChange` / `onSortsChange`, and then a click only REPORTS the new state (a
|
|
361
|
+
* header click calls `onSortsChange` with the next sort and the rows keep their order
|
|
362
|
+
* until the owner re-queries and passes new ones).
|
|
363
|
+
*
|
|
364
|
+
* Defaults to `true` when {@link loadMore} is set — the slices it loads come from the
|
|
365
|
+
* server in its order, and re-filtering them dropped rows (the other leg of each
|
|
366
|
+
* transfer). Pass `serverRows={false}` for a client table that loads more chunks but
|
|
367
|
+
* filters and sorts what it holds itself. `serverPagination` implies it regardless.
|
|
368
|
+
*/
|
|
369
|
+
serverRows?: boolean;
|
|
306
370
|
/**
|
|
307
371
|
* Controlled filter/sort state. When the `on*Change` callback is provided
|
|
308
372
|
* the table stops owning that piece of state: it renders `filters`/`sorts`
|
|
@@ -433,7 +497,9 @@ export interface DataTableProps<T> {
|
|
|
433
497
|
* contiguous (pair with a matching default sort). `mobileGroupLabel` formats
|
|
434
498
|
* the header. Desktop is unaffected. Feedback #317.
|
|
435
499
|
*/
|
|
436
|
-
mobileGroupBy?: (row: T) => string
|
|
500
|
+
mobileGroupBy?: ((row: T) => string) | DataTableGroupBy<T>;
|
|
501
|
+
/** The section header's words for `mobileGroupBy`'s key. Wins over a level's own
|
|
502
|
+
* `label` when `mobileGroupBy` is a {@link DataTableGroupBy}. */
|
|
437
503
|
mobileGroupLabel?: (key: string) => ReactNode;
|
|
438
504
|
/**
|
|
439
505
|
* Fully replace the default mobile card body (bold primary + labelled
|
|
@@ -505,6 +571,42 @@ export interface DataTableProps<T> {
|
|
|
505
571
|
* "hide upcoming/scheduled" toggle).
|
|
506
572
|
*/
|
|
507
573
|
leadingRow?: ReactNode;
|
|
574
|
+
/**
|
|
575
|
+
* 0.48 (keksdose dev #603: "it should rather be sticky, so the user can show hide them
|
|
576
|
+
* everywhere on click"): pin the {@link leadingRow} under the sticky header while the
|
|
577
|
+
* rows scroll beneath both — its `top` is the header's measured height. Desktop only,
|
|
578
|
+
* as `leadingRow` is; it sticks within the table's own scroller (a bounded body:
|
|
579
|
+
* `fillHeight` or `maxBodyHeight`). Default: it scrolls with the rows.
|
|
580
|
+
*/
|
|
581
|
+
leadingRowSticky?: boolean;
|
|
582
|
+
/**
|
|
583
|
+
* 0.48 (keksdose dev #605): group the desktop table's rows under full-width bands, one
|
|
584
|
+
* level or two (period → day). Each band is a row header spanning the visible columns
|
|
585
|
+
* (selection column included, hidden and fit-hidden columns not) with a label and an
|
|
586
|
+
* end slot (counts, totals), and opens its own `<tbody>`, so a screen reader announces
|
|
587
|
+
* it as the reader enters the group; the period band opens the first day's. Rows must
|
|
588
|
+
* already be ordered so equal keys are contiguous (as for `mobileGroupBy`); grouping
|
|
589
|
+
* runs over the rows on screen, page by page.
|
|
590
|
+
*
|
|
591
|
+
* The bands are sticky: the period under the header (and under a sticky
|
|
592
|
+
* {@link leadingRow}), the day under the period — measured offsets, inside the table's
|
|
593
|
+
* own scroller (`fillHeight` or `maxBodyHeight`). The `leadingRow` stays above the first
|
|
594
|
+
* group. On a phone pass the same level to `mobileGroupBy`:
|
|
595
|
+
* `groupBy={[period, day]} mobileGroupBy={period}`. See {@link DataTableGroupBy}.
|
|
596
|
+
*/
|
|
597
|
+
groupBy?: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?];
|
|
598
|
+
/**
|
|
599
|
+
* 0.48 (keksdose dev #605): a "load more" footer in place of the pager. The table
|
|
600
|
+
* renders every row it is given (as `paginated={false}` does — pass that too, to say
|
|
601
|
+
* so), and the footer asks the owner for the next slice: as it scrolls into view (an
|
|
602
|
+
* IntersectionObserver, guarded against double calls while one is in flight) and with
|
|
603
|
+
* a button the keyboard reaches, named by the app ("Load August 2026"). With
|
|
604
|
+
* `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the
|
|
605
|
+
* card list's last item. Since 0.48.1 it implies {@link serverRows} — the rows are
|
|
606
|
+
* rendered in the order given, unfiltered — unless `serverRows={false}`. See
|
|
607
|
+
* {@link DataTableLoadMore}.
|
|
608
|
+
*/
|
|
609
|
+
loadMore?: DataTableLoadMore;
|
|
508
610
|
/**
|
|
509
611
|
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
510
612
|
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
@@ -760,14 +862,17 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
|
|
|
760
862
|
|
|
761
863
|
const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
|
|
762
864
|
|
|
763
|
-
/** Arrow/Home/End between the focusable rows of
|
|
865
|
+
/** Arrow/Home/End between the focusable rows of the table's bodies — the roving half of
|
|
764
866
|
* the keyboard-activatable rows (see the row's `onKeyDown`). */
|
|
765
867
|
function moveRowFocus(from: HTMLElement, key: string): boolean {
|
|
766
868
|
if (!(key in ROW_NAV_KEYS)) return false;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
869
|
+
// Every body of the table: a grouped table (`groupBy`, 0.48) has one per group.
|
|
870
|
+
const table = from.closest("table");
|
|
871
|
+
if (!table) return false;
|
|
872
|
+
const rows = Array.from(table.tBodies).flatMap((body) =>
|
|
873
|
+
Array.from(body.children).filter(
|
|
874
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute("data-row-nav"),
|
|
875
|
+
),
|
|
771
876
|
);
|
|
772
877
|
const i = rows.indexOf(from);
|
|
773
878
|
const target =
|
|
@@ -793,6 +898,7 @@ const ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;
|
|
|
793
898
|
|
|
794
899
|
import { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from "./use-table-state";
|
|
795
900
|
import { useMobileReveal } from "./use-mobile-reveal";
|
|
901
|
+
import { GROW_CELL_CLASS, GROW_PEER_CELL_CLASS, useColumnFit } from "./data-table-fit";
|
|
796
902
|
|
|
797
903
|
export function DataTable<T>({
|
|
798
904
|
rows,
|
|
@@ -812,6 +918,7 @@ export function DataTable<T>({
|
|
|
812
918
|
fillHeight = false,
|
|
813
919
|
maxBodyHeight = "calc(100dvh - 12rem)",
|
|
814
920
|
serverPagination,
|
|
921
|
+
serverRows: serverRowsProp,
|
|
815
922
|
filters: filtersProp,
|
|
816
923
|
onFiltersChange,
|
|
817
924
|
sorts: sortsProp,
|
|
@@ -833,6 +940,9 @@ export function DataTable<T>({
|
|
|
833
940
|
mobileSwipeActions,
|
|
834
941
|
desktopSwipeActions,
|
|
835
942
|
leadingRow,
|
|
943
|
+
leadingRowSticky = false,
|
|
944
|
+
groupBy,
|
|
945
|
+
loadMore,
|
|
836
946
|
density = "comfortable",
|
|
837
947
|
sortCycle = "tri",
|
|
838
948
|
chrome = "full",
|
|
@@ -854,6 +964,10 @@ export function DataTable<T>({
|
|
|
854
964
|
const resizable = resizableProp ?? !minimal;
|
|
855
965
|
const multiSort = multiSortProp ?? !minimal;
|
|
856
966
|
const isServer = !!serverPagination;
|
|
967
|
+
// The server filtered and sorted these rows (0.48.1, K-052-1): no client filter, sort
|
|
968
|
+
// or paging, and the sort/filter UI only reports to the owner. `isServer` keeps
|
|
969
|
+
// meaning "the server pages" (its pager, its page and page size).
|
|
970
|
+
const serverRows = isServer || (serverRowsProp ?? !!loadMore);
|
|
857
971
|
const isLoading = !!serverPagination?.isLoading;
|
|
858
972
|
// prop > provider > English, through the table's own resolver rather than
|
|
859
973
|
// `useKitLabels`: that resolver derives `columnsCount` from a translated `columns`,
|
|
@@ -921,7 +1035,17 @@ export function DataTable<T>({
|
|
|
921
1035
|
// columns are ignored rather than applied: a table cannot be left in a state the
|
|
922
1036
|
// user has no control to get out of. They stay in storage untouched, so turning
|
|
923
1037
|
// the control back on brings them back.
|
|
924
|
-
|
|
1038
|
+
// A `grow` column takes the rest of the row by design (0.48): a width stored for it
|
|
1039
|
+
// (a drag or "Auto-size columns" before it grew, KS N2) is ignored, and it has no
|
|
1040
|
+
// handle, so it can never pin it again. Ignored rather than deleted from storage, as
|
|
1041
|
+
// the widths above; the next resize or auto-size writes a blob without it.
|
|
1042
|
+
const widths = useMemo(() => {
|
|
1043
|
+
if (!resizable) return {};
|
|
1044
|
+
const grow = columns.find((c) => c.grow)?.key;
|
|
1045
|
+
if (!grow || !(grow in storedWidths)) return storedWidths;
|
|
1046
|
+
const { [grow]: _ignored, ...rest } = storedWidths;
|
|
1047
|
+
return rest;
|
|
1048
|
+
}, [resizable, storedWidths, columns]);
|
|
925
1049
|
const hiddenCols = useMemo(
|
|
926
1050
|
() => (columnSettings ? storedHiddenCols : new Set<string>()),
|
|
927
1051
|
[columnSettings, storedHiddenCols],
|
|
@@ -975,7 +1099,7 @@ export function DataTable<T>({
|
|
|
975
1099
|
}, [columnSig, selectOptionsSig, rows]);
|
|
976
1100
|
|
|
977
1101
|
const filtered = useMemo(() => {
|
|
978
|
-
if (
|
|
1102
|
+
if (serverRows) return rows;
|
|
979
1103
|
let result = rows;
|
|
980
1104
|
for (const col of columns) {
|
|
981
1105
|
const state = filters[col.key];
|
|
@@ -984,10 +1108,10 @@ export function DataTable<T>({
|
|
|
984
1108
|
}
|
|
985
1109
|
return result;
|
|
986
1110
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
987
|
-
}, [rows, columnSig, filters,
|
|
1111
|
+
}, [rows, columnSig, filters, serverRows, locale]);
|
|
988
1112
|
|
|
989
1113
|
const sorted = useMemo(() => {
|
|
990
|
-
if (
|
|
1114
|
+
if (serverRows) return filtered;
|
|
991
1115
|
// Priority chain: the first sort key that distinguishes two rows wins;
|
|
992
1116
|
// nulls sort last for that key regardless of direction (as before).
|
|
993
1117
|
const chain = sorts
|
|
@@ -1012,12 +1136,12 @@ export function DataTable<T>({
|
|
|
1012
1136
|
});
|
|
1013
1137
|
return copy;
|
|
1014
1138
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1015
|
-
}, [filtered, sorts, columnSig,
|
|
1139
|
+
}, [filtered, sorts, columnSig, serverRows]);
|
|
1016
1140
|
|
|
1017
1141
|
// `paginated={false}` is expressed as "page size = everything" rather than as a
|
|
1018
1142
|
// second code path, so slicing, the page clamp and the range summary all keep
|
|
1019
1143
|
// exactly one definition.
|
|
1020
|
-
const unpaged = !isServer && !paginated;
|
|
1144
|
+
const unpaged = !isServer && (!paginated || !!loadMore || serverRows);
|
|
1021
1145
|
const effectivePageSize = isServer
|
|
1022
1146
|
? serverPagination!.pageSize
|
|
1023
1147
|
: unpaged || pageSize === Infinity
|
|
@@ -1100,7 +1224,7 @@ export function DataTable<T>({
|
|
|
1100
1224
|
// "clear all"). Only touches keys the table actually exposes as filters.
|
|
1101
1225
|
const clearAllFilters = () => {
|
|
1102
1226
|
const keys = columns
|
|
1103
|
-
.filter((col) => ((!
|
|
1227
|
+
.filter((col) => ((!serverRows || onFiltersChange) ? resolveFilter(col) : null))
|
|
1104
1228
|
.map((col) => col.key);
|
|
1105
1229
|
if (!keys.length) return;
|
|
1106
1230
|
if (onFiltersChange) {
|
|
@@ -1134,9 +1258,9 @@ export function DataTable<T>({
|
|
|
1134
1258
|
// some later render from the caller, and `serverPagination.total` is ALREADY the
|
|
1135
1259
|
// filtered total — so the "n of m" sentence has no second number to give here.
|
|
1136
1260
|
// Saying nothing is better than announcing the page size as a result count.
|
|
1137
|
-
if (
|
|
1261
|
+
if (serverRows) return;
|
|
1138
1262
|
announce(labels.filterResults(filtered.length, rows.length));
|
|
1139
|
-
}, [filterKey, filtered.length, rows.length,
|
|
1263
|
+
}, [filterKey, filtered.length, rows.length, serverRows, announce, labels]);
|
|
1140
1264
|
|
|
1141
1265
|
// 1-based, because that is what the page strip shows and what a person says.
|
|
1142
1266
|
const goToPage = (p: number) => {
|
|
@@ -1145,10 +1269,22 @@ export function DataTable<T>({
|
|
|
1145
1269
|
announce(labels.pageChanged(p + 1, totalPages));
|
|
1146
1270
|
};
|
|
1147
1271
|
|
|
1272
|
+
// 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out
|
|
1273
|
+
// (data-table-fit.ts). Desktop only; the scroller is read after render.
|
|
1274
|
+
const { autoHidden, keep: keepColumn } = useColumnFit({
|
|
1275
|
+
columns,
|
|
1276
|
+
hiddenCols,
|
|
1277
|
+
headRefs,
|
|
1278
|
+
scroller: () => desktopScroller.current,
|
|
1279
|
+
enabled: !usePhoneLayout(),
|
|
1280
|
+
});
|
|
1148
1281
|
const visibleColumns = useMemo(
|
|
1149
|
-
() => columns.filter((c) => !hiddenCols.has(c.key)),
|
|
1150
|
-
[columns, hiddenCols],
|
|
1282
|
+
() => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),
|
|
1283
|
+
[columns, hiddenCols, autoHidden],
|
|
1151
1284
|
);
|
|
1285
|
+
// With a `grow` column drawn, every other cell keeps one line at Normal (kastlan S2,
|
|
1286
|
+
// 0.48): the `w-full` column squeezed them to their min-content width.
|
|
1287
|
+
const growDrawn = visibleColumns.some((c) => c.grow);
|
|
1152
1288
|
|
|
1153
1289
|
// Which cell carries the row's link when `rowHref` is set (feedback #451). The
|
|
1154
1290
|
// `mobilePrimary` column is the row's headline by declaration, so it is both the
|
|
@@ -1174,7 +1310,8 @@ export function DataTable<T>({
|
|
|
1174
1310
|
widths,
|
|
1175
1311
|
setWidths,
|
|
1176
1312
|
columns,
|
|
1177
|
-
|
|
1313
|
+
// The columns left out for width have no header to measure either.
|
|
1314
|
+
hiddenCols: autoHidden.size > 0 ? new Set([...hiddenCols, ...autoHidden]) : hiddenCols,
|
|
1178
1315
|
});
|
|
1179
1316
|
|
|
1180
1317
|
const visibleCount = visibleColumns.length;
|
|
@@ -1216,7 +1353,9 @@ export function DataTable<T>({
|
|
|
1216
1353
|
// 0.47 (keksdose S4): the scroller's visible width, as `--dt-visible-w` on it, for the
|
|
1217
1354
|
// expansion row's pinned content (below). Set on the element, not in state: a resize
|
|
1218
1355
|
// re-renders nothing. Only a table that can expand a row measures.
|
|
1219
|
-
|
|
1356
|
+
// The group bands and the load-more footer pin their content the same way (0.48).
|
|
1357
|
+
const levels = groupLevels(groupBy);
|
|
1358
|
+
const measuresVisibleWidth = isMdUp && (!!expandedRow || levels.length > 0 || !!loadMore);
|
|
1220
1359
|
useEffect(() => {
|
|
1221
1360
|
const el = desktopScroller.current;
|
|
1222
1361
|
if (!measuresVisibleWidth || !el || typeof ResizeObserver === "undefined") return;
|
|
@@ -1419,9 +1558,20 @@ export function DataTable<T>({
|
|
|
1419
1558
|
// Consecutive grouping of the (already-sorted) mobile slice into labelled
|
|
1420
1559
|
// sections for the "assistance"-style list (feedback #317). Null unless the
|
|
1421
1560
|
// caller opts in via `mobileGroupBy`.
|
|
1422
|
-
|
|
1561
|
+
// The desktop's bands (0.48, keksdose dev #605), over the rows on screen.
|
|
1562
|
+
const tableRef = useRef<HTMLTableElement | null>(null);
|
|
1563
|
+
const desktopGroups = isMdUp && levels.length > 0 ? groupRows(slice, levels) : null;
|
|
1564
|
+
const bandTops = useGroupBandTops(
|
|
1565
|
+
tableRef,
|
|
1566
|
+
!!desktopGroups,
|
|
1567
|
+
`${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? ""}`,
|
|
1568
|
+
);
|
|
1569
|
+
// `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.
|
|
1570
|
+
const mobileLevel = typeof mobileGroupBy === "function" ? null : (mobileGroupBy ?? null);
|
|
1571
|
+
const mobileKey = typeof mobileGroupBy === "function" ? mobileGroupBy : mobileGroupBy?.key;
|
|
1572
|
+
const mobileGroups: { key: string; rows: T[] }[] | null = mobileKey
|
|
1423
1573
|
? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {
|
|
1424
|
-
const key =
|
|
1574
|
+
const key = mobileKey(row);
|
|
1425
1575
|
const last = acc[acc.length - 1];
|
|
1426
1576
|
if (last && last.key === key) last.rows.push(row);
|
|
1427
1577
|
else acc.push({ key, rows: [row] });
|
|
@@ -1484,12 +1634,12 @@ export function DataTable<T>({
|
|
|
1484
1634
|
{/* Mobile filter access (feedback #299): the per-column filter popovers
|
|
1485
1635
|
live in the desktop header, which the card list doesn't render — so
|
|
1486
1636
|
expose the same filters through a bottom sheet here. */}
|
|
1487
|
-
{(!
|
|
1637
|
+
{(!serverRows || onFiltersChange) && (
|
|
1488
1638
|
<MobileFilters
|
|
1489
1639
|
columns={columns}
|
|
1490
1640
|
filters={filters}
|
|
1491
1641
|
selectOptionsByKey={selectOptionsByKey}
|
|
1492
|
-
isServer={
|
|
1642
|
+
isServer={serverRows}
|
|
1493
1643
|
hasFilterCallback={!!onFiltersChange}
|
|
1494
1644
|
onSetFilter={setFilterValue}
|
|
1495
1645
|
onClearFilter={clearFilter}
|
|
@@ -1500,7 +1650,7 @@ export function DataTable<T>({
|
|
|
1500
1650
|
)}
|
|
1501
1651
|
{mobileSort && (
|
|
1502
1652
|
<MobileSort
|
|
1503
|
-
columns={visibleColumns.filter((c) => (!
|
|
1653
|
+
columns={visibleColumns.filter((c) => (!serverRows || !!onSortsChange) && !!c.sortBy)}
|
|
1504
1654
|
sorts={sorts}
|
|
1505
1655
|
sortCycle={sortCycle}
|
|
1506
1656
|
compact={compact}
|
|
@@ -1559,8 +1709,15 @@ export function DataTable<T>({
|
|
|
1559
1709
|
{mobileGroups
|
|
1560
1710
|
? mobileGroups.map((g) => (
|
|
1561
1711
|
<Fragment key={`hb-group:${g.key}`}>
|
|
1562
|
-
<li className="sticky top-0 z-10 bg-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted backdrop-blur">
|
|
1563
|
-
|
|
1712
|
+
<li className="sticky top-0 z-10 flex flex-wrap items-baseline justify-between gap-x-3 bg-surface-2 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted backdrop-blur">
|
|
1713
|
+
<span className="min-w-0">
|
|
1714
|
+
{mobileGroupLabel
|
|
1715
|
+
? mobileGroupLabel(g.key)
|
|
1716
|
+
: mobileLevel?.label
|
|
1717
|
+
? mobileLevel.label(g.key, g.rows)
|
|
1718
|
+
: g.key}
|
|
1719
|
+
</span>
|
|
1720
|
+
{mobileLevel?.end && <span className="ms-auto tabular-nums">{mobileLevel.end(g.key, g.rows)}</span>}
|
|
1564
1721
|
</li>
|
|
1565
1722
|
{g.rows.map(renderMobileRow)}
|
|
1566
1723
|
</Fragment>
|
|
@@ -1587,6 +1744,16 @@ export function DataTable<T>({
|
|
|
1587
1744
|
<bdi>{labels.loading}</bdi>
|
|
1588
1745
|
</li>
|
|
1589
1746
|
)}
|
|
1747
|
+
{/* The owner's next slice (0.48), once every row held is revealed. */}
|
|
1748
|
+
{loadMore && !canRevealMoreMobile && (
|
|
1749
|
+
<DataTableLoadMoreFooter
|
|
1750
|
+
as="li"
|
|
1751
|
+
loadMore={loadMore}
|
|
1752
|
+
rowCount={sorted.length}
|
|
1753
|
+
labels={labels}
|
|
1754
|
+
compact={compact}
|
|
1755
|
+
/>
|
|
1756
|
+
)}
|
|
1590
1757
|
</ul>
|
|
1591
1758
|
{/* The phone's totals: a summary card after the list rather than one more
|
|
1592
1759
|
`<li>` in it. The list reveals rows in chunks as it is scrolled, so a last
|
|
@@ -1633,7 +1800,7 @@ export function DataTable<T>({
|
|
|
1633
1800
|
)}
|
|
1634
1801
|
{/* Client-side mobile lists scroll endlessly (sentinel above); only
|
|
1635
1802
|
server-paginated tables keep the pager here. */}
|
|
1636
|
-
{isServer && (
|
|
1803
|
+
{isServer && !loadMore && (
|
|
1637
1804
|
<Pagination
|
|
1638
1805
|
page={safePage}
|
|
1639
1806
|
totalPages={totalPages}
|
|
@@ -1694,7 +1861,9 @@ export function DataTable<T>({
|
|
|
1694
1861
|
the rows that are about to be replaced, and the dimmed body says the same
|
|
1695
1862
|
to the eye. The pager stays live — see `ServerPagination.isLoading`. */}
|
|
1696
1863
|
<table
|
|
1864
|
+
ref={tableRef}
|
|
1697
1865
|
className={cn("w-full", compact ? "text-xs" : "text-sm")}
|
|
1866
|
+
data-fit-grow={growDrawn ? "" : undefined}
|
|
1698
1867
|
aria-label={labels.table}
|
|
1699
1868
|
aria-busy={isLoading || undefined}
|
|
1700
1869
|
>
|
|
@@ -1713,7 +1882,7 @@ export function DataTable<T>({
|
|
|
1713
1882
|
compact={compact}
|
|
1714
1883
|
labels={labels}
|
|
1715
1884
|
locale={locale}
|
|
1716
|
-
isServer={
|
|
1885
|
+
isServer={serverRows}
|
|
1717
1886
|
hasSortCallback={!!onSortsChange}
|
|
1718
1887
|
hasFilterCallback={!!onFiltersChange}
|
|
1719
1888
|
sorts={sorts}
|
|
@@ -1731,17 +1900,14 @@ export function DataTable<T>({
|
|
|
1731
1900
|
moveResize={moveResize}
|
|
1732
1901
|
finishResize={finishResize}
|
|
1733
1902
|
/>
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
</tr>
|
|
1743
|
-
)}
|
|
1744
|
-
{slice.map((row, rowIndex) => {
|
|
1903
|
+
{(() => {
|
|
1904
|
+
const bodyClass = cn(isLoading && slice.length > 0 && "opacity-60 transition-opacity");
|
|
1905
|
+
const leading = leadingRow ? (
|
|
1906
|
+
<DataTableLeadingRow colSpan={totalColSpan} sticky={leadingRowSticky}>
|
|
1907
|
+
{leadingRow}
|
|
1908
|
+
</DataTableLeadingRow>
|
|
1909
|
+
) : null;
|
|
1910
|
+
const renderRow = (row: T, rowIndex: number) => {
|
|
1745
1911
|
const expanded = isExpanded?.(row) ?? false;
|
|
1746
1912
|
const expansion = expanded ? expandedRow?.(row) : null;
|
|
1747
1913
|
const rowInteractive = !!onRowClick || !!selection;
|
|
@@ -1906,6 +2072,7 @@ export function DataTable<T>({
|
|
|
1906
2072
|
"align-top",
|
|
1907
2073
|
compact ? "px-2 py-1" : "px-3 py-2",
|
|
1908
2074
|
width && "overflow-hidden text-ellipsis",
|
|
2075
|
+
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
1909
2076
|
logicalAlign(own.className),
|
|
1910
2077
|
logicalAlign(col.className),
|
|
1911
2078
|
)}
|
|
@@ -1962,20 +2129,70 @@ export function DataTable<T>({
|
|
|
1962
2129
|
)}
|
|
1963
2130
|
</Fragment>
|
|
1964
2131
|
);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
<
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2132
|
+
};
|
|
2133
|
+
const emptyRow =
|
|
2134
|
+
slice.length === 0 ? (
|
|
2135
|
+
<tr>
|
|
2136
|
+
<td
|
|
2137
|
+
colSpan={totalColSpan}
|
|
2138
|
+
className={cn("text-center text-muted", compact ? "px-2 py-2" : "px-3 py-4")}
|
|
2139
|
+
>
|
|
2140
|
+
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
2141
|
+
page is still in flight is a claim the table cannot make. */}
|
|
2142
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
2143
|
+
</td>
|
|
2144
|
+
</tr>
|
|
2145
|
+
) : null;
|
|
2146
|
+
if (!desktopGroups) {
|
|
2147
|
+
return (
|
|
2148
|
+
<tbody className={bodyClass}>
|
|
2149
|
+
{leading}
|
|
2150
|
+
{slice.map(renderRow)}
|
|
2151
|
+
{emptyRow}
|
|
2152
|
+
</tbody>
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
// Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its
|
|
2156
|
+
// band the row header; the leading row (and the empty state) in their own
|
|
2157
|
+
// 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
|
+
const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));
|
|
2169
|
+
return (
|
|
2170
|
+
<>
|
|
2171
|
+
{(leading || emptyRow) && (
|
|
2172
|
+
<tbody className={bodyClass}>
|
|
2173
|
+
{leading}
|
|
2174
|
+
{emptyRow}
|
|
2175
|
+
</tbody>
|
|
2176
|
+
)}
|
|
2177
|
+
{desktopGroups.map((g1) =>
|
|
2178
|
+
g1.children ? (
|
|
2179
|
+
g1.children.map((g2, j) => (
|
|
2180
|
+
<tbody key={`g:${g1.key}:${g2.key}:${g2.start}`} className={bodyClass}>
|
|
2181
|
+
{j === 0 && band(1, levels[0], g1)}
|
|
2182
|
+
{band(2, levels[1], g2)}
|
|
2183
|
+
{rowsOf(g2)}
|
|
2184
|
+
</tbody>
|
|
2185
|
+
))
|
|
2186
|
+
) : (
|
|
2187
|
+
<tbody key={`g:${g1.key}:${g1.start}`} className={bodyClass}>
|
|
2188
|
+
{band(1, levels[0], g1)}
|
|
2189
|
+
{rowsOf(g1)}
|
|
2190
|
+
</tbody>
|
|
2191
|
+
),
|
|
2192
|
+
)}
|
|
2193
|
+
</>
|
|
2194
|
+
);
|
|
2195
|
+
})()}
|
|
1979
2196
|
{showFooter && (
|
|
1980
2197
|
// Sticky to the scroller's bottom edge for the same reason the header is
|
|
1981
2198
|
// sticky to its top: the body scrolls inside a bounded wrapper, and a
|
|
@@ -2019,6 +2236,7 @@ export function DataTable<T>({
|
|
|
2019
2236
|
// A `<th>` centres by default; the label reads from the start
|
|
2020
2237
|
// like the cells above it, unless the column says otherwise.
|
|
2021
2238
|
isLabel && "text-start",
|
|
2239
|
+
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
2022
2240
|
logicalAlign(col.className),
|
|
2023
2241
|
)}
|
|
2024
2242
|
>
|
|
@@ -2030,8 +2248,13 @@ export function DataTable<T>({
|
|
|
2030
2248
|
</tfoot>
|
|
2031
2249
|
)}
|
|
2032
2250
|
</table>
|
|
2251
|
+
{/* In the scroller, after the rows (0.48): it comes into view as the last row
|
|
2252
|
+
does, under `fillHeight` too, where the body is the scroller. */}
|
|
2253
|
+
{loadMore && (
|
|
2254
|
+
<DataTableLoadMoreFooter loadMore={loadMore} rowCount={sorted.length} labels={labels} compact={compact} />
|
|
2255
|
+
)}
|
|
2033
2256
|
</div>
|
|
2034
|
-
{!unpaged && (
|
|
2257
|
+
{!unpaged && !loadMore && (
|
|
2035
2258
|
<Pagination
|
|
2036
2259
|
page={safePage}
|
|
2037
2260
|
totalPages={totalPages}
|
|
@@ -2059,6 +2282,8 @@ export function DataTable<T>({
|
|
|
2059
2282
|
columns={columns}
|
|
2060
2283
|
hiddenCols={hiddenCols}
|
|
2061
2284
|
setHiddenCols={setHiddenCols}
|
|
2285
|
+
autoHidden={autoHidden}
|
|
2286
|
+
keepColumn={keepColumn}
|
|
2062
2287
|
showSettings={showSettings}
|
|
2063
2288
|
setShowSettings={setShowSettings}
|
|
2064
2289
|
columnsCountLabel={columnsCountLabel}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId, useState } from "react";
|
|
1
|
+
import { Children, Fragment, isValidElement, useEffect, useId, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactNode } from "react";
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
|
|
@@ -262,7 +262,9 @@ export interface DisclosureProps extends Omit<ComponentPropsWithRef<"div">, "tit
|
|
|
262
262
|
* ```
|
|
263
263
|
*/
|
|
264
264
|
controls?: string;
|
|
265
|
-
/** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls})
|
|
265
|
+
/** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}),
|
|
266
|
+
* nor when there is none (0.48): no children, no empty box under the header.
|
|
267
|
+
* `null`, `false`, `undefined` and an empty fragment (`<></>`) count as none. */
|
|
266
268
|
children?: ReactNode;
|
|
267
269
|
}
|
|
268
270
|
|
|
@@ -312,8 +314,13 @@ export function Disclosure({
|
|
|
312
314
|
const chevronAtEnd = chevronAt === "end";
|
|
313
315
|
const chevronAfterTitle = chevronAt === "after-title";
|
|
314
316
|
const triggerOnly = controls !== undefined;
|
|
317
|
+
// 0.48 (keksdose dev #603): no children, no body — an empty padded box under the
|
|
318
|
+
// header made an open disclosure 8px taller than a shut one, and its `aria-controls`
|
|
319
|
+
// named a region with nothing in it. `null`, `false` and `undefined` are nothing, and
|
|
320
|
+
// so is a fragment of nothing (`cond ? <X /> : <></>`, keksdose N1).
|
|
321
|
+
const hasBody = !triggerOnly && hasBodyContent(children);
|
|
315
322
|
// The card's header squares its lower corners only when a body opens under it.
|
|
316
|
-
const joined = open &&
|
|
323
|
+
const joined = open && hasBody;
|
|
317
324
|
const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;
|
|
318
325
|
// `trailing` beside the button, which then stretches under it; inside the button,
|
|
319
326
|
// the row is simply the button.
|
|
@@ -334,7 +341,7 @@ export function Disclosure({
|
|
|
334
341
|
type="button"
|
|
335
342
|
aria-expanded={open}
|
|
336
343
|
// `||`: an empty `controls` is trigger-only with nothing to name, not `aria-controls=""`.
|
|
337
|
-
aria-controls={triggerOnly ? controls || undefined : bodyId}
|
|
344
|
+
aria-controls={triggerOnly ? controls || undefined : hasBody ? bodyId : undefined}
|
|
338
345
|
disabled={disabled}
|
|
339
346
|
onClick={toggle}
|
|
340
347
|
data-disclosure-trigger=""
|
|
@@ -424,7 +431,7 @@ export function Disclosure({
|
|
|
424
431
|
) : (
|
|
425
432
|
header
|
|
426
433
|
)}
|
|
427
|
-
{
|
|
434
|
+
{hasBody && (
|
|
428
435
|
<Collapse id={bodyId} open={open} keepMounted={keepMounted}>
|
|
429
436
|
<div className={cn(card ? "space-y-3 px-4 pb-4" : !menu && "space-y-2 pt-2", bodyClassName)}>{children}</div>
|
|
430
437
|
</Collapse>
|
|
@@ -461,3 +468,13 @@ function Chevron({
|
|
|
461
468
|
/>
|
|
462
469
|
);
|
|
463
470
|
}
|
|
471
|
+
|
|
472
|
+
/** Whether `children` holds anything to draw: `null`/`false`/`undefined`/`""` are nothing,
|
|
473
|
+
* and a fragment counts by what is in it (an empty `<></>` is nothing). */
|
|
474
|
+
function hasBodyContent(children: ReactNode): boolean {
|
|
475
|
+
return Children.toArray(children).some((child) =>
|
|
476
|
+
isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment
|
|
477
|
+
? hasBodyContent(child.props.children)
|
|
478
|
+
: child !== "",
|
|
479
|
+
);
|
|
480
|
+
}
|