@eifi1/ui-kit 0.47.0 → 0.48.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 +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 +78 -2
- package/dist/components/data-table.js +348 -257
- 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 +246 -43
- 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. */
|
|
@@ -433,7 +482,9 @@ export interface DataTableProps<T> {
|
|
|
433
482
|
* contiguous (pair with a matching default sort). `mobileGroupLabel` formats
|
|
434
483
|
* the header. Desktop is unaffected. Feedback #317.
|
|
435
484
|
*/
|
|
436
|
-
mobileGroupBy?: (row: T) => string
|
|
485
|
+
mobileGroupBy?: ((row: T) => string) | DataTableGroupBy<T>;
|
|
486
|
+
/** The section header's words for `mobileGroupBy`'s key. Wins over a level's own
|
|
487
|
+
* `label` when `mobileGroupBy` is a {@link DataTableGroupBy}. */
|
|
437
488
|
mobileGroupLabel?: (key: string) => ReactNode;
|
|
438
489
|
/**
|
|
439
490
|
* Fully replace the default mobile card body (bold primary + labelled
|
|
@@ -505,6 +556,40 @@ export interface DataTableProps<T> {
|
|
|
505
556
|
* "hide upcoming/scheduled" toggle).
|
|
506
557
|
*/
|
|
507
558
|
leadingRow?: ReactNode;
|
|
559
|
+
/**
|
|
560
|
+
* 0.48 (keksdose dev #603: "it should rather be sticky, so the user can show hide them
|
|
561
|
+
* everywhere on click"): pin the {@link leadingRow} under the sticky header while the
|
|
562
|
+
* rows scroll beneath both — its `top` is the header's measured height. Desktop only,
|
|
563
|
+
* as `leadingRow` is; it sticks within the table's own scroller (a bounded body:
|
|
564
|
+
* `fillHeight` or `maxBodyHeight`). Default: it scrolls with the rows.
|
|
565
|
+
*/
|
|
566
|
+
leadingRowSticky?: boolean;
|
|
567
|
+
/**
|
|
568
|
+
* 0.48 (keksdose dev #605): group the desktop table's rows under full-width bands, one
|
|
569
|
+
* level or two (period → day). Each band is a row header spanning the visible columns
|
|
570
|
+
* (selection column included, hidden and fit-hidden columns not) with a label and an
|
|
571
|
+
* end slot (counts, totals), and opens its own `<tbody>`, so a screen reader announces
|
|
572
|
+
* it as the reader enters the group; the period band opens the first day's. Rows must
|
|
573
|
+
* already be ordered so equal keys are contiguous (as for `mobileGroupBy`); grouping
|
|
574
|
+
* runs over the rows on screen, page by page.
|
|
575
|
+
*
|
|
576
|
+
* The bands are sticky: the period under the header (and under a sticky
|
|
577
|
+
* {@link leadingRow}), the day under the period — measured offsets, inside the table's
|
|
578
|
+
* own scroller (`fillHeight` or `maxBodyHeight`). The `leadingRow` stays above the first
|
|
579
|
+
* group. On a phone pass the same level to `mobileGroupBy`:
|
|
580
|
+
* `groupBy={[period, day]} mobileGroupBy={period}`. See {@link DataTableGroupBy}.
|
|
581
|
+
*/
|
|
582
|
+
groupBy?: DataTableGroupBy<T> | readonly [DataTableGroupBy<T>, DataTableGroupBy<T>?];
|
|
583
|
+
/**
|
|
584
|
+
* 0.48 (keksdose dev #605): a "load more" footer in place of the pager. The table
|
|
585
|
+
* renders every row it is given (as `paginated={false}` does — pass that too, to say
|
|
586
|
+
* so), and the footer asks the owner for the next slice: as it scrolls into view (an
|
|
587
|
+
* IntersectionObserver, guarded against double calls while one is in flight) and with
|
|
588
|
+
* a button the keyboard reaches, named by the app ("Load August 2026"). With
|
|
589
|
+
* `serverPagination` it replaces that pager as well. Both layouts: on a phone it is the
|
|
590
|
+
* card list's last item. See {@link DataTableLoadMore}.
|
|
591
|
+
*/
|
|
592
|
+
loadMore?: DataTableLoadMore;
|
|
508
593
|
/**
|
|
509
594
|
* Row height. `"compact"` is lenkbank's in-card tables: five `text-xs` tables with
|
|
510
595
|
* `px-2 py-1` cells sitting inside cards, which at the default padding came out at
|
|
@@ -760,14 +845,17 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
|
|
|
760
845
|
|
|
761
846
|
const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
|
|
762
847
|
|
|
763
|
-
/** Arrow/Home/End between the focusable rows of
|
|
848
|
+
/** Arrow/Home/End between the focusable rows of the table's bodies — the roving half of
|
|
764
849
|
* the keyboard-activatable rows (see the row's `onKeyDown`). */
|
|
765
850
|
function moveRowFocus(from: HTMLElement, key: string): boolean {
|
|
766
851
|
if (!(key in ROW_NAV_KEYS)) return false;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
852
|
+
// Every body of the table: a grouped table (`groupBy`, 0.48) has one per group.
|
|
853
|
+
const table = from.closest("table");
|
|
854
|
+
if (!table) return false;
|
|
855
|
+
const rows = Array.from(table.tBodies).flatMap((body) =>
|
|
856
|
+
Array.from(body.children).filter(
|
|
857
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.hasAttribute("data-row-nav"),
|
|
858
|
+
),
|
|
771
859
|
);
|
|
772
860
|
const i = rows.indexOf(from);
|
|
773
861
|
const target =
|
|
@@ -793,6 +881,7 @@ const ROW_NAV_KEYS = { ArrowDown: 1, ArrowUp: 1, Home: 1, End: 1 } as const;
|
|
|
793
881
|
|
|
794
882
|
import { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from "./use-table-state";
|
|
795
883
|
import { useMobileReveal } from "./use-mobile-reveal";
|
|
884
|
+
import { GROW_CELL_CLASS, GROW_PEER_CELL_CLASS, useColumnFit } from "./data-table-fit";
|
|
796
885
|
|
|
797
886
|
export function DataTable<T>({
|
|
798
887
|
rows,
|
|
@@ -833,6 +922,9 @@ export function DataTable<T>({
|
|
|
833
922
|
mobileSwipeActions,
|
|
834
923
|
desktopSwipeActions,
|
|
835
924
|
leadingRow,
|
|
925
|
+
leadingRowSticky = false,
|
|
926
|
+
groupBy,
|
|
927
|
+
loadMore,
|
|
836
928
|
density = "comfortable",
|
|
837
929
|
sortCycle = "tri",
|
|
838
930
|
chrome = "full",
|
|
@@ -921,7 +1013,17 @@ export function DataTable<T>({
|
|
|
921
1013
|
// columns are ignored rather than applied: a table cannot be left in a state the
|
|
922
1014
|
// user has no control to get out of. They stay in storage untouched, so turning
|
|
923
1015
|
// the control back on brings them back.
|
|
924
|
-
|
|
1016
|
+
// A `grow` column takes the rest of the row by design (0.48): a width stored for it
|
|
1017
|
+
// (a drag or "Auto-size columns" before it grew, KS N2) is ignored, and it has no
|
|
1018
|
+
// handle, so it can never pin it again. Ignored rather than deleted from storage, as
|
|
1019
|
+
// the widths above; the next resize or auto-size writes a blob without it.
|
|
1020
|
+
const widths = useMemo(() => {
|
|
1021
|
+
if (!resizable) return {};
|
|
1022
|
+
const grow = columns.find((c) => c.grow)?.key;
|
|
1023
|
+
if (!grow || !(grow in storedWidths)) return storedWidths;
|
|
1024
|
+
const { [grow]: _ignored, ...rest } = storedWidths;
|
|
1025
|
+
return rest;
|
|
1026
|
+
}, [resizable, storedWidths, columns]);
|
|
925
1027
|
const hiddenCols = useMemo(
|
|
926
1028
|
() => (columnSettings ? storedHiddenCols : new Set<string>()),
|
|
927
1029
|
[columnSettings, storedHiddenCols],
|
|
@@ -1017,7 +1119,7 @@ export function DataTable<T>({
|
|
|
1017
1119
|
// `paginated={false}` is expressed as "page size = everything" rather than as a
|
|
1018
1120
|
// second code path, so slicing, the page clamp and the range summary all keep
|
|
1019
1121
|
// exactly one definition.
|
|
1020
|
-
const unpaged = !isServer && !paginated;
|
|
1122
|
+
const unpaged = !isServer && (!paginated || !!loadMore);
|
|
1021
1123
|
const effectivePageSize = isServer
|
|
1022
1124
|
? serverPagination!.pageSize
|
|
1023
1125
|
: unpaged || pageSize === Infinity
|
|
@@ -1145,10 +1247,22 @@ export function DataTable<T>({
|
|
|
1145
1247
|
announce(labels.pageChanged(p + 1, totalPages));
|
|
1146
1248
|
};
|
|
1147
1249
|
|
|
1250
|
+
// 0.48 (keksdose live #451): the `fitPriority` columns the table's width leaves out
|
|
1251
|
+
// (data-table-fit.ts). Desktop only; the scroller is read after render.
|
|
1252
|
+
const { autoHidden, keep: keepColumn } = useColumnFit({
|
|
1253
|
+
columns,
|
|
1254
|
+
hiddenCols,
|
|
1255
|
+
headRefs,
|
|
1256
|
+
scroller: () => desktopScroller.current,
|
|
1257
|
+
enabled: !usePhoneLayout(),
|
|
1258
|
+
});
|
|
1148
1259
|
const visibleColumns = useMemo(
|
|
1149
|
-
() => columns.filter((c) => !hiddenCols.has(c.key)),
|
|
1150
|
-
[columns, hiddenCols],
|
|
1260
|
+
() => columns.filter((c) => !hiddenCols.has(c.key) && !autoHidden.has(c.key)),
|
|
1261
|
+
[columns, hiddenCols, autoHidden],
|
|
1151
1262
|
);
|
|
1263
|
+
// With a `grow` column drawn, every other cell keeps one line at Normal (kastlan S2,
|
|
1264
|
+
// 0.48): the `w-full` column squeezed them to their min-content width.
|
|
1265
|
+
const growDrawn = visibleColumns.some((c) => c.grow);
|
|
1152
1266
|
|
|
1153
1267
|
// Which cell carries the row's link when `rowHref` is set (feedback #451). The
|
|
1154
1268
|
// `mobilePrimary` column is the row's headline by declaration, so it is both the
|
|
@@ -1174,7 +1288,8 @@ export function DataTable<T>({
|
|
|
1174
1288
|
widths,
|
|
1175
1289
|
setWidths,
|
|
1176
1290
|
columns,
|
|
1177
|
-
|
|
1291
|
+
// The columns left out for width have no header to measure either.
|
|
1292
|
+
hiddenCols: autoHidden.size > 0 ? new Set([...hiddenCols, ...autoHidden]) : hiddenCols,
|
|
1178
1293
|
});
|
|
1179
1294
|
|
|
1180
1295
|
const visibleCount = visibleColumns.length;
|
|
@@ -1216,7 +1331,9 @@ export function DataTable<T>({
|
|
|
1216
1331
|
// 0.47 (keksdose S4): the scroller's visible width, as `--dt-visible-w` on it, for the
|
|
1217
1332
|
// expansion row's pinned content (below). Set on the element, not in state: a resize
|
|
1218
1333
|
// re-renders nothing. Only a table that can expand a row measures.
|
|
1219
|
-
|
|
1334
|
+
// The group bands and the load-more footer pin their content the same way (0.48).
|
|
1335
|
+
const levels = groupLevels(groupBy);
|
|
1336
|
+
const measuresVisibleWidth = isMdUp && (!!expandedRow || levels.length > 0 || !!loadMore);
|
|
1220
1337
|
useEffect(() => {
|
|
1221
1338
|
const el = desktopScroller.current;
|
|
1222
1339
|
if (!measuresVisibleWidth || !el || typeof ResizeObserver === "undefined") return;
|
|
@@ -1419,9 +1536,20 @@ export function DataTable<T>({
|
|
|
1419
1536
|
// Consecutive grouping of the (already-sorted) mobile slice into labelled
|
|
1420
1537
|
// sections for the "assistance"-style list (feedback #317). Null unless the
|
|
1421
1538
|
// caller opts in via `mobileGroupBy`.
|
|
1422
|
-
|
|
1539
|
+
// The desktop's bands (0.48, keksdose dev #605), over the rows on screen.
|
|
1540
|
+
const tableRef = useRef<HTMLTableElement | null>(null);
|
|
1541
|
+
const desktopGroups = isMdUp && levels.length > 0 ? groupRows(slice, levels) : null;
|
|
1542
|
+
const bandTops = useGroupBandTops(
|
|
1543
|
+
tableRef,
|
|
1544
|
+
!!desktopGroups,
|
|
1545
|
+
`${levels.length}:${leadingRowSticky && !!leadingRow}:${desktopGroups?.[0]?.key ?? ""}`,
|
|
1546
|
+
);
|
|
1547
|
+
// `mobileGroupBy` may be a desktop `groupBy` level (0.48), its label and end slot too.
|
|
1548
|
+
const mobileLevel = typeof mobileGroupBy === "function" ? null : (mobileGroupBy ?? null);
|
|
1549
|
+
const mobileKey = typeof mobileGroupBy === "function" ? mobileGroupBy : mobileGroupBy?.key;
|
|
1550
|
+
const mobileGroups: { key: string; rows: T[] }[] | null = mobileKey
|
|
1423
1551
|
? mobileSlice.reduce<{ key: string; rows: T[] }[]>((acc, row) => {
|
|
1424
|
-
const key =
|
|
1552
|
+
const key = mobileKey(row);
|
|
1425
1553
|
const last = acc[acc.length - 1];
|
|
1426
1554
|
if (last && last.key === key) last.rows.push(row);
|
|
1427
1555
|
else acc.push({ key, rows: [row] });
|
|
@@ -1559,8 +1687,15 @@ export function DataTable<T>({
|
|
|
1559
1687
|
{mobileGroups
|
|
1560
1688
|
? mobileGroups.map((g) => (
|
|
1561
1689
|
<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
|
-
|
|
1690
|
+
<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">
|
|
1691
|
+
<span className="min-w-0">
|
|
1692
|
+
{mobileGroupLabel
|
|
1693
|
+
? mobileGroupLabel(g.key)
|
|
1694
|
+
: mobileLevel?.label
|
|
1695
|
+
? mobileLevel.label(g.key, g.rows)
|
|
1696
|
+
: g.key}
|
|
1697
|
+
</span>
|
|
1698
|
+
{mobileLevel?.end && <span className="ms-auto tabular-nums">{mobileLevel.end(g.key, g.rows)}</span>}
|
|
1564
1699
|
</li>
|
|
1565
1700
|
{g.rows.map(renderMobileRow)}
|
|
1566
1701
|
</Fragment>
|
|
@@ -1587,6 +1722,16 @@ export function DataTable<T>({
|
|
|
1587
1722
|
<bdi>{labels.loading}</bdi>
|
|
1588
1723
|
</li>
|
|
1589
1724
|
)}
|
|
1725
|
+
{/* The owner's next slice (0.48), once every row held is revealed. */}
|
|
1726
|
+
{loadMore && !canRevealMoreMobile && (
|
|
1727
|
+
<DataTableLoadMoreFooter
|
|
1728
|
+
as="li"
|
|
1729
|
+
loadMore={loadMore}
|
|
1730
|
+
rowCount={sorted.length}
|
|
1731
|
+
labels={labels}
|
|
1732
|
+
compact={compact}
|
|
1733
|
+
/>
|
|
1734
|
+
)}
|
|
1590
1735
|
</ul>
|
|
1591
1736
|
{/* The phone's totals: a summary card after the list rather than one more
|
|
1592
1737
|
`<li>` in it. The list reveals rows in chunks as it is scrolled, so a last
|
|
@@ -1633,7 +1778,7 @@ export function DataTable<T>({
|
|
|
1633
1778
|
)}
|
|
1634
1779
|
{/* Client-side mobile lists scroll endlessly (sentinel above); only
|
|
1635
1780
|
server-paginated tables keep the pager here. */}
|
|
1636
|
-
{isServer && (
|
|
1781
|
+
{isServer && !loadMore && (
|
|
1637
1782
|
<Pagination
|
|
1638
1783
|
page={safePage}
|
|
1639
1784
|
totalPages={totalPages}
|
|
@@ -1694,7 +1839,9 @@ export function DataTable<T>({
|
|
|
1694
1839
|
the rows that are about to be replaced, and the dimmed body says the same
|
|
1695
1840
|
to the eye. The pager stays live — see `ServerPagination.isLoading`. */}
|
|
1696
1841
|
<table
|
|
1842
|
+
ref={tableRef}
|
|
1697
1843
|
className={cn("w-full", compact ? "text-xs" : "text-sm")}
|
|
1844
|
+
data-fit-grow={growDrawn ? "" : undefined}
|
|
1698
1845
|
aria-label={labels.table}
|
|
1699
1846
|
aria-busy={isLoading || undefined}
|
|
1700
1847
|
>
|
|
@@ -1731,17 +1878,14 @@ export function DataTable<T>({
|
|
|
1731
1878
|
moveResize={moveResize}
|
|
1732
1879
|
finishResize={finishResize}
|
|
1733
1880
|
/>
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
</tr>
|
|
1743
|
-
)}
|
|
1744
|
-
{slice.map((row, rowIndex) => {
|
|
1881
|
+
{(() => {
|
|
1882
|
+
const bodyClass = cn(isLoading && slice.length > 0 && "opacity-60 transition-opacity");
|
|
1883
|
+
const leading = leadingRow ? (
|
|
1884
|
+
<DataTableLeadingRow colSpan={totalColSpan} sticky={leadingRowSticky}>
|
|
1885
|
+
{leadingRow}
|
|
1886
|
+
</DataTableLeadingRow>
|
|
1887
|
+
) : null;
|
|
1888
|
+
const renderRow = (row: T, rowIndex: number) => {
|
|
1745
1889
|
const expanded = isExpanded?.(row) ?? false;
|
|
1746
1890
|
const expansion = expanded ? expandedRow?.(row) : null;
|
|
1747
1891
|
const rowInteractive = !!onRowClick || !!selection;
|
|
@@ -1906,6 +2050,7 @@ export function DataTable<T>({
|
|
|
1906
2050
|
"align-top",
|
|
1907
2051
|
compact ? "px-2 py-1" : "px-3 py-2",
|
|
1908
2052
|
width && "overflow-hidden text-ellipsis",
|
|
2053
|
+
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
1909
2054
|
logicalAlign(own.className),
|
|
1910
2055
|
logicalAlign(col.className),
|
|
1911
2056
|
)}
|
|
@@ -1962,20 +2107,70 @@ export function DataTable<T>({
|
|
|
1962
2107
|
)}
|
|
1963
2108
|
</Fragment>
|
|
1964
2109
|
);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
<
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2110
|
+
};
|
|
2111
|
+
const emptyRow =
|
|
2112
|
+
slice.length === 0 ? (
|
|
2113
|
+
<tr>
|
|
2114
|
+
<td
|
|
2115
|
+
colSpan={totalColSpan}
|
|
2116
|
+
className={cn("text-center text-muted", compact ? "px-2 py-2" : "px-3 py-4")}
|
|
2117
|
+
>
|
|
2118
|
+
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
2119
|
+
page is still in flight is a claim the table cannot make. */}
|
|
2120
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
2121
|
+
</td>
|
|
2122
|
+
</tr>
|
|
2123
|
+
) : null;
|
|
2124
|
+
if (!desktopGroups) {
|
|
2125
|
+
return (
|
|
2126
|
+
<tbody className={bodyClass}>
|
|
2127
|
+
{leading}
|
|
2128
|
+
{slice.map(renderRow)}
|
|
2129
|
+
{emptyRow}
|
|
2130
|
+
</tbody>
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
// Grouped (0.48, keksdose dev #605): one `<tbody>` per innermost group, its
|
|
2134
|
+
// band the row header; the leading row (and the empty state) in their own
|
|
2135
|
+
// before the first group.
|
|
2136
|
+
const band = (level: 1 | 2, def: DataTableGroupBy<T>, g: { key: string; rows: T[] }) => (
|
|
2137
|
+
<DataTableGroupBand
|
|
2138
|
+
level={level}
|
|
2139
|
+
colSpan={totalColSpan}
|
|
2140
|
+
top={level === 1 ? bandTops[0] : bandTops[1]}
|
|
2141
|
+
compact={compact}
|
|
2142
|
+
label={def.label ? def.label(g.key, g.rows) : g.key}
|
|
2143
|
+
end={def.end?.(g.key, g.rows)}
|
|
2144
|
+
/>
|
|
2145
|
+
);
|
|
2146
|
+
const rowsOf = (g: { rows: T[]; start: number }) => g.rows.map((r, i) => renderRow(r, g.start + i));
|
|
2147
|
+
return (
|
|
2148
|
+
<>
|
|
2149
|
+
{(leading || emptyRow) && (
|
|
2150
|
+
<tbody className={bodyClass}>
|
|
2151
|
+
{leading}
|
|
2152
|
+
{emptyRow}
|
|
2153
|
+
</tbody>
|
|
2154
|
+
)}
|
|
2155
|
+
{desktopGroups.map((g1) =>
|
|
2156
|
+
g1.children ? (
|
|
2157
|
+
g1.children.map((g2, j) => (
|
|
2158
|
+
<tbody key={`g:${g1.key}:${g2.key}:${g2.start}`} className={bodyClass}>
|
|
2159
|
+
{j === 0 && band(1, levels[0], g1)}
|
|
2160
|
+
{band(2, levels[1], g2)}
|
|
2161
|
+
{rowsOf(g2)}
|
|
2162
|
+
</tbody>
|
|
2163
|
+
))
|
|
2164
|
+
) : (
|
|
2165
|
+
<tbody key={`g:${g1.key}:${g1.start}`} className={bodyClass}>
|
|
2166
|
+
{band(1, levels[0], g1)}
|
|
2167
|
+
{rowsOf(g1)}
|
|
2168
|
+
</tbody>
|
|
2169
|
+
),
|
|
2170
|
+
)}
|
|
2171
|
+
</>
|
|
2172
|
+
);
|
|
2173
|
+
})()}
|
|
1979
2174
|
{showFooter && (
|
|
1980
2175
|
// Sticky to the scroller's bottom edge for the same reason the header is
|
|
1981
2176
|
// sticky to its top: the body scrolls inside a bounded wrapper, and a
|
|
@@ -2019,6 +2214,7 @@ export function DataTable<T>({
|
|
|
2019
2214
|
// A `<th>` centres by default; the label reads from the start
|
|
2020
2215
|
// like the cells above it, unless the column says otherwise.
|
|
2021
2216
|
isLabel && "text-start",
|
|
2217
|
+
col.grow ? GROW_CELL_CLASS : growDrawn && GROW_PEER_CELL_CLASS,
|
|
2022
2218
|
logicalAlign(col.className),
|
|
2023
2219
|
)}
|
|
2024
2220
|
>
|
|
@@ -2030,8 +2226,13 @@ export function DataTable<T>({
|
|
|
2030
2226
|
</tfoot>
|
|
2031
2227
|
)}
|
|
2032
2228
|
</table>
|
|
2229
|
+
{/* In the scroller, after the rows (0.48): it comes into view as the last row
|
|
2230
|
+
does, under `fillHeight` too, where the body is the scroller. */}
|
|
2231
|
+
{loadMore && (
|
|
2232
|
+
<DataTableLoadMoreFooter loadMore={loadMore} rowCount={sorted.length} labels={labels} compact={compact} />
|
|
2233
|
+
)}
|
|
2033
2234
|
</div>
|
|
2034
|
-
{!unpaged && (
|
|
2235
|
+
{!unpaged && !loadMore && (
|
|
2035
2236
|
<Pagination
|
|
2036
2237
|
page={safePage}
|
|
2037
2238
|
totalPages={totalPages}
|
|
@@ -2059,6 +2260,8 @@ export function DataTable<T>({
|
|
|
2059
2260
|
columns={columns}
|
|
2060
2261
|
hiddenCols={hiddenCols}
|
|
2061
2262
|
setHiddenCols={setHiddenCols}
|
|
2263
|
+
autoHidden={autoHidden}
|
|
2264
|
+
keepColumn={keepColumn}
|
|
2062
2265
|
showSettings={showSettings}
|
|
2063
2266
|
setShowSettings={setShowSettings}
|
|
2064
2267
|
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
|
+
}
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
ComponentPropsWithRef,
|
|
5
5
|
KeyboardEvent as ReactKeyboardEvent,
|
|
6
|
+
PointerEvent as ReactPointerEvent,
|
|
6
7
|
ReactNode,
|
|
7
8
|
Ref,
|
|
8
9
|
RefObject,
|
|
@@ -10,7 +11,7 @@ import type {
|
|
|
10
11
|
import { createPortal } from "react-dom";
|
|
11
12
|
import { Search } from "lucide-react";
|
|
12
13
|
import { anchoredInline, useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
13
|
-
import { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
14
|
+
import { pressInside, runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from "../hooks/use-dismiss";
|
|
14
15
|
import type { DismissLayer } from "../hooks/use-dismiss";
|
|
15
16
|
import { useOverlayHistory } from "../hooks/use-overlay-history";
|
|
16
17
|
import { cn } from "../lib/cn";
|
|
@@ -371,6 +372,13 @@ export function DropdownPanel({
|
|
|
371
372
|
runOwnKeyDown(e, onKeyDown, panelRef && listLayers.get(panelRef));
|
|
372
373
|
takeEscape(e);
|
|
373
374
|
};
|
|
375
|
+
// A press in a panel opened from a row (a popover, its lists) is inside the list for
|
|
376
|
+
// `useDropdown`'s outside press, though portalled elsewhere (dev#623).
|
|
377
|
+
const markInside = pressInside(panelRef);
|
|
378
|
+
const handlePointerDownCapture = (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
379
|
+
rest.onPointerDownCapture?.(e);
|
|
380
|
+
markInside?.(e);
|
|
381
|
+
};
|
|
374
382
|
const body = (
|
|
375
383
|
<>
|
|
376
384
|
{header}
|
|
@@ -399,6 +407,7 @@ export function DropdownPanel({
|
|
|
399
407
|
style={style}
|
|
400
408
|
ref={setPanel}
|
|
401
409
|
onKeyDown={handleKeyDown}
|
|
410
|
+
onPointerDownCapture={handlePointerDownCapture}
|
|
402
411
|
className={cn(
|
|
403
412
|
"absolute z-30 mt-1 rounded-md border border-subtle bg-surface shadow-lg",
|
|
404
413
|
"[&>ul]:max-h-64",
|
|
@@ -425,6 +434,7 @@ export function DropdownPanel({
|
|
|
425
434
|
ref={setPanel}
|
|
426
435
|
dir={dir}
|
|
427
436
|
onKeyDown={handleKeyDown}
|
|
437
|
+
onPointerDownCapture={handlePointerDownCapture}
|
|
428
438
|
// The caller's `style` is kept underneath, but the placement is measured rather
|
|
429
439
|
// than chosen (dev#548: an `overflow` ancestor clipped a third of this panel
|
|
430
440
|
// away), so these values are the panel's own. Flipped above, it hangs from its
|
|
@@ -17,6 +17,7 @@ import { cn } from "../lib/cn";
|
|
|
17
17
|
import { FOCUS_RING } from "./focus-ring";
|
|
18
18
|
import { dirOf, type Direction } from "../lib/direction";
|
|
19
19
|
import { registerFloating } from "../lib/floating-stack";
|
|
20
|
+
import { useFloatClearance } from "../lib/float-clearance";
|
|
20
21
|
import { isComposing } from "../lib/ime";
|
|
21
22
|
import { takeEscape } from "../hooks/use-dismiss";
|
|
22
23
|
import { useKitLabels, useKitLink } from "../i18n/kit-labels";
|
|
@@ -179,6 +180,11 @@ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTM
|
|
|
179
180
|
*
|
|
180
181
|
* While mounted it reports its box (`lib/floating-stack.ts`), so `ServerWakeNotice`
|
|
181
182
|
* stacks above it rather than over it.
|
|
183
|
+
*
|
|
184
|
+
* 0.48: while mounted, `hidden` or not, it also publishes the room it takes above the
|
|
185
|
+
* nav (`offset` + 3rem + a 0.5rem gap) as `--app-float-clearance` on `<html>` (the
|
|
186
|
+
* largest of several wins), and `AppShell` pads its `<main>` past it on a phone, so the
|
|
187
|
+
* page's last row no longer ends under the button (keksdose dev#612).
|
|
182
188
|
*/
|
|
183
189
|
export function FloatingActionButton({
|
|
184
190
|
label,
|
|
@@ -206,6 +212,10 @@ export function FloatingActionButton({
|
|
|
206
212
|
// The fixed element — the button, or the wrapper when there is a tooltip.
|
|
207
213
|
const [fixed, setFixed] = useState<HTMLElement | null>(null);
|
|
208
214
|
useEffect(() => (fixed ? registerFloating(fixed) : undefined), [fixed]);
|
|
215
|
+
// The room it takes above the nav, for AppShell's <main> to pad past (keksdose
|
|
216
|
+
// dev#612). Hidden too: the room is the launcher's while it is mounted, so the page
|
|
217
|
+
// does not change length under the reader as it hides and shows.
|
|
218
|
+
useFloatClearance(offset);
|
|
209
219
|
const buttonRef = useCallback(
|
|
210
220
|
(node: HTMLButtonElement | null) => {
|
|
211
221
|
assignRef(ref, node);
|
|
@@ -332,6 +342,14 @@ export interface FloatingPanelProps {
|
|
|
332
342
|
className?: string;
|
|
333
343
|
/** Extra classes for the scrolling body. */
|
|
334
344
|
bodyClassName?: string;
|
|
345
|
+
/**
|
|
346
|
+
* 0.48: take the FAB and the panel off screen while keeping both mounted — the open
|
|
347
|
+
* state and the bottom room (`--app-float-clearance`) stay, and an open panel is back
|
|
348
|
+
* when this turns `false`. keksdose's assistant launcher steps aside while an editor
|
|
349
|
+
* or a modal covers its corner (dev#612); unmounting it instead would give the room
|
|
350
|
+
* back and shorten the page under the reader's thumb.
|
|
351
|
+
*/
|
|
352
|
+
hidden?: boolean;
|
|
335
353
|
children: ReactNode;
|
|
336
354
|
}
|
|
337
355
|
|
|
@@ -382,6 +400,7 @@ export function FloatingPanel({
|
|
|
382
400
|
initialFocus,
|
|
383
401
|
className,
|
|
384
402
|
bodyClassName,
|
|
403
|
+
hidden = false,
|
|
385
404
|
children,
|
|
386
405
|
}: FloatingPanelProps) {
|
|
387
406
|
const labels = useKitLabels(
|
|
@@ -457,8 +476,9 @@ export function FloatingPanel({
|
|
|
457
476
|
offset={offset}
|
|
458
477
|
nativeTitle={fabNativeTitle}
|
|
459
478
|
tooltip={fabTooltip}
|
|
479
|
+
hidden={hidden || undefined}
|
|
460
480
|
aria-expanded={open}
|
|
461
|
-
aria-controls={open ? panelId : undefined}
|
|
481
|
+
aria-controls={open && !hidden ? panelId : undefined}
|
|
462
482
|
onClick={() => {
|
|
463
483
|
// A press ON the FAB closing the panel: the FAB keeps focus by being clicked,
|
|
464
484
|
// nothing to hand back.
|
|
@@ -467,6 +487,7 @@ export function FloatingPanel({
|
|
|
467
487
|
}}
|
|
468
488
|
/>
|
|
469
489
|
{open &&
|
|
490
|
+
!hidden &&
|
|
470
491
|
typeof document !== "undefined" &&
|
|
471
492
|
createPortal(
|
|
472
493
|
// A container for Escape bubbling up from anything inside it, as a dialog is.
|
|
@@ -572,6 +593,8 @@ export function FloatingActionGroup({
|
|
|
572
593
|
...rest
|
|
573
594
|
}: FloatingActionGroupProps) {
|
|
574
595
|
const [marker, dir] = usePortalDir();
|
|
596
|
+
// As the FAB's (keksdose dev#612): its 3rem members plus the frame's 1px borders.
|
|
597
|
+
useFloatClearance(offset, "2px");
|
|
575
598
|
return (
|
|
576
599
|
<>
|
|
577
600
|
<span ref={marker} hidden />
|
|
@@ -163,7 +163,8 @@ export function HoverMenu({
|
|
|
163
163
|
};
|
|
164
164
|
}, [open, close]);
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// A press in a panel opened from a row (a popover, its lists) is inside (dev#623).
|
|
167
|
+
const markInside = useOutsideClick(wrapperRef, close, open);
|
|
167
168
|
|
|
168
169
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
169
170
|
/** Set by a keyboard press on the trigger, so the open that follows moves focus into
|
|
@@ -290,6 +291,10 @@ export function HoverMenu({
|
|
|
290
291
|
className={cn("relative", className)}
|
|
291
292
|
onMouseEnter={handleMouseEnter}
|
|
292
293
|
onMouseLeave={handleMouseLeave}
|
|
294
|
+
onPointerDownCapture={(e) => {
|
|
295
|
+
rest.onPointerDownCapture?.(e);
|
|
296
|
+
markInside(e);
|
|
297
|
+
}}
|
|
293
298
|
onKeyDown={(e) => {
|
|
294
299
|
// The caller's handler sees Escape only while this menu is the layer on top: a
|
|
295
300
|
// tooltip on one of its rows takes that press first (round 0.35 R2, the rule
|