@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,1470 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCascaderLoadState, useCascaderLoader } from "./cascader-async.mjs";
|
|
3
|
+
import { CascaderActionsContext, CascaderHighlightContext, CascaderRenderContext, CascaderStateContext, createCascaderHighlightStore, useCascaderActions, useCascaderState } from "./cascader-context.mjs";
|
|
4
|
+
import { resolveCascaderLabels } from "./cascader-i18n.mjs";
|
|
5
|
+
import { CASCADER_LIST_HEIGHT_CLASS, CASCADER_LIST_PAD_CLASS, CASCADER_ROOT_KEY, CASCADER_ROWS_CLASS, CASCADER_SCROLL_CLASS, applyCascadeSelection, buildCascaderIndex, collapseCascaderPath, createCascaderMoreNode, filterCascaderLevel, findAmbiguousCascaderLabels, findCascaderDataIssues, flattenCascaderTree, focusCascaderHome, getCascaderCheckedValues, getCascaderChildren, getCascaderCount, getCascaderIndeterminateFrom, getCascaderListedValues, getCascaderPath, getCascaderSelectedDescendants, getCascaderTabTarget, isCascaderBranch, isCascaderDisabled, isCascaderMoreNode, isCascaderSelectable, matchesCascaderQuery, mergeCascaderIndex, normalizeCascaderQuery, searchCascaderDeep, warnCascaderOnce, withCascaderMoreRow } from "./cascader-lib.mjs";
|
|
6
|
+
import { ScrollArea } from "../../coss/scroll-area.mjs";
|
|
7
|
+
import { cn } from "cn";
|
|
8
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
9
|
+
import * as React$1 from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
12
|
+
import { ChevronDownIcon, XIcon } from "lucide-react";
|
|
13
|
+
import { Combobox } from "@base-ui/react";
|
|
14
|
+
// src/vendor/reui.io/cascader/cascader.tsx
|
|
15
|
+
/** Stable `filteredItems` for a level swap; a fixed identity cannot loop. */
|
|
16
|
+
const EMPTY = [];
|
|
17
|
+
/** Stable empty array for the `actions` prop, so a cascader with no footer
|
|
18
|
+
* does not republish the actions context on every render. */
|
|
19
|
+
const EMPTY_ACTIONS = [];
|
|
20
|
+
/** The `onSearch` scope of a global search: no path, so the whole tree. */
|
|
21
|
+
const GLOBAL_SEARCH_PATH = [];
|
|
22
|
+
const ANNOUNCE_MARKER = "";
|
|
23
|
+
const ANNOUNCE_DEBOUNCE = 150;
|
|
24
|
+
function useControllable(controlled, defaultValue, onChange, devName) {
|
|
25
|
+
const [uncontrolled, setUncontrolled] = React$1.useState(defaultValue);
|
|
26
|
+
const isControlled = controlled !== void 0;
|
|
27
|
+
const value = isControlled ? controlled : uncontrolled;
|
|
28
|
+
const wasControlled = React$1.useRef(isControlled);
|
|
29
|
+
React$1.useEffect(() => {
|
|
30
|
+
if (process.env.NODE_ENV === "production") return;
|
|
31
|
+
if (wasControlled.current === isControlled || !devName) return;
|
|
32
|
+
const from = wasControlled.current ? "controlled" : "uncontrolled";
|
|
33
|
+
const to = isControlled ? "controlled" : "uncontrolled";
|
|
34
|
+
wasControlled.current = isControlled;
|
|
35
|
+
warnCascaderOnce(`controlled-switch:${devName}`, `\`${devName}\` switched from ${from} to ${to}. Decide once: pass \`${devName}\` for the whole life of the component, or pass only the default and let the cascader own it.`);
|
|
36
|
+
}, [isControlled, devName]);
|
|
37
|
+
const latest = React$1.useRef({
|
|
38
|
+
value,
|
|
39
|
+
isControlled,
|
|
40
|
+
onChange
|
|
41
|
+
});
|
|
42
|
+
React$1.useEffect(() => {
|
|
43
|
+
latest.current = {
|
|
44
|
+
value,
|
|
45
|
+
isControlled,
|
|
46
|
+
onChange
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const lastProp = React$1.useRef(controlled);
|
|
50
|
+
React$1.useEffect(() => {
|
|
51
|
+
lastProp.current = controlled;
|
|
52
|
+
});
|
|
53
|
+
return [value, React$1.useCallback((next) => {
|
|
54
|
+
const current = latest.current.value;
|
|
55
|
+
const resolved = typeof next === "function" ? next(current) : next;
|
|
56
|
+
if (Object.is(resolved, current)) {
|
|
57
|
+
if (!latest.current.isControlled || Object.is(resolved, lastProp.current)) return;
|
|
58
|
+
}
|
|
59
|
+
latest.current = {
|
|
60
|
+
...latest.current,
|
|
61
|
+
value: resolved
|
|
62
|
+
};
|
|
63
|
+
if (!latest.current.isControlled) setUncontrolled(resolved);
|
|
64
|
+
latest.current.onChange?.(resolved);
|
|
65
|
+
}, [])];
|
|
66
|
+
}
|
|
67
|
+
function shallowEqualRecords(a, b) {
|
|
68
|
+
const left = a;
|
|
69
|
+
const right = b;
|
|
70
|
+
const keys = Object.keys(left);
|
|
71
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
72
|
+
for (const key of keys) if (!Object.is(left[key], right[key])) return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function shallowEqualItemLists(a, b) {
|
|
76
|
+
if (a.length !== b.length) return false;
|
|
77
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i] && !shallowEqualRecords(a[i], b[i])) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function useShallowStable(value, equal) {
|
|
81
|
+
const [stable, setStable] = React$1.useState(value);
|
|
82
|
+
if (Object.is(value, stable)) return stable;
|
|
83
|
+
if (typeof value === "object" && value !== null && typeof stable === "object" && stable !== null && equal(value, stable)) return stable;
|
|
84
|
+
setStable(value);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
/** Normalizes to an array. `""` is "no selection", not a node: it is the
|
|
88
|
+
* uncontrolled default, and resolving it would render a blank chip. */
|
|
89
|
+
function toArray(value) {
|
|
90
|
+
if (value == null) return [];
|
|
91
|
+
if (Array.isArray(value)) return value.filter((entry) => entry !== "");
|
|
92
|
+
return value === "" ? [] : [value];
|
|
93
|
+
}
|
|
94
|
+
/** Misconfigurations that are SILENT by design. Once each, never in prod. */
|
|
95
|
+
function useCascaderDevWarnings(options) {
|
|
96
|
+
const { items, getParent, mode, multiple, cascade, indicator, selectable, searchScope, max, value, defaultValue, hasExpanded, hasPath, hasOnSearch } = options;
|
|
97
|
+
const issues = React$1.useMemo(() => process.env.NODE_ENV === "production" ? null : findCascaderDataIssues(items, getParent), [items, getParent]);
|
|
98
|
+
React$1.useEffect(() => {
|
|
99
|
+
if (process.env.NODE_ENV === "production") return;
|
|
100
|
+
for (const duplicate of issues?.duplicates ?? []) warnCascaderOnce(`duplicate-value:${duplicate}`, `Duplicate node value ${JSON.stringify(duplicate)}. \`value\` is the selection key, so only the first occurrence is indexed and the rest never render.`);
|
|
101
|
+
for (const cycle of issues?.cycles ?? []) warnCascaderOnce(`cycle:${cycle}`, `\`getParent\` puts ${JSON.stringify(cycle)} on a cycle. The depth walk is cycle-guarded so nothing hangs, but every depth on that chain is clamped rather than derived.`);
|
|
102
|
+
}, [issues]);
|
|
103
|
+
React$1.useEffect(() => {
|
|
104
|
+
if (process.env.NODE_ENV === "production") return;
|
|
105
|
+
for (const [prop, current] of [["value", value], ["defaultValue", defaultValue]]) {
|
|
106
|
+
if (current == null) continue;
|
|
107
|
+
if (multiple && !Array.isArray(current)) warnCascaderOnce(`value-shape:multiple:${prop}`, `\`multiple\` is set but \`${prop}\` is a string. Multi-select reads and writes a \`string[]\`; a string is treated as no selection at all.`);
|
|
108
|
+
else if (!multiple && Array.isArray(current)) warnCascaderOnce(`value-shape:single:${prop}`, `\`${prop}\` is an array but \`multiple\` is not set. Single-select reads and writes a \`string\`; only the first entry would survive a commit.`);
|
|
109
|
+
}
|
|
110
|
+
if (cascade && !multiple) warnCascaderOnce("cascade-without-multiple", "`cascade` does nothing without `multiple`: a single selection has no subtree to propagate over.");
|
|
111
|
+
if (cascade && selectable === "leaf") warnCascaderOnce("cascade-with-leaf-selectable", "`cascade` does nothing while `selectable=\"leaf\"`: no branch can be committed, so no commit ever has a subtree under it. Pass `selectable=\"any\"` or a predicate that accepts branches.");
|
|
112
|
+
if (!indicator && multiple) warnCascaderOnce("indicator-false-with-multiple", "`indicator={false}` does nothing with `multiple`: the checkbox is the selection control rather than a decoration, so it and its gutter stay. Mark selection your own way from `data-selected` on the row, and drop the prop.");
|
|
113
|
+
if (max != null && !multiple) warnCascaderOnce("max-without-multiple", "`max` does nothing without `multiple`: there is only ever one selection to cap.");
|
|
114
|
+
if (hasExpanded && mode !== "tree") warnCascaderOnce(`expanded-outside-tree:${mode}`, `\`expanded\` only does something in \`mode="tree"\`, and this cascader is in \`mode="${mode}"\`. Drill and columns navigate with \`path\`.`);
|
|
115
|
+
if (hasPath && mode === "tree") warnCascaderOnce("path-in-tree", "`path` does nothing in `mode=\"tree\"`: branches expand in place rather than replacing the level. Use `expanded`.");
|
|
116
|
+
if (searchScope === "deep" && mode === "tree") warnCascaderOnce("deep-search-in-tree", "`searchScope=\"deep\"` does nothing in `mode=\"tree\"`: a tree query already matches at any depth and auto-expands the ancestors of every hit.");
|
|
117
|
+
if (searchScope === "global" && mode === "tree") warnCascaderOnce("global-search-in-tree", "`searchScope=\"global\"` does nothing in `mode=\"tree\"`: a tree query already searches the whole tree and auto-expands the ancestors of every hit.");
|
|
118
|
+
if (hasOnSearch && mode === "tree") warnCascaderOnce("onsearch-in-tree", "`onSearch` does nothing in `mode=\"tree\"`: a tree query filters the loaded tree in place, and a server hit belongs to no visible branch, so its results would never render. The request is not fired. Filter the tree locally, or use drill or columns for server search.");
|
|
119
|
+
}, [
|
|
120
|
+
mode,
|
|
121
|
+
multiple,
|
|
122
|
+
cascade,
|
|
123
|
+
indicator,
|
|
124
|
+
selectable,
|
|
125
|
+
searchScope,
|
|
126
|
+
max,
|
|
127
|
+
value,
|
|
128
|
+
defaultValue,
|
|
129
|
+
hasExpanded,
|
|
130
|
+
hasPath,
|
|
131
|
+
hasOnSearch
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
/** No default on `T`: inferred from `items`, so `node.data` arrives typed. */
|
|
135
|
+
function Cascader({ items, getParent, getChildren, onSearch, searchDebounce, resolveValue, loadKey, prefetch, onLoadError, value: valueProp, defaultValue, onValueChange: onValueChangeProp, multiple = false, selectable = "leaf", indicator = true, actions, cascade = false, max, mode = "drill", expandTrigger, path: pathProp, defaultPath, onPathChange, expanded: expandedProp, defaultExpanded, onExpandedChange, open: openProp, defaultOpen = false, onOpenChange, closeOnSelect = true, inputValue: inputValueProp, defaultInputValue = "", onInputValueChange, searchScope = "level", filter, revealSelected = true, maxHeight, virtualize, virtualizeThreshold = 100, estimateRowSize = 32, overscan = 8, labels: labelsProp, renderItem, renderLabel, disabled, name, form, id, required, readOnly, invalid, inputRef, inline, children }) {
|
|
136
|
+
const baseIndex = React$1.useMemo(() => buildCascaderIndex(items, getParent), [items]);
|
|
137
|
+
const baseId = React$1.useId();
|
|
138
|
+
const stableLabelsProp = useShallowStable(labelsProp, shallowEqualRecords);
|
|
139
|
+
const labels = React$1.useMemo(() => resolveCascaderLabels(stableLabelsProp), [stableLabelsProp]);
|
|
140
|
+
useCascaderDevWarnings({
|
|
141
|
+
items,
|
|
142
|
+
getParent,
|
|
143
|
+
mode,
|
|
144
|
+
multiple,
|
|
145
|
+
cascade,
|
|
146
|
+
indicator,
|
|
147
|
+
selectable,
|
|
148
|
+
searchScope,
|
|
149
|
+
max,
|
|
150
|
+
value: valueProp,
|
|
151
|
+
defaultValue,
|
|
152
|
+
hasExpanded: expandedProp !== void 0 || defaultExpanded !== void 0 || onExpandedChange !== void 0,
|
|
153
|
+
hasPath: pathProp !== void 0 || defaultPath !== void 0 || onPathChange !== void 0,
|
|
154
|
+
hasOnSearch: onSearch !== void 0
|
|
155
|
+
});
|
|
156
|
+
const onValueChange = onValueChangeProp;
|
|
157
|
+
const [value, setValue] = useControllable(valueProp, defaultValue ?? (multiple ? [] : ""), void 0, "value");
|
|
158
|
+
const pathReasonRef = React$1.useRef("external");
|
|
159
|
+
const openReasonRef = React$1.useRef("none");
|
|
160
|
+
const [path, setPath] = useControllable(pathProp, defaultPath ?? [], (next) => onPathChange?.(next, { reason: pathReasonRef.current }), "path");
|
|
161
|
+
const [expandedList, setExpandedList] = useControllable(expandedProp, defaultExpanded ?? [], onExpandedChange, "expanded");
|
|
162
|
+
const [open, setOpen] = useControllable(openProp, defaultOpen, (next) => onOpenChange?.(next, { reason: openReasonRef.current }), "open");
|
|
163
|
+
/** The internal `setPath`: names its reason, then hands back to "external". */
|
|
164
|
+
const setPathWithReason = React$1.useCallback((next, reason) => {
|
|
165
|
+
pathReasonRef.current = reason;
|
|
166
|
+
try {
|
|
167
|
+
setPath(next);
|
|
168
|
+
} finally {
|
|
169
|
+
pathReasonRef.current = "external";
|
|
170
|
+
}
|
|
171
|
+
}, [setPath]);
|
|
172
|
+
const [query, setQuery] = useControllable(inputValueProp, defaultInputValue, onInputValueChange, "inputValue");
|
|
173
|
+
const expanded = React$1.useMemo(() => new Set(expandedList), [expandedList]);
|
|
174
|
+
const selectedValues = React$1.useMemo(() => toArray(value), [value]);
|
|
175
|
+
const levels = React$1.useMemo(() => {
|
|
176
|
+
if (mode === "tree") return [CASCADER_ROOT_KEY, ...expandedList];
|
|
177
|
+
if (mode === "columns") return [CASCADER_ROOT_KEY, ...path];
|
|
178
|
+
return [path.length ? path[path.length - 1] : CASCADER_ROOT_KEY];
|
|
179
|
+
}, [
|
|
180
|
+
mode,
|
|
181
|
+
path,
|
|
182
|
+
expandedList
|
|
183
|
+
]);
|
|
184
|
+
const loader = useCascaderLoader({
|
|
185
|
+
base: baseIndex,
|
|
186
|
+
getChildren,
|
|
187
|
+
onSearch: mode === "tree" ? void 0 : onSearch,
|
|
188
|
+
resolveValue,
|
|
189
|
+
searchDebounce,
|
|
190
|
+
loadKey,
|
|
191
|
+
prefetch,
|
|
192
|
+
onLoadError,
|
|
193
|
+
enabled: open || !!inline,
|
|
194
|
+
query,
|
|
195
|
+
levels,
|
|
196
|
+
path: searchScope === "global" ? GLOBAL_SEARCH_PATH : path,
|
|
197
|
+
values: selectedValues
|
|
198
|
+
});
|
|
199
|
+
const loadStates = loader.states;
|
|
200
|
+
const { ensureLevel, retryLevel: retryLoaderLevel, invalidateLevel } = loader;
|
|
201
|
+
/**
|
|
202
|
+
* Whether pressing this branch has to FETCH first: in flight, failed, or
|
|
203
|
+
* never asked. `baseIndex`, as `ensureLevel` asks it: the two must agree or a
|
|
204
|
+
* press parks a navigation nothing will ever settle.
|
|
205
|
+
*/
|
|
206
|
+
const needsChildren = React$1.useCallback((node) => {
|
|
207
|
+
if (!loader.active) return false;
|
|
208
|
+
const state = loadStates.get(node.value);
|
|
209
|
+
if (state) return state.loading || state.error;
|
|
210
|
+
return !baseIndex.childrenOf.has(node.value);
|
|
211
|
+
}, [
|
|
212
|
+
loader.active,
|
|
213
|
+
loadStates,
|
|
214
|
+
baseIndex
|
|
215
|
+
]);
|
|
216
|
+
const pagingKey = JSON.stringify(Array.from(loadStates).filter(([, state]) => state.hasMore).map(([key]) => key));
|
|
217
|
+
const paging = React$1.useMemo(() => new Set(JSON.parse(pagingKey)), [pagingKey]);
|
|
218
|
+
const { pages, detached, links } = loader.store;
|
|
219
|
+
const index = React$1.useMemo(() => mergeCascaderIndex(baseIndex, pages, detached, links, paging), [
|
|
220
|
+
baseIndex,
|
|
221
|
+
pages,
|
|
222
|
+
detached,
|
|
223
|
+
links,
|
|
224
|
+
paging
|
|
225
|
+
]);
|
|
226
|
+
/** Whether a level needs the paging pseudo-row. A never-fetched level gets
|
|
227
|
+
* nothing; that is the empty state's job. Tree has no per-level empty state,
|
|
228
|
+
* so there the row is also the branch's loading and error surface. */
|
|
229
|
+
const needsMoreRow = React$1.useCallback((key, childCount) => {
|
|
230
|
+
const state = loadStates.get(key);
|
|
231
|
+
if (!state) return false;
|
|
232
|
+
if (childCount > 0) return state.hasMore || state.loading || state.error;
|
|
233
|
+
return mode === "tree" && (state.loading || state.error);
|
|
234
|
+
}, [loadStates, mode]);
|
|
235
|
+
const labelCacheRef = React$1.useRef(/* @__PURE__ */ new Map());
|
|
236
|
+
for (const selected of selectedValues) {
|
|
237
|
+
const node = index.byValue.get(selected);
|
|
238
|
+
if (node) labelCacheRef.current.set(selected, node);
|
|
239
|
+
}
|
|
240
|
+
const resolveNode = React$1.useCallback((nodeValue) => index.byValue.get(nodeValue) ?? labelCacheRef.current.get(nodeValue) ?? {
|
|
241
|
+
value: nodeValue,
|
|
242
|
+
label: nodeValue
|
|
243
|
+
}, [index]);
|
|
244
|
+
const currentParentValue = path.length ? path[path.length - 1] : null;
|
|
245
|
+
const currentParent = currentParentValue ? index.byValue.get(currentParentValue) ?? null : null;
|
|
246
|
+
const currentLevelKey = currentParentValue ?? "\0root";
|
|
247
|
+
const isDeepSearching = (searchScope === "deep" || searchScope === "global") && query.trim().length > 0 && mode !== "tree";
|
|
248
|
+
const deepSearchWithin = searchScope === "global" ? null : currentParentValue;
|
|
249
|
+
const localDeepResults = React$1.useMemo(() => {
|
|
250
|
+
if (!isDeepSearching) return null;
|
|
251
|
+
return searchCascaderDeep(index, query, {
|
|
252
|
+
within: deepSearchWithin,
|
|
253
|
+
matches: filter
|
|
254
|
+
});
|
|
255
|
+
}, [
|
|
256
|
+
isDeepSearching,
|
|
257
|
+
index,
|
|
258
|
+
query,
|
|
259
|
+
deepSearchWithin,
|
|
260
|
+
filter
|
|
261
|
+
]);
|
|
262
|
+
const deepResults = loader.searchResults ?? localDeepResults;
|
|
263
|
+
const levelItems = React$1.useMemo(() => {
|
|
264
|
+
const level = getCascaderChildren(index, currentParentValue);
|
|
265
|
+
return filterCascaderLevel(level, query, filter);
|
|
266
|
+
}, [
|
|
267
|
+
index,
|
|
268
|
+
currentParentValue,
|
|
269
|
+
query,
|
|
270
|
+
filter
|
|
271
|
+
]);
|
|
272
|
+
/** The active level's rows plus the paging pseudo-row, appended HERE because
|
|
273
|
+
* every index Base UI hands out indexes this array: a DOM row missing from
|
|
274
|
+
* it shifts the highlight and every windowed row by one. */
|
|
275
|
+
const levelRendered = React$1.useMemo(() => {
|
|
276
|
+
if (deepResults) return deepResults;
|
|
277
|
+
if (!needsMoreRow(currentLevelKey, levelItems.length)) return levelItems;
|
|
278
|
+
return withCascaderMoreRow(levelItems, createCascaderMoreNode(currentLevelKey, levelItems.length), index.pinned);
|
|
279
|
+
}, [
|
|
280
|
+
deepResults,
|
|
281
|
+
levelItems,
|
|
282
|
+
currentLevelKey,
|
|
283
|
+
needsMoreRow,
|
|
284
|
+
index.pinned
|
|
285
|
+
]);
|
|
286
|
+
const columns = React$1.useMemo(() => {
|
|
287
|
+
if (mode !== "columns") return [];
|
|
288
|
+
const trail = [];
|
|
289
|
+
for (let depth = 0; depth <= path.length; depth += 1) {
|
|
290
|
+
const parentValue = depth === 0 ? null : path[depth - 1];
|
|
291
|
+
const parent = parentValue ? index.byValue.get(parentValue) ?? null : null;
|
|
292
|
+
const isActive = depth === path.length;
|
|
293
|
+
let columnItems;
|
|
294
|
+
if (isActive) columnItems = levelRendered;
|
|
295
|
+
else {
|
|
296
|
+
const key = parentValue ?? "\0root";
|
|
297
|
+
const children = getCascaderChildren(index, parentValue);
|
|
298
|
+
columnItems = needsMoreRow(key, children.length) ? withCascaderMoreRow(children, createCascaderMoreNode(key, children.length), index.pinned) : children;
|
|
299
|
+
}
|
|
300
|
+
trail.push({
|
|
301
|
+
parent,
|
|
302
|
+
depth,
|
|
303
|
+
activeValue: path[depth] ?? null,
|
|
304
|
+
active: isActive,
|
|
305
|
+
items: columnItems
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return trail;
|
|
309
|
+
}, [
|
|
310
|
+
mode,
|
|
311
|
+
path,
|
|
312
|
+
index,
|
|
313
|
+
levelRendered,
|
|
314
|
+
needsMoreRow
|
|
315
|
+
]);
|
|
316
|
+
const treeSentinels = React$1.useMemo(() => {
|
|
317
|
+
if (mode !== "tree") return void 0;
|
|
318
|
+
const keys = /* @__PURE__ */ new Set();
|
|
319
|
+
const consider = (key) => {
|
|
320
|
+
if (needsMoreRow(key, index.childrenOf.get(key)?.length ?? 0)) keys.add(key);
|
|
321
|
+
};
|
|
322
|
+
consider(CASCADER_ROOT_KEY);
|
|
323
|
+
for (const value of expanded) consider(value);
|
|
324
|
+
return keys.size ? keys : void 0;
|
|
325
|
+
}, [
|
|
326
|
+
mode,
|
|
327
|
+
index,
|
|
328
|
+
expanded,
|
|
329
|
+
needsMoreRow
|
|
330
|
+
]);
|
|
331
|
+
const treeRows = React$1.useMemo(() => {
|
|
332
|
+
if (mode !== "tree") return [];
|
|
333
|
+
const rows = flattenCascaderTree(index, expanded, treeSentinels);
|
|
334
|
+
const normalized = normalizeCascaderQuery(query);
|
|
335
|
+
if (!normalized) return rows;
|
|
336
|
+
const keep = /* @__PURE__ */ new Set();
|
|
337
|
+
const listed = getCascaderListedValues(index);
|
|
338
|
+
for (const node of index.all) {
|
|
339
|
+
if (!listed.has(node.value)) continue;
|
|
340
|
+
if (!(filter ? filter(node, normalized) : matchesCascaderQuery(node, normalized))) continue;
|
|
341
|
+
for (const ancestor of getCascaderPath(index, node.value)) keep.add(ancestor.value);
|
|
342
|
+
}
|
|
343
|
+
return flattenCascaderTree(index, /* @__PURE__ */ new Set([...expanded, ...keep])).filter((row) => keep.has(row.node.value));
|
|
344
|
+
}, [
|
|
345
|
+
mode,
|
|
346
|
+
index,
|
|
347
|
+
expanded,
|
|
348
|
+
treeSentinels,
|
|
349
|
+
query,
|
|
350
|
+
filter
|
|
351
|
+
]);
|
|
352
|
+
const renderedItems = React$1.useMemo(() => mode === "tree" ? treeRows.map((row) => row.node) : levelRendered, [
|
|
353
|
+
mode,
|
|
354
|
+
treeRows,
|
|
355
|
+
levelRendered
|
|
356
|
+
]);
|
|
357
|
+
/** Base UI never resets the highlighted index on an `items` change and has
|
|
358
|
+
* no imperative setter; its one automatic reset is an out-of-range clamp, so
|
|
359
|
+
* a single empty frame trips it. The frame rides `filteredItems`, so the
|
|
360
|
+
* `items` identity stays put. */
|
|
361
|
+
const levelKey = currentParentValue ?? "";
|
|
362
|
+
const [swap, setSwap] = React$1.useState({
|
|
363
|
+
key: levelKey,
|
|
364
|
+
active: false
|
|
365
|
+
});
|
|
366
|
+
if (swap.key !== levelKey) setSwap({
|
|
367
|
+
key: levelKey,
|
|
368
|
+
active: true
|
|
369
|
+
});
|
|
370
|
+
const swapping = swap.active && swap.key === levelKey;
|
|
371
|
+
React$1.useLayoutEffect(() => {
|
|
372
|
+
if (swap.active) setSwap((prev) => ({
|
|
373
|
+
...prev,
|
|
374
|
+
active: false
|
|
375
|
+
}));
|
|
376
|
+
}, [swap.active]);
|
|
377
|
+
const resetHighlight = React$1.useCallback(() => {
|
|
378
|
+
setSwap((prev) => prev.active ? prev : {
|
|
379
|
+
...prev,
|
|
380
|
+
active: true
|
|
381
|
+
});
|
|
382
|
+
}, []);
|
|
383
|
+
const [virtualRenderers, setVirtualRenderers] = React$1.useState(0);
|
|
384
|
+
const registerVirtualRenderer = React$1.useCallback(() => {
|
|
385
|
+
setVirtualRenderers((count) => count + 1);
|
|
386
|
+
return () => setVirtualRenderers((count) => Math.max(0, count - 1));
|
|
387
|
+
}, []);
|
|
388
|
+
const hasVirtualRenderer = virtualRenderers > 0;
|
|
389
|
+
const wantsVirtual = virtualize ?? renderedItems.length >= virtualizeThreshold;
|
|
390
|
+
const [virtualLatch, setVirtualLatch] = React$1.useState({
|
|
391
|
+
key: levelKey,
|
|
392
|
+
on: false
|
|
393
|
+
});
|
|
394
|
+
const virtualized = hasVirtualRenderer && (virtualLatch.key === levelKey ? virtualLatch.on || wantsVirtual : wantsVirtual);
|
|
395
|
+
if (!hasVirtualRenderer) {
|
|
396
|
+
if (virtualLatch.on || virtualLatch.key !== levelKey) setVirtualLatch({
|
|
397
|
+
key: levelKey,
|
|
398
|
+
on: false
|
|
399
|
+
});
|
|
400
|
+
} else if (virtualLatch.key !== levelKey) setVirtualLatch({
|
|
401
|
+
key: levelKey,
|
|
402
|
+
on: virtualized
|
|
403
|
+
});
|
|
404
|
+
else if (virtualized && !virtualLatch.on) setVirtualLatch({
|
|
405
|
+
key: levelKey,
|
|
406
|
+
on: true
|
|
407
|
+
});
|
|
408
|
+
/**
|
|
409
|
+
* One forced re-render, one commit AFTER windowing turns on. Base UI sizes
|
|
410
|
+
* `listRef` from `filteredItems` in a layout effect keyed on that array's
|
|
411
|
+
* identity, but `virtualized` reaches the list a commit later and the
|
|
412
|
+
* `CompositeList` teardown REPLACES `listRef` with an empty array nothing
|
|
413
|
+
* re-sizes, so ArrowDown stops at the last WINDOWED row. A passive effect is
|
|
414
|
+
* late enough: the teardown is scheduled from a layout effect.
|
|
415
|
+
*/
|
|
416
|
+
const [virtualSyncs, setVirtualSyncs] = React$1.useState(0);
|
|
417
|
+
React$1.useEffect(() => {
|
|
418
|
+
if (!virtualized) return;
|
|
419
|
+
setVirtualSyncs((count) => count + 1);
|
|
420
|
+
}, [virtualized]);
|
|
421
|
+
React$1.useEffect(() => {
|
|
422
|
+
if (process.env.NODE_ENV === "production") return void 0;
|
|
423
|
+
if (virtualize !== true || hasVirtualRenderer) return void 0;
|
|
424
|
+
const timeout = setTimeout(() => {
|
|
425
|
+
console.error("[Cascader] `virtualize` is true but no windowing renderer is mounted. Render <CascaderVirtualItems /> inside <CascaderList>, or <CascaderVirtualColumn /> through the <CascaderColumns> slot.");
|
|
426
|
+
}, 0);
|
|
427
|
+
return () => clearTimeout(timeout);
|
|
428
|
+
}, [virtualize, hasVirtualRenderer]);
|
|
429
|
+
/** One signal per navigation, feeding the live region, bumped EXACTLY once
|
|
430
|
+
* per action. `seq` alternates the invisible marker so two identical
|
|
431
|
+
* announcements are still two mutations; `toggled` names the tree branch. */
|
|
432
|
+
const [announceSignal, setAnnounceSignal] = React$1.useState({
|
|
433
|
+
seq: 0,
|
|
434
|
+
toggled: null,
|
|
435
|
+
notice: null
|
|
436
|
+
});
|
|
437
|
+
const bumpAnnouncement = React$1.useCallback((toggled) => {
|
|
438
|
+
setAnnounceSignal((prev) => ({
|
|
439
|
+
seq: prev.seq + 1,
|
|
440
|
+
toggled,
|
|
441
|
+
notice: null
|
|
442
|
+
}));
|
|
443
|
+
}, []);
|
|
444
|
+
/** Speaks for interactions that change nothing on screen: a refusal returns
|
|
445
|
+
* before any state moves, a cascade sweep is invisible from the row pressed.
|
|
446
|
+
* The seq bump keeps two identical refusals two mutations. */
|
|
447
|
+
const announceNotice = React$1.useCallback((notice) => {
|
|
448
|
+
setAnnounceSignal((prev) => ({
|
|
449
|
+
seq: prev.seq + 1,
|
|
450
|
+
toggled: null,
|
|
451
|
+
notice
|
|
452
|
+
}));
|
|
453
|
+
}, []);
|
|
454
|
+
/**
|
|
455
|
+
* The navigation a press asked for and a fetch has not answered yet. Load
|
|
456
|
+
* BEFORE you move: drilling in first threw away the list the user was
|
|
457
|
+
* reading and, on failure, stranded them on an error screen for a level they
|
|
458
|
+
* had never seen. Holding here keeps the old rows and puts the progress on
|
|
459
|
+
* the pressed row, whose chevron `CascaderItem` swaps for a spinner.
|
|
460
|
+
*/
|
|
461
|
+
const [pendingNavigation, setPendingNavigation] = React$1.useState(null);
|
|
462
|
+
/** Asks for a branch's children and parks the navigation until they land.
|
|
463
|
+
* BOTH entry points are called unconditionally: `retryLevel` no-ops unless
|
|
464
|
+
* the level errored, `ensureLevel` no-ops if it has state, so a repeat press
|
|
465
|
+
* is free and the readiness decision stays inside the loader. */
|
|
466
|
+
const requestChildren = React$1.useCallback((node, pending) => {
|
|
467
|
+
setPendingNavigation(pending);
|
|
468
|
+
retryLoaderLevel(node.value);
|
|
469
|
+
ensureLevel(node.value, "level");
|
|
470
|
+
bumpAnnouncement(null);
|
|
471
|
+
}, [
|
|
472
|
+
retryLoaderLevel,
|
|
473
|
+
ensureLevel,
|
|
474
|
+
bumpAnnouncement
|
|
475
|
+
]);
|
|
476
|
+
const pushLevel = React$1.useCallback((nodeValue) => {
|
|
477
|
+
setPathWithReason((prev) => [...prev, nodeValue], "drill");
|
|
478
|
+
setQuery("");
|
|
479
|
+
bumpAnnouncement(null);
|
|
480
|
+
}, [
|
|
481
|
+
setPathWithReason,
|
|
482
|
+
setQuery,
|
|
483
|
+
bumpAnnouncement
|
|
484
|
+
]);
|
|
485
|
+
const popLevel = React$1.useCallback(() => {
|
|
486
|
+
setPathWithReason((prev) => prev.length ? prev.slice(0, -1) : prev, "back");
|
|
487
|
+
setQuery("");
|
|
488
|
+
setPendingNavigation(null);
|
|
489
|
+
bumpAnnouncement(null);
|
|
490
|
+
}, [
|
|
491
|
+
setPathWithReason,
|
|
492
|
+
setQuery,
|
|
493
|
+
bumpAnnouncement
|
|
494
|
+
]);
|
|
495
|
+
const goToDepth = React$1.useCallback((depth) => {
|
|
496
|
+
setPathWithReason((prev) => prev.slice(0, Math.max(0, depth)), "breadcrumb");
|
|
497
|
+
setQuery("");
|
|
498
|
+
setPendingNavigation(null);
|
|
499
|
+
bumpAnnouncement(null);
|
|
500
|
+
}, [
|
|
501
|
+
setPathWithReason,
|
|
502
|
+
setQuery,
|
|
503
|
+
bumpAnnouncement
|
|
504
|
+
]);
|
|
505
|
+
const toggleExpanded = React$1.useCallback((nodeValue) => {
|
|
506
|
+
setExpandedList((prev) => prev.includes(nodeValue) ? prev.filter((v) => v !== nodeValue) : [...prev, nodeValue]);
|
|
507
|
+
bumpAnnouncement(nodeValue);
|
|
508
|
+
}, [setExpandedList, bumpAnnouncement]);
|
|
509
|
+
const isBranch = React$1.useCallback((node) => isCascaderBranch(index, node), [index]);
|
|
510
|
+
const isSelectable = React$1.useCallback((node) => isCascaderSelectable(index, node, selectable), [index, selectable]);
|
|
511
|
+
const isDisabled = React$1.useCallback((node) => isCascaderDisabled(index, node), [index]);
|
|
512
|
+
const branchesSelectable = selectable !== "leaf";
|
|
513
|
+
const openFlyoutsRef = React$1.useRef(/* @__PURE__ */ new Set());
|
|
514
|
+
const setFlyoutOpen = React$1.useCallback((key, open) => {
|
|
515
|
+
if (open) openFlyoutsRef.current.add(key);
|
|
516
|
+
else openFlyoutsRef.current.delete(key);
|
|
517
|
+
}, []);
|
|
518
|
+
const hasOpenFlyout = React$1.useCallback(() => openFlyoutsRef.current.size > 0, []);
|
|
519
|
+
const stableActions = useShallowStable(actions, shallowEqualItemLists);
|
|
520
|
+
const resolvedActions = React$1.useMemo(() => stableActions ?? EMPTY_ACTIONS, [stableActions]);
|
|
521
|
+
const selectedSet = React$1.useMemo(() => new Set(selectedValues), [selectedValues]);
|
|
522
|
+
const isSelected = React$1.useCallback((node) => selectedSet.has(node.value), [selectedSet]);
|
|
523
|
+
/** Selected-descendant counts, walked ONCE per selection change and never
|
|
524
|
+
* stored, so the flat value array stays the only source of truth. The walk
|
|
525
|
+
* is `selections x depth`, up from each selection rather than down from each
|
|
526
|
+
* row, which keeps the count and `isIndeterminate` O(1). */
|
|
527
|
+
const selectedDescendants = React$1.useMemo(() => getCascaderSelectedDescendants(index, selectedValues), [index, selectedValues]);
|
|
528
|
+
const selectedDescendantCount = React$1.useCallback((node) => selectedDescendants.get(node.value) ?? 0, [selectedDescendants]);
|
|
529
|
+
const indeterminateSet = React$1.useMemo(() => {
|
|
530
|
+
if (!cascade || !multiple) return null;
|
|
531
|
+
return getCascaderIndeterminateFrom(selectedDescendants, selectedValues);
|
|
532
|
+
}, [
|
|
533
|
+
cascade,
|
|
534
|
+
multiple,
|
|
535
|
+
selectedDescendants,
|
|
536
|
+
selectedValues
|
|
537
|
+
]);
|
|
538
|
+
const isIndeterminate = React$1.useCallback((node) => indeterminateSet?.has(node.value) ?? false, [indeterminateSet]);
|
|
539
|
+
const highlightedRef = React$1.useRef(null);
|
|
540
|
+
const getHighlighted = React$1.useCallback(() => highlightedRef.current, []);
|
|
541
|
+
const [highlightStore] = React$1.useState(createCascaderHighlightStore);
|
|
542
|
+
const currentLoadState = React$1.useMemo(() => loadStates.get(currentLevelKey) ?? null, [loadStates, currentLevelKey]);
|
|
543
|
+
const searchLoadState = loader.searchState;
|
|
544
|
+
/** What the query announcement COUNTS: matches, not rows, since the list
|
|
545
|
+
* also carries a tree query's ancestor context and the Load-more row. Server
|
|
546
|
+
* hits count as-is; re-judging them locally would drop what it matched on
|
|
547
|
+
* data the client cannot see. */
|
|
548
|
+
const announcedMatches = React$1.useMemo(() => {
|
|
549
|
+
if (deepResults) return deepResults.length;
|
|
550
|
+
const normalized = normalizeCascaderQuery(query);
|
|
551
|
+
if (!normalized) return 0;
|
|
552
|
+
const matches = filter ?? matchesCascaderQuery;
|
|
553
|
+
return renderedItems.filter((node) => !isCascaderMoreNode(node) && matches(node, normalized)).length;
|
|
554
|
+
}, [
|
|
555
|
+
deepResults,
|
|
556
|
+
renderedItems,
|
|
557
|
+
query,
|
|
558
|
+
filter
|
|
559
|
+
]);
|
|
560
|
+
const hasRows = renderedItems.some((node) => !isCascaderMoreNode(node));
|
|
561
|
+
const announcementTarget = React$1.useMemo(() => {
|
|
562
|
+
if (!open && !inline) return {
|
|
563
|
+
text: "",
|
|
564
|
+
defer: false
|
|
565
|
+
};
|
|
566
|
+
const total = renderedItems.length;
|
|
567
|
+
const marker = ANNOUNCE_MARKER.repeat(announceSignal.seq % 2);
|
|
568
|
+
if (announceSignal.notice) return {
|
|
569
|
+
text: `${announceSignal.notice}${marker}`,
|
|
570
|
+
defer: false
|
|
571
|
+
};
|
|
572
|
+
const toggled = mode === "tree" && announceSignal.toggled ? index.byValue.get(announceSignal.toggled) ?? null : null;
|
|
573
|
+
const pendingState = pendingNavigation ? loadStates.get(pendingNavigation.value) ?? null : null;
|
|
574
|
+
const levelState = query.trim() ? searchLoadState : currentLoadState;
|
|
575
|
+
let text;
|
|
576
|
+
let defer = false;
|
|
577
|
+
if (pendingState?.error) text = labels.error;
|
|
578
|
+
else if (pendingNavigation) text = labels.loading;
|
|
579
|
+
else if (levelState?.error) text = labels.error;
|
|
580
|
+
else if (levelState?.loading) text = query.trim() ? labels.searchingAnnouncement : hasRows ? labels.loadingMore : labels.loading;
|
|
581
|
+
else if (query.trim()) {
|
|
582
|
+
text = announcedMatches === 0 ? labels.empty : labels.resultsAnnouncement(announcedMatches);
|
|
583
|
+
defer = true;
|
|
584
|
+
} else if (toggled) text = expanded.has(toggled.value) ? labels.expandedAnnouncement(toggled.label, getCascaderCount(index, toggled)) : labels.collapsedAnnouncement(toggled.label);
|
|
585
|
+
else if (currentParent) text = labels.levelAnnouncement(currentParent.label, path.length + 1, total);
|
|
586
|
+
else text = labels.rootAnnouncement(total);
|
|
587
|
+
return {
|
|
588
|
+
text: `${text}${marker}`,
|
|
589
|
+
defer
|
|
590
|
+
};
|
|
591
|
+
}, [
|
|
592
|
+
open,
|
|
593
|
+
inline,
|
|
594
|
+
mode,
|
|
595
|
+
index,
|
|
596
|
+
expanded,
|
|
597
|
+
announceSignal,
|
|
598
|
+
query,
|
|
599
|
+
labels,
|
|
600
|
+
renderedItems.length,
|
|
601
|
+
hasRows,
|
|
602
|
+
announcedMatches,
|
|
603
|
+
currentParent,
|
|
604
|
+
path.length,
|
|
605
|
+
currentLoadState,
|
|
606
|
+
searchLoadState,
|
|
607
|
+
pendingNavigation,
|
|
608
|
+
loadStates
|
|
609
|
+
]);
|
|
610
|
+
/** The COMMITTED announcement. Immediate texts commit during render (the
|
|
611
|
+
* pattern `swap` uses), so an async settle announces in the very commit that
|
|
612
|
+
* shows the rows. Only the deferred query count uses the timer below. */
|
|
613
|
+
const [announced, setAnnounced] = React$1.useState(announcementTarget);
|
|
614
|
+
if (!announcementTarget.defer && announced.text !== announcementTarget.text) setAnnounced(announcementTarget);
|
|
615
|
+
React$1.useEffect(() => {
|
|
616
|
+
if (!announcementTarget.defer) return void 0;
|
|
617
|
+
const timer = setTimeout(() => setAnnounced(announcementTarget), ANNOUNCE_DEBOUNCE);
|
|
618
|
+
return () => clearTimeout(timer);
|
|
619
|
+
}, [announcementTarget]);
|
|
620
|
+
const announcement = announced.text;
|
|
621
|
+
const stateValue = React$1.useMemo(() => ({
|
|
622
|
+
index,
|
|
623
|
+
path,
|
|
624
|
+
expanded,
|
|
625
|
+
query,
|
|
626
|
+
currentParent,
|
|
627
|
+
levelItems,
|
|
628
|
+
deepResults,
|
|
629
|
+
renderedItems,
|
|
630
|
+
columns,
|
|
631
|
+
treeRows,
|
|
632
|
+
selectedValues,
|
|
633
|
+
selectedDescendants,
|
|
634
|
+
loadStates,
|
|
635
|
+
searchState: searchLoadState,
|
|
636
|
+
announcement
|
|
637
|
+
}), [
|
|
638
|
+
index,
|
|
639
|
+
path,
|
|
640
|
+
expanded,
|
|
641
|
+
query,
|
|
642
|
+
currentParent,
|
|
643
|
+
levelItems,
|
|
644
|
+
deepResults,
|
|
645
|
+
renderedItems,
|
|
646
|
+
columns,
|
|
647
|
+
treeRows,
|
|
648
|
+
selectedValues,
|
|
649
|
+
selectedDescendants,
|
|
650
|
+
loadStates,
|
|
651
|
+
searchLoadState,
|
|
652
|
+
announcement
|
|
653
|
+
]);
|
|
654
|
+
/**
|
|
655
|
+
* Latest committed props and derived state, WRITTEN IN AN EFFECT. That is
|
|
656
|
+
* what keeps the callbacks below `[]`-dep and stable for the life of the
|
|
657
|
+
* cascader; writing during render is unsafe under concurrent rendering. The
|
|
658
|
+
* cost: a read sees the PREVIOUS commit until the effect runs, so ONLY event
|
|
659
|
+
* handlers may read it. Anything used during render is memoised instead.
|
|
660
|
+
*/
|
|
661
|
+
const latest = React$1.useRef({
|
|
662
|
+
index,
|
|
663
|
+
state: stateValue,
|
|
664
|
+
mode,
|
|
665
|
+
multiple,
|
|
666
|
+
cascade,
|
|
667
|
+
max,
|
|
668
|
+
labels,
|
|
669
|
+
closeOnSelect,
|
|
670
|
+
selectedValues,
|
|
671
|
+
deepResults,
|
|
672
|
+
onValueChange,
|
|
673
|
+
resolveNode,
|
|
674
|
+
isSelectable,
|
|
675
|
+
needsChildren,
|
|
676
|
+
expanded
|
|
677
|
+
});
|
|
678
|
+
React$1.useEffect(() => {
|
|
679
|
+
latest.current = {
|
|
680
|
+
index,
|
|
681
|
+
state: stateValue,
|
|
682
|
+
mode,
|
|
683
|
+
multiple,
|
|
684
|
+
cascade,
|
|
685
|
+
max,
|
|
686
|
+
labels,
|
|
687
|
+
closeOnSelect,
|
|
688
|
+
selectedValues,
|
|
689
|
+
deepResults,
|
|
690
|
+
onValueChange,
|
|
691
|
+
resolveNode,
|
|
692
|
+
isSelectable,
|
|
693
|
+
needsChildren,
|
|
694
|
+
expanded
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
const getIndex = React$1.useCallback(() => latest.current.index, []);
|
|
698
|
+
const getState = React$1.useCallback(() => latest.current.state, []);
|
|
699
|
+
const goToLevelAt = React$1.useCallback((nodeValue, depth) => {
|
|
700
|
+
setPathWithReason((prev) => [...prev.slice(0, Math.max(0, depth)), nodeValue], "drill");
|
|
701
|
+
setQuery("");
|
|
702
|
+
bumpAnnouncement(null);
|
|
703
|
+
}, [
|
|
704
|
+
setPathWithReason,
|
|
705
|
+
setQuery,
|
|
706
|
+
bumpAnnouncement
|
|
707
|
+
]);
|
|
708
|
+
const goToTrail = React$1.useCallback((trail) => {
|
|
709
|
+
const { path: current } = latest.current.state;
|
|
710
|
+
if (trail.length === current.length && trail.every((entry, i) => entry === current[i])) resetHighlight();
|
|
711
|
+
else setPathWithReason(trail, "drill");
|
|
712
|
+
setQuery("");
|
|
713
|
+
bumpAnnouncement(null);
|
|
714
|
+
}, [
|
|
715
|
+
setPathWithReason,
|
|
716
|
+
resetHighlight,
|
|
717
|
+
setQuery,
|
|
718
|
+
bumpAnnouncement
|
|
719
|
+
]);
|
|
720
|
+
const navigateAt = React$1.useCallback((node, depth) => {
|
|
721
|
+
if (isCascaderDisabled(latest.current.index, node)) return;
|
|
722
|
+
if (latest.current.mode === "tree") {
|
|
723
|
+
toggleExpanded(node.value);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (latest.current.needsChildren(node)) {
|
|
727
|
+
requestChildren(node, {
|
|
728
|
+
value: node.value,
|
|
729
|
+
kind: "at",
|
|
730
|
+
depth
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
goToLevelAt(node.value, depth);
|
|
735
|
+
}, [
|
|
736
|
+
toggleExpanded,
|
|
737
|
+
goToLevelAt,
|
|
738
|
+
requestChildren
|
|
739
|
+
]);
|
|
740
|
+
const navigate = React$1.useCallback((node) => {
|
|
741
|
+
const { mode: currentMode, deepResults: currentDeepResults, index: currentIndex, expanded: currentExpanded, needsChildren: pending } = latest.current;
|
|
742
|
+
if (isCascaderDisabled(currentIndex, node)) return;
|
|
743
|
+
if (currentMode === "tree") {
|
|
744
|
+
if (!currentExpanded.has(node.value) && pending(node)) {
|
|
745
|
+
requestChildren(node, {
|
|
746
|
+
value: node.value,
|
|
747
|
+
kind: "expand",
|
|
748
|
+
depth: 0
|
|
749
|
+
});
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
toggleExpanded(node.value);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (currentDeepResults) {
|
|
756
|
+
const trail = getCascaderPath(currentIndex, node.value).map((entry) => entry.value);
|
|
757
|
+
if (pending(node)) {
|
|
758
|
+
requestChildren(node, {
|
|
759
|
+
value: node.value,
|
|
760
|
+
kind: "trail",
|
|
761
|
+
trail
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
goToTrail(trail);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (pending(node)) {
|
|
769
|
+
requestChildren(node, {
|
|
770
|
+
value: node.value,
|
|
771
|
+
kind: "push",
|
|
772
|
+
depth: 0
|
|
773
|
+
});
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
pushLevel(node.value);
|
|
777
|
+
}, [
|
|
778
|
+
toggleExpanded,
|
|
779
|
+
goToTrail,
|
|
780
|
+
pushLevel,
|
|
781
|
+
requestChildren
|
|
782
|
+
]);
|
|
783
|
+
/**
|
|
784
|
+
* The other half of `requestChildren`: the fetch settled, so the held
|
|
785
|
+
* navigation happens or is dropped. Reads the load STATE, not a promise:
|
|
786
|
+
* only the loader knows a request was superseded, aborted or retried, and no
|
|
787
|
+
* entry at all means it has not reached the store yet. On failure nothing
|
|
788
|
+
* moves and the intent is KEPT, which is what makes the row's retry
|
|
789
|
+
* affordance mean something; only `popLevel`, `goToDepth` and a close drop it.
|
|
790
|
+
*/
|
|
791
|
+
React$1.useEffect(() => {
|
|
792
|
+
if (!pendingNavigation) return;
|
|
793
|
+
const state = loadStates.get(pendingNavigation.value);
|
|
794
|
+
if (!state || state.loading || state.error) return;
|
|
795
|
+
setPendingNavigation(null);
|
|
796
|
+
const target = index.byValue.get(pendingNavigation.value);
|
|
797
|
+
if (target && isDisabled(target)) return;
|
|
798
|
+
if (pendingNavigation.kind === "expand") {
|
|
799
|
+
toggleExpanded(pendingNavigation.value);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (pendingNavigation.kind === "trail") {
|
|
803
|
+
goToTrail(pendingNavigation.trail);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (pendingNavigation.kind === "at") {
|
|
807
|
+
goToLevelAt(pendingNavigation.value, pendingNavigation.depth);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
pushLevel(pendingNavigation.value);
|
|
811
|
+
}, [
|
|
812
|
+
pendingNavigation,
|
|
813
|
+
loadStates,
|
|
814
|
+
index,
|
|
815
|
+
isDisabled,
|
|
816
|
+
toggleExpanded,
|
|
817
|
+
goToTrail,
|
|
818
|
+
goToLevelAt,
|
|
819
|
+
pushLevel
|
|
820
|
+
]);
|
|
821
|
+
React$1.useEffect(() => {
|
|
822
|
+
if (!open && !inline) setPendingNavigation(null);
|
|
823
|
+
}, [open, inline]);
|
|
824
|
+
const emitSelection = React$1.useCallback((nextValues, node, reason) => {
|
|
825
|
+
const { selectedValues: current, multiple: currentMultiple, onValueChange: emit, index: currentIndex, resolveNode: resolve } = latest.current;
|
|
826
|
+
if (nextValues.length === current.length && nextValues.every((entry, i) => entry === current[i])) return;
|
|
827
|
+
const next = currentMultiple ? nextValues : nextValues[0] ?? "";
|
|
828
|
+
setValue(next);
|
|
829
|
+
if (!emit) return;
|
|
830
|
+
const chain = node ? getCascaderPath(currentIndex, node.value) : [];
|
|
831
|
+
emit(next, {
|
|
832
|
+
node,
|
|
833
|
+
path: node ? chain.length ? chain : [node] : [],
|
|
834
|
+
nodes: nextValues.map(resolve),
|
|
835
|
+
reason
|
|
836
|
+
});
|
|
837
|
+
}, [setValue]);
|
|
838
|
+
const setSelection = React$1.useCallback((values, reason) => {
|
|
839
|
+
const { selectedValues: current, resolveNode: resolve } = latest.current;
|
|
840
|
+
const before = new Set(current);
|
|
841
|
+
const after = new Set(values);
|
|
842
|
+
const added = values.find((entry) => !before.has(entry));
|
|
843
|
+
const removed = current.find((entry) => !after.has(entry));
|
|
844
|
+
if (reason === "clear") {
|
|
845
|
+
emitSelection(values, null, "clear");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (added) {
|
|
849
|
+
emitSelection(values, resolve(added), reason ?? "select");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (removed) {
|
|
853
|
+
emitSelection(values, resolve(removed), reason ?? "deselect");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
emitSelection(values, null, reason ?? "select");
|
|
857
|
+
}, [emitSelection]);
|
|
858
|
+
const commit = React$1.useCallback((node) => {
|
|
859
|
+
const { multiple: currentMultiple, cascade: currentCascade, selectedValues: current, max: currentMax, index: currentIndex, isSelectable: currentIsSelectable, labels: currentLabels, closeOnSelect: currentCloseOnSelect } = latest.current;
|
|
860
|
+
const selecting = !currentMultiple || !current.includes(node.value);
|
|
861
|
+
if (selecting && isCascaderDisabled(currentIndex, node)) return;
|
|
862
|
+
if (!currentMultiple) {
|
|
863
|
+
emitSelection([node.value], node, "select");
|
|
864
|
+
if (currentCloseOnSelect) {
|
|
865
|
+
openReasonRef.current = "item-press";
|
|
866
|
+
setOpen(false);
|
|
867
|
+
} else setQuery("");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (currentCascade) {
|
|
871
|
+
const next = applyCascadeSelection(currentIndex, current, node.value, selecting, currentIsSelectable);
|
|
872
|
+
if (selecting && currentMax != null && next.length > currentMax && next.length > current.length) {
|
|
873
|
+
announceNotice(currentLabels.maxReachedAnnouncement(currentMax));
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
emitSelection(next, node, selecting ? "select" : "deselect");
|
|
877
|
+
const before = new Set(current);
|
|
878
|
+
const after = new Set(next);
|
|
879
|
+
let swept = 0;
|
|
880
|
+
for (const entry of selecting ? next : current) {
|
|
881
|
+
if (entry === node.value) continue;
|
|
882
|
+
if (selecting ? !before.has(entry) : !after.has(entry)) swept += 1;
|
|
883
|
+
}
|
|
884
|
+
if (swept > 0) announceNotice(currentLabels.cascadeAnnouncement(node.label ?? node.value, swept, selecting));
|
|
885
|
+
setQuery("");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (current.includes(node.value)) emitSelection(current.filter((v) => v !== node.value), node, "deselect");
|
|
889
|
+
else if (currentMax == null || current.length < currentMax) emitSelection([...current, node.value], node, "select");
|
|
890
|
+
else announceNotice(currentLabels.maxReachedAnnouncement(currentMax));
|
|
891
|
+
setQuery("");
|
|
892
|
+
}, [
|
|
893
|
+
emitSelection,
|
|
894
|
+
setOpen,
|
|
895
|
+
setQuery,
|
|
896
|
+
announceNotice
|
|
897
|
+
]);
|
|
898
|
+
/** Safety net for paths that bypass the row-level veto (the drag-select
|
|
899
|
+
* mouseup). `details.cancel()` suppresses the commit and the close together:
|
|
900
|
+
* Base UI shares one event-details object between them. */
|
|
901
|
+
const handleComboboxValueChange = React$1.useCallback((next, details) => {
|
|
902
|
+
if (!multiple) {
|
|
903
|
+
const node = Array.isArray(next) ? next[0] ?? null : next;
|
|
904
|
+
if (node && isCascaderMoreNode(node)) {
|
|
905
|
+
details.cancel();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (node && isDisabled(node)) {
|
|
909
|
+
details.cancel();
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (node && !isSelectable(node) && isBranch(node)) {
|
|
913
|
+
details.cancel();
|
|
914
|
+
navigate(node);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (node) {
|
|
918
|
+
if (!closeOnSelect) details.cancel();
|
|
919
|
+
commit(node);
|
|
920
|
+
}
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const nextNodes = Array.isArray(next) ? next : [];
|
|
924
|
+
const before = new Set(selectedValues);
|
|
925
|
+
const added = nextNodes.find((node) => !before.has(node.value));
|
|
926
|
+
if (added && (isCascaderMoreNode(added) || isDisabled(added))) {
|
|
927
|
+
details.cancel();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (added && !isSelectable(added) && isBranch(added)) {
|
|
931
|
+
details.cancel();
|
|
932
|
+
navigate(added);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (cascade) {
|
|
936
|
+
const nextValues = new Set(nextNodes.map((node) => node.value));
|
|
937
|
+
const removed = selectedValues.find((entry) => !nextValues.has(entry));
|
|
938
|
+
const toggled = added ?? (removed != null ? resolveNode(removed) : null);
|
|
939
|
+
if (toggled) commit(toggled);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (max != null && nextNodes.length > selectedValues.length && nextNodes.length > max) {
|
|
943
|
+
details.cancel();
|
|
944
|
+
announceNotice(labels.maxReachedAnnouncement(max));
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
setSelection(nextNodes.map((node) => node.value));
|
|
948
|
+
setQuery("");
|
|
949
|
+
}, [
|
|
950
|
+
multiple,
|
|
951
|
+
selectedValues,
|
|
952
|
+
max,
|
|
953
|
+
cascade,
|
|
954
|
+
closeOnSelect,
|
|
955
|
+
labels,
|
|
956
|
+
announceNotice,
|
|
957
|
+
isSelectable,
|
|
958
|
+
isDisabled,
|
|
959
|
+
isBranch,
|
|
960
|
+
navigate,
|
|
961
|
+
commit,
|
|
962
|
+
resolveNode,
|
|
963
|
+
setSelection,
|
|
964
|
+
setQuery
|
|
965
|
+
]);
|
|
966
|
+
const handleOpenChange = React$1.useCallback((nextOpen, details) => {
|
|
967
|
+
if (details.isCanceled) return;
|
|
968
|
+
if (!nextOpen && details.reason === "escape-key" && hasOpenFlyout()) {
|
|
969
|
+
details.cancel();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
openReasonRef.current = details.reason ?? "none";
|
|
973
|
+
setOpen(nextOpen);
|
|
974
|
+
if (!nextOpen) {
|
|
975
|
+
setQuery("");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (revealSelected) {
|
|
979
|
+
setQuery("");
|
|
980
|
+
const first = toArray(value)[0];
|
|
981
|
+
if (mode !== "tree") setPathWithReason(first ? getCascaderPath(index, first).slice(0, -1).map((node) => node.value) : [], "reveal");
|
|
982
|
+
}
|
|
983
|
+
}, [
|
|
984
|
+
setOpen,
|
|
985
|
+
setQuery,
|
|
986
|
+
revealSelected,
|
|
987
|
+
value,
|
|
988
|
+
index,
|
|
989
|
+
mode,
|
|
990
|
+
setPathWithReason,
|
|
991
|
+
hasOpenFlyout
|
|
992
|
+
]);
|
|
993
|
+
const renderContext = React$1.useMemo(() => ({
|
|
994
|
+
renderItem,
|
|
995
|
+
renderLabel
|
|
996
|
+
}), [renderItem, renderLabel]);
|
|
997
|
+
/** The stable half: nothing in these deps moves on a keystroke, a level
|
|
998
|
+
* change or a highlight, which is what makes `React.memo` on `CascaderItem`
|
|
999
|
+
* hold while the user types. Destructured off `loader` so the memo depends
|
|
1000
|
+
* on its `[]`-dep callbacks, not the object it republishes per load. */
|
|
1001
|
+
const { active: hasLoader, loadMore, retryLevel, retrySearch, prefetchNode } = loader;
|
|
1002
|
+
const actionsValue = React$1.useMemo(() => ({
|
|
1003
|
+
index,
|
|
1004
|
+
mode,
|
|
1005
|
+
multiple,
|
|
1006
|
+
cascade,
|
|
1007
|
+
branchesSelectable,
|
|
1008
|
+
indicator,
|
|
1009
|
+
expandTrigger,
|
|
1010
|
+
actions: resolvedActions,
|
|
1011
|
+
searchScope,
|
|
1012
|
+
maxHeight,
|
|
1013
|
+
inline: !!inline,
|
|
1014
|
+
invalid: !!invalid,
|
|
1015
|
+
baseId,
|
|
1016
|
+
labels,
|
|
1017
|
+
virtualized,
|
|
1018
|
+
registerVirtualRenderer,
|
|
1019
|
+
virtualize,
|
|
1020
|
+
virtualizeThreshold,
|
|
1021
|
+
estimateRowSize,
|
|
1022
|
+
overscan,
|
|
1023
|
+
hasLoader,
|
|
1024
|
+
loadMore,
|
|
1025
|
+
retryLevel,
|
|
1026
|
+
retrySearch,
|
|
1027
|
+
invalidateLevel,
|
|
1028
|
+
getIndex,
|
|
1029
|
+
getState,
|
|
1030
|
+
getHighlighted,
|
|
1031
|
+
setPath,
|
|
1032
|
+
pushLevel,
|
|
1033
|
+
popLevel,
|
|
1034
|
+
goToDepth,
|
|
1035
|
+
toggleExpanded,
|
|
1036
|
+
setFlyoutOpen,
|
|
1037
|
+
hasOpenFlyout,
|
|
1038
|
+
setQuery,
|
|
1039
|
+
setSelection,
|
|
1040
|
+
commit,
|
|
1041
|
+
navigate,
|
|
1042
|
+
navigateAt,
|
|
1043
|
+
resolveNode,
|
|
1044
|
+
isBranch,
|
|
1045
|
+
isSelectable,
|
|
1046
|
+
isDisabled,
|
|
1047
|
+
isSelected,
|
|
1048
|
+
isIndeterminate,
|
|
1049
|
+
selectedDescendantCount
|
|
1050
|
+
}), [
|
|
1051
|
+
index,
|
|
1052
|
+
mode,
|
|
1053
|
+
multiple,
|
|
1054
|
+
cascade,
|
|
1055
|
+
branchesSelectable,
|
|
1056
|
+
indicator,
|
|
1057
|
+
expandTrigger,
|
|
1058
|
+
resolvedActions,
|
|
1059
|
+
searchScope,
|
|
1060
|
+
maxHeight,
|
|
1061
|
+
inline,
|
|
1062
|
+
invalid,
|
|
1063
|
+
baseId,
|
|
1064
|
+
labels,
|
|
1065
|
+
virtualized,
|
|
1066
|
+
registerVirtualRenderer,
|
|
1067
|
+
virtualize,
|
|
1068
|
+
virtualizeThreshold,
|
|
1069
|
+
estimateRowSize,
|
|
1070
|
+
overscan,
|
|
1071
|
+
hasLoader,
|
|
1072
|
+
loadMore,
|
|
1073
|
+
retryLevel,
|
|
1074
|
+
retrySearch,
|
|
1075
|
+
invalidateLevel,
|
|
1076
|
+
getIndex,
|
|
1077
|
+
getState,
|
|
1078
|
+
getHighlighted,
|
|
1079
|
+
setPath,
|
|
1080
|
+
pushLevel,
|
|
1081
|
+
popLevel,
|
|
1082
|
+
goToDepth,
|
|
1083
|
+
toggleExpanded,
|
|
1084
|
+
setFlyoutOpen,
|
|
1085
|
+
hasOpenFlyout,
|
|
1086
|
+
setQuery,
|
|
1087
|
+
setSelection,
|
|
1088
|
+
commit,
|
|
1089
|
+
navigate,
|
|
1090
|
+
navigateAt,
|
|
1091
|
+
resolveNode,
|
|
1092
|
+
isBranch,
|
|
1093
|
+
isSelectable,
|
|
1094
|
+
isDisabled,
|
|
1095
|
+
isSelected,
|
|
1096
|
+
isIndeterminate,
|
|
1097
|
+
selectedDescendantCount
|
|
1098
|
+
]);
|
|
1099
|
+
const comboboxItems = React$1.useMemo(() => virtualized ? renderedItems.slice() : renderedItems, [
|
|
1100
|
+
renderedItems,
|
|
1101
|
+
virtualized,
|
|
1102
|
+
virtualSyncs
|
|
1103
|
+
]);
|
|
1104
|
+
const comboboxValue = React$1.useMemo(() => {
|
|
1105
|
+
if (multiple) return selectedValues.map(resolveNode);
|
|
1106
|
+
return selectedValues.length ? resolveNode(selectedValues[0]) : null;
|
|
1107
|
+
}, [
|
|
1108
|
+
multiple,
|
|
1109
|
+
selectedValues,
|
|
1110
|
+
resolveNode
|
|
1111
|
+
]);
|
|
1112
|
+
return /* @__PURE__ */ jsx(CascaderActionsContext.Provider, {
|
|
1113
|
+
value: actionsValue,
|
|
1114
|
+
children: /* @__PURE__ */ jsx(CascaderStateContext.Provider, {
|
|
1115
|
+
value: stateValue,
|
|
1116
|
+
children: /* @__PURE__ */ jsx(CascaderHighlightContext.Provider, {
|
|
1117
|
+
value: highlightStore,
|
|
1118
|
+
children: /* @__PURE__ */ jsx(CascaderRenderContext.Provider, {
|
|
1119
|
+
value: renderContext,
|
|
1120
|
+
children: /* @__PURE__ */ jsx(Combobox.Root, {
|
|
1121
|
+
items: renderedItems,
|
|
1122
|
+
filteredItems: swapping ? EMPTY : comboboxItems,
|
|
1123
|
+
filter: null,
|
|
1124
|
+
value: comboboxValue,
|
|
1125
|
+
onValueChange: handleComboboxValueChange,
|
|
1126
|
+
open,
|
|
1127
|
+
onOpenChange: handleOpenChange,
|
|
1128
|
+
inputValue: query,
|
|
1129
|
+
onInputValueChange: (next) => setQuery(next),
|
|
1130
|
+
multiple,
|
|
1131
|
+
disabled,
|
|
1132
|
+
name,
|
|
1133
|
+
...form != null ? { form } : null,
|
|
1134
|
+
...id != null ? { id } : null,
|
|
1135
|
+
...required != null ? { required } : null,
|
|
1136
|
+
...readOnly != null ? { readOnly } : null,
|
|
1137
|
+
...inputRef ? { inputRef } : null,
|
|
1138
|
+
inline,
|
|
1139
|
+
virtualized,
|
|
1140
|
+
itemToStringValue: (item) => item?.value ?? "",
|
|
1141
|
+
itemToStringLabel: (item) => item?.label ?? "",
|
|
1142
|
+
isItemEqualToValue: (a, b) => a?.value === b?.value,
|
|
1143
|
+
onItemHighlighted: (item, details) => {
|
|
1144
|
+
highlightedRef.current = item ?? null;
|
|
1145
|
+
highlightStore.set({
|
|
1146
|
+
index: details.index,
|
|
1147
|
+
value: item?.value ?? null
|
|
1148
|
+
});
|
|
1149
|
+
prefetchNode(item);
|
|
1150
|
+
},
|
|
1151
|
+
children
|
|
1152
|
+
})
|
|
1153
|
+
})
|
|
1154
|
+
})
|
|
1155
|
+
})
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
function CascaderTrigger({ className, children, showIcon = true, ref, ...props }) {
|
|
1159
|
+
const { invalid } = useCascaderActions();
|
|
1160
|
+
const triggerRef = React$1.useRef(null);
|
|
1161
|
+
const setTrigger = React$1.useCallback((node) => {
|
|
1162
|
+
triggerRef.current = node;
|
|
1163
|
+
if (typeof ref === "function") ref(node);
|
|
1164
|
+
else if (ref) ref.current = node;
|
|
1165
|
+
}, [ref]);
|
|
1166
|
+
/** 18 of 19 audited examples shipped an unnamed combobox: the trigger's
|
|
1167
|
+
* contents are the field's VALUE, so a screen reader never hears its NAME.
|
|
1168
|
+
* Checked in the DOM, since the name may arrive as a `<label for>`. */
|
|
1169
|
+
React$1.useEffect(() => {
|
|
1170
|
+
if (process.env.NODE_ENV === "production") return;
|
|
1171
|
+
const element = triggerRef.current;
|
|
1172
|
+
if (!element) return;
|
|
1173
|
+
if (element.hasAttribute("aria-label") || element.hasAttribute("aria-labelledby") || element.closest("label") !== null || element.id !== "" && element.ownerDocument.querySelector(`label[for="${CSS.escape(element.id)}"]`) !== null) return;
|
|
1174
|
+
warnCascaderOnce("trigger-unnamed", "`CascaderTrigger` has no accessible name. Its contents are the field's VALUE - they change with the selection - so a screen reader hears what is picked but never what the field is for. Pass `aria-label` or `aria-labelledby`, or reference the trigger's `id` from a `<label>`.");
|
|
1175
|
+
});
|
|
1176
|
+
return /* @__PURE__ */ jsxs(Combobox.Trigger, {
|
|
1177
|
+
ref: setTrigger,
|
|
1178
|
+
"data-slot": "cascader-trigger",
|
|
1179
|
+
...invalid ? {
|
|
1180
|
+
"aria-invalid": true,
|
|
1181
|
+
"data-invalid": ""
|
|
1182
|
+
} : null,
|
|
1183
|
+
className: cn(TRIGGER_ICON_FALLBACK_CLASS, className),
|
|
1184
|
+
...props,
|
|
1185
|
+
children: [children, showIcon ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: `${TRIGGER_ICON_CLASS} pointer-events-none shrink-0` }) : null]
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
/** Icon size for a combobox surface: 14px in mira and sera, 16px elsewhere. */
|
|
1189
|
+
const TRIGGER_ICON_CLASS = "text-muted-foreground size-4";
|
|
1190
|
+
/** The same ladder, applied only to trigger children that carry no size. */
|
|
1191
|
+
const TRIGGER_ICON_FALLBACK_CLASS = "[&_svg:not([class*='size-'])]:size-4";
|
|
1192
|
+
/** The chips field: a form-control surface, so it carries the invalid states. */
|
|
1193
|
+
const CHIPS_CLASS = "flex flex-wrap items-center border bg-clip-padding dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 min-h-9 gap-1.5 rounded-md bg-transparent px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1.5";
|
|
1194
|
+
const CHIP_CLASS = "text-foreground flex w-fit items-center justify-center gap-1 font-medium whitespace-nowrap bg-muted h-[calc(--spacing(5.5))] rounded-sm px-1.5 text-xs has-data-[slot=combobox-chip-remove]:pr-0";
|
|
1195
|
+
const CHIP_REMOVE_CLASS = "opacity-50 hover:opacity-100 -ml-1";
|
|
1196
|
+
/** The floating panel's surface. The shared popup's `InputGroup` sizing is
|
|
1197
|
+
* left out (mirrored in `cascader-nav.tsx`), as are its `max-h-72` and
|
|
1198
|
+
* `min-w-*`: `CascaderContent` sets both from the positioner's variables. */
|
|
1199
|
+
const CONTENT_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 overflow-hidden ring-1 duration-100 ring-foreground/10 rounded-md shadow-md data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2";
|
|
1200
|
+
const EMPTY_CLASS = "text-muted-foreground hidden w-full justify-center py-2 text-center group-data-empty/combobox-content:flex text-sm";
|
|
1201
|
+
/**
|
|
1202
|
+
* The multi-select trigger surface: one removable chip per selection, where
|
|
1203
|
+
* `CascaderValue` would collapse to "3 selected". A component rather than a
|
|
1204
|
+
* recipe because of three easy misses: the popup anchored to the chips, an
|
|
1205
|
+
* accessible name on every remove button, and labels that disambiguate when
|
|
1206
|
+
* two branches use the same one.
|
|
1207
|
+
*
|
|
1208
|
+
* ```tsx
|
|
1209
|
+
* const anchor = useCascaderAnchor()
|
|
1210
|
+
* <Cascader multiple items={items}>
|
|
1211
|
+
* <CascaderChips ref={anchor} placeholder="Select attributes" />
|
|
1212
|
+
* <CascaderContent anchor={anchor}>...</CascaderContent>
|
|
1213
|
+
* </Cascader>
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
1216
|
+
function CascaderChips({ className, children, placeholder, strategy = "all", ...props }) {
|
|
1217
|
+
const { labels, multiple, invalid, resolveNode, index, isSelectable, setSelection } = useCascaderActions();
|
|
1218
|
+
const { selectedValues } = useCascaderState();
|
|
1219
|
+
React$1.useEffect(() => {
|
|
1220
|
+
if (process.env.NODE_ENV === "production") return;
|
|
1221
|
+
if (multiple) return;
|
|
1222
|
+
warnCascaderOnce("chips-without-multiple", "`CascaderChips` renders one chip per selection and there is only ever one without `multiple`. Use `CascaderValue` for a single-select trigger.");
|
|
1223
|
+
}, [multiple]);
|
|
1224
|
+
const nodes = (strategy === "all" ? selectedValues : getCascaderCheckedValues(index, selectedValues, strategy)).map(resolveNode);
|
|
1225
|
+
const ambiguous = findAmbiguousCascaderLabels(nodes);
|
|
1226
|
+
const removeClosure = (node) => {
|
|
1227
|
+
setSelection(applyCascadeSelection(index, selectedValues, node.value, false, isSelectable), "deselect");
|
|
1228
|
+
};
|
|
1229
|
+
const content = typeof children === "function" ? children(nodes) : children;
|
|
1230
|
+
return /* @__PURE__ */ jsx(Combobox.Chips, {
|
|
1231
|
+
"data-slot": "cascader-chips",
|
|
1232
|
+
"aria-label": labels.chipsLabel,
|
|
1233
|
+
...invalid ? {
|
|
1234
|
+
"aria-invalid": true,
|
|
1235
|
+
"data-invalid": ""
|
|
1236
|
+
} : null,
|
|
1237
|
+
className: cn(CHIPS_CLASS, className),
|
|
1238
|
+
...props,
|
|
1239
|
+
children: content ?? (nodes.length === 0 ? /* @__PURE__ */ jsx("span", {
|
|
1240
|
+
"data-slot": "cascader-chips-placeholder",
|
|
1241
|
+
className: "text-muted-foreground truncate",
|
|
1242
|
+
children: placeholder
|
|
1243
|
+
}) : nodes.map((node) => /* @__PURE__ */ jsx(CascaderChip, {
|
|
1244
|
+
node,
|
|
1245
|
+
showPath: ambiguous.has(node.value),
|
|
1246
|
+
...strategy !== "all" ? { onRemove: () => removeClosure(node) } : null
|
|
1247
|
+
}, node.value)))
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
/** One chip. Removal goes through Base UI's `ChipRemove`, which reports the
|
|
1251
|
+
* shortened selection to the root, so it lands in the same `setSelection` as
|
|
1252
|
+
* every other deselection and `cascade` applies to it. */
|
|
1253
|
+
function CascaderChip({ className, node, showPath = false, maxSegments = 2, showRemove = true, onRemove, children, ...props }) {
|
|
1254
|
+
const { labels, index } = useCascaderActions();
|
|
1255
|
+
const chain = showPath ? getCascaderPath(index, node.value) : [];
|
|
1256
|
+
const segments = collapseCascaderPath(chain, {
|
|
1257
|
+
maxSegments,
|
|
1258
|
+
collapse: "start"
|
|
1259
|
+
});
|
|
1260
|
+
const label = segments.length ? segments.map((segment) => segment.type === "node" ? segment.node.label : "…").join(` ${labels.pathSeparator} `) : node.label;
|
|
1261
|
+
return /* @__PURE__ */ jsxs(Combobox.Chip, {
|
|
1262
|
+
"data-slot": "cascader-chip",
|
|
1263
|
+
...onRemove ? { onKeyDown: (event) => {
|
|
1264
|
+
if (event.key !== "Backspace" && event.key !== "Delete") return;
|
|
1265
|
+
event.preventBaseUIHandler();
|
|
1266
|
+
onRemove();
|
|
1267
|
+
} } : null,
|
|
1268
|
+
className: cn(CHIP_CLASS, "has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50", className),
|
|
1269
|
+
...props,
|
|
1270
|
+
children: [children ?? /* @__PURE__ */ jsx("span", {
|
|
1271
|
+
className: "truncate",
|
|
1272
|
+
children: label
|
|
1273
|
+
}), showRemove ? /* @__PURE__ */ jsx(Combobox.ChipRemove, {
|
|
1274
|
+
"data-slot": "cascader-chip-remove",
|
|
1275
|
+
"aria-label": labels.removeChip(label),
|
|
1276
|
+
...onRemove ? { onClick: (event) => {
|
|
1277
|
+
event.preventBaseUIHandler();
|
|
1278
|
+
onRemove();
|
|
1279
|
+
} } : null,
|
|
1280
|
+
className: CHIP_REMOVE_CLASS,
|
|
1281
|
+
children: /* @__PURE__ */ jsx(XIcon, { className: "pointer-events-none" })
|
|
1282
|
+
}) : null]
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
/** Portal + Positioner + floating panel. Unlike shadcn's `ComboboxContent` it
|
|
1286
|
+
* does NOT clamp the popup to the anchor width (a cascader panel is routinely
|
|
1287
|
+
* wider), but it keeps `group/combobox-content`, which `CascaderEmpty` needs. */
|
|
1288
|
+
function CascaderContent({ className, side = "bottom", sideOffset = 6, align = "start", alignOffset = 0, anchor, collisionBoundary, collisionPadding, sticky, positionMethod, container, ref, ...props }) {
|
|
1289
|
+
const { labels } = useCascaderActions();
|
|
1290
|
+
const popupRef = React$1.useRef(null);
|
|
1291
|
+
const setPopup = React$1.useCallback((node) => {
|
|
1292
|
+
popupRef.current = node;
|
|
1293
|
+
if (typeof ref === "function") ref(node);
|
|
1294
|
+
else if (ref) ref.current = node;
|
|
1295
|
+
}, [ref]);
|
|
1296
|
+
/**
|
|
1297
|
+
* Focus goes to the search field, where the level keys and
|
|
1298
|
+
* `aria-activedescendant` live. Base UI's own default agrees but is COMPUTED
|
|
1299
|
+
* FROM THE FIRST RENDER, when the field has not mounted, and collapses to
|
|
1300
|
+
* "do not move focus": measured, a KEYBOARD open left focus on the trigger
|
|
1301
|
+
* and cost two Tabs to reach the footer where a pointer open cost one.
|
|
1302
|
+
*
|
|
1303
|
+
* `initialFocus` rather than an effect, because anything scheduled alongside
|
|
1304
|
+
* Base UI's focus manager is a race; the field is resolved INSIDE the
|
|
1305
|
+
* callback, which runs after the layout effects. Touch is handed back to
|
|
1306
|
+
* Base UI on purpose - focusing the popup keeps the Android keyboard shut.
|
|
1307
|
+
*/
|
|
1308
|
+
const initialFocus = React$1.useCallback((openType) => {
|
|
1309
|
+
const popup = popupRef.current;
|
|
1310
|
+
if (!popup) return true;
|
|
1311
|
+
if (openType === "touch") return popup;
|
|
1312
|
+
return popup.querySelector("[data-slot=\"cascader-input\"]") ?? true;
|
|
1313
|
+
}, []);
|
|
1314
|
+
return /* @__PURE__ */ jsx(Combobox.Portal, {
|
|
1315
|
+
...container !== void 0 ? { container } : null,
|
|
1316
|
+
children: /* @__PURE__ */ jsx(Combobox.Positioner, {
|
|
1317
|
+
side,
|
|
1318
|
+
sideOffset,
|
|
1319
|
+
align,
|
|
1320
|
+
alignOffset,
|
|
1321
|
+
anchor,
|
|
1322
|
+
...collisionBoundary !== void 0 ? { collisionBoundary } : null,
|
|
1323
|
+
...collisionPadding !== void 0 ? { collisionPadding } : null,
|
|
1324
|
+
...sticky !== void 0 ? { sticky } : null,
|
|
1325
|
+
...positionMethod !== void 0 ? { positionMethod } : null,
|
|
1326
|
+
className: "isolate z-50",
|
|
1327
|
+
children: /* @__PURE__ */ jsx(Combobox.Popup, {
|
|
1328
|
+
ref: setPopup,
|
|
1329
|
+
"data-slot": "cascader-content",
|
|
1330
|
+
"data-menu-target": "",
|
|
1331
|
+
"aria-label": labels.panelLabel,
|
|
1332
|
+
initialFocus,
|
|
1333
|
+
className: cn(CONTENT_SURFACE_CLASS, "group/combobox-content relative flex max-h-(--available-height) max-w-(--available-width) min-w-(--anchor-width) origin-(--transform-origin) flex-col", className),
|
|
1334
|
+
...props
|
|
1335
|
+
})
|
|
1336
|
+
})
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* The nav + list(s) + footer container, with no positioning of its own: inside
|
|
1341
|
+
* `CascaderContent` for the popup case, alone for the embedded one.
|
|
1342
|
+
*
|
|
1343
|
+
* `min-h-0` and `max-h-full` are load-bearing. A flex child without `min-h-0`
|
|
1344
|
+
* refuses to shrink below its content, which is the usual reason a scroll area
|
|
1345
|
+
* never scrolls - worst embedded, where no positioner publishes
|
|
1346
|
+
* `--available-height`. It also owns the panel's TAB ORDER: the scroll area
|
|
1347
|
+
* makes itself a tab stop whenever the level overflows, stranding the footer,
|
|
1348
|
+
* and the panel is the only ancestor of both ends of that move in BOTH shapes.
|
|
1349
|
+
*/
|
|
1350
|
+
function CascaderPanel({ className, ...props }) {
|
|
1351
|
+
const { mode } = useCascaderActions();
|
|
1352
|
+
const defaultProps = {
|
|
1353
|
+
"data-slot": "cascader-panel",
|
|
1354
|
+
"data-mode": mode,
|
|
1355
|
+
onKeyDown: React$1.useCallback((event) => {
|
|
1356
|
+
if (event.key !== "Tab" || event.defaultPrevented) return;
|
|
1357
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
1358
|
+
const panel = event.currentTarget;
|
|
1359
|
+
const target = event.target;
|
|
1360
|
+
if (!target || !panel.contains(target)) return;
|
|
1361
|
+
const next = getCascaderTabTarget(panel, target, event.shiftKey);
|
|
1362
|
+
if (!next) return;
|
|
1363
|
+
event.preventDefault();
|
|
1364
|
+
next.focus();
|
|
1365
|
+
}, []),
|
|
1366
|
+
className: cn("flex max-h-full min-h-0 w-full flex-col", className)
|
|
1367
|
+
};
|
|
1368
|
+
return useRender({
|
|
1369
|
+
defaultTagName: "div",
|
|
1370
|
+
render: props.render,
|
|
1371
|
+
props: mergeProps(defaultProps, props)
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Scroll container for one level: pattern 3 of the house scroll reference,
|
|
1376
|
+
* viewport-bounded popup scroll. The height is `min()` of `--available-height`
|
|
1377
|
+
* (the positioner's space between anchor and viewport edge) and `maxHeight`,
|
|
1378
|
+
* so neither can be ignored; a fixed pixel cap alone still reserved 288px for
|
|
1379
|
+
* a panel opened 200px above the fold and spilled off screen, and wasted room
|
|
1380
|
+
* in a tall window. The four classes are shared with `CascaderColumns` and
|
|
1381
|
+
* live in `cascader-lib.tsx`.
|
|
1382
|
+
*/
|
|
1383
|
+
function CascaderList({ className, style, maxHeight: maxHeightProp, ...props }) {
|
|
1384
|
+
const { maxHeight, mode, labels, baseId, virtualized, searchScope } = useCascaderActions();
|
|
1385
|
+
const { currentParent, deepResults } = useCascaderState();
|
|
1386
|
+
const resolved = maxHeightProp ?? maxHeight;
|
|
1387
|
+
const listName = searchScope === "global" && deepResults ? labels.rootLevel : currentParent?.label ?? labels.rootLevel;
|
|
1388
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1389
|
+
"data-slot": "cascader-list-shell",
|
|
1390
|
+
style: resolved != null ? {
|
|
1391
|
+
...style,
|
|
1392
|
+
"--cascader-max-height": typeof resolved === "number" ? `${resolved}px` : resolved
|
|
1393
|
+
} : style,
|
|
1394
|
+
className: cn("relative flex max-h-full min-h-0", CASCADER_LIST_PAD_CLASS),
|
|
1395
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1396
|
+
"data-slot": "cascader-list-bounds",
|
|
1397
|
+
className: cn("flex w-full min-w-0 flex-col overscroll-contain", CASCADER_LIST_HEIGHT_CLASS),
|
|
1398
|
+
children: /* @__PURE__ */ jsx(ScrollArea, {
|
|
1399
|
+
className: CASCADER_SCROLL_CLASS,
|
|
1400
|
+
children: /* @__PURE__ */ jsx(Combobox.List, {
|
|
1401
|
+
"data-slot": "cascader-list",
|
|
1402
|
+
"aria-label": listName,
|
|
1403
|
+
id: `${baseId}-column-0`,
|
|
1404
|
+
...mode === "tree" ? { role: "tree" } : null,
|
|
1405
|
+
...virtualized ? { "data-virtualized": true } : null,
|
|
1406
|
+
className: cn(CASCADER_ROWS_CLASS, virtualized && "relative", className),
|
|
1407
|
+
...props
|
|
1408
|
+
})
|
|
1409
|
+
})
|
|
1410
|
+
})
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
/** The empty, loading and error surface, in ONE element that never unmounts.
|
|
1414
|
+
* `Combobox.Empty` renders whenever `filteredItems.length === 0`, as true of a
|
|
1415
|
+
* fetching level as of an empty one, so swapping siblings would announce "No
|
|
1416
|
+
* results found." over every async level. Swap the CHILDREN. */
|
|
1417
|
+
function CascaderEmpty({ className, children, skeletonRows, ...props }) {
|
|
1418
|
+
const { labels, retryLevel, retrySearch } = useCascaderActions();
|
|
1419
|
+
const { query, path, searchState } = useCascaderState();
|
|
1420
|
+
const levelKey = path.length ? path[path.length - 1] : CASCADER_ROOT_KEY;
|
|
1421
|
+
const loadState = useCascaderLoadState(levelKey);
|
|
1422
|
+
const state = query.trim() ? searchState ?? loadState : loadState;
|
|
1423
|
+
let body = children ?? labels.empty;
|
|
1424
|
+
if (state?.error) body = /* @__PURE__ */ jsxs("span", {
|
|
1425
|
+
"data-slot": "cascader-error",
|
|
1426
|
+
className: "flex flex-col items-center gap-1.5",
|
|
1427
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.error }), /* @__PURE__ */ jsx("button", {
|
|
1428
|
+
type: "button",
|
|
1429
|
+
"data-slot": "cascader-retry",
|
|
1430
|
+
onClick: (event) => {
|
|
1431
|
+
focusCascaderHome(event.currentTarget);
|
|
1432
|
+
if (query.trim() && searchState?.error) retrySearch();
|
|
1433
|
+
else retryLevel(levelKey);
|
|
1434
|
+
},
|
|
1435
|
+
className: "text-foreground hover:bg-accent focus-visible:ring-ring/50 rounded-md px-2 py-0.5 font-medium outline-hidden transition-colors focus-visible:ring-2",
|
|
1436
|
+
children: labels.retry
|
|
1437
|
+
})]
|
|
1438
|
+
});
|
|
1439
|
+
else if (state?.loading) body = /* @__PURE__ */ jsx("span", {
|
|
1440
|
+
"data-slot": "cascader-loading",
|
|
1441
|
+
className: "flex w-full items-center justify-center",
|
|
1442
|
+
children: labels.loading
|
|
1443
|
+
});
|
|
1444
|
+
return /* @__PURE__ */ jsx(Combobox.Empty, {
|
|
1445
|
+
"data-slot": "cascader-empty",
|
|
1446
|
+
"data-state": state?.error ? "error" : state?.loading ? "loading" : "empty",
|
|
1447
|
+
className: cn(EMPTY_CLASS, className),
|
|
1448
|
+
role: void 0,
|
|
1449
|
+
"aria-live": void 0,
|
|
1450
|
+
"aria-atomic": void 0,
|
|
1451
|
+
...props,
|
|
1452
|
+
children: body
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Polite live region for the current level and result count, which drill-down
|
|
1457
|
+
* otherwise hides in a visual breadcrumb. Built on `Combobox.Status` for its
|
|
1458
|
+
* initial-mutation marker: Safari and VoiceOver need a text change ~200ms
|
|
1459
|
+
* after mount before reading a polite region at all.
|
|
1460
|
+
*/
|
|
1461
|
+
function CascaderStatus({ className, children, ...props }) {
|
|
1462
|
+
const { announcement } = useCascaderState();
|
|
1463
|
+
return /* @__PURE__ */ jsx(Combobox.Status, {
|
|
1464
|
+
"data-slot": "cascader-status",
|
|
1465
|
+
className: cn("sr-only", className),
|
|
1466
|
+
...props,
|
|
1467
|
+
children: children ?? announcement
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
export { Cascader, CascaderChip, CascaderChips, CascaderContent, CascaderEmpty, CascaderList, CascaderPanel, CascaderStatus, CascaderTrigger, createCascaderHighlightStore, useCascaderActions, useCascaderState };
|