@eifi1/ui-kit 0.39.1 → 0.40.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 +8 -7
- package/dist/billing/billing-overview.d.ts +10 -0
- package/dist/components/autocomplete.js +5 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/combobox-core.d.ts +14 -1
- package/dist/components/combobox-core.js +4 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +9 -3
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +5 -0
- package/dist/components/data-table-labels.js +2 -0
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +36 -1
- package/dist/components/data-table-mobile.js +162 -15
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table.d.ts +39 -12
- package/dist/components/data-table.js +74 -13
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/inline-entity-combobox.js +10 -3
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/picker-sheet.d.ts +18 -1
- package/dist/components/picker-sheet.js +20 -2
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/tabs.d.ts +26 -2
- package/dist/components/tabs.js +41 -2
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +2 -1
- package/dist/components/translation-review-parts.js +1 -1
- package/dist/components/translation-review-parts.js.map +1 -1
- package/dist/components/ui-labels.js +3 -1
- package/dist/components/ui-labels.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-comment-thread.d.ts +8 -1
- package/dist/feedback/feedback-comment-thread.js +26 -1
- package/dist/feedback/feedback-comment-thread.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +2 -1
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu-labels.js +3 -1
- package/dist/feedback/feedback-menu-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +31 -24
- package/dist/feedback/feedback-menu.js +16 -7
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/feedback/feedback-public-labels.d.ts +3 -2
- package/dist/feedback/feedback-public-labels.js +1 -1
- package/dist/feedback/feedback-public-labels.js.map +1 -1
- package/dist/feedback/feedback-public-list.js +1 -0
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-public-types.d.ts +16 -3
- package/dist/feedback/feedback-record.d.ts +20 -3
- package/dist/feedback/feedback-record.js +15 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +13 -2
- package/dist/feedback/feedback-replies.js +67 -33
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-rework-entry-chip.d.ts +9 -0
- package/dist/feedback/feedback-rework-entry-chip.js +13 -0
- package/dist/feedback/feedback-rework-entry-chip.js.map +1 -0
- package/dist/feedback/feedback-row-detail.d.ts +31 -8
- package/dist/feedback/feedback-row-detail.js +31 -7
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-table-labels.js +1 -1
- package/dist/feedback/feedback-table-labels.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -2
- package/dist/feedback/feedback-table.js +2 -2
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.js +3 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback/feedback-vocabulary.d.ts +9 -6
- package/dist/feedback/feedback-vocabulary.js +16 -1
- package/dist/feedback/feedback-vocabulary.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +30 -0
- package/dist/hooks/use-long-press.js +77 -0
- package/dist/hooks/use-long-press.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +37 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/german.js +13 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +12 -5
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +13 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +13 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +13 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +13 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +13 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/shell/app-shell.js +9 -0
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/fill-height.d.ts +38 -0
- package/dist/shell/fill-height.js +12 -0
- package/dist/shell/fill-height.js.map +1 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +3 -0
- package/dist/shell.js.map +1 -1
- package/package.json +1 -1
- package/src/billing/billing-overview.ts +10 -0
- package/src/components/autocomplete.tsx +5 -1
- package/src/components/combobox-core.tsx +20 -1
- package/src/components/combobox.tsx +9 -3
- package/src/components/data-table-labels.ts +7 -0
- package/src/components/data-table-mobile.tsx +193 -9
- package/src/components/data-table.tsx +141 -24
- package/src/components/inline-entity-combobox.tsx +12 -4
- package/src/components/picker-sheet.tsx +38 -1
- package/src/components/tabs.tsx +73 -3
- package/src/components/translation-review-parts.tsx +3 -2
- package/src/components/ui-labels.ts +2 -0
- package/src/feedback/feedback-comment-thread.tsx +39 -1
- package/src/feedback/feedback-labels.ts +3 -1
- package/src/feedback/feedback-menu-labels.ts +2 -0
- package/src/feedback/feedback-menu.tsx +48 -30
- package/src/feedback/feedback-public-labels.ts +4 -3
- package/src/feedback/feedback-public-list.tsx +4 -1
- package/src/feedback/feedback-public-types.ts +17 -3
- package/src/feedback/feedback-record.ts +34 -3
- package/src/feedback/feedback-replies.tsx +100 -31
- package/src/feedback/feedback-rework-entry-chip.tsx +18 -0
- package/src/feedback/feedback-row-detail.tsx +65 -12
- package/src/feedback/feedback-table-labels.ts +1 -1
- package/src/feedback/feedback-table.tsx +7 -4
- package/src/feedback/feedback-thread.tsx +5 -1
- package/src/feedback/feedback-vocabulary.ts +27 -6
- package/src/hooks/use-long-press.ts +126 -0
- package/src/hooks/use-overlay-history.ts +74 -3
- package/src/i18n/german.ts +10 -2
- package/src/i18n/kit-labels.tsx +3 -0
- package/src/i18n/locales/es.ts +10 -2
- package/src/i18n/locales/fr.ts +10 -2
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +10 -2
- package/src/i18n/locales/zh.ts +10 -2
- package/src/i18n/shared-labels.ts +1 -1
- package/src/shell/app-shell.tsx +9 -0
- package/src/shell/fill-height.tsx +45 -0
- package/src/shell.ts +3 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ComboboxFieldLabel,
|
|
13
13
|
DISABLED_ROW_CLASS,
|
|
14
14
|
endHintRowProps,
|
|
15
|
+
GROUP_HEADING_STICKY,
|
|
15
16
|
isOptionEnabled,
|
|
16
17
|
stepEnabled,
|
|
17
18
|
SUGGESTION_LIST_CLASS,
|
|
@@ -547,7 +548,10 @@ export function Autocomplete<V extends string | number = string>(props: Autocomp
|
|
|
547
548
|
{startsGroup && (
|
|
548
549
|
<li
|
|
549
550
|
role="presentation"
|
|
550
|
-
className=
|
|
551
|
+
className={cn(
|
|
552
|
+
GROUP_HEADING_STICKY,
|
|
553
|
+
"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1",
|
|
554
|
+
)}
|
|
551
555
|
>
|
|
552
556
|
{o.group}
|
|
553
557
|
</li>
|
|
@@ -51,7 +51,11 @@ export interface ComboOption<V extends string | number> {
|
|
|
51
51
|
* members indented beneath — {@link InlineEntityCombobox} does — this is a genuine
|
|
52
52
|
* grouping, not decoration: the name appears once per section rather than trailing
|
|
53
53
|
* every row in small grey type, which is barely legible on a phone (feedback #136).
|
|
54
|
-
*
|
|
54
|
+
* The heading stays at the top of the list while its rows scroll beneath it.
|
|
55
|
+
*
|
|
56
|
+
* NOT searched (0.40, kk run 79): a group is where a row is filed, not what it is
|
|
57
|
+
* called — with first-letter groups ("A", "B", … "#"), typing "a" matched every row
|
|
58
|
+
* filed under A. Searching a category's name means putting it in `sublabel`.
|
|
55
59
|
*/
|
|
56
60
|
group?: string;
|
|
57
61
|
/**
|
|
@@ -463,6 +467,16 @@ export function useComboboxCore<V extends string | number>({
|
|
|
463
467
|
|
|
464
468
|
/** The anchored suggestion list's box and rows — one look for {@link Combobox},
|
|
465
469
|
* {@link InlineEntityCombobox} and {@link Autocomplete}. */
|
|
470
|
+
/**
|
|
471
|
+
* A group's heading in a picker list stays at the top of the list while its rows scroll
|
|
472
|
+
* under it (kk run 79 gap 3): with first-letter groups over two hundred payees, the
|
|
473
|
+
* heading is the one thing that says where in the alphabet the thumb is. Opaque — the
|
|
474
|
+
* list's own `bg-surface` — so the rows pass beneath it rather than through it. Shared
|
|
475
|
+
* by every list that heads groups (the panel and the sheet, `Combobox`, `Autocomplete`,
|
|
476
|
+
* `InlineEntityCombobox`).
|
|
477
|
+
*/
|
|
478
|
+
export const GROUP_HEADING_STICKY = "sticky top-0 z-10 bg-surface";
|
|
479
|
+
|
|
466
480
|
export const SUGGESTION_LIST_CLASS =
|
|
467
481
|
"max-h-64 overflow-y-auto rounded-md border border-subtle bg-surface py-1 shadow-lg";
|
|
468
482
|
|
|
@@ -964,6 +978,7 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
964
978
|
<li
|
|
965
979
|
role="presentation"
|
|
966
980
|
className={cn(
|
|
981
|
+
GROUP_HEADING_STICKY,
|
|
967
982
|
"font-semibold uppercase tracking-wide text-muted",
|
|
968
983
|
isPhone ? "px-4 pb-1 pt-3 text-xs" : "px-3 pb-0.5 pt-2 text-caption first:pt-1",
|
|
969
984
|
)}
|
|
@@ -1009,6 +1024,10 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
1009
1024
|
onSearchKeyDown={onKeyDown}
|
|
1010
1025
|
listboxId={listboxId}
|
|
1011
1026
|
activeId={activeId}
|
|
1027
|
+
// A multi list stays open as rows are ticked, so the sheet needs a way out
|
|
1028
|
+
// that is not the X in its corner: the kit's sticky "Done" (keksdose #412).
|
|
1029
|
+
// A single pick closes the sheet itself.
|
|
1030
|
+
done={multi}
|
|
1012
1031
|
>
|
|
1013
1032
|
{toolbar}
|
|
1014
1033
|
<div className="relative">
|
|
@@ -8,7 +8,7 @@ import { takeEscape, useDismissLayer } from "../hooks/use-dismiss";
|
|
|
8
8
|
import { isImeEnter } from "../lib/ime";
|
|
9
9
|
import { usePhoneLayout } from "../hooks/use-breakpoint";
|
|
10
10
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
11
|
-
import { ComboboxFieldLabel, endHintRowProps, useActiveOptionScroll } from "./combobox-core";
|
|
11
|
+
import { ComboboxFieldLabel, endHintRowProps, GROUP_HEADING_STICKY, useActiveOptionScroll } from "./combobox-core";
|
|
12
12
|
import { useKitLocale } from "../i18n/kit-labels";
|
|
13
13
|
import { rankMatch } from "../lib/text-match";
|
|
14
14
|
import {
|
|
@@ -574,7 +574,10 @@ export function Combobox(props: ComboboxProps) {
|
|
|
574
574
|
{group !== undefined && (
|
|
575
575
|
<li
|
|
576
576
|
role="presentation"
|
|
577
|
-
className=
|
|
577
|
+
className={cn(
|
|
578
|
+
GROUP_HEADING_STICKY,
|
|
579
|
+
"px-4 pb-0.5 pt-3 text-caption font-semibold uppercase tracking-wide text-muted",
|
|
580
|
+
)}
|
|
578
581
|
>
|
|
579
582
|
{group}
|
|
580
583
|
</li>
|
|
@@ -631,7 +634,10 @@ export function Combobox(props: ComboboxProps) {
|
|
|
631
634
|
{group !== undefined && (
|
|
632
635
|
<li
|
|
633
636
|
role="presentation"
|
|
634
|
-
className=
|
|
637
|
+
className={cn(
|
|
638
|
+
GROUP_HEADING_STICKY,
|
|
639
|
+
"px-3 pb-0.5 pt-2 text-caption font-semibold uppercase tracking-wide text-muted first:pt-1",
|
|
640
|
+
)}
|
|
635
641
|
>
|
|
636
642
|
{group}
|
|
637
643
|
</li>
|
|
@@ -18,6 +18,11 @@ export interface DataTableLabels extends DataTableSortLabels {
|
|
|
18
18
|
filter: string;
|
|
19
19
|
close: string;
|
|
20
20
|
selectRow: string;
|
|
21
|
+
/** The phone card list's way into selection mode for a keyboard and a screen reader
|
|
22
|
+
* (0.40, #421) — a button, visually hidden until focused; a long press is the touch way in. */
|
|
23
|
+
selectRows: string;
|
|
24
|
+
/** Leaves the phone card list's selection mode, clearing the selection (0.40, #421). */
|
|
25
|
+
cancelSelection: string;
|
|
21
26
|
/**
|
|
22
27
|
* The name of an opening row's disclosure button on desktop — visually hidden, in the
|
|
23
28
|
* row's first cell, carrying `aria-expanded` and `aria-controls` (round 0.34, the
|
|
@@ -116,6 +121,8 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
|
116
121
|
filter: "Filter",
|
|
117
122
|
close: "Close",
|
|
118
123
|
selectRow: "Select row",
|
|
124
|
+
selectRows: "Select rows",
|
|
125
|
+
cancelSelection: "Cancel",
|
|
119
126
|
rowDetails: "Details",
|
|
120
127
|
autoSize: "Auto-size columns",
|
|
121
128
|
loading: "Loading…",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useId, useMemo, useState } from "react";
|
|
1
|
+
import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, ChevronRight, Filter } from "lucide-react";
|
|
4
4
|
import { FOCUS_RING } from "./focus-ring";
|
|
@@ -12,6 +12,8 @@ import type { SortCycle, SortState } from "./data-table-sort";
|
|
|
12
12
|
import { FilterPopover } from "./data-table-filter-popover";
|
|
13
13
|
import { SwipeableRow, type SwipeAction } from "./swipeable-row";
|
|
14
14
|
import { DialogFrame } from "./dialog-frame";
|
|
15
|
+
import { Checkbox } from "./checkbox";
|
|
16
|
+
import { useLongPress } from "../hooks/use-long-press";
|
|
15
17
|
import type { DataTableLabels } from "./data-table-labels";
|
|
16
18
|
import { RowLink, cleanAttrs, columnLabel, fromOutside } from "./data-table-shared";
|
|
17
19
|
import type {
|
|
@@ -202,6 +204,20 @@ export interface MobileRowProps<T> {
|
|
|
202
204
|
compact: boolean;
|
|
203
205
|
/** The text size is Large or Extra large (§4). */
|
|
204
206
|
large: boolean;
|
|
207
|
+
/** Phone selection (0.40, keksdose live #421); null without DataTable's `selection`. */
|
|
208
|
+
select: MobileRowSelect | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A phone card's part in DataTable's `selection` (0.40, keksdose live #421). */
|
|
212
|
+
export interface MobileRowSelect {
|
|
213
|
+
/** The card list is in selection mode: the card shows its box and a tap toggles it. */
|
|
214
|
+
active: boolean;
|
|
215
|
+
/** `selection.isSelectable` for this row. */
|
|
216
|
+
selectable: boolean;
|
|
217
|
+
selected: boolean;
|
|
218
|
+
onToggle: (checked: boolean) => void;
|
|
219
|
+
/** A long press outside the mode: enter it with this row selected. */
|
|
220
|
+
onLongPress: () => void;
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
/** One mobile card. Shared by the flat list and the grouped list, so both draw
|
|
@@ -224,8 +240,17 @@ export function MobileRow<T>({
|
|
|
224
240
|
mobileExpandAsDialog,
|
|
225
241
|
compact,
|
|
226
242
|
large,
|
|
243
|
+
select,
|
|
227
244
|
}: MobileRowProps<T>) {
|
|
228
245
|
const expanded = isExpanded?.(row) ?? false;
|
|
246
|
+
// Selection mode (#421): the whole card is the row's checkbox — a tap toggles it
|
|
247
|
+
// instead of opening the row, and the swipe and the long press stand down, so no
|
|
248
|
+
// gesture means two things at once (keksdose's register list, its feedback #153).
|
|
249
|
+
const selecting = !!select?.active;
|
|
250
|
+
// Not on an open card, as in kk's register: its editor and a selection are two modes
|
|
251
|
+
// fighting over the same row.
|
|
252
|
+
const pressable = !!select && select.selectable && !selecting && !expanded;
|
|
253
|
+
const longPress = useLongPress(() => select?.onLongPress(), { enabled: pressable });
|
|
229
254
|
const expansion = expanded ? openedRowContent(row) : null;
|
|
230
255
|
const tint = rowClassName?.(row);
|
|
231
256
|
// Use div+role="button" rather than a real <button> so cells that
|
|
@@ -241,9 +266,12 @@ export function MobileRow<T>({
|
|
|
241
266
|
const opensDetail = interactive && opensRow;
|
|
242
267
|
const swipeActions = swipeActionsFor?.(row);
|
|
243
268
|
const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
|
|
244
|
-
// An
|
|
245
|
-
//
|
|
246
|
-
|
|
269
|
+
// An OPEN card swipes too (0.40, keksdose live #416, Marcel: "faster than scrolling
|
|
270
|
+
// down to the buttons"). Only the card's header — the summary line — is inside
|
|
271
|
+
// SwipeableRow; the opened body below is a sibling, so its fields, sliders and
|
|
272
|
+
// scrolling never meet the gesture. Until 0.40 an open card silently lost it.
|
|
273
|
+
// In selection mode a sideways drag would be a second meaning for a selecting touch.
|
|
274
|
+
const swipeEnabled = !!swipe && !selecting;
|
|
247
275
|
const href = mobileCardLinkable ? rowHref?.(row) : undefined;
|
|
248
276
|
const cardClass = cn(
|
|
249
277
|
"w-full text-start flex items-center",
|
|
@@ -264,7 +292,13 @@ export function MobileRow<T>({
|
|
|
264
292
|
// AppShell main's `overflow-x-clip` — so the frame went missing on one side. A press
|
|
265
293
|
// keeps the fill and the squeeze above; an outline on every tap of a long list would
|
|
266
294
|
// be louder than the acknowledgement it replaces.
|
|
267
|
-
interactive && cn("[-webkit-tap-highlight-color:transparent] focus-visible:ring-inset", FOCUS_RING),
|
|
295
|
+
(interactive || selecting) && cn("[-webkit-tap-highlight-color:transparent] focus-visible:ring-inset", FOCUS_RING),
|
|
296
|
+
selecting && "cursor-pointer",
|
|
297
|
+
select?.selected && selecting && "bg-brand-soft",
|
|
298
|
+
// A 500 ms hold would otherwise start a text selection, and iOS's link callout —
|
|
299
|
+
// which only yields to this CSS. The trade kk made on its register: the card's text
|
|
300
|
+
// is not selectable on a phone where the table offers selection.
|
|
301
|
+
select && "select-none [-webkit-touch-callout:none]",
|
|
268
302
|
);
|
|
269
303
|
// The card's content is written once and worn by either tag below. Note that
|
|
270
304
|
// the primary cell is rendered RAW here, never through `linkColumn` — the card
|
|
@@ -324,7 +358,7 @@ export function MobileRow<T>({
|
|
|
324
358
|
</>
|
|
325
359
|
)}
|
|
326
360
|
</div>
|
|
327
|
-
{opensDetail &&
|
|
361
|
+
{opensDetail && !selecting &&
|
|
328
362
|
// `aria-hidden`: the row already announces itself as a button with
|
|
329
363
|
// `aria-expanded`, so the icon would only add a second, wordless stop.
|
|
330
364
|
(mobileExpandAsDialog ? (
|
|
@@ -344,13 +378,56 @@ export function MobileRow<T>({
|
|
|
344
378
|
))}
|
|
345
379
|
</>
|
|
346
380
|
);
|
|
347
|
-
|
|
348
|
-
|
|
381
|
+
// The long press rides the card itself; the click it leaves behind is swallowed on
|
|
382
|
+
// the `<li>` below, in the capture phase, before the card (or RowLink) sees it.
|
|
383
|
+
// Attached whenever the table selects, the hook's `enabled` deciding whether a press
|
|
384
|
+
// arms: its pointerdown also clears a stale swallow latch, which must happen in the
|
|
385
|
+
// mode too, or a long press whose click never came would eat the first toggle.
|
|
386
|
+
const press = select ? longPress.handlers : undefined;
|
|
387
|
+
const body = selecting ? (
|
|
388
|
+
// In the mode a card is a toggle — kk's register row: `role="button"` with
|
|
389
|
+
// `aria-pressed`, the box beside it only a picture of that state (the card is the
|
|
390
|
+
// one control, so the two can never fight or fire twice).
|
|
391
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- role="button",
|
|
392
|
+
tabIndex 0 and Enter/Space are all here; the rule cannot follow the conditional role. */
|
|
393
|
+
<div
|
|
394
|
+
role={select!.selectable ? "button" : undefined}
|
|
395
|
+
tabIndex={select!.selectable ? 0 : undefined}
|
|
396
|
+
aria-pressed={select!.selectable ? select!.selected : undefined}
|
|
397
|
+
aria-disabled={select!.selectable ? undefined : true}
|
|
398
|
+
data-card-selecting=""
|
|
399
|
+
{...press}
|
|
400
|
+
onClick={(e) => {
|
|
401
|
+
if (!select!.selectable || fromOutside(e.target, e.currentTarget)) return;
|
|
402
|
+
select!.onToggle(!select!.selected);
|
|
403
|
+
}}
|
|
404
|
+
onKeyDown={(e) => {
|
|
405
|
+
if (!select!.selectable || e.target !== e.currentTarget) return;
|
|
406
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
select!.onToggle(!select!.selected);
|
|
409
|
+
}
|
|
410
|
+
}}
|
|
411
|
+
className={cardClass}
|
|
412
|
+
>
|
|
413
|
+
<Checkbox
|
|
414
|
+
readOnly
|
|
415
|
+
tabIndex={-1}
|
|
416
|
+
aria-hidden
|
|
417
|
+
checked={select!.selected}
|
|
418
|
+
disabled={!select!.selectable}
|
|
419
|
+
className="pointer-events-none shrink-0 self-center"
|
|
420
|
+
inputClassName="pointer-events-none"
|
|
421
|
+
/>
|
|
422
|
+
{cardInner}
|
|
423
|
+
</div>
|
|
424
|
+
) : interactive && href ? (
|
|
349
425
|
// A card with a URL of its own is a LINK, not a div wearing role="button"
|
|
350
426
|
// (feedback #451): the phone's long-press "open in new tab" and a screen
|
|
351
427
|
// reader's link semantics both come free, and the fake role goes away.
|
|
352
428
|
<RowLink
|
|
353
429
|
href={href}
|
|
430
|
+
{...press}
|
|
354
431
|
onActivate={() => onRowClick!(row)}
|
|
355
432
|
aria-expanded={opensRow ? expanded : undefined}
|
|
356
433
|
className={cardClass}
|
|
@@ -371,6 +448,7 @@ export function MobileRow<T>({
|
|
|
371
448
|
only attached when `interactive`, and then so are role="button", tabIndex 0 and the
|
|
372
449
|
Enter/Space handler below; the rule cannot follow the conditional role. */
|
|
373
450
|
<div
|
|
451
|
+
{...press}
|
|
374
452
|
role={interactive ? "button" : undefined}
|
|
375
453
|
tabIndex={interactive ? 0 : undefined}
|
|
376
454
|
onClick={
|
|
@@ -400,7 +478,20 @@ export function MobileRow<T>({
|
|
|
400
478
|
</div>
|
|
401
479
|
);
|
|
402
480
|
return (
|
|
403
|
-
<li
|
|
481
|
+
<li
|
|
482
|
+
className={cn(tint)}
|
|
483
|
+
{...cleanAttrs(rowAttributes?.(row))}
|
|
484
|
+
data-selected={selecting && select!.selected ? "" : undefined}
|
|
485
|
+
onClickCapture={
|
|
486
|
+
select
|
|
487
|
+
? (e) => {
|
|
488
|
+
if (!longPress.consumeClick()) return;
|
|
489
|
+
e.preventDefault();
|
|
490
|
+
e.stopPropagation();
|
|
491
|
+
}
|
|
492
|
+
: undefined
|
|
493
|
+
}
|
|
494
|
+
>
|
|
404
495
|
{swipeEnabled ? (
|
|
405
496
|
<SwipeableRow enabled left={swipe!.left} right={swipe!.right}>
|
|
406
497
|
{body}
|
|
@@ -422,6 +513,99 @@ export function MobileRow<T>({
|
|
|
422
513
|
);
|
|
423
514
|
}
|
|
424
515
|
|
|
516
|
+
// ---------- Phone selection bar (0.40, keksdose live #421) ----------
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The card list's way into and out of selection mode for a keyboard and a screen
|
|
520
|
+
* reader — the long press is a touch gesture, and a gesture alone is no path at all.
|
|
521
|
+
*
|
|
522
|
+
* Outside the mode it is one button, "Select rows", visually hidden until it takes
|
|
523
|
+
* focus (SwipeableRow's keyboard buttons, the same rule): the card list keeps the look
|
|
524
|
+
* kk's register has, where the long press is the way in, and Tab or a screen reader's
|
|
525
|
+
* cursor still finds it. In the mode it is a visible row: "Select all rows" — the
|
|
526
|
+
* desktop header's box — and Cancel, which clears the selection and leaves, as the
|
|
527
|
+
* bulk bar's Clear does. The owner's bulk bar is the owner's, as on desktop: it
|
|
528
|
+
* appears because the selection is no longer empty.
|
|
529
|
+
*/
|
|
530
|
+
export function MobileSelectBar({
|
|
531
|
+
active,
|
|
532
|
+
allSelected,
|
|
533
|
+
someSelected,
|
|
534
|
+
compact,
|
|
535
|
+
labels,
|
|
536
|
+
onEnter,
|
|
537
|
+
onToggleAll,
|
|
538
|
+
onCancel,
|
|
539
|
+
}: {
|
|
540
|
+
active: boolean;
|
|
541
|
+
allSelected: boolean;
|
|
542
|
+
someSelected: boolean;
|
|
543
|
+
compact: boolean;
|
|
544
|
+
labels: DataTableLabels;
|
|
545
|
+
onEnter: () => void;
|
|
546
|
+
onToggleAll: (checked: boolean) => void;
|
|
547
|
+
onCancel: () => void;
|
|
548
|
+
}) {
|
|
549
|
+
// The control that was pressed unmounts with the mode it switched, so focus is
|
|
550
|
+
// handed across: into the bar's box on the way in, back to the button on the way out.
|
|
551
|
+
const handOff = useRef(false);
|
|
552
|
+
const allBox = useRef<HTMLInputElement>(null);
|
|
553
|
+
const enterButton = useRef<HTMLButtonElement>(null);
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
if (!handOff.current) return;
|
|
556
|
+
handOff.current = false;
|
|
557
|
+
(active ? allBox.current : enterButton.current)?.focus();
|
|
558
|
+
}, [active]);
|
|
559
|
+
if (!active)
|
|
560
|
+
return (
|
|
561
|
+
<button
|
|
562
|
+
ref={enterButton}
|
|
563
|
+
type="button"
|
|
564
|
+
data-table-mobile-select=""
|
|
565
|
+
onClick={() => {
|
|
566
|
+
handOff.current = true;
|
|
567
|
+
onEnter();
|
|
568
|
+
}}
|
|
569
|
+
className={cn(
|
|
570
|
+
"sr-only rounded-md text-sm font-medium text-secondary",
|
|
571
|
+
// Padding only once shown: unconditional, it beats sr-only's `padding: 0`
|
|
572
|
+
// (SwipeableRow's 0.32.1 note).
|
|
573
|
+
"focus:not-sr-only focus:m-2 focus:inline-flex focus:px-2 focus:py-1",
|
|
574
|
+
FOCUS_RING,
|
|
575
|
+
)}
|
|
576
|
+
>
|
|
577
|
+
{labels.selectRows}
|
|
578
|
+
</button>
|
|
579
|
+
);
|
|
580
|
+
return (
|
|
581
|
+
<div
|
|
582
|
+
data-table-mobile-select=""
|
|
583
|
+
className={cn(
|
|
584
|
+
"flex flex-wrap items-center justify-between gap-2 border-b border-subtle",
|
|
585
|
+
compact ? "px-3 py-1.5" : "px-4 py-2",
|
|
586
|
+
)}
|
|
587
|
+
>
|
|
588
|
+
<Checkbox
|
|
589
|
+
ref={allBox}
|
|
590
|
+
label={labels.selectAllRows}
|
|
591
|
+
checked={allSelected}
|
|
592
|
+
indeterminate={someSelected && !allSelected}
|
|
593
|
+
onChange={(e) => onToggleAll(e.target.checked)}
|
|
594
|
+
/>
|
|
595
|
+
<button
|
|
596
|
+
type="button"
|
|
597
|
+
onClick={() => {
|
|
598
|
+
handOff.current = true;
|
|
599
|
+
onCancel();
|
|
600
|
+
}}
|
|
601
|
+
className={cn("rounded-md px-2 py-1 text-sm text-muted hover:text-secondary", FOCUS_RING)}
|
|
602
|
+
>
|
|
603
|
+
{labels.cancelSelection}
|
|
604
|
+
</button>
|
|
605
|
+
</div>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
425
609
|
// ---------- Mobile filter sheet ----------
|
|
426
610
|
|
|
427
611
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
|
3
3
|
import { Card } from "./card";
|
|
4
4
|
import { Spinner } from "./spinner";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
resolveDataTableLabels,
|
|
15
15
|
splitCellProps,
|
|
16
16
|
} from "./data-table-shared";
|
|
17
|
-
import { MobileFilters, MobileRow, MobileSort, StackedFields } from "./data-table-mobile";
|
|
17
|
+
import { MobileFilters, MobileRow, MobileSelectBar, MobileSort, StackedFields } from "./data-table-mobile";
|
|
18
18
|
import { DataTableHead } from "./data-table-head";
|
|
19
19
|
import { ColumnSettingsPanel } from "./data-table-column-settings";
|
|
20
20
|
import { useLargeText } from "../hooks/use-large-text";
|
|
@@ -236,10 +236,26 @@ export interface DataTableProps<T> {
|
|
|
236
236
|
urlSync?: TableUrlSync;
|
|
237
237
|
/**
|
|
238
238
|
* When true, the table fills its parent's height and scrolls INTERNALLY, with the
|
|
239
|
-
* header pinned — so the page itself doesn't add a second, outer scrollbar.
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
239
|
+
* header pinned — so the page itself doesn't add a second, outer scrollbar. The parent
|
|
240
|
+
* must give it a bounded height (e.g. a flex column inside the viewport-locked app
|
|
241
|
+
* shell). See feedback #207.
|
|
242
|
+
*
|
|
243
|
+
* **The list page** (0.40, docs/page-layout-harmonization.md §1): the root carries
|
|
244
|
+
* `data-fill-height`, and from `md` up AppShell's `<main>` turns into a flex column
|
|
245
|
+
* while such a table is inside — the shell's banners keep their height, the page takes
|
|
246
|
+
* the rest. Wrap every element between `<main>` and the table in `FillHeight` (or merge
|
|
247
|
+
* `FILL_HEIGHT_CLASS`); nothing else to wire. The body never shrinks below `basis-48`
|
|
248
|
+
* (12rem, about five rows): on a window too short for that, `<main>` scrolls again.
|
|
249
|
+
*
|
|
250
|
+
* **On a phone** the card list becomes the scroller ONLY when its parent really bounds
|
|
251
|
+
* it — measured, not assumed (0.40, keksdose live #420): the list takes the flex
|
|
252
|
+
* room it is given, and only when its rows overflow that room does it turn
|
|
253
|
+
* `overflow-y-auto overscroll-contain`, with the filter button pinned above. Under an
|
|
254
|
+
* unbounded parent — `AppShell` on a phone, where the DOCUMENT scrolls — it stays in
|
|
255
|
+
* the page's flow and grows to its content. Until 0.40 it was a scroller either way,
|
|
256
|
+
* and a scroller with nothing to scroll and `overscroll-contain` swallowed the page's
|
|
257
|
+
* own scroll: a swipe that started on a card moved nothing. So `fillHeight` is safe
|
|
258
|
+
* to pass at every width; `fillHeight={useBreakpoint("md")}` is no longer needed.
|
|
243
259
|
*
|
|
244
260
|
* The cure for TWO SCROLLBARS (round 0.34 C14, kastlan feedback 55): the default
|
|
245
261
|
* `maxBodyHeight` scrolls the body inside a page that scrolls too. How a page fills
|
|
@@ -263,14 +279,13 @@ export interface DataTableProps<T> {
|
|
|
263
279
|
* the cap on a page with anything else on it shows two scrollbars side by side,
|
|
264
280
|
* and the wheel moves whichever is under the pointer. For a list page whose table
|
|
265
281
|
* is the point, fill the height instead, from `md` up (a phone scrolls the page
|
|
266
|
-
* and its card list as one)
|
|
282
|
+
* and its card list as one) — docs/page-layout-harmonization.md §1.2 says which
|
|
283
|
+
* pages fill:
|
|
267
284
|
*
|
|
268
|
-
* 1.
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* `data-fill-height` and `md:flex md:min-h-0 md:flex-1 md:flex-col`;
|
|
273
|
-
* 3. the table takes the rest: `<DataTable fillHeight>`.
|
|
285
|
+
* 1. `<DataTable fillHeight>`: the table marks itself (`data-fill-height`), and
|
|
286
|
+
* AppShell's `<main>` becomes a flex column while it is inside;
|
|
287
|
+
* 2. each element between `<main>` and the table is a `FillHeight` (the page's
|
|
288
|
+
* root, any wrapper on the way), or merges `FILL_HEIGHT_CLASS`.
|
|
274
289
|
*
|
|
275
290
|
* Only the body then scrolls, under its pinned header, and every other page grows
|
|
276
291
|
* with its content as before (kastlan e3985c9, its leases list). A short table that
|
|
@@ -313,11 +328,23 @@ export interface DataTableProps<T> {
|
|
|
313
328
|
pageSize?: number;
|
|
314
329
|
onPageSizeChange?: (pageSize: number) => void;
|
|
315
330
|
/**
|
|
316
|
-
* Opt-in multi-row selection
|
|
331
|
+
* Opt-in multi-row selection. When provided, a leading
|
|
317
332
|
* checkbox column is rendered with a select-all box in the header; the owner
|
|
318
333
|
* holds the selected set and reacts via the callbacks. Rows for which
|
|
319
334
|
* `isSelectable` returns false render no checkbox and are excluded from
|
|
320
335
|
* select-all. See feedback #285 (bulk edit).
|
|
336
|
+
*
|
|
337
|
+
* **On a phone** (0.40, keksdose live #421) the cards enter a selection MODE: a long
|
|
338
|
+
* press on a card (500 ms, keksdose's register gesture) selects it and turns every
|
|
339
|
+
* card into a toggle with its box showing; a tap then selects or deselects. A
|
|
340
|
+
* keyboard or screen reader enters through "Select rows" (`labels.selectRows`),
|
|
341
|
+
* visually hidden until focused; in the mode a bar above the cards holds "Select all
|
|
342
|
+
* rows" and Cancel (`labels.cancelSelection`). The mode lasts while anything is
|
|
343
|
+
* selected — the bulk bar's Clear, the last box unticked or Cancel ends it — and swipe
|
|
344
|
+
* actions rest while it is on. Render the owner's `BulkActionBar` as on desktop; it
|
|
345
|
+
* appears with the first selected row. With `selection` the cards' text is not
|
|
346
|
+
* selectable on a phone, and a linked card's long press selects instead of opening
|
|
347
|
+
* the browser's link menu.
|
|
321
348
|
*/
|
|
322
349
|
selection?: {
|
|
323
350
|
isSelectable?: (row: T) => boolean;
|
|
@@ -1189,6 +1216,30 @@ export function DataTable<T>({
|
|
|
1189
1216
|
if (d !== mobileDir) setMobileDir(d);
|
|
1190
1217
|
};
|
|
1191
1218
|
|
|
1219
|
+
// `fillHeight` on a phone (0.40, keksdose live #420): the card list is a scroller only
|
|
1220
|
+
// while its parent bounds it — its rows overflow the flex room it was given. Under
|
|
1221
|
+
// AppShell's phone layout the DOCUMENT scrolls, the list's room is its own content,
|
|
1222
|
+
// and a scroller there (with `overscroll-contain`) only swallowed the page's scroll.
|
|
1223
|
+
// Measured before paint on every render (rows revealed, filtered, opened), and on a
|
|
1224
|
+
// resize of the list or its parent; one read of two heights.
|
|
1225
|
+
const phoneFill = fillHeight && !isMdUp;
|
|
1226
|
+
const phoneListRef = useRef<HTMLUListElement | null>(null);
|
|
1227
|
+
const [phoneListScrolls, setPhoneListScrolls] = useState(false);
|
|
1228
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- every render on purpose: what overflows is the rows, and React bails out on an unchanged boolean
|
|
1229
|
+
useLayoutEffect(() => {
|
|
1230
|
+
const el = phoneListRef.current;
|
|
1231
|
+
if (!phoneFill || !el) return;
|
|
1232
|
+
setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1);
|
|
1233
|
+
});
|
|
1234
|
+
useEffect(() => {
|
|
1235
|
+
const el = phoneListRef.current;
|
|
1236
|
+
if (!phoneFill || !el || typeof ResizeObserver === "undefined") return;
|
|
1237
|
+
const ro = new ResizeObserver(() => setPhoneListScrolls(el.scrollHeight > el.clientHeight + 1));
|
|
1238
|
+
ro.observe(el);
|
|
1239
|
+
if (el.parentElement) ro.observe(el.parentElement);
|
|
1240
|
+
return () => ro.disconnect();
|
|
1241
|
+
}, [phoneFill]);
|
|
1242
|
+
|
|
1192
1243
|
const { mobileSlice, canRevealMoreMobile, loadMoreRef } = useMobileReveal({
|
|
1193
1244
|
rows: sorted,
|
|
1194
1245
|
slice,
|
|
@@ -1197,6 +1248,23 @@ export function DataTable<T>({
|
|
|
1197
1248
|
unpaged,
|
|
1198
1249
|
defaultPageSize,
|
|
1199
1250
|
});
|
|
1251
|
+
// ---- Phone selection mode (0.40, keksdose live #421) ----
|
|
1252
|
+
// The cards had no boxes, so `selection`'s bulk actions were unreachable on a phone.
|
|
1253
|
+
// A long press on a card (kk's register gesture) — or "Select rows", the keyboard's
|
|
1254
|
+
// and screen reader's way — enters the mode: every card shows its box and a tap
|
|
1255
|
+
// toggles it. The mode lasts while anything is selected, so emptying the selection
|
|
1256
|
+
// (the bulk bar's Clear, the last box unticked, Cancel) leaves it, as kk's does.
|
|
1257
|
+
const anySelected =
|
|
1258
|
+
!!selection && (selection.someSelected || (selection.allSelected && slice.length > 0));
|
|
1259
|
+
const [phoneSelectMode, setPhoneSelectMode] = useState(false);
|
|
1260
|
+
const [hadSelection, setHadSelection] = useState(anySelected);
|
|
1261
|
+
if (anySelected !== hadSelection) {
|
|
1262
|
+
// Adjusted while rendering, so the frame the selection empties is already out of
|
|
1263
|
+
// the mode (an effect would paint a frame of boxes first).
|
|
1264
|
+
setHadSelection(anySelected);
|
|
1265
|
+
if (!anySelected) setPhoneSelectMode(false);
|
|
1266
|
+
}
|
|
1267
|
+
const phoneSelecting = !!selection && !isMdUp && (phoneSelectMode || anySelected);
|
|
1200
1268
|
// ---- Shift+click range selection ---- (data-table-selection.ts; feedback #289,
|
|
1201
1269
|
// anchored on the row's KEY so a sort, filter or page change cannot move it)
|
|
1202
1270
|
const { selectionAnchor, rangeClick, canSelect, selectRange } = useRangeSelection({
|
|
@@ -1268,6 +1336,24 @@ export function DataTable<T>({
|
|
|
1268
1336
|
mobileExpandAsDialog={mobileExpandAsDialog}
|
|
1269
1337
|
compact={compact}
|
|
1270
1338
|
large={large}
|
|
1339
|
+
select={
|
|
1340
|
+
selection
|
|
1341
|
+
? {
|
|
1342
|
+
active: phoneSelecting,
|
|
1343
|
+
selectable: canSelect(row),
|
|
1344
|
+
selected: selection.isSelected(row),
|
|
1345
|
+
onToggle: (checked) => {
|
|
1346
|
+
selectionAnchor.current = rowKey(row);
|
|
1347
|
+
selection.onToggle(row, checked);
|
|
1348
|
+
},
|
|
1349
|
+
onLongPress: () => {
|
|
1350
|
+
setPhoneSelectMode(true);
|
|
1351
|
+
selectionAnchor.current = rowKey(row);
|
|
1352
|
+
if (!selection.isSelected(row)) selection.onToggle(row, true);
|
|
1353
|
+
},
|
|
1354
|
+
}
|
|
1355
|
+
: null
|
|
1356
|
+
}
|
|
1271
1357
|
/>
|
|
1272
1358
|
);
|
|
1273
1359
|
|
|
@@ -1299,13 +1385,19 @@ export function DataTable<T>({
|
|
|
1299
1385
|
// classes and a table-cell tooltip must portal under test just as it does in the
|
|
1300
1386
|
// browser (see `CLIPS_ATTRIBUTE`). The bare root clips nothing and is not marked.
|
|
1301
1387
|
data-clips={frame ? "" : undefined}
|
|
1388
|
+
// The fill's marker (0.40, docs/page-layout-harmonization.md §1.3): AppShell's
|
|
1389
|
+
// `<main>` and every `FillHeight` between it and this table key off it with
|
|
1390
|
+
// `has-[[data-fill-height]]`, so a page asks for the fill by passing the prop.
|
|
1391
|
+
data-fill-height={fillHeight ? "" : undefined}
|
|
1302
1392
|
className={cn(
|
|
1303
1393
|
"group/table",
|
|
1304
1394
|
frame ? "overflow-clip" : "min-w-0",
|
|
1305
1395
|
// Both layouts, not just the table: the phone card list overflowing a bounded
|
|
1306
1396
|
// pane drew itself over whatever followed it, and in a viewport-locked shell
|
|
1307
|
-
// the rows below the fold could not be reached at all.
|
|
1308
|
-
|
|
1397
|
+
// the rows below the fold could not be reached at all. From `md` the minimum is
|
|
1398
|
+
// the content's again (`md:min-h-auto`): the body's floor below then holds the
|
|
1399
|
+
// whole chain up to `<main>`, which scrolls instead of leaving a slit.
|
|
1400
|
+
fillHeight && "flex flex-1 flex-col min-h-0 md:min-h-auto",
|
|
1309
1401
|
className,
|
|
1310
1402
|
)}
|
|
1311
1403
|
>
|
|
@@ -1373,14 +1465,33 @@ export function DataTable<T>({
|
|
|
1373
1465
|
}}
|
|
1374
1466
|
/>
|
|
1375
1467
|
)}
|
|
1468
|
+
{selection && (
|
|
1469
|
+
<MobileSelectBar
|
|
1470
|
+
active={phoneSelecting}
|
|
1471
|
+
allSelected={selection.allSelected}
|
|
1472
|
+
someSelected={selection.someSelected}
|
|
1473
|
+
compact={compact}
|
|
1474
|
+
labels={labels}
|
|
1475
|
+
onEnter={() => setPhoneSelectMode(true)}
|
|
1476
|
+
onToggleAll={selection.onToggleAll}
|
|
1477
|
+
onCancel={() => {
|
|
1478
|
+
setPhoneSelectMode(false);
|
|
1479
|
+
if (anySelected) selection.onToggleAll(false);
|
|
1480
|
+
}}
|
|
1481
|
+
/>
|
|
1482
|
+
)}
|
|
1376
1483
|
<ul
|
|
1484
|
+
ref={phoneListRef}
|
|
1377
1485
|
className={cn(
|
|
1378
1486
|
"divide-y divide-subtle",
|
|
1379
|
-
// Under `fillHeight` the list
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
|
|
1487
|
+
// Under `fillHeight` the list takes the room its parent gives it, and in a
|
|
1488
|
+
// BOUNDED parent it is the scroller, as the table body is on a wide screen:
|
|
1489
|
+
// the filter button stays above it and a server pager below. Only then —
|
|
1490
|
+
// see `phoneListScrolls` (live #420). The endless-scroll sentinel still
|
|
1491
|
+
// fires in here — an IntersectionObserver on the viewport sees through the
|
|
1492
|
+
// clipping ancestor.
|
|
1493
|
+
fillHeight && "min-h-0 flex-1",
|
|
1494
|
+
fillHeight && phoneListScrolls && "overflow-y-auto overscroll-contain",
|
|
1384
1495
|
// Same busy treatment as the desktop body; see `isLoading` below.
|
|
1385
1496
|
isLoading && mobileSlice.length > 0 && "opacity-60 transition-opacity",
|
|
1386
1497
|
)}
|
|
@@ -1490,8 +1601,11 @@ export function DataTable<T>({
|
|
|
1490
1601
|
</div>
|
|
1491
1602
|
)}
|
|
1492
1603
|
{isMdUp && (
|
|
1493
|
-
<div className={cn("flex", fillHeight && "
|
|
1494
|
-
|
|
1604
|
+
<div className={cn("flex", fillHeight && "flex-1")}>
|
|
1605
|
+
{/* Under `fillHeight` neither wrapper is `min-h-0`: a flex item's automatic
|
|
1606
|
+
minimum is its content's, and the content's height counts the body at its
|
|
1607
|
+
basis (`basis-48`, below), not at its rows — so the floor carries up. */}
|
|
1608
|
+
<div className={cn("relative min-w-0 flex-1", fillHeight && "flex flex-col")}>
|
|
1495
1609
|
{/* scrollbar-gutter:stable reserves the vertical-scrollbar space up
|
|
1496
1610
|
front. Expanding a row adds height, which can bring the scrollbar
|
|
1497
1611
|
in, which narrows the content box — and in an `auto` table layout
|
|
@@ -1502,7 +1616,10 @@ export function DataTable<T>({
|
|
|
1502
1616
|
ref={desktopScroller}
|
|
1503
1617
|
// The scroller: marked for the Tooltip's auto-portal (see the root above).
|
|
1504
1618
|
data-clips=""
|
|
1505
|
-
|
|
1619
|
+
// Under `fillHeight` the body grows into the room from a 12rem basis (0.40,
|
|
1620
|
+
// §1.3): about five rows is its floor. A window too short for the page's
|
|
1621
|
+
// header and that much body scrolls `<main>` instead (keksdose's review).
|
|
1622
|
+
className={cn("overflow-auto [scrollbar-gutter:stable]", fillHeight && "min-h-0 grow basis-48")}
|
|
1506
1623
|
// `edgeFade`: the mask, `data-overflow` and the focus handling; nothing
|
|
1507
1624
|
// while off. The height bound is merged over the mask, not replaced by it.
|
|
1508
1625
|
{...edgeFadeProps}
|