@api7/ui-components 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +169 -8
  2. package/dist/components/anchor/anchor.d.ts +28 -0
  3. package/dist/components/anchor/anchor.js +249 -0
  4. package/dist/components/breadcrumb/breadcrumb.d.ts +15 -0
  5. package/dist/components/breadcrumb/breadcrumb.js +60 -0
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/button/button.js +1 -1
  8. package/dist/components/calendar/calendar.js +24 -3
  9. package/dist/components/cascader/cascader-data.d.ts +26 -0
  10. package/dist/components/cascader/cascader-data.js +110 -0
  11. package/dist/components/cascader/cascader.d.ts +41 -0
  12. package/dist/components/cascader/cascader.js +513 -0
  13. package/dist/components/code/code.d.ts +9 -0
  14. package/dist/components/code/code.js +20 -0
  15. package/dist/components/code/copy-button.js +42 -0
  16. package/dist/components/code-block/code-block.d.ts +16 -0
  17. package/dist/components/code-block/code-block.js +45 -0
  18. package/dist/components/code-block/highlight.js +67 -0
  19. package/dist/components/collapsible/collapsible.d.ts +14 -0
  20. package/dist/components/collapsible/collapsible.js +36 -0
  21. package/dist/components/combobox/combobox-loading.d.ts +18 -0
  22. package/dist/components/combobox/combobox-loading.js +148 -0
  23. package/dist/components/combobox/combobox.d.ts +13 -6
  24. package/dist/components/combobox/combobox.js +58 -10
  25. package/dist/components/data-table/data-table.d.ts +27 -3
  26. package/dist/components/data-table/data-table.js +155 -40
  27. package/dist/components/data-table/expandable-row.js +40 -0
  28. package/dist/components/data-table/filter-header.d.ts +28 -5
  29. package/dist/components/data-table/filter-header.js +103 -40
  30. package/dist/components/data-table/index.d.ts +3 -3
  31. package/dist/components/data-table/use-table-layout.js +84 -0
  32. package/dist/components/date-picker/date-picker.js +11 -2
  33. package/dist/components/description-list/description-list.d.ts +1 -1
  34. package/dist/components/dialog/dialog.js +1 -1
  35. package/dist/components/drawer/drawer.js +1 -1
  36. package/dist/components/empty/empty.d.ts +1 -1
  37. package/dist/components/field/labeled-field.js +1 -1
  38. package/dist/components/file-upload/file-upload.d.ts +44 -0
  39. package/dist/components/file-upload/file-upload.js +195 -0
  40. package/dist/components/form/choice-fields.js +2 -2
  41. package/dist/components/form/date-fields.d.ts +1 -1
  42. package/dist/components/form/date-fields.js +1 -1
  43. package/dist/components/form/form-item.js +1 -1
  44. package/dist/components/form/form.js +1 -1
  45. package/dist/components/form/index.d.ts +2 -2
  46. package/dist/components/form/index.js +4 -4
  47. package/dist/components/form/text-field.js +2 -2
  48. package/dist/components/form/toggle-fields.d.ts +1 -1
  49. package/dist/components/form/toggle-fields.js +3 -3
  50. package/dist/components/pagination/pagination.d.ts +20 -4
  51. package/dist/components/pagination/pagination.js +83 -32
  52. package/dist/components/relative-date-range-picker/relative-date-range-picker.d.ts +14 -0
  53. package/dist/components/relative-date-range-picker/relative-date-range-picker.js +245 -0
  54. package/dist/components/relative-date-range-picker/relative-date-range.d.ts +26 -0
  55. package/dist/components/relative-date-range-picker/relative-date-range.js +109 -0
  56. package/dist/components/select/select.js +1 -1
  57. package/dist/components/sidebar/sidebar.js +2 -2
  58. package/dist/components/steps/steps.d.ts +23 -0
  59. package/dist/components/steps/steps.js +86 -0
  60. package/dist/components/table/table.d.ts +2 -1
  61. package/dist/components/table/table.js +2 -1
  62. package/dist/components/toast/toast.d.ts +1 -1
  63. package/dist/components/toast/toast.js +1 -1
  64. package/dist/config/config-provider.d.ts +2 -2
  65. package/dist/config/messages.d.ts +115 -1
  66. package/dist/config/messages.js +140 -2
  67. package/dist/hooks/use-clipboard.d.ts +11 -0
  68. package/dist/hooks/use-clipboard.js +75 -0
  69. package/dist/hooks/use-field-label-with-text.js +32 -0
  70. package/dist/index.d.ts +26 -13
  71. package/dist/index.js +15 -4
  72. package/dist/tokens/index.d.ts +1 -1
  73. package/dist/tokens/index.js +1 -1
  74. package/package.json +7 -6
@@ -0,0 +1,26 @@
1
+ /** One segment of a selected path. */
2
+ export type CascaderValue = string | number;
3
+ /** One node in the hierarchy. */
4
+ export interface CascaderOption {
5
+ /** Values must be unique among siblings, but may repeat under different parents. */
6
+ value: CascaderValue;
7
+ /** Display label, also used for local search and keyboard typeahead. */
8
+ label: string;
9
+ /** Prevents selecting this option or any of its descendants. */
10
+ disabled?: boolean;
11
+ /** Immediate children. Undefined allows lazy loading, while an empty array is already loaded. */
12
+ children?: readonly CascaderOption[];
13
+ /** Marks a branch, even when loading returns no children. Omit children until they are available. */
14
+ hasChildren?: boolean;
15
+ }
16
+ /** Context for loading the immediate children of a branch. */
17
+ export interface CascaderLoadRequest {
18
+ /** Complete parent path from the root to the expanded or unresolved branch. */
19
+ path: readonly CascaderValue[];
20
+ /** Option objects corresponding to the parent path. */
21
+ options: readonly CascaderOption[];
22
+ /** Aborted when the request becomes obsolete or the component unmounts. */
23
+ signal: AbortSignal;
24
+ }
25
+ /** Returns immediate children. Rejections show a generic error and an explicit retry action. */
26
+ export type CascaderLoader = (request: CascaderLoadRequest) => Promise<readonly CascaderOption[]>;
@@ -0,0 +1,110 @@
1
+ "use client";
2
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ // src/components/cascader/cascader-data.ts
4
+ const pathKey = (path) => JSON.stringify(path);
5
+ const isBranch = (option) => !!(option.hasChildren || option.children?.length);
6
+ function useCascaderData(options, loadChildren, resetKey, enabled) {
7
+ const optionsKey = useMemo(() => JSON.stringify(options, [
8
+ "value",
9
+ "label",
10
+ "disabled",
11
+ "children",
12
+ "hasChildren"
13
+ ]), [options]);
14
+ const [cache, setCache] = useState({
15
+ optionsKey,
16
+ resetKey,
17
+ enabled,
18
+ entries: /* @__PURE__ */ new Map()
19
+ });
20
+ const requests = useRef(/* @__PURE__ */ new Map());
21
+ const loader = useRef(loadChildren);
22
+ const source = useRef({
23
+ optionsKey,
24
+ resetKey,
25
+ enabled
26
+ });
27
+ const entries = cache.optionsKey === optionsKey && Object.is(cache.resetKey, resetKey) ? cache.entries : /* @__PURE__ */ new Map();
28
+ if (cache.optionsKey !== optionsKey || !Object.is(cache.resetKey, resetKey) || cache.enabled !== enabled) setCache({
29
+ optionsKey,
30
+ resetKey,
31
+ enabled,
32
+ entries: new Map([...entries].filter(([, entry]) => entry.status !== "loading"))
33
+ });
34
+ useLayoutEffect(() => {
35
+ loader.current = loadChildren;
36
+ }, [loadChildren]);
37
+ useLayoutEffect(() => {
38
+ source.current = {
39
+ optionsKey,
40
+ resetKey,
41
+ enabled
42
+ };
43
+ const pending = requests.current;
44
+ return () => {
45
+ for (const controller of pending.values()) controller.abort();
46
+ pending.clear();
47
+ };
48
+ }, [
49
+ optionsKey,
50
+ resetKey,
51
+ enabled
52
+ ]);
53
+ const load = useCallback(async (pathOptions) => {
54
+ const { optionsKey: root, resetKey: key, enabled: active } = source.current;
55
+ if (!active || !loader.current) return;
56
+ const path = pathOptions.map((item) => item.value);
57
+ const id = pathKey(path);
58
+ if (requests.current.has(id)) return;
59
+ const controller = new AbortController();
60
+ requests.current.set(id, controller);
61
+ const update = (entry) => {
62
+ setCache((prev) => ({
63
+ optionsKey: root,
64
+ resetKey: key,
65
+ enabled: active,
66
+ entries: new Map(prev.optionsKey === root && Object.is(prev.resetKey, key) ? prev.entries : []).set(id, entry)
67
+ }));
68
+ };
69
+ update({ status: "loading" });
70
+ try {
71
+ const items = await loader.current({
72
+ path,
73
+ options: pathOptions,
74
+ signal: controller.signal
75
+ });
76
+ if (!Array.isArray(items)) throw new TypeError("Cascader loadChildren must resolve to an array.");
77
+ if (!controller.signal.aborted) update({
78
+ status: "ready",
79
+ items
80
+ });
81
+ } catch {
82
+ if (!controller.signal.aborted) update({ status: "error" });
83
+ } finally {
84
+ if (requests.current.get(id) === controller) requests.current.delete(id);
85
+ }
86
+ }, []);
87
+ const children = (option, path) => {
88
+ const entry = entries.get(pathKey(path));
89
+ return option.children ?? (entry?.status === "ready" ? entry.items : void 0);
90
+ };
91
+ const resolve = (path) => {
92
+ const result = [];
93
+ let items = options;
94
+ for (const value of path) {
95
+ const option = items?.find((item) => item.value === value);
96
+ if (!option) break;
97
+ result.push(option);
98
+ items = children(option, result.map((item) => item.value));
99
+ }
100
+ return result;
101
+ };
102
+ return {
103
+ optionsKey,
104
+ entries,
105
+ children,
106
+ resolve,
107
+ load
108
+ };
109
+ }
110
+ export { isBranch, pathKey, useCascaderData };
@@ -0,0 +1,41 @@
1
+ import { CascaderLoadRequest, CascaderLoader, CascaderOption, CascaderValue } from "./cascader-data.js";
2
+ import { PopoverContentProps } from "../popover/popover.js";
3
+ import { ComponentProps } from "react";
4
+ export type CascaderProps = Omit<ComponentProps<'input'>, 'children' | 'value' | 'defaultValue' | 'onChange' | 'type'> & {
5
+ /** Root options. Changing their contents invalidates loaded children and restores the selected path. */
6
+ options: readonly CascaderOption[];
7
+ /** Selected path. Passing the value key, including undefined, makes selection controlled. */
8
+ value?: readonly CascaderValue[] | null;
9
+ /** Initial uncontrolled path, also restored on a native form reset. */
10
+ defaultValue?: readonly CascaderValue[];
11
+ /** Receives the path and its option objects on every selection, including reselection. Clearing supplies null and []. */
12
+ onValueChange?: (value: CascaderValue[] | null, options: readonly CascaderOption[]) => void;
13
+ /** Allows selecting a branch while keeping its children open. */
14
+ changeOnSelect?: boolean;
15
+ /** Loads immediate children on expansion or while resolving a selected path. */
16
+ loadChildren?: CascaderLoader;
17
+ /** Invalidates loaded children. The caller owns clearing an obsolete selected value. */
18
+ resetKey?: unknown;
19
+ /** Filters supplied and already loaded paths, without fetching unknown descendants. */
20
+ searchable?: boolean;
21
+ /** Shows the root loading state and temporarily hides root options. */
22
+ loading?: boolean;
23
+ /** Shows a clear button when a value is selected and the control is editable. */
24
+ clearable?: boolean;
25
+ /** Preserves the value while preventing opening, editing, and clearing. */
26
+ readOnly?: boolean;
27
+ /** Text shown without a selection. Defaults to the configured locale message. */
28
+ placeholder?: string;
29
+ /** Controlled popup visibility. Disabled and read-only controls remain closed. */
30
+ open?: boolean;
31
+ /** Initial uncontrolled popup visibility. */
32
+ defaultOpen?: boolean;
33
+ /** Called when an interaction requests opening or closing, with the requested visibility. */
34
+ onOpenChange?: (open: boolean) => void;
35
+ /** Formats the complete path and its resolved option prefix, including empty or unresolved paths. */
36
+ displayValue?: (value: readonly CascaderValue[], options: readonly CascaderOption[]) => string;
37
+ /** Customizes the popup's placement, styling, focus behavior, and accessible attributes. */
38
+ popupProps?: Omit<PopoverContentProps, 'children'>;
39
+ };
40
+ declare function Cascader(props: CascaderProps): import("react").JSX.Element;
41
+ export { Cascader, type CascaderLoadRequest, type CascaderLoader, type CascaderOption, type CascaderValue };
@@ -0,0 +1,513 @@
1
+ "use client";
2
+ import { isBranch, pathKey, useCascaderData } from "./cascader-data.js";
3
+ import { Popover as Popover$1, PopoverContent } from "../popover/popover.js";
4
+ import { useMessages } from "../../config/config-provider.js";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { controlFocus, controlSurface } from "../../lib/control.js";
7
+ import { listItemClassName } from "../../lib/popup.js";
8
+ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { CheckIcon, ChevronDownIcon, ChevronRightIcon, XIcon } from "lucide-react";
11
+ import { Popover } from "@base-ui/react/popover";
12
+ import { Select } from "@base-ui/react/select";
13
+ // src/components/cascader/cascader.tsx
14
+ const EMPTY = [];
15
+ function scrollSelectedOptions(lists) {
16
+ for (const list of lists) {
17
+ const selected = list?.querySelector("[aria-selected=\"true\"]");
18
+ const scroller = list?.parentElement;
19
+ if (!selected || !scroller) continue;
20
+ const item = selected.getBoundingClientRect();
21
+ const viewport = scroller.getBoundingClientRect();
22
+ if (item.top < viewport.top) scroller.scrollTop += item.top - viewport.top;
23
+ else if (item.bottom > viewport.bottom) scroller.scrollTop += item.bottom - viewport.bottom;
24
+ }
25
+ }
26
+ function setRef(ref, value) {
27
+ if (typeof ref === "function") {
28
+ const cleanup = ref(value);
29
+ return typeof cleanup === "function" ? cleanup : () => ref(null);
30
+ }
31
+ if (ref) ref.current = value;
32
+ return () => {
33
+ if (ref) ref.current = null;
34
+ };
35
+ }
36
+ function Cascader(props) {
37
+ const { value, defaultValue, onValueChange, ...controlOptions } = props;
38
+ const [internalValue, setInternalValue] = useState(defaultValue ?? null);
39
+ const currentValue = ("value" in props ? value : internalValue) ?? EMPTY;
40
+ const changeValue = (value, options) => {
41
+ if (!("value" in props)) setInternalValue(value);
42
+ onValueChange?.(value, options);
43
+ };
44
+ return /* @__PURE__ */ jsx(Select.Root, {
45
+ id: props.id,
46
+ name: props.name,
47
+ form: props.form,
48
+ disabled: props.disabled,
49
+ readOnly: props.readOnly,
50
+ required: props.required,
51
+ value: currentValue.length ? JSON.stringify(currentValue) : "",
52
+ open: false,
53
+ onOpenChange: (_, details) => details.cancel(),
54
+ children: /* @__PURE__ */ jsx(Select.Trigger, {
55
+ nativeButton: false,
56
+ render: (controlProps, state) => /* @__PURE__ */ jsx(CascaderControl, {
57
+ ...controlOptions,
58
+ disabled: props.disabled || state.disabled,
59
+ currentValue,
60
+ changeValue,
61
+ resetValue: () => {
62
+ if (!("value" in props)) setInternalValue(defaultValue ?? null);
63
+ },
64
+ controlProps: {
65
+ id: controlProps.id,
66
+ ref: controlProps.ref,
67
+ "aria-labelledby": controlProps["aria-labelledby"],
68
+ "aria-describedby": controlProps["aria-describedby"],
69
+ "aria-invalid": controlProps["aria-invalid"],
70
+ "aria-required": controlProps["aria-required"],
71
+ onFocus: controlProps.onFocus,
72
+ onBlur: controlProps.onBlur
73
+ }
74
+ })
75
+ })
76
+ });
77
+ }
78
+ function CascaderControl({ options, loadChildren, resetKey, currentValue, changeValue, resetValue, controlProps, changeOnSelect = false, searchable = false, loading = false, clearable = false, disabled = false, readOnly = false, placeholder, open: controlledOpen, defaultOpen = false, onOpenChange, displayValue, popupProps, className, ref: externalRef, form, ...inputProps }) {
79
+ const messages = useMessages().cascader;
80
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
81
+ const open = !disabled && !readOnly && (controlledOpen ?? internalOpen);
82
+ const [expanded, setExpanded] = useState(currentValue);
83
+ const [query, setQuery] = useState("");
84
+ const [active, setActive] = useState({});
85
+ const listRefs = useRef([]);
86
+ const columnsRef = useRef(null);
87
+ const pendingFocus = useRef(null);
88
+ const typeahead = useRef({
89
+ text: "",
90
+ time: 0
91
+ });
92
+ const id = useId();
93
+ const triggerRef = useRef(null);
94
+ const labelledBy = inputProps["aria-labelledby"] ?? controlProps["aria-labelledby"];
95
+ const triggerRefCallback = useMemo(() => (node) => {
96
+ triggerRef.current = node;
97
+ if (!node) return;
98
+ const cleanups = [setRef(controlProps.ref, node), setRef(externalRef, node)];
99
+ return () => {
100
+ triggerRef.current = null;
101
+ cleanups.forEach((cleanup) => cleanup());
102
+ };
103
+ }, [
104
+ controlProps.ref,
105
+ externalRef,
106
+ triggerRef
107
+ ]);
108
+ const data = useCascaderData(options, loadChildren, resetKey, !disabled && !readOnly && (open || currentValue.length > 1));
109
+ const selectedOptions = data.resolve(currentValue);
110
+ const selectedKey = pathKey(currentValue);
111
+ const optionsKey = data.optionsKey;
112
+ const [previous, setPrevious] = useState({
113
+ selectedKey,
114
+ optionsKey,
115
+ resetKey,
116
+ open
117
+ });
118
+ if (previous.selectedKey !== selectedKey || previous.optionsKey !== optionsKey || !Object.is(previous.resetKey, resetKey) || previous.open !== open) {
119
+ setPrevious({
120
+ selectedKey,
121
+ optionsKey,
122
+ resetKey,
123
+ open
124
+ });
125
+ setExpanded(currentValue);
126
+ setActive({});
127
+ if (previous.open !== open && !open) setQuery("");
128
+ }
129
+ const resetRef = useRef(resetValue);
130
+ useLayoutEffect(() => {
131
+ resetRef.current = resetValue;
132
+ }, [resetValue]);
133
+ useEffect(() => {
134
+ const element = triggerRef.current?.form;
135
+ let timer;
136
+ const reset = (event) => {
137
+ timer = setTimeout(() => {
138
+ if (!event.defaultPrevented) resetRef.current();
139
+ }, 0);
140
+ };
141
+ element?.addEventListener("reset", reset);
142
+ return () => {
143
+ element?.removeEventListener("reset", reset);
144
+ clearTimeout(timer);
145
+ };
146
+ }, [form, triggerRef]);
147
+ useEffect(() => {
148
+ if (disabled || readOnly || loading || !loadChildren) return;
149
+ for (const path of [currentValue, ...open ? [expanded] : []]) {
150
+ const resolved = data.resolve(path);
151
+ const last = resolved.at(-1);
152
+ if (!last || last.disabled || !isBranch(last)) continue;
153
+ const values = resolved.map((item) => item.value);
154
+ if ((resolved.length < path.length || open && path === expanded) && !data.children(last, values) && data.entries.get(pathKey(values))?.status !== "error") data.load(resolved);
155
+ }
156
+ }, [
157
+ data,
158
+ currentValue,
159
+ expanded,
160
+ open,
161
+ disabled,
162
+ readOnly,
163
+ loading,
164
+ loadChildren
165
+ ]);
166
+ const changeOpen = (next) => {
167
+ if (next && (disabled || readOnly)) return;
168
+ if (controlledOpen === void 0) setInternalOpen(next);
169
+ onOpenChange?.(next);
170
+ };
171
+ const choose = (path, commit) => {
172
+ if (disabled || readOnly || path.some((item) => item.disabled)) return;
173
+ const values = path.map((item) => item.value);
174
+ const last = path.at(-1);
175
+ setExpanded(values);
176
+ setActive((prev) => Object.fromEntries(Object.entries(prev).filter(([level]) => Number(level) < values.length)));
177
+ if (commit && (changeOnSelect || !isBranch(last))) {
178
+ changeValue(values, path);
179
+ if (!isBranch(last) || query.trim()) changeOpen(false);
180
+ }
181
+ };
182
+ const columns = [{
183
+ parent: [],
184
+ items: loading ? [] : options
185
+ }];
186
+ let parent = [];
187
+ for (const value of expanded) {
188
+ const option = columns.at(-1).items.find((item) => item.value === value);
189
+ if (!option || option.disabled || !isBranch(option)) break;
190
+ parent = [...parent, option];
191
+ columns.push({
192
+ parent,
193
+ items: data.children(option, parent.map((item) => item.value)) ?? []
194
+ });
195
+ }
196
+ const searchPaths = [];
197
+ const search = query.trim().toLowerCase();
198
+ const collect = (items, ancestors) => {
199
+ for (const item of items) {
200
+ if (item.disabled) continue;
201
+ const path = [...ancestors, item];
202
+ if ((changeOnSelect || !isBranch(item)) && path.some((part) => part.label.toLowerCase().includes(search))) searchPaths.push(path);
203
+ const children = data.children(item, path.map((part) => part.value));
204
+ if (children) collect(children, path);
205
+ }
206
+ };
207
+ if (search) collect(loading ? [] : options, []);
208
+ const visibleColumns = search ? [{
209
+ parent: [],
210
+ items: searchPaths.map((path) => path.at(-1))
211
+ }] : columns;
212
+ const columnCount = visibleColumns.length;
213
+ useLayoutEffect(() => {
214
+ if (!open || search) return;
215
+ const revealLastColumn = () => {
216
+ const scroller = columnsRef.current;
217
+ if (scroller) scroller.scrollLeft = getComputedStyle(scroller).direction === "rtl" ? -scroller.scrollWidth : scroller.scrollWidth;
218
+ };
219
+ revealLastColumn();
220
+ const frame = requestAnimationFrame(revealLastColumn);
221
+ return () => cancelAnimationFrame(frame);
222
+ }, [
223
+ open,
224
+ search,
225
+ columnCount
226
+ ]);
227
+ useLayoutEffect(() => {
228
+ if (open && !search) scrollSelectedOptions(listRefs.current.slice(0, columnCount));
229
+ }, [
230
+ open,
231
+ search,
232
+ columnCount,
233
+ selectedKey,
234
+ data.entries,
235
+ options
236
+ ]);
237
+ const rowIndex = (items, level) => {
238
+ const index = active[level];
239
+ const expandedIndex = search ? -1 : items.findIndex((item) => !item.disabled && item.value === expanded[level]);
240
+ return index !== void 0 && items[index] && !items[index].disabled ? index : expandedIndex >= 0 ? expandedIndex : items.findIndex((item) => !item.disabled);
241
+ };
242
+ useLayoutEffect(() => {
243
+ if (pendingFocus.current !== null) {
244
+ listRefs.current[pendingFocus.current]?.focus();
245
+ pendingFocus.current = null;
246
+ }
247
+ for (const [level, index] of Object.entries(active)) if (document.activeElement === listRefs.current[Number(level)]) document.getElementById(`${id}-${level}-${index}`)?.scrollIntoView({
248
+ block: "nearest",
249
+ inline: "nearest"
250
+ });
251
+ });
252
+ const handleKey = (event, column, level) => {
253
+ event.currentTarget.focus();
254
+ const { items } = column;
255
+ const index = rowIndex(items, level);
256
+ const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
257
+ const forward = rtl ? "ArrowLeft" : "ArrowRight";
258
+ const back = rtl ? "ArrowRight" : "ArrowLeft";
259
+ let next = index;
260
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
261
+ const step = event.key === "ArrowDown" ? 1 : -1;
262
+ for (let count = 1; count <= items.length; count += 1) {
263
+ const candidate = (index + step * count + items.length) % items.length;
264
+ if (!items[candidate].disabled) {
265
+ next = candidate;
266
+ break;
267
+ }
268
+ }
269
+ } else if (event.key === "Home" || event.key === "End") next = event.key === "Home" ? items.findIndex((item) => !item.disabled) : items.reduce((last, item, position) => item.disabled ? last : position, -1);
270
+ else if (event.key === back && !search) {
271
+ event.preventDefault();
272
+ listRefs.current[level - 1]?.focus();
273
+ return;
274
+ } else if ([
275
+ "Enter",
276
+ " ",
277
+ forward
278
+ ].includes(event.key) && index >= 0) {
279
+ event.preventDefault();
280
+ const path = search ? searchPaths[index] : [...column.parent, items[index]];
281
+ if (event.key === forward && (search || !isBranch(items[index]))) return;
282
+ choose(path, event.key !== forward);
283
+ if (event.key === forward) pendingFocus.current = level + 1;
284
+ return;
285
+ } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
286
+ const now = event.timeStamp;
287
+ typeahead.current = {
288
+ text: (now - typeahead.current.time < 700 ? typeahead.current.text : "") + event.key.toLowerCase(),
289
+ time: now
290
+ };
291
+ const match = items.findIndex((item) => !item.disabled && item.label.toLowerCase().startsWith(typeahead.current.text));
292
+ if (match >= 0) next = match;
293
+ } else return;
294
+ event.preventDefault();
295
+ setActive((prev) => ({
296
+ ...prev,
297
+ [level]: next
298
+ }));
299
+ };
300
+ const label = placeholder ?? messages.placeholder;
301
+ const renderedValue = displayValue?.(currentValue, selectedOptions) ?? currentValue.map((value, index) => selectedOptions[index]?.label ?? String(value)).join(" / ");
302
+ const clear = () => {
303
+ changeValue(null, []);
304
+ setExpanded([]);
305
+ setQuery("");
306
+ triggerRef.current?.focus();
307
+ };
308
+ return /* @__PURE__ */ jsxs(Popover$1, {
309
+ open,
310
+ onOpenChange: changeOpen,
311
+ onOpenChangeComplete: (opened) => {
312
+ if (opened && !search) scrollSelectedOptions(listRefs.current.slice(0, columnCount));
313
+ },
314
+ children: [/* @__PURE__ */ jsxs("div", {
315
+ className: cn("relative w-full", className),
316
+ "data-slot": "cascader",
317
+ children: [
318
+ /* @__PURE__ */ jsx(Popover.Trigger, {
319
+ nativeButton: false,
320
+ role: "combobox",
321
+ disabled,
322
+ "aria-readonly": readOnly || void 0,
323
+ "aria-autocomplete": searchable ? "list" : "none",
324
+ "aria-labelledby": labelledBy,
325
+ "aria-label": inputProps["aria-label"] ?? (labelledBy ? void 0 : label),
326
+ "aria-busy": loading || void 0,
327
+ className: cn(controlSurface, controlFocus, "h-8 truncate ps-3 pe-10 text-start", !searchable && "cursor-pointer", clearable && currentValue.length > 0 && !disabled && !readOnly && "pe-16"),
328
+ "data-slot": "cascader-trigger",
329
+ render: /* @__PURE__ */ jsx("input", {
330
+ ...controlProps,
331
+ ...inputProps,
332
+ ref: triggerRefCallback,
333
+ name: void 0,
334
+ required: false,
335
+ form,
336
+ type: "text",
337
+ readOnly: !searchable || readOnly,
338
+ autoComplete: "off",
339
+ value: searchable && open ? query : renderedValue,
340
+ placeholder: searchable && open && currentValue.length ? renderedValue : label,
341
+ onFocus: (event) => {
342
+ controlProps.onFocus?.(event);
343
+ inputProps.onFocus?.(event);
344
+ if (searchable && !open) event.currentTarget.select();
345
+ },
346
+ onBlur: (event) => {
347
+ controlProps.onBlur?.(event);
348
+ inputProps.onBlur?.(event);
349
+ },
350
+ onChange: (event) => {
351
+ if (!open) changeOpen(true);
352
+ setQuery(event.target.value);
353
+ setActive({});
354
+ }
355
+ }),
356
+ onClick: (event) => {
357
+ event.preventBaseUIHandler();
358
+ if (!event.defaultPrevented && !open) changeOpen(true);
359
+ },
360
+ onKeyDown: (event) => {
361
+ event.preventBaseUIHandler();
362
+ if (event.defaultPrevented || event.nativeEvent.isComposing) return;
363
+ if ([
364
+ "ArrowDown",
365
+ "ArrowUp",
366
+ "Enter"
367
+ ].includes(event.key) || !searchable && event.key === " ") {
368
+ event.preventDefault();
369
+ if (open) listRefs.current[0]?.focus();
370
+ else changeOpen(true);
371
+ }
372
+ },
373
+ onKeyUp: (event) => event.preventBaseUIHandler()
374
+ }),
375
+ /* @__PURE__ */ jsx(ChevronDownIcon, {
376
+ "aria-hidden": true,
377
+ className: "pointer-events-none absolute end-3 top-1/2 size-5 -translate-y-1/2"
378
+ }),
379
+ clearable && currentValue.length > 0 && !disabled && !readOnly && /* @__PURE__ */ jsx("button", {
380
+ type: "button",
381
+ "aria-label": messages.clear,
382
+ className: cn("absolute end-10 top-1/2 -translate-y-1/2 rounded-sm text-muted-foreground hover:text-foreground", "focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 aria-invalid:focus-visible:ring-destructive/40 data-invalid:focus-visible:ring-destructive/40"),
383
+ onClick: clear,
384
+ children: /* @__PURE__ */ jsx(XIcon, {
385
+ "aria-hidden": true,
386
+ className: "size-3"
387
+ })
388
+ })
389
+ ]
390
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
391
+ "aria-label": inputProps["aria-label"] ?? label,
392
+ initialFocus: () => searchable ? triggerRef.current : listRefs.current[0],
393
+ ...popupProps,
394
+ className: cn("w-auto min-w-(--anchor-width) overflow-hidden p-0", popupProps?.className),
395
+ children: [
396
+ searchable && loadChildren && /* @__PURE__ */ jsx("p", {
397
+ className: "border-b border-border p-2 text-xs text-muted-foreground",
398
+ children: messages.searchHint
399
+ }),
400
+ /* @__PURE__ */ jsx("span", {
401
+ id: `${id}-branch`,
402
+ className: "sr-only",
403
+ children: messages.branch
404
+ }),
405
+ /* @__PURE__ */ jsx("div", {
406
+ ref: columnsRef,
407
+ className: "flex max-w-[calc(100vw-2rem)] overflow-x-auto",
408
+ "data-slot": "cascader-columns",
409
+ children: visibleColumns.map((column, level) => {
410
+ const values = column.parent.map((item) => item.value);
411
+ const entry = data.entries.get(pathKey(values));
412
+ const busy = loading || entry?.status === "loading";
413
+ const failed = entry?.status === "error";
414
+ const index = rowIndex(column.items, level);
415
+ return /* @__PURE__ */ jsxs("div", {
416
+ className: "max-h-[300px] min-w-44 flex-1 overflow-y-auto border-e border-border bg-muted last:border-e-0",
417
+ children: [
418
+ /* @__PURE__ */ jsx("div", {
419
+ role: "listbox",
420
+ tabIndex: 0,
421
+ ref: (node) => {
422
+ listRefs.current[level] = node;
423
+ },
424
+ "aria-label": search ? messages.results : messages.level({
425
+ level: level + 1,
426
+ label: column.parent.at(-1)?.label ?? label
427
+ }),
428
+ "aria-activedescendant": index >= 0 ? `${id}-${level}-${index}` : void 0,
429
+ "aria-busy": busy || void 0,
430
+ className: "min-h-1 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40",
431
+ onKeyDown: (event) => handleKey(event, column, level),
432
+ children: column.items.map((item, itemIndex) => {
433
+ const path = search ? searchPaths[itemIndex] : [...column.parent, item];
434
+ const pathValues = path.map((part) => part.value);
435
+ const selected = pathKey(pathValues) === pathKey(search ? currentValue : currentValue.slice(0, path.length));
436
+ return /* @__PURE__ */ jsxs("div", {
437
+ id: `${id}-${level}-${itemIndex}`,
438
+ role: "option",
439
+ tabIndex: -1,
440
+ "aria-selected": selected,
441
+ "aria-describedby": !search && isBranch(item) ? `${id}-branch` : void 0,
442
+ "aria-disabled": item.disabled || void 0,
443
+ "data-selected": selected || void 0,
444
+ "data-disabled": item.disabled || void 0,
445
+ "data-highlighted": index === itemIndex || void 0,
446
+ className: cn(listItemClassName, "min-w-44 hover:bg-accent-hover"),
447
+ onMouseDown: (event) => {
448
+ if (searchable) event.preventDefault();
449
+ },
450
+ onClick: () => {
451
+ if (searchable) triggerRef.current?.focus();
452
+ else listRefs.current[level]?.focus();
453
+ setActive((prev) => ({
454
+ ...prev,
455
+ [level]: itemIndex
456
+ }));
457
+ choose(path, true);
458
+ },
459
+ onKeyDown: (event) => {
460
+ if (event.key === "Enter" || event.key === " ") {
461
+ event.preventDefault();
462
+ event.stopPropagation();
463
+ choose(path, true);
464
+ }
465
+ },
466
+ children: [
467
+ /* @__PURE__ */ jsx("span", {
468
+ className: "min-w-0 flex-1 break-words",
469
+ children: search ? path.map((part) => part.label).join(" / ") : item.label
470
+ }),
471
+ selected && /* @__PURE__ */ jsx(CheckIcon, {
472
+ "aria-hidden": true,
473
+ className: "size-4 shrink-0"
474
+ }),
475
+ !search && isBranch(item) && /* @__PURE__ */ jsx(ChevronRightIcon, {
476
+ "aria-hidden": true,
477
+ className: "size-4 shrink-0 rtl:rotate-180"
478
+ })
479
+ ]
480
+ }, pathKey(path.map((part) => part.value)));
481
+ })
482
+ }),
483
+ busy && /* @__PURE__ */ jsx("p", {
484
+ role: "status",
485
+ className: "px-4 py-3 text-sm text-muted-foreground",
486
+ children: messages.loading
487
+ }),
488
+ failed && /* @__PURE__ */ jsxs("div", {
489
+ className: "px-4 py-3 text-sm",
490
+ children: [/* @__PURE__ */ jsx("p", {
491
+ role: "status",
492
+ children: messages.error
493
+ }), /* @__PURE__ */ jsx("button", {
494
+ type: "button",
495
+ className: cn("mt-2 text-primary-text hover:underline", "focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 aria-invalid:focus-visible:ring-destructive/40 data-invalid:focus-visible:ring-destructive/40"),
496
+ onClick: () => void data.load(column.parent),
497
+ children: messages.retry
498
+ })]
499
+ }),
500
+ !busy && !failed && column.items.length === 0 && /* @__PURE__ */ jsx("p", {
501
+ role: "status",
502
+ className: "px-4 py-3 text-sm text-muted-foreground",
503
+ children: messages.empty
504
+ })
505
+ ]
506
+ }, search ? "search" : pathKey(values));
507
+ })
508
+ })
509
+ ]
510
+ })]
511
+ });
512
+ }
513
+ export { Cascader };