@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
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
HOVER_INK,
|
|
26
26
|
ICON_BUTTON_PRESSED,
|
|
27
27
|
RenderedButtonLink,
|
|
28
|
+
HIT_AREA_LARGE,
|
|
28
29
|
TOUCH_TARGET_LARGE,
|
|
29
30
|
buttonVariantClasses,
|
|
30
31
|
} from "./button-look";
|
|
@@ -106,7 +107,7 @@ const ICON_BUTTON_TEXT_STRETCH_SIZES: Record<IconButtonSize, string> = {
|
|
|
106
107
|
// (2.75rem is 55 px at Large). `md` … `xs` grow their box to it. `2xs` is the action ON
|
|
107
108
|
// a chip or a tab, whose box cannot grow without growing the chip: it keeps its box and
|
|
108
109
|
// gets a 48 px hit area instead — an invisible `::after` centred on it, which takes the
|
|
109
|
-
// taps a finger lands round the glyph.
|
|
110
|
+
// taps a finger lands round the glyph (`HIT_AREA_LARGE`).
|
|
110
111
|
const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
|
|
111
112
|
"2xl": "",
|
|
112
113
|
xl: "",
|
|
@@ -114,8 +115,7 @@ const ICON_BUTTON_TOUCH_LARGE: Record<IconButtonSize, string> = {
|
|
|
114
115
|
md: TOUCH_TARGET_LARGE,
|
|
115
116
|
sm: TOUCH_TARGET_LARGE,
|
|
116
117
|
xs: TOUCH_TARGET_LARGE,
|
|
117
|
-
"2xs":
|
|
118
|
-
"relative large:after:absolute large:after:inset-[calc((100%-48px)/2)] large:after:content-['']",
|
|
118
|
+
"2xs": HIT_AREA_LARGE,
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
@@ -157,7 +157,9 @@ export function Popover({
|
|
|
157
157
|
}
|
|
158
158
|
: null;
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
// A press in a list opened from inside — a CurrencySelect's, a combobox's, another
|
|
161
|
+
// popover's — reaches the panel through the React tree and is inside (dev#623).
|
|
162
|
+
const markInside = useOutsideClick([triggerRef, panelRef], close, open);
|
|
161
163
|
// A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes
|
|
162
164
|
// first, and the dialog this popover sits in only after it — the calculator on an
|
|
163
165
|
// amount field in a Modal is exactly that.
|
|
@@ -216,6 +218,10 @@ export function Popover({
|
|
|
216
218
|
// silently do nothing on a panel whose children are not yet tabbable.
|
|
217
219
|
tabIndex={-1}
|
|
218
220
|
onKeyDown={handleKeyDown}
|
|
221
|
+
onPointerDownCapture={(e) => {
|
|
222
|
+
rest.onPointerDownCapture?.(e);
|
|
223
|
+
markInside(e);
|
|
224
|
+
}}
|
|
219
225
|
style={{
|
|
220
226
|
// Merged over the caller's, never replaced by it: these are measured
|
|
221
227
|
// against the trigger every scroll and resize, and a panel that dropped
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
4
|
+
import { Skeleton } from "./skeleton";
|
|
5
|
+
|
|
6
|
+
export interface TableSkeletonProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
7
|
+
/** Rows per group (or in all, without `groups`). Default 5. */
|
|
8
|
+
rows?: number;
|
|
9
|
+
/** Columns, the name column included. Default 3. */
|
|
10
|
+
columns?: number;
|
|
11
|
+
/**
|
|
12
|
+
* `list` (default): a name bar of varying width, then the other columns as figures at
|
|
13
|
+
* the row's end — from three columns up they go under the name on a phone, as a card
|
|
14
|
+
* list draws them. `grid`: a label column, then one equal block per column, clipped
|
|
15
|
+
* at the edge — a heatmap or a matrix.
|
|
16
|
+
*/
|
|
17
|
+
variant?: "list" | "grid";
|
|
18
|
+
/** This many group header bars, each over `rows` rows (a budget's groups). Default none. */
|
|
19
|
+
groups?: number;
|
|
20
|
+
/** A row of this many stat-tile outlines above the panel. Default none. */
|
|
21
|
+
tiles?: number;
|
|
22
|
+
/** A title and a hint line at the top of the panel, as a card's head. */
|
|
23
|
+
heading?: boolean;
|
|
24
|
+
/** The panel's border and surface. Default `true`; `false` for a panel the caller frames. */
|
|
25
|
+
framed?: boolean;
|
|
26
|
+
/** What is said, once. Default `common.loading` ("Loading…") from the provider. */
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Name widths that differ from row to row, so the outline reads as a list rather than
|
|
31
|
+
* a striped block. */
|
|
32
|
+
const NAME_WIDTHS = ["w-2/5", "w-1/2", "w-1/3", "w-5/12"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The outline of a table or a list while its data loads (0.48, keksdose dev #628) — so a
|
|
36
|
+
* page does not jump from a centred spinner to a long list when the data lands.
|
|
37
|
+
* keksdose hand-built three of these (TabSkeleton, BudgetSkeleton,
|
|
38
|
+
* VarianceHeatmapSkeleton), each a stack of `Skeleton`s with its own sr-only line.
|
|
39
|
+
*
|
|
40
|
+
* ONE `role="status"` region says "Loading…" once; every placeholder inside is
|
|
41
|
+
* `aria-hidden` — not one announcement per row. The pulse stops under reduced motion
|
|
42
|
+
* (`Skeleton`'s own rule); the shapes stay. Props and `ref` go to the region.
|
|
43
|
+
*/
|
|
44
|
+
export function TableSkeleton({
|
|
45
|
+
rows = 5,
|
|
46
|
+
columns = 3,
|
|
47
|
+
variant = "list",
|
|
48
|
+
groups = 0,
|
|
49
|
+
tiles = 0,
|
|
50
|
+
heading = false,
|
|
51
|
+
framed = true,
|
|
52
|
+
label,
|
|
53
|
+
className,
|
|
54
|
+
...rest
|
|
55
|
+
}: TableSkeletonProps) {
|
|
56
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
57
|
+
const figures = Math.max(0, columns - 1);
|
|
58
|
+
const grid = variant === "grid";
|
|
59
|
+
// Three figures don't fit beside a name on a phone; one or two do.
|
|
60
|
+
const stack = !grid && figures >= 2;
|
|
61
|
+
|
|
62
|
+
const row = (key: number) =>
|
|
63
|
+
grid ? (
|
|
64
|
+
<div key={key} className="flex items-center gap-1" data-skeleton-row="">
|
|
65
|
+
<Skeleton className="h-7 w-28 shrink-0" />
|
|
66
|
+
{Array.from({ length: figures }, (_, c) => (
|
|
67
|
+
<Skeleton key={c} className="h-7 w-12 shrink-0" />
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
<div
|
|
72
|
+
key={key}
|
|
73
|
+
className={cn("flex items-center gap-4 px-4 py-3", stack && "max-sm:flex-col max-sm:items-stretch max-sm:gap-2")}
|
|
74
|
+
data-skeleton-row=""
|
|
75
|
+
>
|
|
76
|
+
<Skeleton className={cn("h-4", NAME_WIDTHS[key % NAME_WIDTHS.length])} />
|
|
77
|
+
{figures > 0 && (
|
|
78
|
+
<div className={cn("ms-auto flex gap-6", stack && "max-sm:ms-0 max-sm:justify-between max-sm:gap-2")}>
|
|
79
|
+
{Array.from({ length: figures }, (_, f) => (
|
|
80
|
+
<Skeleton key={f} className="h-4 w-16 md:w-24" />
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const block = (offset: number) => (
|
|
88
|
+
<div className={grid ? "space-y-1 overflow-hidden" : "divide-y divide-subtle"}>
|
|
89
|
+
{Array.from({ length: rows }, (_, r) => row(offset + r))}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
// `relative`: the containing block for the sr-only text (sr-only-containment.test).
|
|
95
|
+
<div {...rest} role="status" className={cn("relative", className)} data-skeleton-preset="table">
|
|
96
|
+
<span className="sr-only">{label ?? common.loading}</span>
|
|
97
|
+
<div aria-hidden="true" className="flex flex-col gap-4">
|
|
98
|
+
{tiles > 0 && (
|
|
99
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
|
|
100
|
+
{Array.from({ length: tiles }, (_, i) => (
|
|
101
|
+
<div key={i} className="rounded-lg border border-subtle bg-surface p-4" data-skeleton-tile="">
|
|
102
|
+
<Skeleton className="h-3 w-24 max-w-full" />
|
|
103
|
+
<Skeleton className="mt-3 h-7 w-32 max-w-full" />
|
|
104
|
+
</div>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
<div
|
|
109
|
+
className={cn("overflow-clip", framed && "rounded-lg border border-subtle bg-surface", grid && "p-3")}
|
|
110
|
+
data-skeleton-panel=""
|
|
111
|
+
>
|
|
112
|
+
{heading && (
|
|
113
|
+
<div className={cn(grid ? "mb-3" : "px-4 pt-4 pb-2")} data-skeleton-heading="">
|
|
114
|
+
<Skeleton className="h-4 w-40 max-w-full" />
|
|
115
|
+
<Skeleton className="mt-2 h-3 w-64 max-w-full" />
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
{groups > 0
|
|
119
|
+
? Array.from({ length: groups }, (_, g) => (
|
|
120
|
+
<div key={g} className={cn(g > 0 && "border-t border-subtle")}>
|
|
121
|
+
<div className="flex items-center gap-3 bg-surface-2 px-4 py-2.5" data-skeleton-group="">
|
|
122
|
+
<Skeleton className={cn("h-4", g % 2 ? "w-24" : "w-28")} />
|
|
123
|
+
<Skeleton className="ms-auto h-4 w-12" />
|
|
124
|
+
</div>
|
|
125
|
+
{block(g * rows)}
|
|
126
|
+
</div>
|
|
127
|
+
))
|
|
128
|
+
: block(0)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
|
|
|
11
11
|
import { mergeRefs } from "./field-parts";
|
|
12
12
|
import { DEFAULT_TABS_LABELS } from "./ui-labels";
|
|
13
13
|
import { IconButton } from "./icon-button";
|
|
14
|
+
import { HIT_AREA_LARGE } from "./button-look";
|
|
14
15
|
|
|
15
16
|
/** One tab of a {@link Tabs} strip. */
|
|
16
17
|
export interface TabItem<T extends string> {
|
|
@@ -48,16 +49,10 @@ export interface TabsLabels {
|
|
|
48
49
|
add: string;
|
|
49
50
|
/** Accessible name of a tab's × — "Remove <tab>". */
|
|
50
51
|
remove: (tab: string) => string;
|
|
51
|
-
/** The `overflowCue` ›
|
|
52
|
+
/** The `overflowCue` › after the strip's end, while tabs lie past it: "More tabs". */
|
|
52
53
|
more: string;
|
|
53
|
-
/** The `overflowCue` ‹
|
|
54
|
+
/** The `overflowCue` ‹ before the strip's start, while tabs lie before it: "Previous tabs". */
|
|
54
55
|
previous: string;
|
|
55
|
-
/**
|
|
56
|
-
* @deprecated 0.47: not drawn any more — the caret that turned into "back to the
|
|
57
|
-
* first tab" at the strip's end is now a ‹ of its own (`previous`). Accepted and
|
|
58
|
-
* ignored; removed in 0.48.
|
|
59
|
-
*/
|
|
60
|
-
backToFirst?: string;
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
// Defined in ./ui-labels, which holds no component code (0.34, KS U8).
|
|
@@ -84,8 +79,8 @@ export interface TabsProps<T extends string>
|
|
|
84
79
|
onChange: (id: T) => void;
|
|
85
80
|
className?: string;
|
|
86
81
|
/**
|
|
87
|
-
* Carets
|
|
88
|
-
* live #410 round 4): a ›
|
|
82
|
+
* Carets at the ends of a SCROLLING strip (0.40, kk run 79 gap 1; 0.47, keksdose
|
|
83
|
+
* live #410 round 4): a › after the strip's end while tabs lie past it, and a ‹ before
|
|
89
84
|
* its start while tabs lie before it — each there only while something is hidden
|
|
90
85
|
* past ITS edge (1 px slack, so a few cut-off pixels count), each a step of three
|
|
91
86
|
* quarters of the strip's width toward its own side. Neither ever changes direction:
|
|
@@ -94,8 +89,12 @@ export interface TabsProps<T extends string>
|
|
|
94
89
|
*
|
|
95
90
|
* The edge fade alone could read as the end of the row: the last visible tab can
|
|
96
91
|
* finish exactly where the fade begins, so nothing looks cut off (keksdose live #410).
|
|
97
|
-
*
|
|
98
|
-
* it,
|
|
92
|
+
* Since 0.48 (live #410 rounds 5–6) the carets are bare glyphs BESIDE the strip — the ‹
|
|
93
|
+
* before it, the › after it — centred on the labels, muted and primary on hover, with
|
|
94
|
+
* no frame, fill or shadow; until 0.47 they were framed buttons laid over the strip's
|
|
95
|
+
* last visible tabs. While the strip overflows at all it keeps a 24px gutter at both
|
|
96
|
+
* ends, and each caret sits in its own, so scrolling never changes the strip's width
|
|
97
|
+
* or moves its labels; a strip that fits has no gutters. Out of the tab order —
|
|
99
98
|
* the keyboard walks the strip with the arrow keys, which bring the focused tab into
|
|
100
99
|
* view. At every width. Ignored on a vertical strip (a side nav is a column, not a
|
|
101
100
|
* row). Names: `tabs.more` / `tabs.previous`. keksdose's `scroll-edge-cues.tsx` is
|
|
@@ -262,7 +261,7 @@ export function Tabs<T extends string>({
|
|
|
262
261
|
const stripRef = useRef<HTMLDivElement>(null);
|
|
263
262
|
const setStrip = useMemo(() => mergeRefs(stripRef, ref), [ref]);
|
|
264
263
|
const fade = useStripFade(stripRef);
|
|
265
|
-
// See `overflowCue`: a ‹
|
|
264
|
+
// See `overflowCue`: a ‹ before the strip while tabs lie before it, a › after it
|
|
266
265
|
// while tabs lie past it — `fade.overflow` names the cut ends in reading-direction terms.
|
|
267
266
|
const cue = overflowCue && !vertical;
|
|
268
267
|
const cueStart = cue && (fade.overflow === "start" || fade.overflow === "both");
|
|
@@ -276,27 +275,56 @@ export function Tabs<T extends string>({
|
|
|
276
275
|
const toward = (side === "end" ? 1 : -1) * (rtl ? -1 : 1);
|
|
277
276
|
strip.scrollBy({ left: toward * Math.round(strip.clientWidth * 0.75), behavior });
|
|
278
277
|
};
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
278
|
+
// In a GUTTER of its own at the strip's end (0.48, keksdose live #410 rounds 5–6): a
|
|
279
|
+
// bare glyph — no frame, fill or shadow — muted until the pointer comes. Round 5 put
|
|
280
|
+
// each cue in the flow beside the strip, so it took 24px the moment it appeared and the
|
|
281
|
+
// labels jumped sideways at every edge the scroll crossed ("It has problems with moving
|
|
282
|
+
// to left and right especially when moving the scroll bar"). Now the strip box keeps a
|
|
283
|
+
// 24px gutter at BOTH ends for as long as the strip overflows at all (`px-[24px]`, see
|
|
284
|
+
// `stripBox`), and each gutter is absolute in that padding, so a cue coming or going
|
|
285
|
+
// never moves the strip, its labels or the scroll position under the thumb. A strip
|
|
286
|
+
// that fits has no gutters: its width changes only when it starts or stops overflowing.
|
|
287
|
+
// The gutter carries the strip's 1px rule on beneath the glyph, and lifts the glyph
|
|
288
|
+
// onto the LABELS' centre rather than the box's: a tab's 2px underline (`border-b-2`)
|
|
289
|
+
// sits on the rule (`-mb-px`), so the labels are centred 2px above the box's bottom —
|
|
290
|
+
// the gutter's 1px rule (`border-b`) plus 1px more (`pb-px`).
|
|
291
|
+
// 24px wide at every text size (KS N3): at Large `sm`'s 48px minimum made each cue
|
|
292
|
+
// 48px, and at 360px the two left a two-tab strip 100px. 24 PIXELS, not `w-6`: the rem
|
|
293
|
+
// grows with the text size (24 / 30 / 36px), and at Extra large on a 360px phone the two
|
|
294
|
+
// gutters took 72px of a 226px strip and a sheet tab wrapped to seven lines (0.48 build
|
|
295
|
+
// review 2, KS N1). The 48px touch target is an
|
|
296
|
+
// invisible hit area round the glyph instead (`HIT_AREA_LARGE`, as `2xs`), over the
|
|
297
|
+
// strip's faded edge, not room taken from the row. The wrapper inside the gutter is
|
|
298
|
+
// ours because IconButton puts its tooltip's span around the button.
|
|
299
|
+
const gutterAt = (side: "start" | "end", shown: boolean) => (
|
|
284
300
|
<div
|
|
285
|
-
data-tabs-
|
|
286
|
-
className={cn(
|
|
301
|
+
data-tabs-cue-gutter={side}
|
|
302
|
+
className={cn(
|
|
303
|
+
"absolute inset-y-0 flex w-[24px] items-center justify-center border-b border-subtle pb-px",
|
|
304
|
+
side === "start" ? "start-0" : "end-0",
|
|
305
|
+
)}
|
|
287
306
|
>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
{shown && (
|
|
308
|
+
<div data-tabs-overflow-cue={side} className="flex">
|
|
309
|
+
<IconButton
|
|
310
|
+
size="sm"
|
|
311
|
+
variant="ghost"
|
|
312
|
+
className={cn(
|
|
313
|
+
"w-[24px] large:min-w-0 bg-transparent text-muted hover:bg-transparent hover:text-primary",
|
|
314
|
+
// Over the strip's faded edge, for ‹ too.
|
|
315
|
+
HIT_AREA_LARGE,
|
|
316
|
+
"large:after:z-[1]",
|
|
317
|
+
)}
|
|
318
|
+
tabIndex={-1}
|
|
319
|
+
type="button"
|
|
320
|
+
label={side === "start" ? text.previous : text.more}
|
|
321
|
+
labelVisible={false}
|
|
322
|
+
onClick={() => onCue(side)}
|
|
323
|
+
>
|
|
324
|
+
{side === "start" ? <ChevronLeft className="rtl:-scale-x-100" /> : <ChevronRight className="rtl:-scale-x-100" />}
|
|
325
|
+
</IconButton>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
300
328
|
</div>
|
|
301
329
|
);
|
|
302
330
|
// The OPEN tab is scrolled into the strip whenever it changes — on mount too, for a
|
|
@@ -590,13 +618,15 @@ export function Tabs<T extends string>({
|
|
|
590
618
|
{text.remove(nameOf(activeTab))}
|
|
591
619
|
</button>
|
|
592
620
|
) : null;
|
|
593
|
-
// The
|
|
594
|
-
// There whether or not the strip overflows right now, so a resize
|
|
621
|
+
// The box the cues sit in, ‹ in the gutter before the strip and › in the one after it
|
|
622
|
+
// (see `gutterAt`). There whether or not the strip overflows right now, so a resize
|
|
623
|
+
// never re-parents it; the gutters only while it overflows at all.
|
|
624
|
+
const gutters = cue && fade.overflow !== undefined;
|
|
595
625
|
const stripBox = cue ? (
|
|
596
|
-
<div className={cn("relative flex min-w-0", !onAdd && "flex-1")}>
|
|
626
|
+
<div className={cn("relative flex min-w-0", !onAdd && "flex-1", gutters && "px-[24px]")}>
|
|
597
627
|
{tablist}
|
|
598
|
-
{
|
|
599
|
-
{
|
|
628
|
+
{gutters && gutterAt("start", cueStart)}
|
|
629
|
+
{gutters && gutterAt("end", cueEnd)}
|
|
600
630
|
</div>
|
|
601
631
|
) : (
|
|
602
632
|
tablist
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { SetStateAction } from "react";
|
|
2
3
|
import { useLocation, useSearchParams } from "react-router";
|
|
3
4
|
import { useKitTableStorageKeyPrefix } from "../i18n/kit-labels";
|
|
4
5
|
import { readStored, writeStored } from "../lib/safe-storage";
|
|
@@ -24,6 +25,17 @@ interface PersistedState {
|
|
|
24
25
|
pageSize: number | "all";
|
|
25
26
|
widths?: Record<string, number>;
|
|
26
27
|
hidden?: string[];
|
|
28
|
+
/** 0.48: the `defaultHidden` columns the user ticked back on — an entry that beats the
|
|
29
|
+
* default. Written only when non-empty, so a table without such columns stores what
|
|
30
|
+
* it always did. */
|
|
31
|
+
shown?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The hidden set as drawn: the stored one plus every `defaultHidden` column the view
|
|
35
|
+
* holds no entry for (neither hidden nor `shown`). */
|
|
36
|
+
function withDefaultHidden(view: { hidden: Set<string>; shown: Set<string> }, defaults: string[]): Set<string> {
|
|
37
|
+
const extra = defaults.filter((k) => !view.hidden.has(k) && !view.shown.has(k));
|
|
38
|
+
return extra.length === 0 ? view.hidden : new Set([...view.hidden, ...extra]);
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export const DEFAULT_PERSIST_PREFIX = "hbui-table:";
|
|
@@ -338,7 +350,45 @@ export function useTableState<T>({
|
|
|
338
350
|
);
|
|
339
351
|
const pageSize = pageSizeProp ?? internalPageSize;
|
|
340
352
|
const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});
|
|
341
|
-
|
|
353
|
+
// Hidden columns (0.48, keksdose #617): what the user chose — `hidden`, and `shown`
|
|
354
|
+
// for a `defaultHidden` column ticked back on — kept apart from the defaults, which
|
|
355
|
+
// are applied on read and never stored. Keyed on the default keys as a string: every
|
|
356
|
+
// call site builds `columns` inline, so the array is new on every render.
|
|
357
|
+
const [view, setView] = useState(() => ({
|
|
358
|
+
hidden: new Set<string>(initial.hidden ?? []),
|
|
359
|
+
shown: new Set<string>(initial.shown ?? []),
|
|
360
|
+
}));
|
|
361
|
+
const defaultHiddenSig = columns
|
|
362
|
+
.filter((c) => c.defaultHidden)
|
|
363
|
+
.map((c) => c.key)
|
|
364
|
+
.join("\u0000");
|
|
365
|
+
const hiddenCols = useMemo(
|
|
366
|
+
() => withDefaultHidden(view, defaultHiddenSig ? defaultHiddenSig.split("\u0000") : []),
|
|
367
|
+
[view, defaultHiddenSig],
|
|
368
|
+
);
|
|
369
|
+
// The panel hands over the next DRAWN set; what changed is recorded as a choice.
|
|
370
|
+
const setHiddenCols = useCallback(
|
|
371
|
+
(action: SetStateAction<Set<string>>) =>
|
|
372
|
+
setView((v) => {
|
|
373
|
+
const defaults = defaultHiddenSig ? defaultHiddenSig.split("\u0000") : [];
|
|
374
|
+
const prev = withDefaultHidden(v, defaults);
|
|
375
|
+
const next = typeof action === "function" ? action(prev) : action;
|
|
376
|
+
const hidden = new Set(v.hidden);
|
|
377
|
+
const shown = new Set(v.shown);
|
|
378
|
+
for (const k of next) {
|
|
379
|
+
if (prev.has(k)) continue;
|
|
380
|
+
hidden.add(k);
|
|
381
|
+
shown.delete(k);
|
|
382
|
+
}
|
|
383
|
+
for (const k of prev) {
|
|
384
|
+
if (next.has(k)) continue;
|
|
385
|
+
hidden.delete(k);
|
|
386
|
+
if (defaults.includes(k)) shown.add(k);
|
|
387
|
+
}
|
|
388
|
+
return { hidden, shown };
|
|
389
|
+
}),
|
|
390
|
+
[defaultHiddenSig],
|
|
391
|
+
);
|
|
342
392
|
const [showSettings, setShowSettings] = useState(false);
|
|
343
393
|
const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);
|
|
344
394
|
|
|
@@ -360,7 +410,8 @@ export function useTableState<T>({
|
|
|
360
410
|
filters,
|
|
361
411
|
pageSize: pageSize === Infinity ? "all" : pageSize,
|
|
362
412
|
widths,
|
|
363
|
-
hidden: Array.from(
|
|
413
|
+
hidden: Array.from(view.hidden),
|
|
414
|
+
...(view.shown.size > 0 ? { shown: Array.from(view.shown) } : {}),
|
|
364
415
|
});
|
|
365
416
|
useEffect(() => {
|
|
366
417
|
if (!storageKey) return;
|
package/src/data-table.ts
CHANGED
|
@@ -30,6 +30,8 @@ export type {
|
|
|
30
30
|
// 0.46.1 (keksdose N3): a swipe plan as `RowActions`' actions — the desktop column's own
|
|
31
31
|
// derivation, for an app's hand-built actions column.
|
|
32
32
|
export { swipePlanRowActions } from "./components/data-table-row-actions";
|
|
33
|
+
// 0.48 (keksdose dev #605): a `groupBy` level (also `mobileGroupBy`'s) and the `loadMore` footer.
|
|
34
|
+
export type { DataTableGroupBy, DataTableLoadMore } from "./components/data-table-groups";
|
|
33
35
|
export * from "./components/data-table-cells";
|
|
34
36
|
export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
|
|
35
37
|
export type {
|
|
@@ -7,6 +7,7 @@ import { EMPTY_FORMATTED_VALUE, formatDate, toDate } from "../lib/format";
|
|
|
7
7
|
import { useKitDateFormatter, useKitLabels, useKitLink, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
|
|
8
8
|
import type { KitLinkProps } from "../i18n/kit-labels";
|
|
9
9
|
import { Chip } from "../components/chip";
|
|
10
|
+
import { Tooltip } from "../components/tooltip";
|
|
10
11
|
import { FloatingAction, FloatingActionGroup } from "../components/floating-panel";
|
|
11
12
|
import type { FloatingCorner } from "../components/floating-panel";
|
|
12
13
|
import { pickLinkRenderer } from "../components/text-link";
|
|
@@ -449,6 +450,11 @@ function statusRank(status: FeedbackStatus): number {
|
|
|
449
450
|
* filter that never offered it.
|
|
450
451
|
* - The status column owns the status filter, and only it (keksdose live #330's revert:
|
|
451
452
|
* the phone's filter sheet already exposes every column filter).
|
|
453
|
+
* - Fitted to the table's width (0.48, keksdose live #451): Subject `grow`s and
|
|
454
|
+
* ellipsises, and a narrow table leaves out URL, then Email, then Resolved
|
|
455
|
+
* (`fitPriority` 1–3) — the open report repeats all three — then Votes and Category
|
|
456
|
+
* (4, 5) before Subject would drop below 10rem. Drop the two keys with
|
|
457
|
+
* `{ ...col, grow: undefined, fitPriority: undefined }` for the 0.47 layout.
|
|
452
458
|
* - `/my-feedback` shows the same nine: the Email cell's context fallback exists for it
|
|
453
459
|
* (the admin list's join is absent there, §3.1), and its phone card drops the submitter
|
|
454
460
|
* instead ({@link FeedbackMobileCardProps.showSubmitter}).
|
|
@@ -479,6 +485,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
479
485
|
sortBy: (row) => row.votes ?? 0,
|
|
480
486
|
filter: { type: "number", getValue: (row) => row.votes ?? 0 },
|
|
481
487
|
mobileHidden: true,
|
|
488
|
+
// Fourth to go (Kurvenschmiede S1, 0.48): with only the first three, Subject was
|
|
489
|
+
// crushed to 24px at 1100px. Still reachable from the Columns panel.
|
|
490
|
+
fitPriority: 4,
|
|
482
491
|
},
|
|
483
492
|
]
|
|
484
493
|
: [];
|
|
@@ -508,6 +517,9 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
508
517
|
getValue: (row) => row.category,
|
|
509
518
|
options: FEEDBACK_CATEGORY_ORDER.map((value) => ({ value, label: categoryLabels[value] })),
|
|
510
519
|
},
|
|
520
|
+
// Last to go (Kurvenschmiede S1, 0.48), before Subject would drop below 10rem:
|
|
521
|
+
// the open report's Subject section repeats the badge.
|
|
522
|
+
fitPriority: 5,
|
|
511
523
|
},
|
|
512
524
|
{
|
|
513
525
|
key: "title",
|
|
@@ -521,11 +533,25 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
521
533
|
return env ? `${row.title} ${labels.environment(env)}` : row.title;
|
|
522
534
|
},
|
|
523
535
|
mobilePrimary: true,
|
|
536
|
+
// The rest of the row, one line and an ellipsis (0.48, keksdose live #451: one long
|
|
537
|
+
// crash title pushed Status and the row actions off the edge).
|
|
538
|
+
grow: true,
|
|
524
539
|
},
|
|
525
540
|
{
|
|
526
541
|
key: "user",
|
|
527
542
|
header: labels.columnUser,
|
|
528
|
-
|
|
543
|
+
// At most 12rem, one line and an ellipsis, the whole value in the kit's Tooltip (0.48 build
|
|
544
|
+
// review 2, keksdose N2): beside the grow Subject every other cell keeps one line, so
|
|
545
|
+
// an email standing in for a missing name widened the column and cost Votes and
|
|
546
|
+
// Category their room first. The open report shows both in full.
|
|
547
|
+
cell: (row) => {
|
|
548
|
+
const who = feedbackSubmitter(row, labels);
|
|
549
|
+
return (
|
|
550
|
+
<Tooltip label={who} portal lazy>
|
|
551
|
+
<span className="block max-w-[12rem] truncate-until-large text-secondary">{who}</span>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
);
|
|
554
|
+
},
|
|
529
555
|
sortBy: (row) => feedbackSubmitter(row, labels),
|
|
530
556
|
// Name AND email, so either finds the person; the erased ones by the placeholder
|
|
531
557
|
// (their context holds no name any more).
|
|
@@ -552,6 +578,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
552
578
|
sortBy: feedbackSubmitterEmail,
|
|
553
579
|
filterBy: feedbackSubmitterEmail,
|
|
554
580
|
mobileHidden: true,
|
|
581
|
+
// Second to go when the table is narrow: the open report shows the submitter.
|
|
582
|
+
fitPriority: 2,
|
|
555
583
|
},
|
|
556
584
|
{
|
|
557
585
|
key: "url",
|
|
@@ -566,6 +594,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
566
594
|
sortBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
567
595
|
filterBy: (row) => feedbackPagePath(row.context) ?? "",
|
|
568
596
|
mobileHidden: true,
|
|
597
|
+
// First to go when the table is narrow (0.48): the open report repeats the page.
|
|
598
|
+
fitPriority: 1,
|
|
569
599
|
},
|
|
570
600
|
...votesColumn,
|
|
571
601
|
{
|
|
@@ -600,6 +630,8 @@ export function feedbackColumns<T extends FeedbackTableRow>({
|
|
|
600
630
|
sortBy: (row) => instant(row.resolved_at),
|
|
601
631
|
filter: { type: "date", getValue: (row) => row.resolved_at },
|
|
602
632
|
mobileHidden: true,
|
|
633
|
+
// Third to go (0.48).
|
|
634
|
+
fitPriority: 3,
|
|
603
635
|
},
|
|
604
636
|
];
|
|
605
637
|
}
|
package/src/hooks/use-dismiss.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { KeyboardEvent as ReactKeyboardEvent, RefObject } from "react";
|
|
3
3
|
import { isComposing } from "../lib/ime";
|
|
4
4
|
|
|
@@ -220,6 +220,29 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
220
220
|
useDismissLayer(enabled, handler);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/** Called with each native press an owner's React tree saw — see {@link useOutsideClick}. */
|
|
224
|
+
type PressMark = (press: Event) => void;
|
|
225
|
+
|
|
226
|
+
/** The outside-click hooks watching each ref, for {@link pressInside}. */
|
|
227
|
+
const watchers = new WeakMap<RefObject<HTMLElement | null>, Set<PressMark>>();
|
|
228
|
+
|
|
229
|
+
/** What an `onPointerDownCapture` hands over: a React event, its press inside. */
|
|
230
|
+
type ReactPress = { nativeEvent: Event };
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* An `onPointerDownCapture` for a panel whose `ref` an outside-click hook watches but
|
|
234
|
+
* whose element the hook's owner does not render — {@link DropdownPanel} and
|
|
235
|
+
* `ComboboxPanel` take `panelRef` from `useDropdown` / `useComboboxCore`. Every press
|
|
236
|
+
* that reaches the panel through the React tree, a portalled list opened inside it
|
|
237
|
+
* included, is marked inside for each hook watching `ref`. See {@link useOutsideClick}.
|
|
238
|
+
*/
|
|
239
|
+
export function pressInside(ref: RefObject<HTMLElement | null> | undefined): ((e: ReactPress) => void) | undefined {
|
|
240
|
+
if (!ref) return undefined;
|
|
241
|
+
return (e) => {
|
|
242
|
+
watchers.get(ref)?.forEach((mark) => mark(e.nativeEvent));
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
/**
|
|
224
247
|
* Calls `handler` on a press that lands outside every supplied element, while
|
|
225
248
|
* `enabled` — the outside-click-to-close half of the same dismissal story. Pass one
|
|
@@ -227,6 +250,23 @@ export function useEscapeKey(handler: () => void, enabled = true): void {
|
|
|
227
250
|
* `handler` are read live, so an inline `[triggerRef, panelRef]` array is fine and
|
|
228
251
|
* won't churn the listener.
|
|
229
252
|
*
|
|
253
|
+
* ## A panel opened from inside is inside (0.48, keksdose dev#623)
|
|
254
|
+
*
|
|
255
|
+
* A CurrencySelect in a Popover: the currency list is portalled to `<body>`, so in the
|
|
256
|
+
* DOM it is outside the popover's panel, and the first press on a currency closed the
|
|
257
|
+
* POPOVER at pointerdown — the list went with it and the choice never landed. The same
|
|
258
|
+
* for a combobox, a menu, a date picker's popover, a dropdown, at any depth.
|
|
259
|
+
*
|
|
260
|
+
* In the React tree the portal is still where it was opened from, and React passes a
|
|
261
|
+
* press up that tree, through portals. So the hook hands back an
|
|
262
|
+
* `onPointerDownCapture`: put it on the element the press should count as inside of —
|
|
263
|
+
* the kit's Popover panel, HoverMenu, DropdownPanel and ComboboxPanel do — and every
|
|
264
|
+
* press that travels through that element in React, portalled panels opened inside it
|
|
265
|
+
* included, is not outside. React's listeners sit on the root and on each portal's
|
|
266
|
+
* container, so they hear the press before this document listener does. No owner chain
|
|
267
|
+
* to keep: depth is free, and an app's own portal inside a kit panel is covered too. A
|
|
268
|
+
* panel the hook's owner does not render takes {@link pressInside} with the watched ref.
|
|
269
|
+
*
|
|
230
270
|
* **`pointerdown`, not `mousedown`** — one event covering mouse, touch and pen.
|
|
231
271
|
* This listened for `mousedown` alone, which on a touch platform is a SYNTHESISED
|
|
232
272
|
* event: the browser emits it for most taps, which is why nothing obviously broke,
|
|
@@ -245,17 +285,35 @@ export function useOutsideClick(
|
|
|
245
285
|
refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[],
|
|
246
286
|
handler: () => void,
|
|
247
287
|
enabled = true,
|
|
248
|
-
): void {
|
|
288
|
+
): (e: ReactPress) => void {
|
|
249
289
|
const refsRef = useRef(refs);
|
|
250
290
|
const handlerRef = useRef(handler);
|
|
291
|
+
// The last press this owner's React tree saw: the one the document listener is about
|
|
292
|
+
// to hear, when it went through a panel the owner marked.
|
|
293
|
+
const insideRef = useRef<Event | null>(null);
|
|
251
294
|
// In an effect rather than during render — see the note in useDismissLayer.
|
|
252
295
|
useEffect(() => {
|
|
253
296
|
refsRef.current = refs;
|
|
254
297
|
handlerRef.current = handler;
|
|
255
298
|
});
|
|
299
|
+
const markInside = useCallback((e: ReactPress) => {
|
|
300
|
+
insideRef.current = e.nativeEvent;
|
|
301
|
+
}, []);
|
|
256
302
|
useEffect(() => {
|
|
257
303
|
if (!enabled) return;
|
|
304
|
+
const mark: PressMark = (press) => {
|
|
305
|
+
insideRef.current = press;
|
|
306
|
+
};
|
|
307
|
+
const watched = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
308
|
+
for (const r of watched) {
|
|
309
|
+
const set = watchers.get(r) ?? new Set<PressMark>();
|
|
310
|
+
set.add(mark);
|
|
311
|
+
watchers.set(r, set);
|
|
312
|
+
}
|
|
258
313
|
const onPointerDown = (e: Event) => {
|
|
314
|
+
const inside = insideRef.current === e;
|
|
315
|
+
insideRef.current = null;
|
|
316
|
+
if (inside) return;
|
|
259
317
|
const target = e.target as Node;
|
|
260
318
|
const list = Array.isArray(refsRef.current) ? refsRef.current : [refsRef.current];
|
|
261
319
|
for (const r of list) {
|
|
@@ -264,6 +322,10 @@ export function useOutsideClick(
|
|
|
264
322
|
handlerRef.current();
|
|
265
323
|
};
|
|
266
324
|
document.addEventListener("pointerdown", onPointerDown);
|
|
267
|
-
return () =>
|
|
325
|
+
return () => {
|
|
326
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
327
|
+
for (const r of watched) watchers.get(r)?.delete(mark);
|
|
328
|
+
};
|
|
268
329
|
}, [enabled]);
|
|
330
|
+
return markInside;
|
|
269
331
|
}
|