@xsolla/xui-context-menu 0.172.2 → 0.173.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.
- package/README.md +1 -0
- package/native/index.d.mts +79 -75
- package/native/index.d.ts +79 -75
- package/native/index.js +796 -478
- package/native/index.js.map +1 -1
- package/native/index.mjs +807 -481
- package/native/index.mjs.map +1 -1
- package/package.json +10 -10
- package/web/index.d.mts +79 -75
- package/web/index.d.ts +79 -75
- package/web/index.js +896 -501
- package/web/index.js.map +1 -1
- package/web/index.mjs +889 -490
- package/web/index.mjs.map +1 -1
package/native/index.mjs
CHANGED
|
@@ -1,18 +1,196 @@
|
|
|
1
1
|
// src/ContextMenu.tsx
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useState as useState3,
|
|
2
|
+
import {
|
|
3
|
+
cloneElement,
|
|
4
|
+
forwardRef,
|
|
8
5
|
isValidElement,
|
|
9
|
-
|
|
6
|
+
useCallback as useCallback3,
|
|
7
|
+
useEffect as useEffect4,
|
|
8
|
+
useLayoutEffect as useLayoutEffect3,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef as useRef2,
|
|
11
|
+
useState as useState4
|
|
10
12
|
} from "react";
|
|
11
|
-
|
|
13
|
+
|
|
14
|
+
// ../../foundation/primitives-native/src/Box.tsx
|
|
12
15
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
View,
|
|
17
|
+
Pressable,
|
|
18
|
+
Image
|
|
19
|
+
} from "react-native";
|
|
20
|
+
import { jsx } from "react/jsx-runtime";
|
|
21
|
+
var Box = ({
|
|
22
|
+
children,
|
|
23
|
+
onPress,
|
|
24
|
+
onLayout,
|
|
25
|
+
onMoveShouldSetResponder,
|
|
26
|
+
onResponderGrant,
|
|
27
|
+
onResponderMove,
|
|
28
|
+
onResponderRelease,
|
|
29
|
+
onResponderTerminate,
|
|
30
|
+
backgroundColor,
|
|
31
|
+
borderColor,
|
|
32
|
+
borderWidth,
|
|
33
|
+
borderBottomWidth,
|
|
34
|
+
borderBottomColor,
|
|
35
|
+
borderTopWidth,
|
|
36
|
+
borderTopColor,
|
|
37
|
+
borderLeftWidth,
|
|
38
|
+
borderLeftColor,
|
|
39
|
+
borderRightWidth,
|
|
40
|
+
borderRightColor,
|
|
41
|
+
borderRadius,
|
|
42
|
+
borderStyle,
|
|
43
|
+
height,
|
|
44
|
+
padding,
|
|
45
|
+
paddingHorizontal,
|
|
46
|
+
paddingVertical,
|
|
47
|
+
margin,
|
|
48
|
+
marginTop,
|
|
49
|
+
marginBottom,
|
|
50
|
+
marginLeft,
|
|
51
|
+
marginRight,
|
|
52
|
+
flexDirection,
|
|
53
|
+
alignItems,
|
|
54
|
+
justifyContent,
|
|
55
|
+
position,
|
|
56
|
+
top,
|
|
57
|
+
bottom,
|
|
58
|
+
left,
|
|
59
|
+
right,
|
|
60
|
+
width,
|
|
61
|
+
minWidth,
|
|
62
|
+
minHeight,
|
|
63
|
+
maxWidth,
|
|
64
|
+
maxHeight,
|
|
65
|
+
flex,
|
|
66
|
+
overflow,
|
|
67
|
+
zIndex,
|
|
68
|
+
hoverStyle,
|
|
69
|
+
pressStyle,
|
|
70
|
+
style,
|
|
71
|
+
"data-testid": dataTestId,
|
|
72
|
+
testID,
|
|
73
|
+
as,
|
|
74
|
+
src,
|
|
75
|
+
alt,
|
|
76
|
+
...rest
|
|
77
|
+
}) => {
|
|
78
|
+
const getContainerStyle = (pressed) => ({
|
|
79
|
+
backgroundColor: pressed && pressStyle?.backgroundColor ? pressStyle.backgroundColor : backgroundColor,
|
|
80
|
+
borderColor,
|
|
81
|
+
borderWidth,
|
|
82
|
+
borderBottomWidth,
|
|
83
|
+
borderBottomColor,
|
|
84
|
+
borderTopWidth,
|
|
85
|
+
borderTopColor,
|
|
86
|
+
borderLeftWidth,
|
|
87
|
+
borderLeftColor,
|
|
88
|
+
borderRightWidth,
|
|
89
|
+
borderRightColor,
|
|
90
|
+
borderRadius,
|
|
91
|
+
borderStyle,
|
|
92
|
+
overflow,
|
|
93
|
+
zIndex,
|
|
94
|
+
height,
|
|
95
|
+
width,
|
|
96
|
+
minWidth,
|
|
97
|
+
minHeight,
|
|
98
|
+
maxWidth,
|
|
99
|
+
maxHeight,
|
|
100
|
+
padding,
|
|
101
|
+
paddingHorizontal,
|
|
102
|
+
paddingVertical,
|
|
103
|
+
margin,
|
|
104
|
+
marginTop,
|
|
105
|
+
marginBottom,
|
|
106
|
+
marginLeft,
|
|
107
|
+
marginRight,
|
|
108
|
+
flexDirection,
|
|
109
|
+
alignItems,
|
|
110
|
+
justifyContent,
|
|
111
|
+
position,
|
|
112
|
+
top,
|
|
113
|
+
bottom,
|
|
114
|
+
left,
|
|
115
|
+
right,
|
|
116
|
+
flex,
|
|
117
|
+
...style
|
|
118
|
+
});
|
|
119
|
+
const finalTestID = dataTestId || testID;
|
|
120
|
+
const {
|
|
121
|
+
role,
|
|
122
|
+
tabIndex,
|
|
123
|
+
onKeyDown,
|
|
124
|
+
onKeyUp,
|
|
125
|
+
"aria-label": _ariaLabel,
|
|
126
|
+
"aria-labelledby": _ariaLabelledBy,
|
|
127
|
+
"aria-current": _ariaCurrent,
|
|
128
|
+
"aria-disabled": _ariaDisabled,
|
|
129
|
+
"aria-live": _ariaLive,
|
|
130
|
+
className,
|
|
131
|
+
"data-testid": _dataTestId,
|
|
132
|
+
...nativeRest
|
|
133
|
+
} = rest;
|
|
134
|
+
if (as === "img" && src) {
|
|
135
|
+
const imageStyle = {
|
|
136
|
+
width,
|
|
137
|
+
height,
|
|
138
|
+
borderRadius,
|
|
139
|
+
position,
|
|
140
|
+
top,
|
|
141
|
+
bottom,
|
|
142
|
+
left,
|
|
143
|
+
right,
|
|
144
|
+
...style
|
|
145
|
+
};
|
|
146
|
+
return /* @__PURE__ */ jsx(
|
|
147
|
+
Image,
|
|
148
|
+
{
|
|
149
|
+
source: { uri: src },
|
|
150
|
+
style: imageStyle,
|
|
151
|
+
testID: finalTestID,
|
|
152
|
+
resizeMode: "cover",
|
|
153
|
+
...nativeRest
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (onPress) {
|
|
158
|
+
return /* @__PURE__ */ jsx(
|
|
159
|
+
Pressable,
|
|
160
|
+
{
|
|
161
|
+
onPress,
|
|
162
|
+
onLayout,
|
|
163
|
+
onMoveShouldSetResponder,
|
|
164
|
+
onResponderGrant,
|
|
165
|
+
onResponderMove,
|
|
166
|
+
onResponderRelease,
|
|
167
|
+
onResponderTerminate,
|
|
168
|
+
style: ({ pressed }) => getContainerStyle(pressed),
|
|
169
|
+
testID: finalTestID,
|
|
170
|
+
...nativeRest,
|
|
171
|
+
children
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return /* @__PURE__ */ jsx(
|
|
176
|
+
View,
|
|
177
|
+
{
|
|
178
|
+
style: getContainerStyle(),
|
|
179
|
+
testID: finalTestID,
|
|
180
|
+
onLayout,
|
|
181
|
+
onMoveShouldSetResponder,
|
|
182
|
+
onResponderGrant,
|
|
183
|
+
onResponderMove,
|
|
184
|
+
onResponderRelease,
|
|
185
|
+
onResponderTerminate,
|
|
186
|
+
...nativeRest,
|
|
187
|
+
children
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// src/ContextMenu.tsx
|
|
193
|
+
import { useDesignSystem as useDesignSystem2, useId as useId2 } from "@xsolla/xui-core";
|
|
16
194
|
import { Spinner } from "@xsolla/xui-spinner";
|
|
17
195
|
|
|
18
196
|
// src/ContextMenuContext.tsx
|
|
@@ -43,7 +221,7 @@ import { Typography } from "@xsolla/xui-typography";
|
|
|
43
221
|
import { Checkbox } from "@xsolla/xui-checkbox";
|
|
44
222
|
import { Radio } from "@xsolla/xui-radio";
|
|
45
223
|
import { Search } from "@xsolla/xui-icons-base";
|
|
46
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
224
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
47
225
|
var sizeToVariants = {
|
|
48
226
|
xl: { label: "bodyLg", description: "bodyLg", headingAccent: "bodyLgAccent" },
|
|
49
227
|
lg: { label: "bodyLg", description: "bodyMd", headingAccent: "bodyMdAccent" },
|
|
@@ -53,18 +231,51 @@ var sizeToVariants = {
|
|
|
53
231
|
var sizeLabelOverride = {
|
|
54
232
|
xl: { fontSize: 20, lineHeight: "26px" }
|
|
55
233
|
};
|
|
234
|
+
var SUBMENU_GAP = 8;
|
|
235
|
+
var SUBMENU_VIEWPORT_PADDING = 8;
|
|
236
|
+
var clipsOverflow = (style) => {
|
|
237
|
+
return /(auto|scroll|hidden|clip)/.test(
|
|
238
|
+
`${style.overflow}${style.overflowX}${style.overflowY}`
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
var getSubmenuBoundary = (node) => {
|
|
242
|
+
const viewport = {
|
|
243
|
+
top: SUBMENU_VIEWPORT_PADDING,
|
|
244
|
+
right: window.innerWidth - SUBMENU_VIEWPORT_PADDING,
|
|
245
|
+
bottom: window.innerHeight - SUBMENU_VIEWPORT_PADDING,
|
|
246
|
+
left: SUBMENU_VIEWPORT_PADDING
|
|
247
|
+
};
|
|
248
|
+
let current = node.parentElement;
|
|
249
|
+
while (current && current !== document.body) {
|
|
250
|
+
const style = window.getComputedStyle(current);
|
|
251
|
+
if (clipsOverflow(style) && current.getAttribute("role") !== "menu") {
|
|
252
|
+
const rect = current.getBoundingClientRect();
|
|
253
|
+
return {
|
|
254
|
+
top: Math.max(viewport.top, rect.top + SUBMENU_VIEWPORT_PADDING),
|
|
255
|
+
right: Math.min(viewport.right, rect.right - SUBMENU_VIEWPORT_PADDING),
|
|
256
|
+
bottom: Math.min(
|
|
257
|
+
viewport.bottom,
|
|
258
|
+
rect.bottom - SUBMENU_VIEWPORT_PADDING
|
|
259
|
+
),
|
|
260
|
+
left: Math.max(viewport.left, rect.left + SUBMENU_VIEWPORT_PADDING)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
current = current.parentElement;
|
|
264
|
+
}
|
|
265
|
+
return viewport;
|
|
266
|
+
};
|
|
56
267
|
var ContextMenuItem = (props) => {
|
|
57
|
-
if (props.type === "option") return /* @__PURE__ */
|
|
58
|
-
if (props.type === "heading") return /* @__PURE__ */
|
|
59
|
-
if (props.type === "divider") return /* @__PURE__ */
|
|
60
|
-
if (props.type === "search") return /* @__PURE__ */
|
|
268
|
+
if (props.type === "option") return /* @__PURE__ */ jsx2(OptionCell, { ...props });
|
|
269
|
+
if (props.type === "heading") return /* @__PURE__ */ jsx2(HeadingCell, { ...props });
|
|
270
|
+
if (props.type === "divider") return /* @__PURE__ */ jsx2(DividerCell, { ...props });
|
|
271
|
+
if (props.type === "search") return /* @__PURE__ */ jsx2(SearchCell, { ...props });
|
|
61
272
|
return null;
|
|
62
273
|
};
|
|
63
274
|
ContextMenuItem.displayName = "ContextMenuItem";
|
|
64
275
|
var SubmenuChevron = ({
|
|
65
276
|
color,
|
|
66
277
|
size
|
|
67
|
-
}) => /* @__PURE__ */
|
|
278
|
+
}) => /* @__PURE__ */ jsx2(
|
|
68
279
|
"span",
|
|
69
280
|
{
|
|
70
281
|
"data-testid": "ctxmenu-submenu-chevron",
|
|
@@ -76,7 +287,7 @@ var SubmenuChevron = ({
|
|
|
76
287
|
width: size,
|
|
77
288
|
height: size
|
|
78
289
|
},
|
|
79
|
-
children: /* @__PURE__ */
|
|
290
|
+
children: /* @__PURE__ */ jsx2(
|
|
80
291
|
"svg",
|
|
81
292
|
{
|
|
82
293
|
width: size,
|
|
@@ -84,7 +295,7 @@ var SubmenuChevron = ({
|
|
|
84
295
|
viewBox: "0 0 24 24",
|
|
85
296
|
fill: "none",
|
|
86
297
|
xmlns: "http://www.w3.org/2000/svg",
|
|
87
|
-
children: /* @__PURE__ */
|
|
298
|
+
children: /* @__PURE__ */ jsx2(
|
|
88
299
|
"path",
|
|
89
300
|
{
|
|
90
301
|
d: "M17.0605 11.6464C17.2558 11.8417 17.2558 12.1583 17.0605 12.3536L9.70703 19.707L8.29297 18.293L14.5859 12L8.29297 5.70703L9.70703 4.29297L17.0605 11.6464Z",
|
|
@@ -106,6 +317,7 @@ var OptionCell = ({
|
|
|
106
317
|
leadingIcon,
|
|
107
318
|
status,
|
|
108
319
|
iconWrapper,
|
|
320
|
+
slot,
|
|
109
321
|
slotContent,
|
|
110
322
|
value,
|
|
111
323
|
hint,
|
|
@@ -114,12 +326,17 @@ var OptionCell = ({
|
|
|
114
326
|
hasSubmenu,
|
|
115
327
|
submenu,
|
|
116
328
|
onSelect,
|
|
329
|
+
onCheckedChange,
|
|
117
330
|
testID,
|
|
118
331
|
themeMode,
|
|
119
332
|
themeProductContext,
|
|
120
333
|
"data-testid": testId
|
|
121
334
|
}) => {
|
|
122
|
-
const { theme } = useResolvedTheme({
|
|
335
|
+
const { theme: rawTheme } = useResolvedTheme({
|
|
336
|
+
themeMode,
|
|
337
|
+
themeProductContext
|
|
338
|
+
});
|
|
339
|
+
const theme = rawTheme;
|
|
123
340
|
const ctx = useContextMenu();
|
|
124
341
|
const size = propSize ?? ctx?.size ?? "md";
|
|
125
342
|
const sizing = theme.sizing.contextMenu(size);
|
|
@@ -176,7 +393,32 @@ var OptionCell = ({
|
|
|
176
393
|
const node = optionRef.current;
|
|
177
394
|
if (!node) return;
|
|
178
395
|
const rect = node.getBoundingClientRect();
|
|
179
|
-
|
|
396
|
+
const submenuRect = submenuWrapperRef.current?.getBoundingClientRect();
|
|
397
|
+
const submenuWidth = submenuRect?.width ?? 0;
|
|
398
|
+
const submenuHeight = submenuRect?.height ?? 0;
|
|
399
|
+
const boundary = getSubmenuBoundary(node);
|
|
400
|
+
const rightSideLeft = rect.right + SUBMENU_GAP;
|
|
401
|
+
const leftSideLeft = rect.left - SUBMENU_GAP - submenuWidth;
|
|
402
|
+
const opensLeft = submenuWidth > 0 && rightSideLeft + submenuWidth > boundary.right;
|
|
403
|
+
let left = opensLeft ? leftSideLeft : rightSideLeft;
|
|
404
|
+
if (submenuWidth > 0) {
|
|
405
|
+
if (opensLeft) {
|
|
406
|
+
left = Math.max(SUBMENU_VIEWPORT_PADDING, left);
|
|
407
|
+
} else {
|
|
408
|
+
left = Math.min(
|
|
409
|
+
Math.max(boundary.left, left),
|
|
410
|
+
Math.max(boundary.left, boundary.right - submenuWidth)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
let top = rect.top;
|
|
415
|
+
if (submenuHeight > 0 && top + submenuHeight > boundary.bottom) {
|
|
416
|
+
top = boundary.bottom - submenuHeight;
|
|
417
|
+
}
|
|
418
|
+
top = Math.max(boundary.top, top);
|
|
419
|
+
setSubmenuPos(
|
|
420
|
+
(prev) => prev?.top === top && prev.left === left ? prev : { top, left }
|
|
421
|
+
);
|
|
180
422
|
};
|
|
181
423
|
update();
|
|
182
424
|
window.addEventListener("scroll", update, true);
|
|
@@ -185,7 +427,7 @@ var OptionCell = ({
|
|
|
185
427
|
window.removeEventListener("scroll", update, true);
|
|
186
428
|
window.removeEventListener("resize", update);
|
|
187
429
|
};
|
|
188
|
-
}
|
|
430
|
+
});
|
|
189
431
|
const onSelectRef = React.useRef(onSelect);
|
|
190
432
|
onSelectRef.current = onSelect;
|
|
191
433
|
useEffect(() => {
|
|
@@ -219,7 +461,7 @@ var OptionCell = ({
|
|
|
219
461
|
};
|
|
220
462
|
const labelColor = disabled ? theme.colors.control.input.textDisable : destructive ? theme.colors.content.alert.primary : theme.colors.content.primary;
|
|
221
463
|
const bg = inHoverState ? theme.colors.control.input.bgHover : "transparent";
|
|
222
|
-
const role = !hasSubmenu && checked !== void 0 ? "menuitemcheckbox" : "menuitem";
|
|
464
|
+
const role = !hasSubmenu && checked !== void 0 ? leadingControl === "radio" ? "menuitemradio" : "menuitemcheckbox" : "menuitem";
|
|
223
465
|
const ariaChecked = !hasSubmenu && checked !== void 0 ? checked ? "true" : "false" : void 0;
|
|
224
466
|
const handleClick = () => {
|
|
225
467
|
if (disabled) return;
|
|
@@ -227,6 +469,9 @@ var OptionCell = ({
|
|
|
227
469
|
setSubmenuOpen(true);
|
|
228
470
|
return;
|
|
229
471
|
}
|
|
472
|
+
if (checked !== void 0) {
|
|
473
|
+
onCheckedChange?.(!checked);
|
|
474
|
+
}
|
|
230
475
|
onSelect?.();
|
|
231
476
|
};
|
|
232
477
|
const closeSubmenuAndFocus = () => {
|
|
@@ -256,6 +501,9 @@ var OptionCell = ({
|
|
|
256
501
|
}
|
|
257
502
|
if (e.key === "Enter" || e.key === " ") {
|
|
258
503
|
e.preventDefault();
|
|
504
|
+
if (checked !== void 0) {
|
|
505
|
+
onCheckedChange?.(!checked);
|
|
506
|
+
}
|
|
259
507
|
onSelect?.();
|
|
260
508
|
}
|
|
261
509
|
};
|
|
@@ -299,13 +547,13 @@ var OptionCell = ({
|
|
|
299
547
|
outline: "none"
|
|
300
548
|
},
|
|
301
549
|
children: [
|
|
302
|
-
leadingControl === "checkbox" && /* @__PURE__ */
|
|
550
|
+
leadingControl === "checkbox" && /* @__PURE__ */ jsx2(
|
|
303
551
|
"span",
|
|
304
552
|
{
|
|
305
553
|
"data-testid": "ctxmenu-leading-checkbox",
|
|
306
554
|
"aria-hidden": "true",
|
|
307
555
|
style: { pointerEvents: "none", display: "inline-flex" },
|
|
308
|
-
children: /* @__PURE__ */
|
|
556
|
+
children: /* @__PURE__ */ jsx2(
|
|
309
557
|
Checkbox,
|
|
310
558
|
{
|
|
311
559
|
size,
|
|
@@ -317,13 +565,13 @@ var OptionCell = ({
|
|
|
317
565
|
)
|
|
318
566
|
}
|
|
319
567
|
),
|
|
320
|
-
leadingControl === "radio" && /* @__PURE__ */
|
|
568
|
+
leadingControl === "radio" && /* @__PURE__ */ jsx2(
|
|
321
569
|
"span",
|
|
322
570
|
{
|
|
323
571
|
"data-testid": "ctxmenu-leading-radio",
|
|
324
572
|
"aria-hidden": "true",
|
|
325
573
|
style: { pointerEvents: "none", display: "inline-flex" },
|
|
326
|
-
children: /* @__PURE__ */
|
|
574
|
+
children: /* @__PURE__ */ jsx2(
|
|
327
575
|
Radio,
|
|
328
576
|
{
|
|
329
577
|
size,
|
|
@@ -338,6 +586,7 @@ var OptionCell = ({
|
|
|
338
586
|
leadingIcon,
|
|
339
587
|
status,
|
|
340
588
|
iconWrapper,
|
|
589
|
+
slot,
|
|
341
590
|
slotContent,
|
|
342
591
|
/* @__PURE__ */ jsxs(
|
|
343
592
|
"span",
|
|
@@ -350,7 +599,7 @@ var OptionCell = ({
|
|
|
350
599
|
minWidth: 0
|
|
351
600
|
},
|
|
352
601
|
children: [
|
|
353
|
-
/* @__PURE__ */
|
|
602
|
+
/* @__PURE__ */ jsx2(
|
|
354
603
|
Typography,
|
|
355
604
|
{
|
|
356
605
|
variant: variants.label,
|
|
@@ -363,7 +612,7 @@ var OptionCell = ({
|
|
|
363
612
|
children: label
|
|
364
613
|
}
|
|
365
614
|
),
|
|
366
|
-
description !== void 0 && /* @__PURE__ */
|
|
615
|
+
description !== void 0 && /* @__PURE__ */ jsx2(
|
|
367
616
|
Typography,
|
|
368
617
|
{
|
|
369
618
|
variant: variants.description,
|
|
@@ -383,7 +632,7 @@ var OptionCell = ({
|
|
|
383
632
|
alignItems: "flex-end"
|
|
384
633
|
},
|
|
385
634
|
children: [
|
|
386
|
-
value !== void 0 && /* @__PURE__ */
|
|
635
|
+
value !== void 0 && /* @__PURE__ */ jsx2(
|
|
387
636
|
Typography,
|
|
388
637
|
{
|
|
389
638
|
variant: variants.label,
|
|
@@ -392,7 +641,7 @@ var OptionCell = ({
|
|
|
392
641
|
children: value
|
|
393
642
|
}
|
|
394
643
|
),
|
|
395
|
-
hint !== void 0 && /* @__PURE__ */
|
|
644
|
+
hint !== void 0 && /* @__PURE__ */ jsx2(
|
|
396
645
|
Typography,
|
|
397
646
|
{
|
|
398
647
|
variant: variants.description,
|
|
@@ -403,7 +652,7 @@ var OptionCell = ({
|
|
|
403
652
|
]
|
|
404
653
|
}
|
|
405
654
|
),
|
|
406
|
-
keyboardShortcut && /* @__PURE__ */
|
|
655
|
+
keyboardShortcut && /* @__PURE__ */ jsx2(
|
|
407
656
|
Typography,
|
|
408
657
|
{
|
|
409
658
|
as: "kbd",
|
|
@@ -412,7 +661,7 @@ var OptionCell = ({
|
|
|
412
661
|
children: keyboardShortcut
|
|
413
662
|
}
|
|
414
663
|
),
|
|
415
|
-
hasSubmenu && /* @__PURE__ */
|
|
664
|
+
hasSubmenu && /* @__PURE__ */ jsx2(
|
|
416
665
|
SubmenuChevron,
|
|
417
666
|
{
|
|
418
667
|
color: theme.colors.content.tertiary,
|
|
@@ -421,7 +670,7 @@ var OptionCell = ({
|
|
|
421
670
|
),
|
|
422
671
|
trailingIcon,
|
|
423
672
|
hasSubmenu && submenuOpen && submenu && submenuPos && typeof document !== "undefined" && createPortal(
|
|
424
|
-
/* @__PURE__ */
|
|
673
|
+
/* @__PURE__ */ jsx2(
|
|
425
674
|
"div",
|
|
426
675
|
{
|
|
427
676
|
ref: submenuWrapperRef,
|
|
@@ -452,7 +701,11 @@ var HeadingCell = ({
|
|
|
452
701
|
themeProductContext,
|
|
453
702
|
"data-testid": testId
|
|
454
703
|
}) => {
|
|
455
|
-
const { theme } = useResolvedTheme({
|
|
704
|
+
const { theme: rawTheme } = useResolvedTheme({
|
|
705
|
+
themeMode,
|
|
706
|
+
themeProductContext
|
|
707
|
+
});
|
|
708
|
+
const theme = rawTheme;
|
|
456
709
|
const ctx = useContextMenu();
|
|
457
710
|
const size = propSize ?? ctx?.size ?? "md";
|
|
458
711
|
const sizing = theme.sizing.contextMenu(size);
|
|
@@ -480,7 +733,7 @@ var HeadingCell = ({
|
|
|
480
733
|
paddingBottom: sizing.itemPaddingVertical
|
|
481
734
|
},
|
|
482
735
|
children: [
|
|
483
|
-
/* @__PURE__ */
|
|
736
|
+
/* @__PURE__ */ jsx2(
|
|
484
737
|
Typography,
|
|
485
738
|
{
|
|
486
739
|
variant: variants.headingAccent,
|
|
@@ -489,7 +742,7 @@ var HeadingCell = ({
|
|
|
489
742
|
children: label
|
|
490
743
|
}
|
|
491
744
|
),
|
|
492
|
-
description !== void 0 && /* @__PURE__ */
|
|
745
|
+
description !== void 0 && /* @__PURE__ */ jsx2(
|
|
493
746
|
Typography,
|
|
494
747
|
{
|
|
495
748
|
variant: variants.description,
|
|
@@ -504,6 +757,7 @@ var HeadingCell = ({
|
|
|
504
757
|
var SearchCell = ({
|
|
505
758
|
size: propSize,
|
|
506
759
|
value,
|
|
760
|
+
onChange,
|
|
507
761
|
onValueChange,
|
|
508
762
|
placeholder = "Search",
|
|
509
763
|
autoFocus,
|
|
@@ -513,7 +767,11 @@ var SearchCell = ({
|
|
|
513
767
|
themeMode,
|
|
514
768
|
themeProductContext
|
|
515
769
|
}) => {
|
|
516
|
-
const { theme } = useResolvedTheme({
|
|
770
|
+
const { theme: rawTheme } = useResolvedTheme({
|
|
771
|
+
themeMode,
|
|
772
|
+
themeProductContext
|
|
773
|
+
});
|
|
774
|
+
const theme = rawTheme;
|
|
517
775
|
const ctx = useContextMenu();
|
|
518
776
|
const size = propSize ?? ctx?.size ?? "md";
|
|
519
777
|
const sizing = theme.sizing.contextMenu(size);
|
|
@@ -525,7 +783,7 @@ var SearchCell = ({
|
|
|
525
783
|
registerCell(id, { type: "search" });
|
|
526
784
|
return () => unregisterCell(id);
|
|
527
785
|
}, [registerCell, unregisterCell, id]);
|
|
528
|
-
return /* @__PURE__ */
|
|
786
|
+
return /* @__PURE__ */ jsx2(
|
|
529
787
|
"div",
|
|
530
788
|
{
|
|
531
789
|
style: {
|
|
@@ -546,7 +804,7 @@ var SearchCell = ({
|
|
|
546
804
|
borderBottom: `1px solid ${theme.colors.border.secondary}`
|
|
547
805
|
},
|
|
548
806
|
children: [
|
|
549
|
-
/* @__PURE__ */
|
|
807
|
+
/* @__PURE__ */ jsx2(
|
|
550
808
|
Search,
|
|
551
809
|
{
|
|
552
810
|
variant: "line",
|
|
@@ -555,16 +813,19 @@ var SearchCell = ({
|
|
|
555
813
|
"aria-hidden": true
|
|
556
814
|
}
|
|
557
815
|
),
|
|
558
|
-
/* @__PURE__ */
|
|
816
|
+
/* @__PURE__ */ jsx2(
|
|
559
817
|
"input",
|
|
560
818
|
{
|
|
561
819
|
type: "search",
|
|
562
820
|
role: "searchbox",
|
|
563
821
|
"aria-label": ariaLabel,
|
|
564
822
|
placeholder,
|
|
565
|
-
value,
|
|
823
|
+
value: value ?? "",
|
|
566
824
|
autoFocus,
|
|
567
|
-
onChange: (e) =>
|
|
825
|
+
onChange: (e) => {
|
|
826
|
+
onChange?.(e);
|
|
827
|
+
onValueChange?.(e.target.value);
|
|
828
|
+
},
|
|
568
829
|
"data-testid": testId || testID,
|
|
569
830
|
style: {
|
|
570
831
|
flex: 1,
|
|
@@ -587,7 +848,11 @@ var SearchCell = ({
|
|
|
587
848
|
);
|
|
588
849
|
};
|
|
589
850
|
var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) => {
|
|
590
|
-
const { theme } = useResolvedTheme({
|
|
851
|
+
const { theme: rawTheme } = useResolvedTheme({
|
|
852
|
+
themeMode,
|
|
853
|
+
themeProductContext
|
|
854
|
+
});
|
|
855
|
+
const theme = rawTheme;
|
|
591
856
|
const ctx = useContextMenu();
|
|
592
857
|
const id = useId();
|
|
593
858
|
const registerCell = ctx?.registerCell;
|
|
@@ -597,7 +862,7 @@ var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) =>
|
|
|
597
862
|
registerCell(id, { type: "divider" });
|
|
598
863
|
return () => unregisterCell(id);
|
|
599
864
|
}, [registerCell, unregisterCell, id]);
|
|
600
|
-
return /* @__PURE__ */
|
|
865
|
+
return /* @__PURE__ */ jsx2(
|
|
601
866
|
"div",
|
|
602
867
|
{
|
|
603
868
|
role: "separator",
|
|
@@ -611,8 +876,156 @@ var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) =>
|
|
|
611
876
|
);
|
|
612
877
|
};
|
|
613
878
|
|
|
879
|
+
// src/ContextMenuSubmenu.tsx
|
|
880
|
+
import {
|
|
881
|
+
useState as useState2,
|
|
882
|
+
useRef,
|
|
883
|
+
useEffect as useEffect2,
|
|
884
|
+
useLayoutEffect as useLayoutEffect2,
|
|
885
|
+
useCallback
|
|
886
|
+
} from "react";
|
|
887
|
+
import { useDesignSystem } from "@xsolla/xui-core";
|
|
888
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
889
|
+
var SUBMENU_GAP2 = 4;
|
|
890
|
+
var OPEN_DELAY_MS = 200;
|
|
891
|
+
var CLOSE_GRACE_MS = 100;
|
|
892
|
+
var ContextMenuSubmenu = ({
|
|
893
|
+
label,
|
|
894
|
+
icon,
|
|
895
|
+
disabled,
|
|
896
|
+
children,
|
|
897
|
+
size: propSize,
|
|
898
|
+
"data-testid": testId = "context-menu-submenu"
|
|
899
|
+
}) => {
|
|
900
|
+
const { theme } = useDesignSystem();
|
|
901
|
+
const xuiTheme = theme;
|
|
902
|
+
const context = useContextMenu();
|
|
903
|
+
const size = propSize || context?.size || "md";
|
|
904
|
+
const sizeStyles = xuiTheme.sizing.contextMenu(size);
|
|
905
|
+
const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
|
|
906
|
+
const [isOpen, setIsOpen] = useState2(false);
|
|
907
|
+
const [visible, setVisible] = useState2(false);
|
|
908
|
+
const [openLeft, setOpenLeft] = useState2(false);
|
|
909
|
+
const [topOffset, setTopOffset] = useState2(0);
|
|
910
|
+
const triggerRef = useRef(null);
|
|
911
|
+
const submenuRef = useRef(null);
|
|
912
|
+
const openTimerRef = useRef(null);
|
|
913
|
+
const closeTimerRef = useRef(null);
|
|
914
|
+
const clearTimers = useCallback(() => {
|
|
915
|
+
if (openTimerRef.current) clearTimeout(openTimerRef.current);
|
|
916
|
+
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
917
|
+
}, []);
|
|
918
|
+
const calculatePlacement = useCallback(() => {
|
|
919
|
+
if (!triggerRef.current) return;
|
|
920
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
921
|
+
const estimatedWidth = sizeStyles.minWidth + 32;
|
|
922
|
+
const wouldOverflowRight = triggerRect.right + estimatedWidth + SUBMENU_GAP2 > window.innerWidth - 8;
|
|
923
|
+
setOpenLeft(wouldOverflowRight);
|
|
924
|
+
setTopOffset(0);
|
|
925
|
+
}, [sizeStyles.minWidth]);
|
|
926
|
+
useLayoutEffect2(() => {
|
|
927
|
+
if (!isOpen || !submenuRef.current || !triggerRef.current) return;
|
|
928
|
+
const submenuRect = submenuRef.current.getBoundingClientRect();
|
|
929
|
+
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
930
|
+
const wouldOverflowRight = triggerRect.right + submenuRect.width + SUBMENU_GAP2 > window.innerWidth - 8;
|
|
931
|
+
setOpenLeft(wouldOverflowRight);
|
|
932
|
+
const overflowBottom = triggerRect.top + submenuRect.height - (window.innerHeight - 8);
|
|
933
|
+
if (overflowBottom > 0) {
|
|
934
|
+
setTopOffset(-Math.min(overflowBottom, triggerRect.top - 8));
|
|
935
|
+
} else {
|
|
936
|
+
setTopOffset(0);
|
|
937
|
+
}
|
|
938
|
+
}, [isOpen]);
|
|
939
|
+
useEffect2(() => {
|
|
940
|
+
if (!isOpen) {
|
|
941
|
+
setVisible(false);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const raf = requestAnimationFrame(() => setVisible(true));
|
|
945
|
+
return () => cancelAnimationFrame(raf);
|
|
946
|
+
}, [isOpen]);
|
|
947
|
+
useEffect2(() => () => clearTimers(), [clearTimers]);
|
|
948
|
+
const handleTriggerEnter = () => {
|
|
949
|
+
if (disabled) return;
|
|
950
|
+
clearTimers();
|
|
951
|
+
openTimerRef.current = setTimeout(() => {
|
|
952
|
+
calculatePlacement();
|
|
953
|
+
setIsOpen(true);
|
|
954
|
+
}, OPEN_DELAY_MS);
|
|
955
|
+
};
|
|
956
|
+
const handleTriggerLeave = () => {
|
|
957
|
+
clearTimers();
|
|
958
|
+
closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
|
|
959
|
+
};
|
|
960
|
+
const handleSubmenuEnter = () => clearTimers();
|
|
961
|
+
const handleSubmenuLeave = () => {
|
|
962
|
+
clearTimers();
|
|
963
|
+
closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
|
|
964
|
+
};
|
|
965
|
+
const submenuPositionStyle = openLeft ? { right: `calc(100% + ${SUBMENU_GAP2}px)` } : { left: `calc(100% + ${SUBMENU_GAP2}px)` };
|
|
966
|
+
return /* @__PURE__ */ jsxs2(
|
|
967
|
+
"div",
|
|
968
|
+
{
|
|
969
|
+
ref: triggerRef,
|
|
970
|
+
style: { position: "relative" },
|
|
971
|
+
onMouseEnter: handleTriggerEnter,
|
|
972
|
+
onMouseLeave: handleTriggerLeave,
|
|
973
|
+
"data-testid": testId,
|
|
974
|
+
children: [
|
|
975
|
+
/* @__PURE__ */ jsx3(
|
|
976
|
+
ContextMenuItem,
|
|
977
|
+
{
|
|
978
|
+
type: "option",
|
|
979
|
+
label,
|
|
980
|
+
leadingIcon: icon,
|
|
981
|
+
disabled,
|
|
982
|
+
hasSubmenu: true,
|
|
983
|
+
size,
|
|
984
|
+
"data-testid": `${testId}-trigger`
|
|
985
|
+
}
|
|
986
|
+
),
|
|
987
|
+
isOpen && /* @__PURE__ */ jsx3(
|
|
988
|
+
"div",
|
|
989
|
+
{
|
|
990
|
+
ref: submenuRef,
|
|
991
|
+
onMouseEnter: handleSubmenuEnter,
|
|
992
|
+
onMouseLeave: handleSubmenuLeave,
|
|
993
|
+
style: {
|
|
994
|
+
position: "absolute",
|
|
995
|
+
top: topOffset,
|
|
996
|
+
...submenuPositionStyle,
|
|
997
|
+
zIndex: 1001,
|
|
998
|
+
opacity: visible ? 1 : 0,
|
|
999
|
+
transform: visible ? "translateX(0)" : openLeft ? "translateX(4px)" : "translateX(-4px)",
|
|
1000
|
+
transition: "opacity 100ms ease, transform 100ms ease"
|
|
1001
|
+
},
|
|
1002
|
+
"data-testid": `${testId}-content`,
|
|
1003
|
+
children: /* @__PURE__ */ jsx3(
|
|
1004
|
+
Box,
|
|
1005
|
+
{
|
|
1006
|
+
role: "menu",
|
|
1007
|
+
backgroundColor: xuiTheme.colors.background.secondary,
|
|
1008
|
+
borderColor: xuiTheme.colors.border.secondary,
|
|
1009
|
+
borderWidth: 1,
|
|
1010
|
+
borderRadius,
|
|
1011
|
+
paddingVertical: sizeStyles.paddingVertical,
|
|
1012
|
+
style: {
|
|
1013
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
1014
|
+
minWidth: sizeStyles.minWidth
|
|
1015
|
+
},
|
|
1016
|
+
children
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
}
|
|
1020
|
+
)
|
|
1021
|
+
]
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
};
|
|
1025
|
+
ContextMenuSubmenu.displayName = "ContextMenuSubmenu";
|
|
1026
|
+
|
|
614
1027
|
// src/hooks/useContextMenuPosition.ts
|
|
615
|
-
import { useEffect as
|
|
1028
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
616
1029
|
var splitPlacement = (placement) => {
|
|
617
1030
|
const [vertical, horizontal] = placement.split("-");
|
|
618
1031
|
return { vertical, horizontal };
|
|
@@ -625,8 +1038,8 @@ var useContextMenuPosition = ({
|
|
|
625
1038
|
placement = "bottom-start",
|
|
626
1039
|
offset = 4
|
|
627
1040
|
}) => {
|
|
628
|
-
const [resolved, setResolved] =
|
|
629
|
-
|
|
1041
|
+
const [resolved, setResolved] = useState3();
|
|
1042
|
+
useEffect3(() => {
|
|
630
1043
|
if (!isOpen) {
|
|
631
1044
|
setResolved(void 0);
|
|
632
1045
|
return;
|
|
@@ -673,30 +1086,19 @@ var useContextMenuPosition = ({
|
|
|
673
1086
|
(prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
|
|
674
1087
|
);
|
|
675
1088
|
};
|
|
676
|
-
|
|
1089
|
+
const rafId = window.requestAnimationFrame(compute);
|
|
677
1090
|
const onResize = () => compute();
|
|
678
|
-
let scrollRafPending = false;
|
|
679
|
-
const onScroll = () => {
|
|
680
|
-
if (scrollRafPending) return;
|
|
681
|
-
scrollRafPending = true;
|
|
682
|
-
rafId = window.requestAnimationFrame(() => {
|
|
683
|
-
scrollRafPending = false;
|
|
684
|
-
compute();
|
|
685
|
-
});
|
|
686
|
-
};
|
|
687
1091
|
window.addEventListener("resize", onResize);
|
|
688
|
-
window.addEventListener("scroll", onScroll, true);
|
|
689
1092
|
return () => {
|
|
690
1093
|
window.cancelAnimationFrame(rafId);
|
|
691
1094
|
window.removeEventListener("resize", onResize);
|
|
692
|
-
window.removeEventListener("scroll", onScroll, true);
|
|
693
1095
|
};
|
|
694
1096
|
}, [isOpen, placement, offset, triggerRef, panelRef]);
|
|
695
1097
|
return resolved;
|
|
696
1098
|
};
|
|
697
1099
|
|
|
698
1100
|
// src/hooks/useKeyboardNavigation.ts
|
|
699
|
-
import { useCallback } from "react";
|
|
1101
|
+
import { useCallback as useCallback2 } from "react";
|
|
700
1102
|
var isNavigableOption = (meta) => meta.type === "option" && !meta.disabled;
|
|
701
1103
|
var isTextInputTarget = (target) => {
|
|
702
1104
|
if (!(target instanceof HTMLElement)) return false;
|
|
@@ -711,19 +1113,19 @@ var useKeyboardNavigation = ({
|
|
|
711
1113
|
onClose,
|
|
712
1114
|
triggerRef
|
|
713
1115
|
}) => {
|
|
714
|
-
const findFirstOption =
|
|
1116
|
+
const findFirstOption = useCallback2(() => {
|
|
715
1117
|
for (let i = 0; i < cells.length; i += 1) {
|
|
716
1118
|
if (isNavigableOption(cells[i].meta)) return i;
|
|
717
1119
|
}
|
|
718
1120
|
return -1;
|
|
719
1121
|
}, [cells]);
|
|
720
|
-
const findLastOption =
|
|
1122
|
+
const findLastOption = useCallback2(() => {
|
|
721
1123
|
for (let i = cells.length - 1; i >= 0; i -= 1) {
|
|
722
1124
|
if (isNavigableOption(cells[i].meta)) return i;
|
|
723
1125
|
}
|
|
724
1126
|
return -1;
|
|
725
1127
|
}, [cells]);
|
|
726
|
-
const findNextOption =
|
|
1128
|
+
const findNextOption = useCallback2(
|
|
727
1129
|
(from) => {
|
|
728
1130
|
const len = cells.length;
|
|
729
1131
|
if (len === 0) return -1;
|
|
@@ -735,7 +1137,7 @@ var useKeyboardNavigation = ({
|
|
|
735
1137
|
},
|
|
736
1138
|
[cells]
|
|
737
1139
|
);
|
|
738
|
-
const findPrevOption =
|
|
1140
|
+
const findPrevOption = useCallback2(
|
|
739
1141
|
(from) => {
|
|
740
1142
|
const len = cells.length;
|
|
741
1143
|
if (len === 0) return -1;
|
|
@@ -747,7 +1149,7 @@ var useKeyboardNavigation = ({
|
|
|
747
1149
|
},
|
|
748
1150
|
[cells]
|
|
749
1151
|
);
|
|
750
|
-
const handleKeyDown =
|
|
1152
|
+
const handleKeyDown = useCallback2(
|
|
751
1153
|
(event) => {
|
|
752
1154
|
if (!isOpen) return;
|
|
753
1155
|
switch (event.key) {
|
|
@@ -817,458 +1219,382 @@ var useKeyboardNavigation = ({
|
|
|
817
1219
|
};
|
|
818
1220
|
|
|
819
1221
|
// src/ContextMenu.tsx
|
|
820
|
-
import {
|
|
1222
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1223
|
+
var DEFAULT_EMPTY_MESSAGE = "No results";
|
|
821
1224
|
var SEARCH_DEBOUNCE_MS = 200;
|
|
822
|
-
var
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
"
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1225
|
+
var textFromNode = (node) => {
|
|
1226
|
+
if (node === null || node === void 0 || typeof node === "boolean")
|
|
1227
|
+
return "";
|
|
1228
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
1229
|
+
if (Array.isArray(node)) return node.map(textFromNode).join(" ");
|
|
1230
|
+
if (isValidElement(node)) return textFromNode(node.props.children);
|
|
1231
|
+
return "";
|
|
1232
|
+
};
|
|
1233
|
+
var filterItems = (items, query) => {
|
|
1234
|
+
const normalized = query.trim().toLowerCase();
|
|
1235
|
+
if (!normalized) return [...items];
|
|
1236
|
+
const result = [];
|
|
1237
|
+
let pendingStructural = [];
|
|
1238
|
+
for (const item of items) {
|
|
1239
|
+
if (item.type === "option") {
|
|
1240
|
+
const label = textFromNode(item.label).toLowerCase();
|
|
1241
|
+
if (label.includes(normalized)) {
|
|
1242
|
+
result.push(...pendingStructural, item);
|
|
1243
|
+
pendingStructural = [];
|
|
1244
|
+
}
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (item.type === "heading") {
|
|
1248
|
+
pendingStructural = [item];
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (item.type === "divider") {
|
|
1252
|
+
if (result.length > 0) pendingStructural.push(item);
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
835
1255
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1256
|
+
return result;
|
|
1257
|
+
};
|
|
1258
|
+
var isOption = (item) => item.type === "option";
|
|
1259
|
+
var ContextMenuRoot = forwardRef(
|
|
1260
|
+
({
|
|
841
1261
|
children,
|
|
1262
|
+
type = "list",
|
|
1263
|
+
items = [],
|
|
842
1264
|
size = "md",
|
|
843
1265
|
searchable,
|
|
844
1266
|
loading,
|
|
845
|
-
|
|
1267
|
+
isLoading,
|
|
1268
|
+
emptyMessage = DEFAULT_EMPTY_MESSAGE,
|
|
846
1269
|
empty,
|
|
847
|
-
|
|
848
|
-
isOpen,
|
|
1270
|
+
isOpen: propIsOpen,
|
|
849
1271
|
onOpenChange,
|
|
850
|
-
|
|
851
|
-
width,
|
|
852
|
-
maxHeight,
|
|
1272
|
+
trigger,
|
|
853
1273
|
placement = "bottom-start",
|
|
1274
|
+
position,
|
|
1275
|
+
width,
|
|
1276
|
+
maxHeight = 300,
|
|
854
1277
|
onSelect,
|
|
1278
|
+
closeOnSelect,
|
|
855
1279
|
"aria-label": ariaLabel,
|
|
856
|
-
"data-testid": testId,
|
|
857
1280
|
testID,
|
|
1281
|
+
"data-testid": dataTestId,
|
|
858
1282
|
themeMode,
|
|
859
|
-
themeProductContext
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
(
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const
|
|
896
|
-
|
|
1283
|
+
themeProductContext,
|
|
1284
|
+
style
|
|
1285
|
+
}, ref) => {
|
|
1286
|
+
const { theme } = useDesignSystem2();
|
|
1287
|
+
const xuiTheme = theme;
|
|
1288
|
+
const menuId = useId2();
|
|
1289
|
+
const [internalIsOpen, setInternalIsOpen] = useState4(false);
|
|
1290
|
+
const [activeIndex, setActiveIndex] = useState4(-1);
|
|
1291
|
+
const [cellsVersion, setCellsVersion] = useState4(0);
|
|
1292
|
+
const [query, setQuery] = useState4("");
|
|
1293
|
+
const [debouncedQuery, setDebouncedQuery] = useState4("");
|
|
1294
|
+
const containerRef = useRef2(null);
|
|
1295
|
+
const triggerRef = useRef2(null);
|
|
1296
|
+
const panelRef = useRef2(null);
|
|
1297
|
+
const cellsRef = useRef2([]);
|
|
1298
|
+
const isOpen = propIsOpen !== void 0 ? propIsOpen : internalIsOpen;
|
|
1299
|
+
const sizeStyles = xuiTheme.sizing.contextMenu(size);
|
|
1300
|
+
const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
|
|
1301
|
+
const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
|
|
1302
|
+
const positioned = useContextMenuPosition({
|
|
1303
|
+
triggerRef,
|
|
1304
|
+
panelRef,
|
|
1305
|
+
isOpen: isOpen && !!trigger && !position,
|
|
1306
|
+
placement
|
|
1307
|
+
});
|
|
1308
|
+
const setOpen = useCallback3(
|
|
1309
|
+
(nextOpen) => {
|
|
1310
|
+
if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
|
|
1311
|
+
onOpenChange?.(nextOpen);
|
|
1312
|
+
if (!nextOpen) setActiveIndex(-1);
|
|
1313
|
+
},
|
|
1314
|
+
[propIsOpen, onOpenChange]
|
|
1315
|
+
);
|
|
1316
|
+
const closeMenu = useCallback3(() => {
|
|
1317
|
+
setOpen(false);
|
|
1318
|
+
}, [setOpen]);
|
|
1319
|
+
const toggleMenu = useCallback3(() => {
|
|
1320
|
+
setOpen(!isOpen);
|
|
1321
|
+
}, [isOpen, setOpen]);
|
|
1322
|
+
const registerCell = useCallback3((id, meta) => {
|
|
1323
|
+
const existingIndex = cellsRef.current.findIndex(
|
|
1324
|
+
(cell) => cell.id === id
|
|
1325
|
+
);
|
|
1326
|
+
if (existingIndex >= 0) {
|
|
1327
|
+
cellsRef.current[existingIndex] = { id, meta };
|
|
1328
|
+
setCellsVersion((version) => version + 1);
|
|
1329
|
+
return existingIndex;
|
|
1330
|
+
}
|
|
897
1331
|
cellsRef.current.push({ id, meta });
|
|
898
|
-
setCellsVersion((
|
|
1332
|
+
setCellsVersion((version) => version + 1);
|
|
899
1333
|
return cellsRef.current.length - 1;
|
|
900
|
-
}
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
const getCellIndex = useCallback2(
|
|
916
|
-
(id) => cellsRef.current.findIndex((c) => c.id === id),
|
|
917
|
-
[]
|
|
918
|
-
);
|
|
919
|
-
const ctx = useMemo(
|
|
920
|
-
() => ({
|
|
921
|
-
size,
|
|
922
|
-
menuId,
|
|
923
|
-
closeMenu,
|
|
924
|
-
registerCell,
|
|
925
|
-
unregisterCell,
|
|
926
|
-
getCellIndex,
|
|
927
|
-
cellsVersion,
|
|
1334
|
+
}, []);
|
|
1335
|
+
const unregisterCell = useCallback3((id) => {
|
|
1336
|
+
const index = cellsRef.current.findIndex((cell) => cell.id === id);
|
|
1337
|
+
if (index >= 0) {
|
|
1338
|
+
cellsRef.current.splice(index, 1);
|
|
1339
|
+
setCellsVersion((version) => version + 1);
|
|
1340
|
+
}
|
|
1341
|
+
}, []);
|
|
1342
|
+
const getCellIndex = useCallback3((id) => {
|
|
1343
|
+
return cellsRef.current.findIndex((cell) => cell.id === id);
|
|
1344
|
+
}, []);
|
|
1345
|
+
const cells = useMemo(() => [...cellsRef.current], [cellsVersion]);
|
|
1346
|
+
const keyboard = useKeyboardNavigation({
|
|
1347
|
+
isOpen,
|
|
1348
|
+
cells,
|
|
928
1349
|
activeIndex,
|
|
929
1350
|
setActiveIndex,
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}),
|
|
933
|
-
[
|
|
934
|
-
size,
|
|
935
|
-
menuId,
|
|
936
|
-
closeMenu,
|
|
937
|
-
registerCell,
|
|
938
|
-
unregisterCell,
|
|
939
|
-
getCellIndex,
|
|
940
|
-
cellsVersion,
|
|
941
|
-
activeIndex,
|
|
942
|
-
query
|
|
943
|
-
]
|
|
944
|
-
);
|
|
945
|
-
const triggerNode = useMemo(() => {
|
|
946
|
-
if (!trigger) return null;
|
|
947
|
-
const inner = isValidElement(trigger) ? cloneElement(
|
|
948
|
-
trigger,
|
|
949
|
-
{
|
|
950
|
-
"aria-haspopup": "menu",
|
|
951
|
-
"aria-expanded": open ? "true" : "false"
|
|
952
|
-
}
|
|
953
|
-
) : trigger;
|
|
954
|
-
return /* @__PURE__ */ jsx2(
|
|
955
|
-
"span",
|
|
956
|
-
{
|
|
957
|
-
ref: (node) => {
|
|
958
|
-
if (!node) {
|
|
959
|
-
triggerRef.current = null;
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
const focusable = node.querySelector(
|
|
963
|
-
"button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
|
|
964
|
-
);
|
|
965
|
-
triggerRef.current = focusable ?? node;
|
|
966
|
-
},
|
|
967
|
-
onClick: () => setOpen(!open),
|
|
968
|
-
style: { display: "inline-flex" },
|
|
969
|
-
children: inner
|
|
970
|
-
}
|
|
971
|
-
);
|
|
972
|
-
}, [trigger, open, setOpen]);
|
|
973
|
-
const usePortal = !!trigger && typeof document !== "undefined";
|
|
974
|
-
const position = useContextMenuPosition({
|
|
975
|
-
triggerRef,
|
|
976
|
-
panelRef,
|
|
977
|
-
isOpen: open && usePortal,
|
|
978
|
-
placement
|
|
979
|
-
});
|
|
980
|
-
const cellsForNav = useMemo(
|
|
981
|
-
() => cellsRef.current.map((c) => ({ id: c.id, meta: c.meta })),
|
|
982
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
983
|
-
[cellsVersion]
|
|
984
|
-
);
|
|
985
|
-
const { handleKeyDown } = useKeyboardNavigation({
|
|
986
|
-
isOpen: open,
|
|
987
|
-
cells: cellsForNav,
|
|
988
|
-
activeIndex,
|
|
989
|
-
setActiveIndex,
|
|
990
|
-
onClose: closeMenu,
|
|
991
|
-
triggerRef
|
|
992
|
-
});
|
|
993
|
-
const sizingFn = theme.sizing.contextMenu;
|
|
994
|
-
const sizing = sizingFn ? sizingFn(size) : {};
|
|
995
|
-
const radiusObj = theme.radius;
|
|
996
|
-
const radiusVal = sizing.borderRadius ?? radiusObj?.contextMenu ?? 8;
|
|
997
|
-
const shadowObj = theme.shadow;
|
|
998
|
-
const shadowVal = shadowObj?.popover ?? "";
|
|
999
|
-
const panelPaddingVertical = sizing.paddingVertical ?? 8;
|
|
1000
|
-
const glassBackground = theme.colors.layer?.float ?? theme.colors.background.primary;
|
|
1001
|
-
const panelStyle = {
|
|
1002
|
-
background: glassBackground,
|
|
1003
|
-
backdropFilter: "blur(12px)",
|
|
1004
|
-
WebkitBackdropFilter: "blur(12px)",
|
|
1005
|
-
border: `1px solid ${theme.colors.border.secondary}`,
|
|
1006
|
-
borderRadius: radiusVal,
|
|
1007
|
-
boxShadow: shadowVal,
|
|
1008
|
-
width: width ?? sizing.panelWidth,
|
|
1009
|
-
maxHeight,
|
|
1010
|
-
overflow: "hidden",
|
|
1011
|
-
display: open ? "flex" : "none",
|
|
1012
|
-
flexDirection: "column",
|
|
1013
|
-
outline: "none",
|
|
1014
|
-
fontFamily: theme.fonts.body,
|
|
1015
|
-
paddingTop: panelPaddingVertical,
|
|
1016
|
-
paddingBottom: panelPaddingVertical
|
|
1017
|
-
};
|
|
1018
|
-
if (usePortal) {
|
|
1019
|
-
panelStyle.position = "fixed";
|
|
1020
|
-
panelStyle.top = position?.top ?? 0;
|
|
1021
|
-
panelStyle.left = position?.left ?? 0;
|
|
1022
|
-
}
|
|
1023
|
-
const filteredItems = useMemo(() => {
|
|
1024
|
-
if (!items) return void 0;
|
|
1025
|
-
if (!searchable || !debouncedQuery) return items.slice();
|
|
1026
|
-
const q = debouncedQuery.toLowerCase();
|
|
1027
|
-
const matchedFlags = items.map((item) => {
|
|
1028
|
-
if (item.type === "option") {
|
|
1029
|
-
return String(item.label ?? "").toLowerCase().includes(q);
|
|
1030
|
-
}
|
|
1031
|
-
return false;
|
|
1351
|
+
onClose: closeMenu,
|
|
1352
|
+
triggerRef
|
|
1032
1353
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (item.type === "heading") {
|
|
1040
|
-
pendingHeading = { item, idx: i };
|
|
1041
|
-
groupHasOption = false;
|
|
1042
|
-
} else if (item.type === "divider") {
|
|
1043
|
-
if (lastEmittedWasContent) {
|
|
1044
|
-
result.push(item);
|
|
1045
|
-
lastEmittedWasContent = false;
|
|
1046
|
-
}
|
|
1047
|
-
pendingHeading = null;
|
|
1048
|
-
groupHasOption = false;
|
|
1049
|
-
} else if (item.type === "option") {
|
|
1050
|
-
if (matchedFlags[i]) {
|
|
1051
|
-
if (pendingHeading) {
|
|
1052
|
-
result.push(pendingHeading.item);
|
|
1053
|
-
pendingHeading = null;
|
|
1054
|
-
}
|
|
1055
|
-
result.push(item);
|
|
1056
|
-
lastEmittedWasContent = true;
|
|
1057
|
-
groupHasOption = true;
|
|
1058
|
-
}
|
|
1354
|
+
useEffect4(() => {
|
|
1355
|
+
if (!isOpen) {
|
|
1356
|
+
cellsRef.current = [];
|
|
1357
|
+
setCellsVersion((version) => version + 1);
|
|
1358
|
+
setQuery("");
|
|
1359
|
+
setDebouncedQuery("");
|
|
1059
1360
|
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
return result;
|
|
1066
|
-
}, [items, searchable, debouncedQuery]);
|
|
1067
|
-
const effectiveCloseOnSelect = closeOnSelect !== void 0 ? closeOnSelect : type !== "checkbox";
|
|
1068
|
-
const renderPresetItem = (item, key) => {
|
|
1069
|
-
if (item.type === "heading") {
|
|
1070
|
-
return /* @__PURE__ */ jsx2(
|
|
1071
|
-
ContextMenuItem,
|
|
1072
|
-
{
|
|
1073
|
-
...item,
|
|
1074
|
-
size: item.size ?? size
|
|
1075
|
-
},
|
|
1076
|
-
`h-${key}`
|
|
1361
|
+
}, [isOpen]);
|
|
1362
|
+
useEffect4(() => {
|
|
1363
|
+
const timer = setTimeout(
|
|
1364
|
+
() => setDebouncedQuery(query),
|
|
1365
|
+
SEARCH_DEBOUNCE_MS
|
|
1077
1366
|
);
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1367
|
+
return () => clearTimeout(timer);
|
|
1368
|
+
}, [query]);
|
|
1369
|
+
useEffect4(() => {
|
|
1370
|
+
if (!isOpen || !trigger) return;
|
|
1371
|
+
const onMouseDown = (event) => {
|
|
1372
|
+
const target = event.target;
|
|
1373
|
+
if (!target || containerRef.current?.contains(target)) return;
|
|
1374
|
+
closeMenu();
|
|
1375
|
+
};
|
|
1376
|
+
const onScroll = () => closeMenu();
|
|
1377
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
1378
|
+
window.addEventListener("scroll", onScroll);
|
|
1379
|
+
return () => {
|
|
1380
|
+
document.removeEventListener("mousedown", onMouseDown);
|
|
1381
|
+
window.removeEventListener("scroll", onScroll);
|
|
1382
|
+
};
|
|
1383
|
+
}, [isOpen, trigger, closeMenu]);
|
|
1384
|
+
useLayoutEffect3(() => {
|
|
1385
|
+
if (!isOpen || !panelRef.current) return;
|
|
1386
|
+
const searchbox = panelRef.current.querySelector('[role="searchbox"]');
|
|
1387
|
+
const firstOption = panelRef.current.querySelector(
|
|
1388
|
+
'[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
|
|
1389
|
+
);
|
|
1390
|
+
(searchbox ?? firstOption)?.focus();
|
|
1391
|
+
}, [isOpen]);
|
|
1392
|
+
useLayoutEffect3(() => {
|
|
1393
|
+
if (!isOpen || !panelRef.current) return;
|
|
1394
|
+
const activeElement = document.activeElement;
|
|
1395
|
+
if (activeElement && panelRef.current.contains(activeElement)) return;
|
|
1396
|
+
panelRef.current.focus();
|
|
1397
|
+
}, [isOpen, cellsVersion]);
|
|
1398
|
+
useEffect4(() => {
|
|
1399
|
+
if (isOpen) return;
|
|
1400
|
+
triggerRef.current?.focus();
|
|
1401
|
+
}, [isOpen]);
|
|
1402
|
+
const contextValue = useMemo(
|
|
1403
|
+
() => ({
|
|
1404
|
+
size,
|
|
1405
|
+
menuId,
|
|
1406
|
+
closeMenu,
|
|
1407
|
+
activeIndex,
|
|
1408
|
+
setActiveIndex,
|
|
1409
|
+
registerCell,
|
|
1410
|
+
unregisterCell,
|
|
1411
|
+
getCellIndex,
|
|
1412
|
+
cellsVersion,
|
|
1413
|
+
query,
|
|
1414
|
+
setQuery
|
|
1415
|
+
}),
|
|
1416
|
+
[
|
|
1417
|
+
size,
|
|
1418
|
+
menuId,
|
|
1419
|
+
closeMenu,
|
|
1420
|
+
activeIndex,
|
|
1421
|
+
registerCell,
|
|
1422
|
+
unregisterCell,
|
|
1423
|
+
getCellIndex,
|
|
1424
|
+
cellsVersion,
|
|
1425
|
+
query
|
|
1426
|
+
]
|
|
1427
|
+
);
|
|
1428
|
+
const renderPresetItem = (item, index) => {
|
|
1429
|
+
if (!isOption(item)) {
|
|
1430
|
+
return /* @__PURE__ */ jsx4(
|
|
1431
|
+
ContextMenuItem,
|
|
1432
|
+
{
|
|
1433
|
+
...item,
|
|
1434
|
+
themeMode,
|
|
1435
|
+
themeProductContext
|
|
1436
|
+
},
|
|
1437
|
+
`context-menu-item-${index}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const leadingControl = item.leadingControl ?? (type === "checkbox" ? "checkbox" : type === "radio" ? "radio" : void 0);
|
|
1441
|
+
return /* @__PURE__ */ jsx4(
|
|
1081
1442
|
ContextMenuItem,
|
|
1082
1443
|
{
|
|
1083
1444
|
...item,
|
|
1084
|
-
|
|
1445
|
+
leadingControl,
|
|
1446
|
+
themeMode,
|
|
1447
|
+
themeProductContext,
|
|
1448
|
+
onSelect: () => {
|
|
1449
|
+
item.onSelect?.();
|
|
1450
|
+
onSelect?.(item);
|
|
1451
|
+
if (shouldCloseOnSelect) closeMenu();
|
|
1452
|
+
}
|
|
1085
1453
|
},
|
|
1086
|
-
`
|
|
1454
|
+
`context-menu-item-${index}`
|
|
1087
1455
|
);
|
|
1088
|
-
}
|
|
1089
|
-
const composed = composeItemForPreset(type, item);
|
|
1090
|
-
const originalSelect = composed.onSelect;
|
|
1091
|
-
const wrappedSelect = () => {
|
|
1092
|
-
originalSelect?.();
|
|
1093
|
-
onSelect?.(item);
|
|
1094
|
-
if (effectiveCloseOnSelect) closeMenu();
|
|
1095
1456
|
};
|
|
1096
|
-
|
|
1097
|
-
|
|
1457
|
+
const renderedItems = searchable ? filterItems(items, debouncedQuery) : [...items];
|
|
1458
|
+
const renderContent = () => {
|
|
1459
|
+
if (loading || isLoading || type === "loading") {
|
|
1460
|
+
const brandColor = xuiTheme.colors.control.brand.primary.bg;
|
|
1461
|
+
return /* @__PURE__ */ jsx4(
|
|
1462
|
+
Box,
|
|
1463
|
+
{
|
|
1464
|
+
padding: 16,
|
|
1465
|
+
alignItems: "center",
|
|
1466
|
+
justifyContent: "center",
|
|
1467
|
+
minHeight: 60,
|
|
1468
|
+
children: /* @__PURE__ */ jsx4(Spinner, { size: "md", color: brandColor })
|
|
1469
|
+
}
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
if (children) return children;
|
|
1473
|
+
const content = renderedItems.map(renderPresetItem);
|
|
1474
|
+
if (searchable) {
|
|
1475
|
+
content.unshift(
|
|
1476
|
+
/* @__PURE__ */ jsx4(
|
|
1477
|
+
"div",
|
|
1478
|
+
{
|
|
1479
|
+
"data-sticky": "top",
|
|
1480
|
+
style: {
|
|
1481
|
+
position: "sticky",
|
|
1482
|
+
top: 0,
|
|
1483
|
+
zIndex: 1,
|
|
1484
|
+
backgroundColor: xuiTheme.colors.background.secondary
|
|
1485
|
+
},
|
|
1486
|
+
children: /* @__PURE__ */ jsx4(
|
|
1487
|
+
ContextMenuItem,
|
|
1488
|
+
{
|
|
1489
|
+
type: "search",
|
|
1490
|
+
value: query,
|
|
1491
|
+
onValueChange: setQuery,
|
|
1492
|
+
placeholder: "Search",
|
|
1493
|
+
autoFocus: true,
|
|
1494
|
+
themeMode,
|
|
1495
|
+
themeProductContext
|
|
1496
|
+
}
|
|
1497
|
+
)
|
|
1498
|
+
},
|
|
1499
|
+
"context-menu-search"
|
|
1500
|
+
)
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
if (content.length > (searchable ? 1 : 0)) return content;
|
|
1504
|
+
return empty ?? /* @__PURE__ */ jsx4("div", { style: { padding: 16 }, children: emptyMessage });
|
|
1505
|
+
};
|
|
1506
|
+
const assignPanelRef = (node) => {
|
|
1507
|
+
panelRef.current = node;
|
|
1508
|
+
if (typeof ref === "function") ref(node);
|
|
1509
|
+
else if (ref) ref.current = node;
|
|
1510
|
+
};
|
|
1511
|
+
const assignTriggerRef = (node) => {
|
|
1512
|
+
triggerRef.current = node;
|
|
1513
|
+
};
|
|
1514
|
+
const triggerNode = trigger && isValidElement(trigger) ? cloneElement(trigger, {
|
|
1515
|
+
ref: assignTriggerRef,
|
|
1516
|
+
"aria-haspopup": "menu",
|
|
1517
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
1518
|
+
onClick: (event) => {
|
|
1519
|
+
trigger.props.onClick?.(event);
|
|
1520
|
+
if (!event.defaultPrevented) toggleMenu();
|
|
1521
|
+
}
|
|
1522
|
+
}) : trigger ? /* @__PURE__ */ jsx4(
|
|
1523
|
+
"span",
|
|
1098
1524
|
{
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1525
|
+
ref: assignTriggerRef,
|
|
1526
|
+
role: "button",
|
|
1527
|
+
tabIndex: 0,
|
|
1528
|
+
"aria-haspopup": "menu",
|
|
1529
|
+
"aria-expanded": isOpen ? "true" : "false",
|
|
1530
|
+
onClick: toggleMenu,
|
|
1531
|
+
children: trigger
|
|
1532
|
+
}
|
|
1533
|
+
) : null;
|
|
1534
|
+
const positionStyle = position ? {
|
|
1535
|
+
position: "fixed",
|
|
1536
|
+
left: position.x,
|
|
1537
|
+
top: position.y
|
|
1538
|
+
} : trigger ? {
|
|
1539
|
+
position: "fixed",
|
|
1540
|
+
left: positioned?.left ?? 0,
|
|
1541
|
+
top: positioned?.top ?? 0
|
|
1542
|
+
} : void 0;
|
|
1543
|
+
return /* @__PURE__ */ jsx4(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
|
|
1112
1544
|
"div",
|
|
1113
1545
|
{
|
|
1546
|
+
ref: containerRef,
|
|
1114
1547
|
style: {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
justifyContent: "center",
|
|
1118
|
-
padding: 16
|
|
1548
|
+
position: trigger || position ? "relative" : void 0,
|
|
1549
|
+
display: trigger ? "inline-block" : void 0
|
|
1119
1550
|
},
|
|
1120
|
-
children:
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
bodyContent = empty ?? /* @__PURE__ */ jsx2(EmptyMessage, { color: theme.colors.content.tertiary, children: emptyMessage ?? "No results" });
|
|
1154
|
-
}
|
|
1155
|
-
const hasStickySearch = !!searchNode;
|
|
1156
|
-
const prevOpenRef = useRef(false);
|
|
1157
|
-
const skipFocusRestoreRef = useRef(false);
|
|
1158
|
-
useEffect3(() => {
|
|
1159
|
-
const wasOpen = prevOpenRef.current;
|
|
1160
|
-
prevOpenRef.current = open;
|
|
1161
|
-
if (!wasOpen && open) {
|
|
1162
|
-
const timer = setTimeout(() => {
|
|
1163
|
-
const panel2 = panelRef.current;
|
|
1164
|
-
if (!panel2) return;
|
|
1165
|
-
const search = panel2.querySelector("[role='searchbox']");
|
|
1166
|
-
if (search) {
|
|
1167
|
-
search.focus();
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
const firstOption = panel2.querySelector(
|
|
1171
|
-
"[role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio']"
|
|
1172
|
-
);
|
|
1173
|
-
if (firstOption) {
|
|
1174
|
-
firstOption.focus();
|
|
1175
|
-
setActiveIndex(-1);
|
|
1176
|
-
} else {
|
|
1177
|
-
panel2.focus();
|
|
1178
|
-
}
|
|
1179
|
-
}, 0);
|
|
1180
|
-
return () => clearTimeout(timer);
|
|
1181
|
-
}
|
|
1182
|
-
if (wasOpen && !open) {
|
|
1183
|
-
if (!skipFocusRestoreRef.current) {
|
|
1184
|
-
triggerRef.current?.focus();
|
|
1185
|
-
}
|
|
1186
|
-
skipFocusRestoreRef.current = false;
|
|
1187
|
-
}
|
|
1188
|
-
}, [open]);
|
|
1189
|
-
useEffect3(() => {
|
|
1190
|
-
if (!open || !usePortal || typeof document === "undefined") return;
|
|
1191
|
-
const handlePointerDown = (event) => {
|
|
1192
|
-
const target = event.target;
|
|
1193
|
-
if (!target) return;
|
|
1194
|
-
if (panelRef.current?.contains(target)) return;
|
|
1195
|
-
if (triggerRef.current?.contains(target)) return;
|
|
1196
|
-
if (target instanceof Element) {
|
|
1197
|
-
const portals = document.querySelectorAll(
|
|
1198
|
-
"[data-xui-context-menu-portal]"
|
|
1199
|
-
);
|
|
1200
|
-
for (let i = 0; i < portals.length; i += 1) {
|
|
1201
|
-
const portal = portals[i];
|
|
1202
|
-
if (portal.getAttribute("data-xui-context-menu-portal") === menuId && portal.contains(target)) {
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1551
|
+
children: [
|
|
1552
|
+
triggerNode,
|
|
1553
|
+
isOpen && /* @__PURE__ */ jsx4(
|
|
1554
|
+
Box,
|
|
1555
|
+
{
|
|
1556
|
+
ref: assignPanelRef,
|
|
1557
|
+
role: "menu",
|
|
1558
|
+
"aria-label": ariaLabel,
|
|
1559
|
+
"data-testid": dataTestId ?? testID ?? "context-menu",
|
|
1560
|
+
"data-placement": positioned?.placement ?? placement,
|
|
1561
|
+
backgroundColor: xuiTheme.colors.background.secondary,
|
|
1562
|
+
borderColor: xuiTheme.colors.border.secondary,
|
|
1563
|
+
borderWidth: 1,
|
|
1564
|
+
borderRadius,
|
|
1565
|
+
paddingVertical: sizeStyles.paddingVertical,
|
|
1566
|
+
width,
|
|
1567
|
+
minWidth: sizeStyles.minWidth,
|
|
1568
|
+
tabIndex: -1,
|
|
1569
|
+
onKeyDown: keyboard.handleKeyDown,
|
|
1570
|
+
onMouseLeave: () => setActiveIndex(-1),
|
|
1571
|
+
style: {
|
|
1572
|
+
...positionStyle,
|
|
1573
|
+
...style,
|
|
1574
|
+
zIndex: 1e3,
|
|
1575
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
1576
|
+
maxHeight,
|
|
1577
|
+
overflowY: "auto",
|
|
1578
|
+
outline: "none"
|
|
1579
|
+
},
|
|
1580
|
+
children: renderContent()
|
|
1581
|
+
}
|
|
1582
|
+
)
|
|
1583
|
+
]
|
|
1206
1584
|
}
|
|
1207
|
-
|
|
1208
|
-
closeMenu();
|
|
1209
|
-
};
|
|
1210
|
-
document.addEventListener("mousedown", handlePointerDown);
|
|
1211
|
-
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
1212
|
-
}, [open, usePortal, closeMenu, menuId]);
|
|
1213
|
-
const resolvedPlacement = position?.placement ?? placement;
|
|
1214
|
-
const scrollContainerStyle = {
|
|
1215
|
-
overflowY: "auto",
|
|
1216
|
-
flex: 1,
|
|
1217
|
-
minHeight: 0
|
|
1218
|
-
};
|
|
1219
|
-
const stickyHeaderStyle = {
|
|
1220
|
-
position: "sticky",
|
|
1221
|
-
top: 0,
|
|
1222
|
-
zIndex: 1,
|
|
1223
|
-
// Match the glass panel so options scrolling underneath blur instead of
|
|
1224
|
-
// showing through the translucent header.
|
|
1225
|
-
background: glassBackground,
|
|
1226
|
-
backdropFilter: "blur(12px)",
|
|
1227
|
-
WebkitBackdropFilter: "blur(12px)"
|
|
1228
|
-
};
|
|
1229
|
-
const panel = open ? /* @__PURE__ */ jsx2(ContextMenuContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs2(
|
|
1230
|
-
"div",
|
|
1231
|
-
{
|
|
1232
|
-
ref: panelRef,
|
|
1233
|
-
role: "menu",
|
|
1234
|
-
"aria-label": ariaLabel,
|
|
1235
|
-
"data-testid": testId || testID,
|
|
1236
|
-
"data-placement": usePortal ? resolvedPlacement : void 0,
|
|
1237
|
-
tabIndex: -1,
|
|
1238
|
-
onKeyDown: handleKeyDown,
|
|
1239
|
-
onMouseLeave: () => setActiveIndex(-1),
|
|
1240
|
-
style: panelStyle,
|
|
1241
|
-
children: [
|
|
1242
|
-
hasStickySearch && /* @__PURE__ */ jsx2("div", { "data-sticky": "top", style: stickyHeaderStyle, children: searchNode }),
|
|
1243
|
-
/* @__PURE__ */ jsx2("div", { style: scrollContainerStyle, children: bodyContent })
|
|
1244
|
-
]
|
|
1245
|
-
}
|
|
1246
|
-
) }) : null;
|
|
1247
|
-
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1248
|
-
triggerNode,
|
|
1249
|
-
usePortal ? panel && createPortal2(panel, document.body) : panel
|
|
1250
|
-
] });
|
|
1251
|
-
};
|
|
1252
|
-
ContextMenu.displayName = "ContextMenu";
|
|
1253
|
-
function composeItemForPreset(type, item) {
|
|
1254
|
-
switch (type) {
|
|
1255
|
-
case "checkbox":
|
|
1256
|
-
return { ...item, leadingControl: "checkbox" };
|
|
1257
|
-
case "radio":
|
|
1258
|
-
return { ...item, leadingControl: "radio" };
|
|
1259
|
-
case "list":
|
|
1260
|
-
case "phone":
|
|
1261
|
-
case "status":
|
|
1262
|
-
case "brandLogo":
|
|
1263
|
-
case "avatar":
|
|
1264
|
-
default:
|
|
1265
|
-
return { ...item };
|
|
1585
|
+
) });
|
|
1266
1586
|
}
|
|
1267
|
-
|
|
1587
|
+
);
|
|
1588
|
+
ContextMenuRoot.displayName = "ContextMenu";
|
|
1589
|
+
var ContextMenu = Object.assign(ContextMenuRoot, {
|
|
1590
|
+
Item: ContextMenuItem,
|
|
1591
|
+
Submenu: ContextMenuSubmenu
|
|
1592
|
+
});
|
|
1268
1593
|
export {
|
|
1269
1594
|
ContextMenu,
|
|
1270
1595
|
ContextMenuContext,
|
|
1271
1596
|
ContextMenuItem,
|
|
1597
|
+
ContextMenuSubmenu,
|
|
1272
1598
|
useContextMenu,
|
|
1273
1599
|
useContextMenuPosition,
|
|
1274
1600
|
useContextMenuRequired,
|