@deftui/react 0.3.0 → 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/dist/index.d.mts +4 -3
- package/dist/index.mjs +2 -3
- package/dist/vendor/README.md +2 -1
- 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/widgets/export.d.mts +28 -0
- package/dist/widgets/index.d.mts +55 -11
- package/dist/widgets/index.mjs +45 -2
- package/package.json +2 -2
- package/dist/vendor/tailored/file-tree.d.mts +0 -21
- package/dist/vendor/tailored/file-tree.mjs +0 -146
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CascaderCollapse, CascaderNode, CascaderValueDisplay } from "./cascader-types.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
4
|
+
import { Combobox } from "@base-ui/react";
|
|
5
|
+
export type CascaderNavProps = useRender.ComponentProps<"div">;
|
|
6
|
+
/**
|
|
7
|
+
* Header: back control, search input, and the separator under them. The
|
|
8
|
+
* breadcrumb belongs with the list below: it describes the rows, not the field.
|
|
9
|
+
*/
|
|
10
|
+
declare function CascaderNav({ className, ...props }: CascaderNavProps): React$1.ReactElement<unknown, string | React$1.JSXElementConstructor<any>>;
|
|
11
|
+
export interface CascaderBackProps extends Omit<useRender.ComponentProps<"button">, "children"> {
|
|
12
|
+
children?: React$1.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/** Pops one level. Renders nothing at the root, so no dead header space. */
|
|
15
|
+
declare function CascaderBack({ className, children, ...props }: CascaderBackProps): React$1.ReactElement<unknown, string | React$1.JSXElementConstructor<any>> | null;
|
|
16
|
+
export interface CascaderBreadcrumbProps extends Omit<useRender.ComponentProps<"nav">, "children"> {
|
|
17
|
+
/** Maximum visible node segments before the middle collapses. */
|
|
18
|
+
maxSegments?: number;
|
|
19
|
+
collapse?: CascaderCollapse;
|
|
20
|
+
/** Clicking a segment navigates back to that level. Defaults to true. */
|
|
21
|
+
interactive?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compact trail of the current level AND its ancestors, collapsed by the same
|
|
25
|
+
* helper as `CascaderValue` so panel and trigger cannot disagree. The current
|
|
26
|
+
* level IS the last crumb: dropping it read as a rendering bug, a trail naming
|
|
27
|
+
* only the places you are not.
|
|
28
|
+
*/
|
|
29
|
+
declare function CascaderBreadcrumb({ className, maxSegments, collapse, interactive, ...props }: CascaderBreadcrumbProps): React$1.ReactElement<unknown, string | React$1.JSXElementConstructor<any>> | null;
|
|
30
|
+
export interface CascaderInputProps extends Combobox.Input.Props {
|
|
31
|
+
/** Renders the back control inline, before the field. Defaults to true. */
|
|
32
|
+
showBack?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Search field. MUST render inside the positioner: only there does Base UI skip
|
|
36
|
+
* the refill from the committed selection that would fight every level swap.
|
|
37
|
+
*/
|
|
38
|
+
declare function CascaderInput({ className, showBack, placeholder, onKeyDown, "aria-describedby": ariaDescribedBy, ...props }: CascaderInputProps): React$1.JSX.Element;
|
|
39
|
+
export interface CascaderValueProps extends Omit<useRender.ComponentProps<"span">, "children"> {
|
|
40
|
+
/** `path` shows the full trail, `leaf` only the node, `count` only a total. */
|
|
41
|
+
display?: CascaderValueDisplay;
|
|
42
|
+
maxSegments?: number;
|
|
43
|
+
collapse?: CascaderCollapse;
|
|
44
|
+
separator?: React$1.ReactNode;
|
|
45
|
+
/** Renders the selected node's icon before the trail. */
|
|
46
|
+
showIcon?: boolean;
|
|
47
|
+
placeholder?: React$1.ReactNode;
|
|
48
|
+
/** Replaces the whole rendering. Receives the resolved selection and path. */
|
|
49
|
+
children?: (selected: CascaderNode[], path: CascaderNode[]) => React$1.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Trigger display. Defaults to the collapsed selection path rather than a bare
|
|
53
|
+
* leaf label, which in a nested picker is frequently ambiguous.
|
|
54
|
+
*/
|
|
55
|
+
declare function CascaderValue({ className, display, maxSegments, collapse, separator, showIcon, placeholder, children, ...props }: CascaderValueProps): React$1.ReactElement<unknown, string | React$1.JSXElementConstructor<any>>;
|
|
56
|
+
export { CascaderBack, CascaderBreadcrumb, CascaderInput, CascaderNav, CascaderValue };
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCascaderActions, useCascaderState } from "./cascader-context.mjs";
|
|
3
|
+
import { resolveCascaderSearchLabel } from "./cascader-i18n.mjs";
|
|
4
|
+
import { collapseCascaderPath, getCascaderFooterStops, getCascaderPath, isCascaderRtl } from "./cascader-lib.mjs";
|
|
5
|
+
import { cn } from "cn";
|
|
6
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
7
|
+
import * as React$1 from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
10
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
11
|
+
import { Combobox } from "@base-ui/react";
|
|
12
|
+
import { useDirection } from "@base-ui/react/direction-provider";
|
|
13
|
+
// src/vendor/reui.io/cascader/cascader-nav.tsx
|
|
14
|
+
function PathChevron() {
|
|
15
|
+
return /* @__PURE__ */ jsx(ChevronRightIcon, { className: "size-3 shrink-0 opacity-50 rtl:-scale-x-100" });
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Header: back control, search input, and the separator under them. The
|
|
19
|
+
* breadcrumb belongs with the list below: it describes the rows, not the field.
|
|
20
|
+
*/
|
|
21
|
+
function CascaderNav({ className, ...props }) {
|
|
22
|
+
const defaultProps = {
|
|
23
|
+
"data-slot": "cascader-nav",
|
|
24
|
+
className: cn("border-border/60 flex shrink-0 flex-col gap-1 border-b px-1.5 py-1", className)
|
|
25
|
+
};
|
|
26
|
+
return useRender({
|
|
27
|
+
defaultTagName: "div",
|
|
28
|
+
render: props.render,
|
|
29
|
+
props: mergeProps(defaultProps, props)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Pops one level. Renders nothing at the root, so no dead header space. */
|
|
33
|
+
function CascaderBack({ className, children, ...props }) {
|
|
34
|
+
const { popLevel, labels, mode } = useCascaderActions();
|
|
35
|
+
const { path } = useCascaderState();
|
|
36
|
+
const hidden = mode !== "drill" || path.length === 0;
|
|
37
|
+
const defaultProps = {
|
|
38
|
+
"data-slot": "cascader-back",
|
|
39
|
+
type: "button",
|
|
40
|
+
"aria-label": labels.back,
|
|
41
|
+
onClick: () => popLevel(),
|
|
42
|
+
className: cn("text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex shrink-0 items-center justify-center rounded-md outline-hidden transition-colors focus-visible:ring-2", "size-6", className),
|
|
43
|
+
children: children ?? /* @__PURE__ */ jsx(ChevronLeftIcon, { className: "size-4 rtl:-scale-x-100" })
|
|
44
|
+
};
|
|
45
|
+
const element = useRender({
|
|
46
|
+
defaultTagName: "button",
|
|
47
|
+
render: props.render,
|
|
48
|
+
props: mergeProps(defaultProps, props)
|
|
49
|
+
});
|
|
50
|
+
return hidden ? null : element;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compact trail of the current level AND its ancestors, collapsed by the same
|
|
54
|
+
* helper as `CascaderValue` so panel and trigger cannot disagree. The current
|
|
55
|
+
* level IS the last crumb: dropping it read as a rendering bug, a trail naming
|
|
56
|
+
* only the places you are not.
|
|
57
|
+
*/
|
|
58
|
+
function CascaderBreadcrumb({ className, maxSegments = 3, collapse = "middle", interactive = true, ...props }) {
|
|
59
|
+
const { goToDepth, mode, labels } = useCascaderActions();
|
|
60
|
+
const { path, index } = useCascaderState();
|
|
61
|
+
const nodes = path.map((value) => index.byValue.get(value)).filter(Boolean);
|
|
62
|
+
const segments = collapseCascaderPath(nodes, {
|
|
63
|
+
maxSegments,
|
|
64
|
+
collapse
|
|
65
|
+
});
|
|
66
|
+
const currentValue = nodes.length ? nodes[nodes.length - 1].value : null;
|
|
67
|
+
const hidden = mode !== "drill" || nodes.length === 0;
|
|
68
|
+
const goTo = (node) => {
|
|
69
|
+
const depth = path.indexOf(node.value);
|
|
70
|
+
if (depth < 0) return;
|
|
71
|
+
goToDepth(depth + 1);
|
|
72
|
+
};
|
|
73
|
+
const defaultProps = {
|
|
74
|
+
"data-slot": "cascader-breadcrumb",
|
|
75
|
+
"aria-label": labels.breadcrumbLabel,
|
|
76
|
+
className: cn("text-muted-foreground flex min-w-0 shrink-0 items-center gap-0.5 pt-1.5 pb-0.5 text-xs", "px-3", className),
|
|
77
|
+
children: segments.map((segment, i) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [i > 0 ? /* @__PURE__ */ jsx(PathChevron, {}) : null, segment.type === "ellipsis" ? /* @__PURE__ */ jsx("span", {
|
|
78
|
+
"data-slot": "cascader-breadcrumb-ellipsis",
|
|
79
|
+
title: segment.hidden.map((n) => n.label).join(` ${labels.pathSeparator} `),
|
|
80
|
+
className: "shrink-0",
|
|
81
|
+
children: "…"
|
|
82
|
+
}) : interactive && segment.node.value !== currentValue ? /* @__PURE__ */ jsx("button", {
|
|
83
|
+
type: "button",
|
|
84
|
+
"data-slot": "cascader-breadcrumb-item",
|
|
85
|
+
onClick: () => goTo(segment.node),
|
|
86
|
+
className: "hover:text-foreground focus-visible:ring-ring/50 max-w-32 truncate rounded-sm outline-hidden transition-colors focus-visible:ring-2",
|
|
87
|
+
children: segment.node.label
|
|
88
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
89
|
+
"data-slot": "cascader-breadcrumb-item",
|
|
90
|
+
...segment.node.value === currentValue ? { "aria-current": "page" } : null,
|
|
91
|
+
className: cn("max-w-32 truncate", segment.node.value === currentValue && "text-foreground font-medium"),
|
|
92
|
+
children: segment.node.label
|
|
93
|
+
})] }, segment.type === "node" ? segment.node.value : `gap-${i}`))
|
|
94
|
+
};
|
|
95
|
+
const element = useRender({
|
|
96
|
+
defaultTagName: "nav",
|
|
97
|
+
render: props.render,
|
|
98
|
+
props: mergeProps(defaultProps, props)
|
|
99
|
+
});
|
|
100
|
+
return hidden ? null : element;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Search field. MUST render inside the positioner: only there does Base UI skip
|
|
104
|
+
* the refill from the committed selection that would fight every level swap.
|
|
105
|
+
*/
|
|
106
|
+
function CascaderInput({ className, showBack = true, placeholder, onKeyDown, "aria-describedby": ariaDescribedBy, ...props }) {
|
|
107
|
+
const { labels, popLevel, mode, getHighlighted, isBranch, isDisabled, navigate, index, toggleExpanded, inline, invalid, baseId, searchScope } = useCascaderActions();
|
|
108
|
+
const direction = useDirection();
|
|
109
|
+
const { currentParent, query, path, expanded, renderedItems, treeRows } = useCascaderState();
|
|
110
|
+
const resolvedPlaceholder = placeholder ?? resolveCascaderSearchLabel(labels, searchScope === "global" ? void 0 : currentParent?.label);
|
|
111
|
+
const backVisible = showBack && mode === "drill" && path.length > 0;
|
|
112
|
+
const hintId = `${baseId}-hint`;
|
|
113
|
+
const listId = `${baseId}-column-${mode === "columns" ? path.length : 0}`;
|
|
114
|
+
const hintRef = React$1.useRef(null);
|
|
115
|
+
const [hintDir, setHintDir] = React$1.useState("ltr");
|
|
116
|
+
React$1.useLayoutEffect(() => {
|
|
117
|
+
const hint = hintRef.current;
|
|
118
|
+
if (!hint) return;
|
|
119
|
+
setHintDir(isCascaderRtl(hint, direction) ? "rtl" : "ltr");
|
|
120
|
+
}, [direction]);
|
|
121
|
+
/**
|
|
122
|
+
* Moves the highlight to `targetIndex` in `treeRows`. Base UI has no
|
|
123
|
+
* imperative setter (`actionsRef` is `{ unmount }`), so the move is arrow
|
|
124
|
+
* presses, sound only because `useListNavigation` fires `onItemHighlighted`
|
|
125
|
+
* SYNCHRONOUSLY per keydown. A press that fails to close the gap ends it.
|
|
126
|
+
*/
|
|
127
|
+
const moveHighlightTo = (field, targetIndex) => {
|
|
128
|
+
const rowIndex = () => {
|
|
129
|
+
const highlighted = getHighlighted();
|
|
130
|
+
if (!highlighted) return -1;
|
|
131
|
+
return treeRows.findIndex((row) => row.node.value === highlighted.value);
|
|
132
|
+
};
|
|
133
|
+
let current = rowIndex();
|
|
134
|
+
for (let step = 0; step < treeRows.length; step += 1) {
|
|
135
|
+
if (current === -1 || current === targetIndex) return;
|
|
136
|
+
const key = current > targetIndex ? "ArrowUp" : "ArrowDown";
|
|
137
|
+
field.dispatchEvent(new KeyboardEvent("keydown", {
|
|
138
|
+
key,
|
|
139
|
+
bubbles: true,
|
|
140
|
+
cancelable: true
|
|
141
|
+
}));
|
|
142
|
+
const next = rowIndex();
|
|
143
|
+
if (next === -1 || !(key === "ArrowUp" ? next < current && next >= targetIndex : next > current && next <= targetIndex)) return;
|
|
144
|
+
current = next;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* The tree pattern's two level keys; Base UI leaves both alone (list
|
|
149
|
+
* navigation is vertical-only, the chip handler no-ops without
|
|
150
|
+
* `Combobox.Chips`). Passed in rather than hardcoded because RTL swaps them:
|
|
151
|
+
* APG defines them as "toward the children", not as a physical arrow.
|
|
152
|
+
*/
|
|
153
|
+
const handleTreeKeyDown = (event, field, caretAtStart, caretAtEnd, forwardKey, backKey) => {
|
|
154
|
+
const forward = event.key === forwardKey;
|
|
155
|
+
if (!forward && event.key !== backKey) return;
|
|
156
|
+
if (forward ? !caretAtEnd : !caretAtStart) return;
|
|
157
|
+
const highlighted = getHighlighted();
|
|
158
|
+
if (!highlighted) return;
|
|
159
|
+
const rowIndex = treeRows.findIndex((row) => row.node.value === highlighted.value);
|
|
160
|
+
if (rowIndex < 0) return;
|
|
161
|
+
const row = treeRows[rowIndex];
|
|
162
|
+
if (forward) {
|
|
163
|
+
if (!row.branch) return;
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
if (!row.expanded) {
|
|
166
|
+
navigate(row.node);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const child = treeRows[rowIndex + 1];
|
|
170
|
+
if (child && child.depth === row.depth + 1) moveHighlightTo(field, rowIndex + 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const heldByQuery = isDisabled(row.node) && !expanded.has(row.node.value);
|
|
174
|
+
if (row.branch && row.expanded && !heldByQuery) {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
toggleExpanded(row.node.value);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const parentValue = index.parentOf.get(row.node.value);
|
|
180
|
+
if (!parentValue) return;
|
|
181
|
+
const parentIndex = treeRows.findIndex((entry) => entry.node.value === parentValue);
|
|
182
|
+
if (parentIndex < 0) return;
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
moveHighlightTo(field, parentIndex);
|
|
185
|
+
};
|
|
186
|
+
const handleKeyDown = (event) => {
|
|
187
|
+
onKeyDown?.(event);
|
|
188
|
+
if (event.defaultPrevented) return;
|
|
189
|
+
const field = event.currentTarget;
|
|
190
|
+
const caretAtStart = field.selectionStart === 0 && field.selectionEnd === 0;
|
|
191
|
+
const caretAtEnd = field.selectionStart === query.length && field.selectionEnd === query.length;
|
|
192
|
+
if (event.key === "ArrowDown" && !event.altKey) {
|
|
193
|
+
const rowCount = mode === "tree" ? treeRows.length : renderedItems.length;
|
|
194
|
+
const lastValue = mode === "tree" ? treeRows[rowCount - 1]?.node.value : renderedItems[rowCount - 1]?.value;
|
|
195
|
+
const highlighted = getHighlighted();
|
|
196
|
+
if (rowCount === 0 || highlighted != null && highlighted.value === lastValue) {
|
|
197
|
+
const panel = field.closest("[data-slot=\"cascader-panel\"]");
|
|
198
|
+
const stop = panel ? getCascaderFooterStops(panel)[0] : void 0;
|
|
199
|
+
if (stop) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
stop.focus();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const rtl = isCascaderRtl(field, direction);
|
|
207
|
+
const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
|
|
208
|
+
const backKey = rtl ? "ArrowRight" : "ArrowLeft";
|
|
209
|
+
if (mode === "tree") {
|
|
210
|
+
handleTreeKeyDown(event, field, caretAtStart, caretAtEnd, forwardKey, backKey);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (event.key === forwardKey && caretAtEnd) {
|
|
214
|
+
const highlighted = getHighlighted();
|
|
215
|
+
if (highlighted && isBranch(highlighted)) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
navigate(highlighted);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (event.key === backKey && caretAtStart || event.key === "Backspace" && query.length === 0) {
|
|
222
|
+
if (path.length > 0) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
popLevel();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
229
|
+
"data-slot": "cascader-input-row",
|
|
230
|
+
className: cn("flex shrink-0 items-center gap-1", "h-8"),
|
|
231
|
+
children: [
|
|
232
|
+
showBack ? /* @__PURE__ */ jsx(CascaderBack, {}) : null,
|
|
233
|
+
/* @__PURE__ */ jsx(Combobox.Input, {
|
|
234
|
+
"data-slot": "cascader-input",
|
|
235
|
+
placeholder: resolvedPlaceholder,
|
|
236
|
+
onKeyDown: handleKeyDown,
|
|
237
|
+
"aria-describedby": [ariaDescribedBy, hintId].filter(Boolean).join(" "),
|
|
238
|
+
...mode === "tree" ? { "aria-haspopup": "tree" } : null,
|
|
239
|
+
...inline ? {
|
|
240
|
+
"aria-expanded": true,
|
|
241
|
+
"aria-controls": listId
|
|
242
|
+
} : null,
|
|
243
|
+
...invalid ? {
|
|
244
|
+
"aria-invalid": true,
|
|
245
|
+
"data-invalid": ""
|
|
246
|
+
} : null,
|
|
247
|
+
className: cn("placeholder:text-muted-foreground h-full w-full min-w-0 flex-1 bg-transparent px-1.5 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", backVisible && "ps-0", className),
|
|
248
|
+
...props
|
|
249
|
+
}),
|
|
250
|
+
/* @__PURE__ */ jsx("span", {
|
|
251
|
+
id: hintId,
|
|
252
|
+
ref: hintRef,
|
|
253
|
+
className: "sr-only",
|
|
254
|
+
children: labels.keyboardHint(mode, hintDir)
|
|
255
|
+
})
|
|
256
|
+
]
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Trigger display. Defaults to the collapsed selection path rather than a bare
|
|
261
|
+
* leaf label, which in a nested picker is frequently ambiguous.
|
|
262
|
+
*/
|
|
263
|
+
function CascaderValue({ className, display = "path", maxSegments = 3, collapse = "middle", separator, showIcon = true, placeholder, children, ...props }) {
|
|
264
|
+
const { labels, multiple, resolveNode } = useCascaderActions();
|
|
265
|
+
const { index, selectedValues } = useCascaderState();
|
|
266
|
+
const selected = selectedValues.map(resolveNode);
|
|
267
|
+
const resolvedPath = selectedValues.length === 1 ? getCascaderPath(index, selectedValues[0]) : [];
|
|
268
|
+
const path = resolvedPath.length > 0 ? resolvedPath : selected.length === 1 ? selected : [];
|
|
269
|
+
let content;
|
|
270
|
+
if (children) content = children(selected, path);
|
|
271
|
+
else if (selectedValues.length === 0) content = /* @__PURE__ */ jsx("span", {
|
|
272
|
+
className: "text-muted-foreground truncate",
|
|
273
|
+
children: placeholder
|
|
274
|
+
});
|
|
275
|
+
else if (display === "count" || multiple && selectedValues.length > 1) content = labels.selectedCount(selectedValues.length);
|
|
276
|
+
else {
|
|
277
|
+
const leaf = path[path.length - 1] ?? selected[0];
|
|
278
|
+
const segments = display === "leaf" ? [{
|
|
279
|
+
type: "node",
|
|
280
|
+
node: leaf
|
|
281
|
+
}] : collapseCascaderPath(path, {
|
|
282
|
+
maxSegments,
|
|
283
|
+
collapse
|
|
284
|
+
});
|
|
285
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [showIcon && leaf?.icon ? /* @__PURE__ */ jsx("span", {
|
|
286
|
+
"data-slot": "cascader-value-icon",
|
|
287
|
+
className: "text-muted-foreground flex shrink-0 items-center",
|
|
288
|
+
children: leaf.icon
|
|
289
|
+
}) : null, segments.map((segment, i) => /* @__PURE__ */ jsxs(React$1.Fragment, { children: [i > 0 ? separator ?? /* @__PURE__ */ jsx(PathChevron, {}) : null, segment.type === "ellipsis" ? /* @__PURE__ */ jsx("span", {
|
|
290
|
+
"data-slot": "cascader-value-ellipsis",
|
|
291
|
+
title: segment.hidden.map((n) => n.label).join(` ${labels.pathSeparator} `),
|
|
292
|
+
className: "text-muted-foreground shrink-0",
|
|
293
|
+
children: "…"
|
|
294
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
295
|
+
className: cn("truncate", i < segments.length - 1 && "text-muted-foreground"),
|
|
296
|
+
children: segment.node?.label
|
|
297
|
+
})] }, segment.type === "node" ? segment.node.value : `gap-${i}`))] });
|
|
298
|
+
}
|
|
299
|
+
const defaultProps = {
|
|
300
|
+
"data-slot": "cascader-value",
|
|
301
|
+
className: cn("flex min-w-0 items-center gap-1 truncate", className),
|
|
302
|
+
children: content
|
|
303
|
+
};
|
|
304
|
+
return useRender({
|
|
305
|
+
defaultTagName: "span",
|
|
306
|
+
render: props.render,
|
|
307
|
+
props: mergeProps(defaultProps, props)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
export { CascaderBack, CascaderBreadcrumb, CascaderInput, CascaderNav, CascaderValue };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A node in the cascader tree. Two input shapes normalize to one internal
|
|
4
|
+
* index: nested (nodes carry `children`), or flat adjacency plus the root's
|
|
5
|
+
* `getParent`, which skips the re-nesting step for large datasets.
|
|
6
|
+
*/
|
|
7
|
+
export interface CascaderNode<T = unknown> {
|
|
8
|
+
/** Stable id. Also the committed selection value. */
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: React$1.ReactNode;
|
|
12
|
+
description?: string;
|
|
13
|
+
children?: CascaderNode<T>[];
|
|
14
|
+
/** Declares a branch before load; async nodes read as leaves without it. */
|
|
15
|
+
hasChildren?: boolean;
|
|
16
|
+
/** Trailing count. Defaults to known children; set it for async nodes. */
|
|
17
|
+
count?: number;
|
|
18
|
+
/** Listed, never selected or opened; on a branch, its whole subtree too. */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
keywords?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Only read on a node nested inside a `getChildren` result. For the level a
|
|
23
|
+
* node OWNS the authoritative signal is that level's `CascaderLoadResult`.
|
|
24
|
+
*/
|
|
25
|
+
hasMore?: boolean;
|
|
26
|
+
data?: T;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One footer ACTION. Deliberately not a `CascaderNode`, so a command can never
|
|
30
|
+
* be passed where an option is expected and join the ring, filter or selection.
|
|
31
|
+
*/
|
|
32
|
+
export interface CascaderActionItem {
|
|
33
|
+
/** Stable key. Falls back to the label when it is a string, then the index. */
|
|
34
|
+
value?: string;
|
|
35
|
+
label: React$1.ReactNode;
|
|
36
|
+
icon?: React$1.ReactNode;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Ignored when `items` is present - a flyout opens instead. */
|
|
39
|
+
onSelect?: () => void;
|
|
40
|
+
/** Turns the row into a submenu trigger. One level deep on purpose. */
|
|
41
|
+
items?: CascaderActionItem[];
|
|
42
|
+
/** Consecutive entries sharing a heading are drawn under one. */
|
|
43
|
+
group?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Panel layout. See the docs for the keyboard map of each. */
|
|
46
|
+
export type CascaderMode = "drill" | "columns" | "tree";
|
|
47
|
+
export type CascaderSearchScope = "level" | "deep" | "global";
|
|
48
|
+
/**
|
|
49
|
+
* Which nodes may be committed. The predicate arm is generic over the payload
|
|
50
|
+
* rather than quantified per call: a per-call `<T>` would force every predicate
|
|
51
|
+
* to accept EVERY payload, so `(node: CascaderNode<Member>) => boolean` could
|
|
52
|
+
* never satisfy it.
|
|
53
|
+
*/
|
|
54
|
+
export type CascaderSelectable<T = unknown> = "leaf" | "any" | ((node: CascaderNode<T>) => boolean);
|
|
55
|
+
export type CascaderCollapse = "middle" | "start" | "none";
|
|
56
|
+
export type CascaderValueDisplay = "path" | "leaf" | "count";
|
|
57
|
+
export type CascaderChangeReason = "select" | "deselect" | "clear";
|
|
58
|
+
/** Second argument to `onValueChange`: resolved nodes, so no re-lookup. */
|
|
59
|
+
export interface CascaderChangeDetails<T = unknown> {
|
|
60
|
+
/** The node committed or toggled. Null when the selection was cleared. */
|
|
61
|
+
node: CascaderNode<T> | null;
|
|
62
|
+
/** Ancestor chain of `node`, root first, node last. */
|
|
63
|
+
path: CascaderNode<T>[];
|
|
64
|
+
nodes: CascaderNode<T>[];
|
|
65
|
+
reason: CascaderChangeReason;
|
|
66
|
+
}
|
|
67
|
+
/** Why a level was fetched. `resolve`: `resolveValue` hit an unloaded node. */
|
|
68
|
+
export type CascaderLoadReason = "level" | "more" | "prefetch" | "retry" | "resolve";
|
|
69
|
+
/** Argument handed to `getChildren` and `resolveValue`. */
|
|
70
|
+
export interface CascaderLoadContext {
|
|
71
|
+
/**
|
|
72
|
+
* Aborted when the request is superseded, `loadKey` changes or the cascader
|
|
73
|
+
* unmounts, and for a `getChildren` load also when the popup closes.
|
|
74
|
+
*/
|
|
75
|
+
signal: AbortSignal;
|
|
76
|
+
cursor?: string;
|
|
77
|
+
reason?: CascaderLoadReason;
|
|
78
|
+
}
|
|
79
|
+
/** Value returned by `getChildren`. A bare array is also accepted. */
|
|
80
|
+
export interface CascaderLoadResult<T = unknown> {
|
|
81
|
+
items: CascaderNode<T>[];
|
|
82
|
+
nextCursor?: string;
|
|
83
|
+
/** Defaults to whether `nextCursor` was supplied. */
|
|
84
|
+
hasMore?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** Argument handed to `onSearch`. */
|
|
87
|
+
export interface CascaderSearchContext {
|
|
88
|
+
signal: AbortSignal;
|
|
89
|
+
/** The path the user is searching within, deepest last. Empty under a
|
|
90
|
+
* global search, whose scope is the whole tree. */
|
|
91
|
+
path: string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Every user facing string, so the primitive ships no hardcoded copy. The
|
|
95
|
+
* callbacks take plain labels, not nodes: a `CascaderNode<T>` parameter would
|
|
96
|
+
* force this object to carry the item generic. `*Announcement` is live-region.
|
|
97
|
+
*/
|
|
98
|
+
export interface CascaderLabels {
|
|
99
|
+
/** `parentLabel` is absent at the root and under a global search. */
|
|
100
|
+
search: string | ((parentLabel?: string) => string);
|
|
101
|
+
back: string;
|
|
102
|
+
/** A level's FIRST page. `loadingMore` is the next, `loadMore` its idle row. */
|
|
103
|
+
loading: string;
|
|
104
|
+
loadingMore: string;
|
|
105
|
+
loadMore: string;
|
|
106
|
+
error: string;
|
|
107
|
+
retry: string;
|
|
108
|
+
empty: string;
|
|
109
|
+
/** Rendered by `CascaderValue` when `display="count"`. */
|
|
110
|
+
selectedCount: (count: number) => string;
|
|
111
|
+
breadcrumbLabel: string;
|
|
112
|
+
chipsLabel: string;
|
|
113
|
+
removeChip: (label: string) => string;
|
|
114
|
+
/** Trail separator. Not every locale writes one with a slash. */
|
|
115
|
+
pathSeparator: string;
|
|
116
|
+
/** Names the root level wherever there is no parent node to name it. */
|
|
117
|
+
rootLevel: string;
|
|
118
|
+
itemCount: (count: number) => string;
|
|
119
|
+
/** Appended to a branch row's name outside tree mode: it opens another list. */
|
|
120
|
+
branchAffordance: string;
|
|
121
|
+
/** Columns-trail rows are plain buttons, so they carry no `aria-selected`. */
|
|
122
|
+
selectedState: string;
|
|
123
|
+
/** Same for the mixed state, which a `role="button"` row may not carry. */
|
|
124
|
+
partiallySelectedState: string;
|
|
125
|
+
columnsLabel: string;
|
|
126
|
+
actionsLabel: string;
|
|
127
|
+
submenuAffordance: string;
|
|
128
|
+
panelLabel: string;
|
|
129
|
+
/** Read per mode AND per direction: the level keys mirror in RTL. */
|
|
130
|
+
keyboardHint: (mode: CascaderMode, dir: "ltr" | "rtl") => string;
|
|
131
|
+
rootAnnouncement: (count: number) => string;
|
|
132
|
+
expandedAnnouncement: (label: string, count: number) => string;
|
|
133
|
+
collapsedAnnouncement: (label: string) => string;
|
|
134
|
+
levelAnnouncement: (parentLabel: string, depth: number, count: number) => string;
|
|
135
|
+
resultsAnnouncement: (count: number) => string;
|
|
136
|
+
maxReachedAnnouncement: (max: number) => string;
|
|
137
|
+
/** `count` is the descendants swept along; `selecting` is the direction. */
|
|
138
|
+
cascadeAnnouncement: (label: string, count: number, selecting: boolean) => string;
|
|
139
|
+
searchingAnnouncement: string;
|
|
140
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "./cascader-context.mjs";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
import "@tanstack/react-virtual";
|
|
4
|
+
export interface CascaderVirtualItemsProps {
|
|
5
|
+
/** Row height before measurement. Defaults to the root `estimateRowSize`. */
|
|
6
|
+
estimateSize?: number;
|
|
7
|
+
/** Rows rendered beyond each edge. Defaults to the root `overscan`. */
|
|
8
|
+
overscan?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Windowed replacement for `CascaderItems`, dropped inside `CascaderList`. The
|
|
12
|
+
* registration is a LAYOUT effect because the root derives `virtualized` from
|
|
13
|
+
* it and the flip has to land before the browser paints. Until the root has
|
|
14
|
+
* seen it, this renders exactly what `CascaderItems` does, so no frame carries
|
|
15
|
+
* indexed rows while Base UI still owns the composite list.
|
|
16
|
+
*/
|
|
17
|
+
declare function CascaderVirtualItems(props: CascaderVirtualItemsProps): React$1.JSX.Element;
|
|
18
|
+
export { CascaderVirtualItems };
|