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