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