@deftui/react 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.mts +7 -5
- package/dist/index.mjs +4 -4
- package/dist/styles.css +3 -0
- package/dist/vendor/README.md +10 -9
- package/dist/vendor/coss/calendar.d.mts +4 -0
- package/dist/vendor/coss/calendar.mjs +70 -0
- package/dist/vendor/coss/card.d.mts +23 -0
- package/dist/vendor/coss/card.mjs +160 -0
- package/dist/vendor/coss/dialog.d.mts +1 -1
- package/dist/vendor/coss/dialog.mjs +1 -1
- package/dist/vendor/coss/table.d.mts +15 -0
- package/dist/vendor/coss/table.mjs +73 -0
- package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
- package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
- package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
- package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
- package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
- package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
- package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
- package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
- package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
- package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
- package/dist/vendor/reui.io/timeline.d.mts +21 -0
- package/dist/vendor/reui.io/timeline.mjs +127 -0
- package/dist/vendor/tailored/lightbox.d.mts +1 -1
- package/dist/vendor/tailored/lightbox.mjs +1 -1
- package/dist/widgets/datetime.d.mts +1 -1
- package/dist/widgets/datetime.mjs +6 -1
- package/dist/widgets/export.d.mts +28 -0
- package/dist/widgets/index.d.mts +69 -20
- package/dist/widgets/index.mjs +61 -11
- package/package.json +2 -2
- package/dist/vendor/shadcn/calendar.d.mts +0 -10
- package/dist/vendor/shadcn/calendar.mjs +0 -107
- package/dist/vendor/shadcn/card.d.mts +0 -11
- package/dist/vendor/shadcn/card.mjs +0 -55
- package/dist/vendor/shadcn/table.d.mts +0 -9
- package/dist/vendor/shadcn/table.mjs +0 -59
- package/dist/vendor/tailored/file-tree.d.mts +0 -21
- package/dist/vendor/tailored/file-tree.mjs +0 -146
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCascaderActions } from "./cascader-context.mjs";
|
|
3
|
+
import { CASCADER_ACTION_CLASS, CascaderGroup, CascaderLabel } from "./cascader-item.mjs";
|
|
4
|
+
import { CASCADER_LIST_PAD_CLASS, getCascaderFooterStops, isCascaderRtl } from "./cascader-lib.mjs";
|
|
5
|
+
import { cn } from "cn";
|
|
6
|
+
import * as React$1 from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { ChevronRightIcon } from "lucide-react";
|
|
9
|
+
import { Popover } from "@base-ui/react";
|
|
10
|
+
import { useDirection } from "@base-ui/react/direction-provider";
|
|
11
|
+
// src/vendor/reui.io/cascader/cascader-footer.tsx
|
|
12
|
+
/**
|
|
13
|
+
* The pinned footer, and the side-anchored flyout a footer row can open. These
|
|
14
|
+
* are COMMANDS: nothing here joins the selection, the filter set or the
|
|
15
|
+
* highlight. The flyout is a Base UI `Popover` rendered as a REACT CHILD of
|
|
16
|
+
* `Combobox.Popup` with its OWN `Portal` and NO `container`: a nested portal
|
|
17
|
+
* resolves to the parent portal node, so it is a DOM sibling of the combobox
|
|
18
|
+
* popup (not clipped, not `aria-hidden`) but a React descendant, which is what
|
|
19
|
+
* the outside-press and focus-out whitelists read. `Combobox` builds no
|
|
20
|
+
* `FloatingTree`, so the flyout is not consulted first and one Escape would
|
|
21
|
+
* dismiss both; hence `CascaderSubmenu` registering with the root to turn one
|
|
22
|
+
* Escape into two. `Combobox.List` clicks its highlighted row on Enter, hence
|
|
23
|
+
* the footer sitting outside `CascaderList`. And a `Positioner` throws without
|
|
24
|
+
* its `Portal`, while `modal` stays `false` on the `Root` so the combobox
|
|
25
|
+
* keeps its own dismissal behaviour.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Keys the option list acts on, swallowed at the footer boundary. Escape and
|
|
29
|
+
* Tab are absent on purpose: Escape must reach the root, Tab must keep moving.
|
|
30
|
+
*/
|
|
31
|
+
const FOOTER_SWALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
32
|
+
"Enter",
|
|
33
|
+
" ",
|
|
34
|
+
"ArrowUp",
|
|
35
|
+
"ArrowDown",
|
|
36
|
+
"Home",
|
|
37
|
+
"End",
|
|
38
|
+
"PageUp",
|
|
39
|
+
"PageDown"
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Actions pinned below the list, a SIBLING of `CascaderList`. Children win
|
|
43
|
+
* over the root's `actions` prop; with neither it renders nothing.
|
|
44
|
+
*/
|
|
45
|
+
function CascaderFooter({ className, children, onKeyDown, ...props }) {
|
|
46
|
+
const { actions, labels } = useCascaderActions();
|
|
47
|
+
const hasChildren = React$1.Children.count(children) > 0;
|
|
48
|
+
const handleKeyDown = React$1.useCallback((event) => {
|
|
49
|
+
onKeyDown?.(event);
|
|
50
|
+
if (event.defaultPrevented) return;
|
|
51
|
+
if (!FOOTER_SWALLOWED_KEYS.has(event.key)) return;
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
54
|
+
const footer = event.currentTarget;
|
|
55
|
+
const stops = getCascaderFooterStops(footer);
|
|
56
|
+
const active = document.activeElement;
|
|
57
|
+
const index = active ? stops.indexOf(active) : -1;
|
|
58
|
+
if (index === -1) return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
const next = event.key === "ArrowDown" ? stops[index + 1] : stops[index - 1];
|
|
61
|
+
if (next) {
|
|
62
|
+
next.focus();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (event.key === "ArrowUp" && index > 0) return;
|
|
66
|
+
footer.closest("[data-slot=\"cascader-panel\"]")?.querySelector("[data-slot=\"cascader-input\"]")?.focus();
|
|
67
|
+
}, [onKeyDown]);
|
|
68
|
+
if (!hasChildren && actions.length === 0) return null;
|
|
69
|
+
return /* @__PURE__ */ jsx("div", {
|
|
70
|
+
"data-slot": "cascader-footer",
|
|
71
|
+
role: "group",
|
|
72
|
+
"aria-label": labels.actionsLabel,
|
|
73
|
+
onKeyDown: handleKeyDown,
|
|
74
|
+
className: cn("border-border/60 flex shrink-0 flex-col gap-0.5 border-t", CASCADER_LIST_PAD_CLASS, "p-(--cascader-list-pad,4px)", className),
|
|
75
|
+
...props,
|
|
76
|
+
children: hasChildren ? children : /* @__PURE__ */ jsx(CascaderFooterActions, { actions })
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function CascaderFooterActions({ actions }) {
|
|
80
|
+
return /* @__PURE__ */ jsx(Fragment, { children: actions.map((action, i) => action.items?.length ? /* @__PURE__ */ jsxs(CascaderSubmenu, { children: [/* @__PURE__ */ jsx(CascaderSubmenuTrigger, {
|
|
81
|
+
icon: action.icon,
|
|
82
|
+
disabled: action.disabled,
|
|
83
|
+
children: action.label
|
|
84
|
+
}), /* @__PURE__ */ jsx(CascaderSubmenuContent, { children: /* @__PURE__ */ jsx(CascaderActionList, { items: action.items }) })] }, actionKey(action, i)) : /* @__PURE__ */ jsx(CascaderAction, {
|
|
85
|
+
icon: action.icon,
|
|
86
|
+
disabled: action.disabled,
|
|
87
|
+
onSelect: action.onSelect,
|
|
88
|
+
children: action.label
|
|
89
|
+
}, actionKey(action, i))) });
|
|
90
|
+
}
|
|
91
|
+
function actionKey(action, index) {
|
|
92
|
+
if (action.value != null) return action.value;
|
|
93
|
+
if (typeof action.label === "string") return action.label;
|
|
94
|
+
return String(index);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Consecutive entries sharing a `group`, as runs not buckets: two separated
|
|
98
|
+
* runs with the same name stay two, so the author's order survives.
|
|
99
|
+
*/
|
|
100
|
+
function groupActionRuns(items) {
|
|
101
|
+
const runs = [];
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const last = runs[runs.length - 1];
|
|
104
|
+
if (last && last.group === item.group) last.items.push(item);
|
|
105
|
+
else runs.push({
|
|
106
|
+
group: item.group,
|
|
107
|
+
items: [item]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return runs;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Flyout body for a data-driven submenu. A named run becomes a real
|
|
114
|
+
* `CascaderGroup`; unnamed runs stay unwrapped, as an unnamed group is noise.
|
|
115
|
+
*/
|
|
116
|
+
function CascaderActionList({ items }) {
|
|
117
|
+
const { close } = useCascaderSubmenu();
|
|
118
|
+
const runs = React$1.useMemo(() => groupActionRuns(items), [items]);
|
|
119
|
+
const renderAction = (item, i) => /* @__PURE__ */ jsx(CascaderAction, {
|
|
120
|
+
icon: item.icon,
|
|
121
|
+
disabled: item.disabled,
|
|
122
|
+
onSelect: () => {
|
|
123
|
+
item.onSelect?.();
|
|
124
|
+
close();
|
|
125
|
+
},
|
|
126
|
+
children: item.label
|
|
127
|
+
}, actionKey(item, i));
|
|
128
|
+
return /* @__PURE__ */ jsx(Fragment, { children: runs.map((run, runIndex) => run.group ? /* @__PURE__ */ jsxs(CascaderGroup, {
|
|
129
|
+
className: "gap-0.5",
|
|
130
|
+
children: [/* @__PURE__ */ jsx(CascaderLabel, { children: run.group }), run.items.map(renderAction)]
|
|
131
|
+
}, `${run.group}-${runIndex}`) : /* @__PURE__ */ jsx(React$1.Fragment, { children: run.items.map(renderAction) }, `run-${runIndex}`)) });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The cascader popup's panel per style, spelled with `style-<name>:` variants
|
|
135
|
+
* rather than ReUI theme CSS so an installed footer needs only Tailwind.
|
|
136
|
+
*/
|
|
137
|
+
const FLYOUT_SURFACE_CLASS = "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-72 overflow-hidden ring-1 duration-100 ring-foreground/10 rounded-md shadow-md";
|
|
138
|
+
/**
|
|
139
|
+
* One footer command, shaped like a row and deliberately NOT one. A real
|
|
140
|
+
* `<button>`, never a `Combobox.Item`: an item would join the arrow-key ring,
|
|
141
|
+
* appear in `filteredItems`, and vanish the moment a query matched nothing -
|
|
142
|
+
* exactly when "Create new attribute" is most useful. Disabled is
|
|
143
|
+
* `aria-disabled`, never the native attribute: that one is not a tab stop and
|
|
144
|
+
* the panel's Tab order reads `button:not([disabled])`, so a footer whose ONLY
|
|
145
|
+
* row is a disabled command had no stop after the search field (measured on
|
|
146
|
+
* `c-cascader-8`). Staying focusable costs the guards below, plus the
|
|
147
|
+
* greyed-out look: `CASCADER_ACTION_CLASS` keys that off `aria-disabled`
|
|
148
|
+
* instead of `:disabled`.
|
|
149
|
+
*/
|
|
150
|
+
function CascaderAction({ className, icon, children, onSelect, onClick, onKeyDown, disabled, ...props }) {
|
|
151
|
+
const inMenu = React$1.useContext(CascaderMenuContext);
|
|
152
|
+
const handleClick = React$1.useCallback((event) => {
|
|
153
|
+
if (disabled) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
onClick?.(event);
|
|
158
|
+
if (event.defaultPrevented) return;
|
|
159
|
+
onSelect?.();
|
|
160
|
+
}, [
|
|
161
|
+
disabled,
|
|
162
|
+
onClick,
|
|
163
|
+
onSelect
|
|
164
|
+
]);
|
|
165
|
+
const handleKeyDown = React$1.useCallback((event) => {
|
|
166
|
+
if (disabled) {
|
|
167
|
+
if (event.key === "Enter" || event.key === " ") event.preventDefault();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
onKeyDown?.(event);
|
|
171
|
+
}, [disabled, onKeyDown]);
|
|
172
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
173
|
+
type: "button",
|
|
174
|
+
"data-slot": "cascader-action",
|
|
175
|
+
...disabled ? {
|
|
176
|
+
"aria-disabled": true,
|
|
177
|
+
"data-disabled": ""
|
|
178
|
+
} : null,
|
|
179
|
+
onClick: handleClick,
|
|
180
|
+
onKeyDown: handleKeyDown,
|
|
181
|
+
...inMenu ? {
|
|
182
|
+
role: "menuitem",
|
|
183
|
+
tabIndex: -1
|
|
184
|
+
} : null,
|
|
185
|
+
className: cn(CASCADER_ACTION_CLASS, className),
|
|
186
|
+
...props,
|
|
187
|
+
children: [icon ? /* @__PURE__ */ jsx("span", {
|
|
188
|
+
"data-slot": "cascader-action-icon",
|
|
189
|
+
className: "text-muted-foreground flex shrink-0 items-center justify-center",
|
|
190
|
+
children: icon
|
|
191
|
+
}) : null, /* @__PURE__ */ jsx("span", {
|
|
192
|
+
className: "min-w-0 flex-1 truncate text-start",
|
|
193
|
+
children
|
|
194
|
+
})]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/** Marks the subtree INSIDE a flyout. See `inMenu` in `CascaderAction`. */
|
|
198
|
+
const CascaderMenuContext = React$1.createContext(false);
|
|
199
|
+
/**
|
|
200
|
+
* Every entry a menu's roving focus may land on, in DOM order, read from the
|
|
201
|
+
* DOM because the entries are whatever the consumer composed. A disabled
|
|
202
|
+
* `CascaderAction` is INCLUDED: it carries `aria-disabled`, not the native
|
|
203
|
+
* attribute, so `:not([disabled])` excludes only a consumer's own natively
|
|
204
|
+
* disabled `menuitem`, which cannot take focus.
|
|
205
|
+
*/
|
|
206
|
+
function menuItems(popup) {
|
|
207
|
+
if (!popup) return [];
|
|
208
|
+
return Array.from(popup.querySelectorAll("[role=\"menuitem\"]:not([disabled])"));
|
|
209
|
+
}
|
|
210
|
+
const CascaderSubmenuContext = React$1.createContext(void 0);
|
|
211
|
+
/** The flyout's own state. `close()` is the one a custom entry usually wants. */
|
|
212
|
+
function useCascaderSubmenu() {
|
|
213
|
+
const context = React$1.useContext(CascaderSubmenuContext);
|
|
214
|
+
if (!context) throw new Error("useCascaderSubmenu must be used within a CascaderSubmenu");
|
|
215
|
+
return context;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* A footer row plus the flyout it opens. Registers with the cascader root
|
|
219
|
+
* while open, which turns one Escape into two. Cleared in an EFFECT, so the
|
|
220
|
+
* flyout still reads as open during the event that closed it.
|
|
221
|
+
*/
|
|
222
|
+
function CascaderSubmenu({ open: openProp, defaultOpen = false, onOpenChange, children }) {
|
|
223
|
+
const { setFlyoutOpen } = useCascaderActions();
|
|
224
|
+
const key = React$1.useId();
|
|
225
|
+
const triggerId = React$1.useId();
|
|
226
|
+
const rowRef = React$1.useRef(null);
|
|
227
|
+
const keyboardRef = React$1.useRef(false);
|
|
228
|
+
const [uncontrolled, setUncontrolled] = React$1.useState(defaultOpen);
|
|
229
|
+
const open = openProp ?? uncontrolled;
|
|
230
|
+
const setOpen = React$1.useCallback((next) => {
|
|
231
|
+
if (openProp == null) setUncontrolled(next);
|
|
232
|
+
onOpenChange?.(next);
|
|
233
|
+
}, [openProp, onOpenChange]);
|
|
234
|
+
const close = React$1.useCallback(() => setOpen(false), [setOpen]);
|
|
235
|
+
React$1.useEffect(() => {
|
|
236
|
+
setFlyoutOpen(key, open);
|
|
237
|
+
return () => setFlyoutOpen(key, false);
|
|
238
|
+
}, [
|
|
239
|
+
setFlyoutOpen,
|
|
240
|
+
key,
|
|
241
|
+
open
|
|
242
|
+
]);
|
|
243
|
+
const context = React$1.useMemo(() => ({
|
|
244
|
+
rowRef,
|
|
245
|
+
open,
|
|
246
|
+
setOpen,
|
|
247
|
+
close,
|
|
248
|
+
triggerId,
|
|
249
|
+
keyboardRef
|
|
250
|
+
}), [
|
|
251
|
+
open,
|
|
252
|
+
setOpen,
|
|
253
|
+
close,
|
|
254
|
+
triggerId
|
|
255
|
+
]);
|
|
256
|
+
return /* @__PURE__ */ jsx(CascaderSubmenuContext.Provider, {
|
|
257
|
+
value: context,
|
|
258
|
+
children: /* @__PURE__ */ jsx(Popover.Root, {
|
|
259
|
+
open,
|
|
260
|
+
onOpenChange: setOpen,
|
|
261
|
+
modal: false,
|
|
262
|
+
children
|
|
263
|
+
})
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The footer row that opens the flyout, and its anchor. `aria-haspopup="menu"`
|
|
268
|
+
* rather than the `dialog` Base UI would announce: what opens is a list of
|
|
269
|
+
* commands with roving focus. `disabled` is intercepted, not forwarded:
|
|
270
|
+
* `Popover.Trigger` runs it through `useButton`, which writes the NATIVE
|
|
271
|
+
* attribute for a native `<button>` and takes the row out of the panel's Tab
|
|
272
|
+
* ring, the defect `CascaderAction` documents, and this component exposes no
|
|
273
|
+
* `focusableWhenDisabled` to opt out. Published as `aria-disabled`, with the
|
|
274
|
+
* arrow keys, Enter and Space, and the click closed by hand (`useClick`
|
|
275
|
+
* ignores `defaultPrevented`).
|
|
276
|
+
*/
|
|
277
|
+
function CascaderSubmenuTrigger({ className, icon, children, onKeyDown, onClick, disabled, ...props }) {
|
|
278
|
+
const { labels } = useCascaderActions();
|
|
279
|
+
const { rowRef, setOpen, triggerId, keyboardRef } = useCascaderSubmenu();
|
|
280
|
+
const direction = useDirection();
|
|
281
|
+
const handleClick = React$1.useCallback((event) => {
|
|
282
|
+
if (disabled) {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
event.preventBaseUIHandler();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
onClick?.(event);
|
|
288
|
+
}, [disabled, onClick]);
|
|
289
|
+
const handleKeyDown = React$1.useCallback((event) => {
|
|
290
|
+
if (disabled) {
|
|
291
|
+
if (event.key === "Enter" || event.key === " ") event.preventDefault();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
onKeyDown?.(event);
|
|
295
|
+
if (event.defaultPrevented) return;
|
|
296
|
+
const openKey = isCascaderRtl(event.currentTarget, direction) ? "ArrowLeft" : "ArrowRight";
|
|
297
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
298
|
+
keyboardRef.current = true;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (event.key !== openKey) return;
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
keyboardRef.current = true;
|
|
304
|
+
setOpen(true);
|
|
305
|
+
}, [
|
|
306
|
+
disabled,
|
|
307
|
+
onKeyDown,
|
|
308
|
+
direction,
|
|
309
|
+
setOpen,
|
|
310
|
+
keyboardRef
|
|
311
|
+
]);
|
|
312
|
+
return /* @__PURE__ */ jsxs(Popover.Trigger, {
|
|
313
|
+
ref: rowRef,
|
|
314
|
+
id: triggerId,
|
|
315
|
+
"data-slot": "cascader-submenu-trigger",
|
|
316
|
+
"aria-haspopup": "menu",
|
|
317
|
+
...disabled ? {
|
|
318
|
+
"aria-disabled": true,
|
|
319
|
+
"data-disabled": ""
|
|
320
|
+
} : null,
|
|
321
|
+
onClick: handleClick,
|
|
322
|
+
onKeyDown: handleKeyDown,
|
|
323
|
+
className: cn(CASCADER_ACTION_CLASS, "aria-expanded:bg-accent aria-expanded:text-accent-foreground", className),
|
|
324
|
+
...props,
|
|
325
|
+
children: [
|
|
326
|
+
icon ? /* @__PURE__ */ jsx("span", {
|
|
327
|
+
"data-slot": "cascader-action-icon",
|
|
328
|
+
className: "text-muted-foreground flex shrink-0 items-center justify-center",
|
|
329
|
+
children: icon
|
|
330
|
+
}) : null,
|
|
331
|
+
/* @__PURE__ */ jsx("span", {
|
|
332
|
+
className: "min-w-0 flex-1 truncate text-start",
|
|
333
|
+
children
|
|
334
|
+
}),
|
|
335
|
+
/* @__PURE__ */ jsxs("span", {
|
|
336
|
+
className: "sr-only",
|
|
337
|
+
children: [", ", labels.submenuAffordance]
|
|
338
|
+
}),
|
|
339
|
+
/* @__PURE__ */ jsx(ChevronRightIcon, {
|
|
340
|
+
"aria-hidden": "true",
|
|
341
|
+
className: "text-muted-foreground -me-0.5 size-4 shrink-0 rtl:-scale-x-100"
|
|
342
|
+
})
|
|
343
|
+
]
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The flyout itself. `Portal` with NO `container`: a nested portal resolves to
|
|
348
|
+
* the parent portal node, the mechanism the file header describes.
|
|
349
|
+
*/
|
|
350
|
+
function CascaderSubmenuContent({ className, children, onKeyDown, side = "inline-end", align = "end", sideOffset = 8, alignOffset = 0, ...props }) {
|
|
351
|
+
const { rowRef, close, triggerId, keyboardRef } = useCascaderSubmenu();
|
|
352
|
+
const direction = useDirection();
|
|
353
|
+
const popupRef = React$1.useRef(null);
|
|
354
|
+
const typeaheadRef = React$1.useRef({
|
|
355
|
+
buffer: "",
|
|
356
|
+
at: 0
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* Keyboard opens land on the first entry; a pointer open falls through to
|
|
360
|
+
* Base UI's default, the popup, so a click paints no focus ring. Not an
|
|
361
|
+
* effect of our own: Base UI's focus manager runs on open and wins the race.
|
|
362
|
+
*/
|
|
363
|
+
const initialFocus = React$1.useCallback(() => {
|
|
364
|
+
const byKeyboard = keyboardRef.current;
|
|
365
|
+
keyboardRef.current = false;
|
|
366
|
+
if (!byKeyboard) return true;
|
|
367
|
+
return menuItems(popupRef.current)[0] ?? true;
|
|
368
|
+
}, [keyboardRef]);
|
|
369
|
+
const closeAndReturn = React$1.useCallback(() => {
|
|
370
|
+
close();
|
|
371
|
+
rowRef.current?.focus();
|
|
372
|
+
}, [close, rowRef]);
|
|
373
|
+
const handleKeyDown = React$1.useCallback((event) => {
|
|
374
|
+
onKeyDown?.(event);
|
|
375
|
+
event.stopPropagation();
|
|
376
|
+
if (event.defaultPrevented) return;
|
|
377
|
+
const popup = popupRef.current;
|
|
378
|
+
const items = menuItems(popup);
|
|
379
|
+
if (items.length === 0) return;
|
|
380
|
+
const active = document.activeElement;
|
|
381
|
+
const index = active ? items.indexOf(active) : -1;
|
|
382
|
+
const move = (next) => {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
items[(next + items.length) % items.length]?.focus();
|
|
385
|
+
};
|
|
386
|
+
const closeKey = isCascaderRtl(event.currentTarget, direction) ? "ArrowRight" : "ArrowLeft";
|
|
387
|
+
switch (event.key) {
|
|
388
|
+
case "ArrowDown": return move(index + 1);
|
|
389
|
+
case "ArrowUp": return move(index === -1 ? items.length - 1 : index - 1);
|
|
390
|
+
case "Home": return move(0);
|
|
391
|
+
case "End": return move(items.length - 1);
|
|
392
|
+
case closeKey:
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
return closeAndReturn();
|
|
395
|
+
case "Tab":
|
|
396
|
+
close();
|
|
397
|
+
rowRef.current?.focus();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
401
|
+
const now = event.timeStamp;
|
|
402
|
+
const state = typeaheadRef.current;
|
|
403
|
+
state.buffer = now - state.at > 500 ? event.key : state.buffer + event.key;
|
|
404
|
+
state.at = now;
|
|
405
|
+
const prefix = state.buffer.toLowerCase();
|
|
406
|
+
const from = index === -1 ? 0 : index;
|
|
407
|
+
const hit = [...items.slice(state.buffer.length > 1 ? from : from + 1), ...items.slice(0, state.buffer.length > 1 ? from : from + 1)].find((item) => (item.textContent ?? "").trim().toLowerCase().startsWith(prefix));
|
|
408
|
+
if (hit) {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
hit.focus();
|
|
411
|
+
}
|
|
412
|
+
}, [
|
|
413
|
+
onKeyDown,
|
|
414
|
+
direction,
|
|
415
|
+
close,
|
|
416
|
+
closeAndReturn,
|
|
417
|
+
rowRef
|
|
418
|
+
]);
|
|
419
|
+
return /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
|
|
420
|
+
anchor: rowRef,
|
|
421
|
+
side,
|
|
422
|
+
align,
|
|
423
|
+
sideOffset,
|
|
424
|
+
alignOffset,
|
|
425
|
+
className: "isolate z-50",
|
|
426
|
+
children: /* @__PURE__ */ jsx(Popover.Popup, {
|
|
427
|
+
ref: popupRef,
|
|
428
|
+
initialFocus,
|
|
429
|
+
"data-slot": "cascader-submenu-content",
|
|
430
|
+
role: "menu",
|
|
431
|
+
"aria-orientation": "vertical",
|
|
432
|
+
"aria-labelledby": triggerId,
|
|
433
|
+
tabIndex: -1,
|
|
434
|
+
onKeyDown: handleKeyDown,
|
|
435
|
+
"data-menu-target": "",
|
|
436
|
+
className: cn(FLYOUT_SURFACE_CLASS, "flex max-w-(--available-width) min-w-48 flex-col gap-0.5 outline-hidden", CASCADER_LIST_PAD_CLASS, "p-(--cascader-list-pad,4px)", className),
|
|
437
|
+
...props,
|
|
438
|
+
children: /* @__PURE__ */ jsx(CascaderMenuContext.Provider, {
|
|
439
|
+
value: true,
|
|
440
|
+
children
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
}) });
|
|
444
|
+
}
|
|
445
|
+
export { CascaderAction, CascaderFooter, CascaderSubmenu, CascaderSubmenuContent, CascaderSubmenuTrigger, useCascaderSubmenu };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/vendor/reui.io/cascader/cascader-i18n.tsx
|
|
2
|
+
/** Name of the root level. Hoisted so the root announcement can reuse it. */
|
|
3
|
+
const ROOT_LEVEL = "Top level";
|
|
4
|
+
/** Hoisted for the same reason: several defaults end in an item count. */
|
|
5
|
+
const itemCount = (count) => `${count} ${count === 1 ? "item" : "items"}`;
|
|
6
|
+
/**
|
|
7
|
+
* English defaults. Every string the primitive can render lives here, so a
|
|
8
|
+
* consumer can translate the whole surface by passing `labels`.
|
|
9
|
+
*/
|
|
10
|
+
const CASCADER_LABELS = {
|
|
11
|
+
search: (parentLabel) => parentLabel ? `Search ${parentLabel}...` : "Search...",
|
|
12
|
+
back: "Back",
|
|
13
|
+
loading: "Loading...",
|
|
14
|
+
loadingMore: "Loading more...",
|
|
15
|
+
loadMore: "Load more",
|
|
16
|
+
error: "Could not load items.",
|
|
17
|
+
retry: "Retry",
|
|
18
|
+
empty: "No results found.",
|
|
19
|
+
selectedCount: (count) => `${count} selected`,
|
|
20
|
+
breadcrumbLabel: "Breadcrumb",
|
|
21
|
+
chipsLabel: "Selected items",
|
|
22
|
+
removeChip: (label) => `Remove ${label}`,
|
|
23
|
+
pathSeparator: "/",
|
|
24
|
+
rootLevel: ROOT_LEVEL,
|
|
25
|
+
itemCount,
|
|
26
|
+
branchAffordance: "submenu",
|
|
27
|
+
selectedState: "selected",
|
|
28
|
+
partiallySelectedState: "partially selected",
|
|
29
|
+
columnsLabel: "Levels",
|
|
30
|
+
actionsLabel: "Actions",
|
|
31
|
+
submenuAffordance: "opens a menu",
|
|
32
|
+
panelLabel: "Options",
|
|
33
|
+
keyboardHint: (mode, dir) => {
|
|
34
|
+
const open = dir === "rtl" ? "Left" : "Right";
|
|
35
|
+
const back = dir === "rtl" ? "Right" : "Left";
|
|
36
|
+
if (mode === "tree") return `Use the ${open} Arrow key to expand and the ${back} Arrow key to collapse.`;
|
|
37
|
+
if (mode === "columns") return `Use the ${open} Arrow key to open the next column and the ${back} Arrow key to go back.`;
|
|
38
|
+
return `Use the ${open} Arrow key to open a branch and the ${back} Arrow key to go back.`;
|
|
39
|
+
},
|
|
40
|
+
rootAnnouncement: (count) => `${ROOT_LEVEL}, ${itemCount(count)}`,
|
|
41
|
+
expandedAnnouncement: (label, count) => `${label} expanded, ${itemCount(count)}`,
|
|
42
|
+
collapsedAnnouncement: (label) => `${label} collapsed`,
|
|
43
|
+
levelAnnouncement: (parentLabel, depth, count) => `${parentLabel}, level ${depth}, ${itemCount(count)}`,
|
|
44
|
+
resultsAnnouncement: (count) => count === 1 ? "1 result" : `${count} results`,
|
|
45
|
+
maxReachedAnnouncement: (max) => `Selection limit of ${max} reached`,
|
|
46
|
+
cascadeAnnouncement: (label, count, selecting) => `${label} ${selecting ? "selected" : "deselected"}, ${itemCount(count)} followed`,
|
|
47
|
+
searchingAnnouncement: "Searching..."
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Shallow-merges consumer overrides over the defaults, so `labels` can carry a
|
|
51
|
+
* single key without restating the rest.
|
|
52
|
+
*/
|
|
53
|
+
function resolveCascaderLabels(labels) {
|
|
54
|
+
if (!labels) return CASCADER_LABELS;
|
|
55
|
+
return {
|
|
56
|
+
...CASCADER_LABELS,
|
|
57
|
+
...labels
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Resolves the search placeholder, which may be a string or a function. */
|
|
61
|
+
function resolveCascaderSearchLabel(labels, parentLabel) {
|
|
62
|
+
return typeof labels.search === "function" ? labels.search(parentLabel) : labels.search;
|
|
63
|
+
}
|
|
64
|
+
export { CASCADER_LABELS, resolveCascaderLabels, resolveCascaderSearchLabel };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { CascaderNode } from "./cascader-types.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
import { Combobox } from "@base-ui/react";
|
|
4
|
+
type VetoableEvent = {
|
|
5
|
+
preventBaseUIHandler?: () => void;
|
|
6
|
+
};
|
|
7
|
+
/** A row press. The veto hook is optional: an `as="button"` row is outside Base
|
|
8
|
+
* UI's listbox and gets a plain React event. */
|
|
9
|
+
export type CascaderRowEvent = React$1.MouseEvent<HTMLElement> & VetoableEvent;
|
|
10
|
+
export interface CascaderItemProps extends Omit<Combobox.Item.Props, "value" | "children" | "className" | "style" | "onClick" | "onMouseUp"> {
|
|
11
|
+
/** No state callback: the row merges `className` and `style` itself. */
|
|
12
|
+
className?: string;
|
|
13
|
+
style?: React$1.CSSProperties;
|
|
14
|
+
onClick?: (event: CascaderRowEvent) => void;
|
|
15
|
+
onMouseUp?: (event: CascaderRowEvent) => void;
|
|
16
|
+
node: CascaderNode;
|
|
17
|
+
/** Explicit render index. Required while virtualized and DELIBERATELY IGNORED
|
|
18
|
+
* otherwise: it self-registers the row and breaks `aria-activedescendant`. */
|
|
19
|
+
index?: number;
|
|
20
|
+
/** Indentation depth. Drives `--cascader-indent` in tree mode. */
|
|
21
|
+
depth?: number;
|
|
22
|
+
/** Renders the ancestor chain under the label, for deep and global search. */
|
|
23
|
+
showPath?: boolean;
|
|
24
|
+
/** `option` renders a real `Combobox.Item`; `button` renders identical markup
|
|
25
|
+
* outside the listbox, for the ancestor columns in columns mode. */
|
|
26
|
+
as?: "option" | "button";
|
|
27
|
+
/** Tree mode: this row is an expanded branch. */
|
|
28
|
+
expanded?: boolean;
|
|
29
|
+
/** Indent each depth by this many pixels. Tree mode only. */
|
|
30
|
+
indent?: number;
|
|
31
|
+
/** Whether the node has children. Defaults to the cascader's own answer, as do
|
|
32
|
+
* the three below: callers pass them because they already hold the level,
|
|
33
|
+
* which keeps a memoised row off the volatile context. */
|
|
34
|
+
branch?: boolean;
|
|
35
|
+
/** Whether the node may be committed. Defaults to the cascader's own answer. */
|
|
36
|
+
selectable?: boolean;
|
|
37
|
+
/** Whether the node is currently selected. Defaults to the cascader's answer. */
|
|
38
|
+
selected?: boolean;
|
|
39
|
+
/** Some but not all of the loaded subtree selected. `cascade` only. */
|
|
40
|
+
indeterminate?: boolean;
|
|
41
|
+
/** Selected nodes below this one, at any depth. Drives the trailing count. */
|
|
42
|
+
selectedCount?: number;
|
|
43
|
+
/** Paging row only: a request for this level is in flight. */
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
/** Paging row only: this level's last request failed. */
|
|
46
|
+
error?: boolean;
|
|
47
|
+
/** Branch row only: this node's OWN children are being fetched. The chevron
|
|
48
|
+
* (or, in tree mode, the expander) becomes a spinner IN PLACE, in the same
|
|
49
|
+
* 16px box: no skeleton row appears below, nothing reflows. */
|
|
50
|
+
childrenLoading?: boolean;
|
|
51
|
+
/** Branch row only: the last fetch of this node's children failed. The same
|
|
52
|
+
* box becomes the retry. The press path is unchanged: it goes through
|
|
53
|
+
* `navigate`, which refires the failed level rather than moving. */
|
|
54
|
+
childrenError?: boolean;
|
|
55
|
+
children?: React$1.ReactNode;
|
|
56
|
+
}
|
|
57
|
+
export interface CascaderItemsProps {
|
|
58
|
+
/** Replaces the default row for every item. */
|
|
59
|
+
children?: (node: CascaderNode, index: number) => React$1.ReactNode;
|
|
60
|
+
}
|
|
61
|
+
/** Whatever the active view is: the current level, the deepest column, or the
|
|
62
|
+
* flattened tree rows. Lives here so `cascader.tsx` never imports a row
|
|
63
|
+
* component, which would cycle with the context. `index` is deliberately not
|
|
64
|
+
* passed; see `CascaderItemProps`. */
|
|
65
|
+
declare function CascaderItems({ children }: CascaderItemsProps): React$1.JSX.Element;
|
|
66
|
+
export interface CascaderGroupProps extends Omit<Combobox.Group.Props, "className"> {
|
|
67
|
+
/** Base UI also accepts a state callback here; the group has no state. */
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
/** A run of related rows, named by the `CascaderLabel` inside it: the group is
|
|
71
|
+
* `role="group"` and Base UI points its `aria-labelledby` at the label, where a
|
|
72
|
+
* heading alone inside a listbox is dropped from the accessibility tree.
|
|
73
|
+
* Grouping is composition, not data: `CascaderItems` renders one flat run per
|
|
74
|
+
* level, so its indices line up with `listRef`. */
|
|
75
|
+
declare function CascaderGroup({ className, ...props }: CascaderGroupProps): React$1.JSX.Element;
|
|
76
|
+
export interface CascaderLabelProps extends Omit<Combobox.GroupLabel.Props, "className" | "style"> {
|
|
77
|
+
/** No state callback; outside a group this is not a Base UI part at all. */
|
|
78
|
+
className?: string;
|
|
79
|
+
style?: React$1.CSSProperties;
|
|
80
|
+
}
|
|
81
|
+
/** A heading. Inside `CascaderGroup` it is the group's accessible name; outside
|
|
82
|
+
* one (the footer flyout is a menu, not a listbox) it is decoration only. */
|
|
83
|
+
declare function CascaderLabel(props: CascaderLabelProps): React$1.JSX.Element;
|
|
84
|
+
export interface CascaderSeparatorProps extends Omit<Combobox.Separator.Props, "className"> {
|
|
85
|
+
/** Base UI also accepts a state callback here; only the orientation varies. */
|
|
86
|
+
className?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Decorative on purpose: Base UI's separator is `role="separator"`, which a
|
|
89
|
+
* `listbox` may not own, and a run that needs separating for a screen reader
|
|
90
|
+
* needs a `CascaderGroup`, not a line. A consumer can pass the role back. */
|
|
91
|
+
declare function CascaderSeparator({ className, ...props }: CascaderSeparatorProps): React$1.JSX.Element;
|
|
92
|
+
export { CascaderGroup, CascaderItems, CascaderLabel, CascaderSeparator };
|