@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/web/index.mjs CHANGED
@@ -1,18 +1,269 @@
1
1
  // src/ContextMenu.tsx
2
- import React2, {
3
- useCallback as useCallback2,
4
- useEffect as useEffect3,
5
- useMemo,
6
- useRef,
7
- useState as useState3,
2
+ import {
3
+ cloneElement,
4
+ forwardRef,
8
5
  isValidElement,
9
- cloneElement
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
- import { createPortal as createPortal2 } from "react-dom";
12
- import {
13
- useResolvedTheme as useResolvedTheme2,
14
- useId as useId2
15
- } from "@xsolla/xui-core";
13
+
14
+ // ../../foundation/primitives-web/src/Box.tsx
15
+ import React2 from "react";
16
+ import styled from "styled-components";
17
+
18
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
19
+ import React from "react";
20
+
21
+ // ../../../node_modules/@emotion/memoize/dist/memoize.esm.js
22
+ function memoize(fn) {
23
+ var cache = {};
24
+ return function(arg) {
25
+ if (cache[arg] === void 0) cache[arg] = fn(arg);
26
+ return cache[arg];
27
+ };
28
+ }
29
+ var memoize_esm_default = memoize;
30
+
31
+ // ../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.esm.js
32
+ var reactPropsRegex = /^((children|dangerouslySetInnerHTML|key|ref|autoFocus|defaultValue|defaultChecked|innerHTML|suppressContentEditableWarning|suppressHydrationWarning|valueLink|accept|acceptCharset|accessKey|action|allow|allowUserMedia|allowPaymentRequest|allowFullScreen|allowTransparency|alt|async|autoComplete|autoPlay|capture|cellPadding|cellSpacing|challenge|charSet|checked|cite|classID|className|cols|colSpan|content|contentEditable|contextMenu|controls|controlsList|coords|crossOrigin|data|dateTime|decoding|default|defer|dir|disabled|disablePictureInPicture|download|draggable|encType|form|formAction|formEncType|formMethod|formNoValidate|formTarget|frameBorder|headers|height|hidden|high|href|hrefLang|htmlFor|httpEquiv|id|inputMode|integrity|is|keyParams|keyType|kind|label|lang|list|loading|loop|low|marginHeight|marginWidth|max|maxLength|media|mediaGroup|method|min|minLength|multiple|muted|name|nonce|noValidate|open|optimum|pattern|placeholder|playsInline|poster|preload|profile|radioGroup|readOnly|referrerPolicy|rel|required|reversed|role|rows|rowSpan|sandbox|scope|scoped|scrolling|seamless|selected|shape|size|sizes|slot|span|spellCheck|src|srcDoc|srcLang|srcSet|start|step|style|summary|tabIndex|target|title|type|useMap|value|width|wmode|wrap|about|datatype|inlist|prefix|property|resource|typeof|vocab|autoCapitalize|autoCorrect|autoSave|color|inert|itemProp|itemScope|itemType|itemID|itemRef|on|results|security|unselectable|accentHeight|accumulate|additive|alignmentBaseline|allowReorder|alphabetic|amplitude|arabicForm|ascent|attributeName|attributeType|autoReverse|azimuth|baseFrequency|baselineShift|baseProfile|bbox|begin|bias|by|calcMode|capHeight|clip|clipPathUnits|clipPath|clipRule|colorInterpolation|colorInterpolationFilters|colorProfile|colorRendering|contentScriptType|contentStyleType|cursor|cx|cy|d|decelerate|descent|diffuseConstant|direction|display|divisor|dominantBaseline|dur|dx|dy|edgeMode|elevation|enableBackground|end|exponent|externalResourcesRequired|fill|fillOpacity|fillRule|filter|filterRes|filterUnits|floodColor|floodOpacity|focusable|fontFamily|fontSize|fontSizeAdjust|fontStretch|fontStyle|fontVariant|fontWeight|format|from|fr|fx|fy|g1|g2|glyphName|glyphOrientationHorizontal|glyphOrientationVertical|glyphRef|gradientTransform|gradientUnits|hanging|horizAdvX|horizOriginX|ideographic|imageRendering|in|in2|intercept|k|k1|k2|k3|k4|kernelMatrix|kernelUnitLength|kerning|keyPoints|keySplines|keyTimes|lengthAdjust|letterSpacing|lightingColor|limitingConeAngle|local|markerEnd|markerMid|markerStart|markerHeight|markerUnits|markerWidth|mask|maskContentUnits|maskUnits|mathematical|mode|numOctaves|offset|opacity|operator|order|orient|orientation|origin|overflow|overlinePosition|overlineThickness|panose1|paintOrder|pathLength|patternContentUnits|patternTransform|patternUnits|pointerEvents|points|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|r|radius|refX|refY|renderingIntent|repeatCount|repeatDur|requiredExtensions|requiredFeatures|restart|result|rotate|rx|ry|scale|seed|shapeRendering|slope|spacing|specularConstant|specularExponent|speed|spreadMethod|startOffset|stdDeviation|stemh|stemv|stitchTiles|stopColor|stopOpacity|strikethroughPosition|strikethroughThickness|string|stroke|strokeDasharray|strokeDashoffset|strokeLinecap|strokeLinejoin|strokeMiterlimit|strokeOpacity|strokeWidth|surfaceScale|systemLanguage|tableValues|targetX|targetY|textAnchor|textDecoration|textRendering|textLength|to|transform|u1|u2|underlinePosition|underlineThickness|unicode|unicodeBidi|unicodeRange|unitsPerEm|vAlphabetic|vHanging|vIdeographic|vMathematical|values|vectorEffect|version|vertAdvY|vertOriginX|vertOriginY|viewBox|viewTarget|visibility|widths|wordSpacing|writingMode|x|xHeight|x1|x2|xChannelSelector|xlinkActuate|xlinkArcrole|xlinkHref|xlinkRole|xlinkShow|xlinkTitle|xlinkType|xmlBase|xmlns|xmlnsXlink|xmlLang|xmlSpace|y|y1|y2|yChannelSelector|z|zoomAndPan|for|class|autofocus)|(([Dd][Aa][Tt][Aa]|[Aa][Rr][Ii][Aa]|x)-.*))$/;
33
+ var index = memoize_esm_default(
34
+ function(prop) {
35
+ return reactPropsRegex.test(prop) || prop.charCodeAt(0) === 111 && prop.charCodeAt(1) === 110 && prop.charCodeAt(2) < 91;
36
+ }
37
+ /* Z+1 */
38
+ );
39
+ var is_prop_valid_esm_default = index;
40
+
41
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
42
+ var ADDITIONAL_BLOCKED_PROPS = /* @__PURE__ */ new Set([
43
+ // RN-only event handlers (pass isPropValid's on* pattern)
44
+ "onPress",
45
+ "onChangeText",
46
+ "onLayout",
47
+ "onMoveShouldSetResponder",
48
+ "onResponderGrant",
49
+ "onResponderMove",
50
+ "onResponderRelease",
51
+ "onResponderTerminate",
52
+ // SVG attributes that pass isPropValid
53
+ "strokeWidth",
54
+ // CSS properties that pass isPropValid but are used as component props
55
+ "overflow",
56
+ "cursor",
57
+ "fontSize",
58
+ "fontWeight",
59
+ "fontFamily",
60
+ "textDecoration"
61
+ ]);
62
+ function shouldForwardProp(key) {
63
+ if (ADDITIONAL_BLOCKED_PROPS.has(key)) return false;
64
+ return is_prop_valid_esm_default(key);
65
+ }
66
+ function createFilteredElement(defaultTag) {
67
+ const Component = React.forwardRef(
68
+ ({ children, elementType, ...props }, ref) => {
69
+ const Tag = elementType || defaultTag;
70
+ const htmlProps = {};
71
+ for (const key of Object.keys(props)) {
72
+ if (shouldForwardProp(key)) {
73
+ htmlProps[key] = props[key];
74
+ }
75
+ }
76
+ return React.createElement(
77
+ Tag,
78
+ { ref, ...htmlProps },
79
+ children
80
+ );
81
+ }
82
+ );
83
+ Component.displayName = `Filtered(${defaultTag})`;
84
+ return Component;
85
+ }
86
+
87
+ // ../../foundation/primitives-web/src/Box.tsx
88
+ import { jsx } from "react/jsx-runtime";
89
+ var FilteredDiv = createFilteredElement("div");
90
+ var StyledBox = styled(FilteredDiv)`
91
+ display: flex;
92
+ box-sizing: border-box;
93
+ background-color: ${(props) => props.backgroundColor || "transparent"};
94
+ border-color: ${(props) => props.borderColor || "transparent"};
95
+ border-width: ${(props) => typeof props.borderWidth === "number" ? `${props.borderWidth}px` : props.borderWidth || 0};
96
+
97
+ ${(props) => props.borderBottomWidth !== void 0 && `
98
+ border-bottom-width: ${typeof props.borderBottomWidth === "number" ? `${props.borderBottomWidth}px` : props.borderBottomWidth};
99
+ border-bottom-color: ${props.borderBottomColor || props.borderColor || "transparent"};
100
+ border-bottom-style: solid;
101
+ `}
102
+ ${(props) => props.borderTopWidth !== void 0 && `
103
+ border-top-width: ${typeof props.borderTopWidth === "number" ? `${props.borderTopWidth}px` : props.borderTopWidth};
104
+ border-top-color: ${props.borderTopColor || props.borderColor || "transparent"};
105
+ border-top-style: solid;
106
+ `}
107
+ ${(props) => props.borderLeftWidth !== void 0 && `
108
+ border-left-width: ${typeof props.borderLeftWidth === "number" ? `${props.borderLeftWidth}px` : props.borderLeftWidth};
109
+ border-left-color: ${props.borderLeftColor || props.borderColor || "transparent"};
110
+ border-left-style: solid;
111
+ `}
112
+ ${(props) => props.borderRightWidth !== void 0 && `
113
+ border-right-width: ${typeof props.borderRightWidth === "number" ? `${props.borderRightWidth}px` : props.borderRightWidth};
114
+ border-right-color: ${props.borderRightColor || props.borderColor || "transparent"};
115
+ border-right-style: solid;
116
+ `}
117
+
118
+ border-style: ${(props) => props.borderStyle || (props.borderWidth || props.borderBottomWidth || props.borderTopWidth || props.borderLeftWidth || props.borderRightWidth ? "solid" : "none")};
119
+ border-radius: ${(props) => typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius || 0};
120
+ height: ${(props) => typeof props.height === "number" ? `${props.height}px` : props.height || "auto"};
121
+ width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width || "auto"};
122
+ min-width: ${(props) => typeof props.minWidth === "number" ? `${props.minWidth}px` : props.minWidth || "auto"};
123
+ min-height: ${(props) => typeof props.minHeight === "number" ? `${props.minHeight}px` : props.minHeight || "auto"};
124
+ max-width: ${(props) => typeof props.maxWidth === "number" ? `${props.maxWidth}px` : props.maxWidth || "none"};
125
+ max-height: ${(props) => typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight || "none"};
126
+
127
+ padding: ${(props) => typeof props.padding === "number" ? `${props.padding}px` : props.padding || 0};
128
+ ${(props) => props.paddingHorizontal && `
129
+ padding-left: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
130
+ padding-right: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
131
+ `}
132
+ ${(props) => props.paddingVertical && `
133
+ padding-top: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
134
+ padding-bottom: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
135
+ `}
136
+ ${(props) => props.paddingTop !== void 0 && `padding-top: ${typeof props.paddingTop === "number" ? `${props.paddingTop}px` : props.paddingTop};`}
137
+ ${(props) => props.paddingBottom !== void 0 && `padding-bottom: ${typeof props.paddingBottom === "number" ? `${props.paddingBottom}px` : props.paddingBottom};`}
138
+ ${(props) => props.paddingLeft !== void 0 && `padding-left: ${typeof props.paddingLeft === "number" ? `${props.paddingLeft}px` : props.paddingLeft};`}
139
+ ${(props) => props.paddingRight !== void 0 && `padding-right: ${typeof props.paddingRight === "number" ? `${props.paddingRight}px` : props.paddingRight};`}
140
+
141
+ margin: ${(props) => typeof props.margin === "number" ? `${props.margin}px` : props.margin || 0};
142
+ ${(props) => props.marginTop !== void 0 && `margin-top: ${typeof props.marginTop === "number" ? `${props.marginTop}px` : props.marginTop};`}
143
+ ${(props) => props.marginBottom !== void 0 && `margin-bottom: ${typeof props.marginBottom === "number" ? `${props.marginBottom}px` : props.marginBottom};`}
144
+ ${(props) => props.marginLeft !== void 0 && `margin-left: ${typeof props.marginLeft === "number" ? `${props.marginLeft}px` : props.marginLeft};`}
145
+ ${(props) => props.marginRight !== void 0 && `margin-right: ${typeof props.marginRight === "number" ? `${props.marginRight}px` : props.marginRight};`}
146
+
147
+ flex-direction: ${(props) => props.flexDirection || "column"};
148
+ flex-wrap: ${(props) => props.flexWrap || "nowrap"};
149
+ align-items: ${(props) => props.alignItems || "stretch"};
150
+ justify-content: ${(props) => props.justifyContent || "flex-start"};
151
+ cursor: ${(props) => props.cursor ? props.cursor : props.onClick || props.onPress ? "pointer" : "inherit"};
152
+ position: ${(props) => props.position || "static"};
153
+ top: ${(props) => typeof props.top === "number" ? `${props.top}px` : props.top};
154
+ bottom: ${(props) => typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom};
155
+ left: ${(props) => typeof props.left === "number" ? `${props.left}px` : props.left};
156
+ right: ${(props) => typeof props.right === "number" ? `${props.right}px` : props.right};
157
+ flex: ${(props) => props.flex};
158
+ flex-shrink: ${(props) => props.flexShrink ?? 1};
159
+ gap: ${(props) => typeof props.gap === "number" ? `${props.gap}px` : props.gap || 0};
160
+ align-self: ${(props) => props.alignSelf || "auto"};
161
+ overflow: ${(props) => props.overflow || "visible"};
162
+ overflow-x: ${(props) => props.overflowX || "visible"};
163
+ overflow-y: ${(props) => props.overflowY || "visible"};
164
+ z-index: ${(props) => props.zIndex};
165
+ opacity: ${(props) => props.disabled ? 0.5 : 1};
166
+ pointer-events: ${(props) => props.disabled ? "none" : "auto"};
167
+
168
+ &:hover {
169
+ ${(props) => props.hoverStyle?.backgroundColor && `background-color: ${props.hoverStyle.backgroundColor};`}
170
+ ${(props) => props.hoverStyle?.borderColor && `border-color: ${props.hoverStyle.borderColor};`}
171
+ }
172
+
173
+ &:active {
174
+ ${(props) => props.pressStyle?.backgroundColor && `background-color: ${props.pressStyle.backgroundColor};`}
175
+ }
176
+ `;
177
+ var Box = React2.forwardRef(
178
+ ({
179
+ children,
180
+ onPress,
181
+ onKeyDown,
182
+ onKeyUp,
183
+ role,
184
+ "aria-label": ariaLabel,
185
+ "aria-labelledby": ariaLabelledBy,
186
+ "aria-current": ariaCurrent,
187
+ "aria-disabled": ariaDisabled,
188
+ "aria-live": ariaLive,
189
+ "aria-busy": ariaBusy,
190
+ "aria-describedby": ariaDescribedBy,
191
+ "aria-expanded": ariaExpanded,
192
+ "aria-haspopup": ariaHasPopup,
193
+ "aria-pressed": ariaPressed,
194
+ "aria-controls": ariaControls,
195
+ tabIndex,
196
+ as,
197
+ src,
198
+ alt,
199
+ onError,
200
+ onLoad,
201
+ type,
202
+ disabled,
203
+ id,
204
+ testID,
205
+ "data-testid": dataTestId,
206
+ ...props
207
+ }, ref) => {
208
+ if (as === "img" && src) {
209
+ return /* @__PURE__ */ jsx(
210
+ "img",
211
+ {
212
+ src,
213
+ alt: alt || "",
214
+ onError,
215
+ onLoad,
216
+ style: {
217
+ display: "block",
218
+ objectFit: "cover",
219
+ width: typeof props.width === "number" ? `${props.width}px` : props.width,
220
+ height: typeof props.height === "number" ? `${props.height}px` : props.height,
221
+ borderRadius: typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius,
222
+ position: props.position,
223
+ top: typeof props.top === "number" ? `${props.top}px` : props.top,
224
+ left: typeof props.left === "number" ? `${props.left}px` : props.left,
225
+ right: typeof props.right === "number" ? `${props.right}px` : props.right,
226
+ bottom: typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom,
227
+ ...props.style
228
+ }
229
+ }
230
+ );
231
+ }
232
+ return /* @__PURE__ */ jsx(
233
+ StyledBox,
234
+ {
235
+ ref,
236
+ elementType: as,
237
+ id,
238
+ type: as === "button" ? type || "button" : void 0,
239
+ disabled: as === "button" ? disabled : void 0,
240
+ onClick: onPress,
241
+ onKeyDown,
242
+ onKeyUp,
243
+ role,
244
+ "aria-label": ariaLabel,
245
+ "aria-labelledby": ariaLabelledBy,
246
+ "aria-current": ariaCurrent,
247
+ "aria-disabled": ariaDisabled,
248
+ "aria-busy": ariaBusy,
249
+ "aria-describedby": ariaDescribedBy,
250
+ "aria-expanded": ariaExpanded,
251
+ "aria-haspopup": ariaHasPopup,
252
+ "aria-pressed": ariaPressed,
253
+ "aria-controls": ariaControls,
254
+ "aria-live": ariaLive,
255
+ tabIndex: tabIndex !== void 0 ? tabIndex : void 0,
256
+ "data-testid": dataTestId || testID,
257
+ ...props,
258
+ children
259
+ }
260
+ );
261
+ }
262
+ );
263
+ Box.displayName = "Box";
264
+
265
+ // src/ContextMenu.tsx
266
+ import { useDesignSystem as useDesignSystem2, useId as useId2 } from "@xsolla/xui-core";
16
267
  import { Spinner } from "@xsolla/xui-spinner";
17
268
 
18
269
  // src/ContextMenuContext.tsx
@@ -33,7 +284,7 @@ var useContextMenuRequired = () => {
33
284
  };
34
285
 
35
286
  // src/ContextMenuItem.tsx
36
- import React, { useEffect, useLayoutEffect, useState } from "react";
287
+ import React3, { useEffect, useLayoutEffect, useState } from "react";
37
288
  import { createPortal } from "react-dom";
38
289
  import {
39
290
  useResolvedTheme,
@@ -43,7 +294,7 @@ import { Typography } from "@xsolla/xui-typography";
43
294
  import { Checkbox } from "@xsolla/xui-checkbox";
44
295
  import { Radio } from "@xsolla/xui-radio";
45
296
  import { Search } from "@xsolla/xui-icons-base";
46
- import { jsx, jsxs } from "react/jsx-runtime";
297
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
47
298
  var sizeToVariants = {
48
299
  xl: { label: "bodyLg", description: "bodyLg", headingAccent: "bodyLgAccent" },
49
300
  lg: { label: "bodyLg", description: "bodyMd", headingAccent: "bodyMdAccent" },
@@ -53,18 +304,51 @@ var sizeToVariants = {
53
304
  var sizeLabelOverride = {
54
305
  xl: { fontSize: 20, lineHeight: "26px" }
55
306
  };
307
+ var SUBMENU_GAP = 8;
308
+ var SUBMENU_VIEWPORT_PADDING = 8;
309
+ var clipsOverflow = (style) => {
310
+ return /(auto|scroll|hidden|clip)/.test(
311
+ `${style.overflow}${style.overflowX}${style.overflowY}`
312
+ );
313
+ };
314
+ var getSubmenuBoundary = (node) => {
315
+ const viewport = {
316
+ top: SUBMENU_VIEWPORT_PADDING,
317
+ right: window.innerWidth - SUBMENU_VIEWPORT_PADDING,
318
+ bottom: window.innerHeight - SUBMENU_VIEWPORT_PADDING,
319
+ left: SUBMENU_VIEWPORT_PADDING
320
+ };
321
+ let current = node.parentElement;
322
+ while (current && current !== document.body) {
323
+ const style = window.getComputedStyle(current);
324
+ if (clipsOverflow(style) && current.getAttribute("role") !== "menu") {
325
+ const rect = current.getBoundingClientRect();
326
+ return {
327
+ top: Math.max(viewport.top, rect.top + SUBMENU_VIEWPORT_PADDING),
328
+ right: Math.min(viewport.right, rect.right - SUBMENU_VIEWPORT_PADDING),
329
+ bottom: Math.min(
330
+ viewport.bottom,
331
+ rect.bottom - SUBMENU_VIEWPORT_PADDING
332
+ ),
333
+ left: Math.max(viewport.left, rect.left + SUBMENU_VIEWPORT_PADDING)
334
+ };
335
+ }
336
+ current = current.parentElement;
337
+ }
338
+ return viewport;
339
+ };
56
340
  var ContextMenuItem = (props) => {
57
- if (props.type === "option") return /* @__PURE__ */ jsx(OptionCell, { ...props });
58
- if (props.type === "heading") return /* @__PURE__ */ jsx(HeadingCell, { ...props });
59
- if (props.type === "divider") return /* @__PURE__ */ jsx(DividerCell, { ...props });
60
- if (props.type === "search") return /* @__PURE__ */ jsx(SearchCell, { ...props });
341
+ if (props.type === "option") return /* @__PURE__ */ jsx2(OptionCell, { ...props });
342
+ if (props.type === "heading") return /* @__PURE__ */ jsx2(HeadingCell, { ...props });
343
+ if (props.type === "divider") return /* @__PURE__ */ jsx2(DividerCell, { ...props });
344
+ if (props.type === "search") return /* @__PURE__ */ jsx2(SearchCell, { ...props });
61
345
  return null;
62
346
  };
63
347
  ContextMenuItem.displayName = "ContextMenuItem";
64
348
  var SubmenuChevron = ({
65
349
  color,
66
350
  size
67
- }) => /* @__PURE__ */ jsx(
351
+ }) => /* @__PURE__ */ jsx2(
68
352
  "span",
69
353
  {
70
354
  "data-testid": "ctxmenu-submenu-chevron",
@@ -76,7 +360,7 @@ var SubmenuChevron = ({
76
360
  width: size,
77
361
  height: size
78
362
  },
79
- children: /* @__PURE__ */ jsx(
363
+ children: /* @__PURE__ */ jsx2(
80
364
  "svg",
81
365
  {
82
366
  width: size,
@@ -84,7 +368,7 @@ var SubmenuChevron = ({
84
368
  viewBox: "0 0 24 24",
85
369
  fill: "none",
86
370
  xmlns: "http://www.w3.org/2000/svg",
87
- children: /* @__PURE__ */ jsx(
371
+ children: /* @__PURE__ */ jsx2(
88
372
  "path",
89
373
  {
90
374
  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 +390,7 @@ var OptionCell = ({
106
390
  leadingIcon,
107
391
  status,
108
392
  iconWrapper,
393
+ slot,
109
394
  slotContent,
110
395
  value,
111
396
  hint,
@@ -114,12 +399,17 @@ var OptionCell = ({
114
399
  hasSubmenu,
115
400
  submenu,
116
401
  onSelect,
402
+ onCheckedChange,
117
403
  testID,
118
404
  themeMode,
119
405
  themeProductContext,
120
406
  "data-testid": testId
121
407
  }) => {
122
- const { theme } = useResolvedTheme({ themeMode, themeProductContext });
408
+ const { theme: rawTheme } = useResolvedTheme({
409
+ themeMode,
410
+ themeProductContext
411
+ });
412
+ const theme = rawTheme;
123
413
  const ctx = useContextMenu();
124
414
  const size = propSize ?? ctx?.size ?? "md";
125
415
  const sizing = theme.sizing.contextMenu(size);
@@ -131,9 +421,9 @@ var OptionCell = ({
131
421
  const [isHovered, setIsHovered] = useState(false);
132
422
  const [submenuOpen, setSubmenuOpen] = useState(false);
133
423
  const [submenuPos, setSubmenuPos] = useState(null);
134
- const optionRef = React.useRef(null);
135
- const submenuWrapperRef = React.useRef(null);
136
- const closeTimerRef = React.useRef(
424
+ const optionRef = React3.useRef(null);
425
+ const submenuWrapperRef = React3.useRef(null);
426
+ const closeTimerRef = React3.useRef(
137
427
  null
138
428
  );
139
429
  const cancelClose = () => {
@@ -176,7 +466,32 @@ var OptionCell = ({
176
466
  const node = optionRef.current;
177
467
  if (!node) return;
178
468
  const rect = node.getBoundingClientRect();
179
- setSubmenuPos({ top: rect.top, left: rect.right });
469
+ const submenuRect = submenuWrapperRef.current?.getBoundingClientRect();
470
+ const submenuWidth = submenuRect?.width ?? 0;
471
+ const submenuHeight = submenuRect?.height ?? 0;
472
+ const boundary = getSubmenuBoundary(node);
473
+ const rightSideLeft = rect.right + SUBMENU_GAP;
474
+ const leftSideLeft = rect.left - SUBMENU_GAP - submenuWidth;
475
+ const opensLeft = submenuWidth > 0 && rightSideLeft + submenuWidth > boundary.right;
476
+ let left = opensLeft ? leftSideLeft : rightSideLeft;
477
+ if (submenuWidth > 0) {
478
+ if (opensLeft) {
479
+ left = Math.max(SUBMENU_VIEWPORT_PADDING, left);
480
+ } else {
481
+ left = Math.min(
482
+ Math.max(boundary.left, left),
483
+ Math.max(boundary.left, boundary.right - submenuWidth)
484
+ );
485
+ }
486
+ }
487
+ let top = rect.top;
488
+ if (submenuHeight > 0 && top + submenuHeight > boundary.bottom) {
489
+ top = boundary.bottom - submenuHeight;
490
+ }
491
+ top = Math.max(boundary.top, top);
492
+ setSubmenuPos(
493
+ (prev) => prev?.top === top && prev.left === left ? prev : { top, left }
494
+ );
180
495
  };
181
496
  update();
182
497
  window.addEventListener("scroll", update, true);
@@ -185,8 +500,8 @@ var OptionCell = ({
185
500
  window.removeEventListener("scroll", update, true);
186
501
  window.removeEventListener("resize", update);
187
502
  };
188
- }, [hasSubmenu, submenuOpen]);
189
- const onSelectRef = React.useRef(onSelect);
503
+ });
504
+ const onSelectRef = React3.useRef(onSelect);
190
505
  onSelectRef.current = onSelect;
191
506
  useEffect(() => {
192
507
  if (!registerCell || !unregisterCell) return;
@@ -204,12 +519,12 @@ var OptionCell = ({
204
519
  onSelect: () => onSelectRef.current?.()
205
520
  });
206
521
  }, [registerCell, id, disabled]);
207
- const index = getCellIndex ? getCellIndex(id) : -1;
208
- const isActive = ctx ? index >= 0 && ctx.activeIndex === index : false;
522
+ const index2 = getCellIndex ? getCellIndex(id) : -1;
523
+ const isActive = ctx ? index2 >= 0 && ctx.activeIndex === index2 : false;
209
524
  const inHoverState = isActive || !ctx && isHovered || hasSubmenu && submenuOpen;
210
525
  const handleEnter = () => {
211
526
  if (disabled) return;
212
- if (ctx && index >= 0) ctx.setActiveIndex(index);
527
+ if (ctx && index2 >= 0) ctx.setActiveIndex(index2);
213
528
  if (!ctx) setIsHovered(true);
214
529
  if (hasSubmenu) setSubmenuOpen(true);
215
530
  };
@@ -219,7 +534,7 @@ var OptionCell = ({
219
534
  };
220
535
  const labelColor = disabled ? theme.colors.control.input.textDisable : destructive ? theme.colors.content.alert.primary : theme.colors.content.primary;
221
536
  const bg = inHoverState ? theme.colors.control.input.bgHover : "transparent";
222
- const role = !hasSubmenu && checked !== void 0 ? "menuitemcheckbox" : "menuitem";
537
+ const role = !hasSubmenu && checked !== void 0 ? leadingControl === "radio" ? "menuitemradio" : "menuitemcheckbox" : "menuitem";
223
538
  const ariaChecked = !hasSubmenu && checked !== void 0 ? checked ? "true" : "false" : void 0;
224
539
  const handleClick = () => {
225
540
  if (disabled) return;
@@ -227,6 +542,9 @@ var OptionCell = ({
227
542
  setSubmenuOpen(true);
228
543
  return;
229
544
  }
545
+ if (checked !== void 0) {
546
+ onCheckedChange?.(!checked);
547
+ }
230
548
  onSelect?.();
231
549
  };
232
550
  const closeSubmenuAndFocus = () => {
@@ -256,6 +574,9 @@ var OptionCell = ({
256
574
  }
257
575
  if (e.key === "Enter" || e.key === " ") {
258
576
  e.preventDefault();
577
+ if (checked !== void 0) {
578
+ onCheckedChange?.(!checked);
579
+ }
259
580
  onSelect?.();
260
581
  }
261
582
  };
@@ -299,13 +620,13 @@ var OptionCell = ({
299
620
  outline: "none"
300
621
  },
301
622
  children: [
302
- leadingControl === "checkbox" && /* @__PURE__ */ jsx(
623
+ leadingControl === "checkbox" && /* @__PURE__ */ jsx2(
303
624
  "span",
304
625
  {
305
626
  "data-testid": "ctxmenu-leading-checkbox",
306
627
  "aria-hidden": "true",
307
628
  style: { pointerEvents: "none", display: "inline-flex" },
308
- children: /* @__PURE__ */ jsx(
629
+ children: /* @__PURE__ */ jsx2(
309
630
  Checkbox,
310
631
  {
311
632
  size,
@@ -317,13 +638,13 @@ var OptionCell = ({
317
638
  )
318
639
  }
319
640
  ),
320
- leadingControl === "radio" && /* @__PURE__ */ jsx(
641
+ leadingControl === "radio" && /* @__PURE__ */ jsx2(
321
642
  "span",
322
643
  {
323
644
  "data-testid": "ctxmenu-leading-radio",
324
645
  "aria-hidden": "true",
325
646
  style: { pointerEvents: "none", display: "inline-flex" },
326
- children: /* @__PURE__ */ jsx(
647
+ children: /* @__PURE__ */ jsx2(
327
648
  Radio,
328
649
  {
329
650
  size,
@@ -338,6 +659,7 @@ var OptionCell = ({
338
659
  leadingIcon,
339
660
  status,
340
661
  iconWrapper,
662
+ slot,
341
663
  slotContent,
342
664
  /* @__PURE__ */ jsxs(
343
665
  "span",
@@ -350,7 +672,7 @@ var OptionCell = ({
350
672
  minWidth: 0
351
673
  },
352
674
  children: [
353
- /* @__PURE__ */ jsx(
675
+ /* @__PURE__ */ jsx2(
354
676
  Typography,
355
677
  {
356
678
  variant: variants.label,
@@ -363,7 +685,7 @@ var OptionCell = ({
363
685
  children: label
364
686
  }
365
687
  ),
366
- description !== void 0 && /* @__PURE__ */ jsx(
688
+ description !== void 0 && /* @__PURE__ */ jsx2(
367
689
  Typography,
368
690
  {
369
691
  variant: variants.description,
@@ -383,7 +705,7 @@ var OptionCell = ({
383
705
  alignItems: "flex-end"
384
706
  },
385
707
  children: [
386
- value !== void 0 && /* @__PURE__ */ jsx(
708
+ value !== void 0 && /* @__PURE__ */ jsx2(
387
709
  Typography,
388
710
  {
389
711
  variant: variants.label,
@@ -392,7 +714,7 @@ var OptionCell = ({
392
714
  children: value
393
715
  }
394
716
  ),
395
- hint !== void 0 && /* @__PURE__ */ jsx(
717
+ hint !== void 0 && /* @__PURE__ */ jsx2(
396
718
  Typography,
397
719
  {
398
720
  variant: variants.description,
@@ -403,7 +725,7 @@ var OptionCell = ({
403
725
  ]
404
726
  }
405
727
  ),
406
- keyboardShortcut && /* @__PURE__ */ jsx(
728
+ keyboardShortcut && /* @__PURE__ */ jsx2(
407
729
  Typography,
408
730
  {
409
731
  as: "kbd",
@@ -412,7 +734,7 @@ var OptionCell = ({
412
734
  children: keyboardShortcut
413
735
  }
414
736
  ),
415
- hasSubmenu && /* @__PURE__ */ jsx(
737
+ hasSubmenu && /* @__PURE__ */ jsx2(
416
738
  SubmenuChevron,
417
739
  {
418
740
  color: theme.colors.content.tertiary,
@@ -421,7 +743,7 @@ var OptionCell = ({
421
743
  ),
422
744
  trailingIcon,
423
745
  hasSubmenu && submenuOpen && submenu && submenuPos && typeof document !== "undefined" && createPortal(
424
- /* @__PURE__ */ jsx(
746
+ /* @__PURE__ */ jsx2(
425
747
  "div",
426
748
  {
427
749
  ref: submenuWrapperRef,
@@ -452,7 +774,11 @@ var HeadingCell = ({
452
774
  themeProductContext,
453
775
  "data-testid": testId
454
776
  }) => {
455
- const { theme } = useResolvedTheme({ themeMode, themeProductContext });
777
+ const { theme: rawTheme } = useResolvedTheme({
778
+ themeMode,
779
+ themeProductContext
780
+ });
781
+ const theme = rawTheme;
456
782
  const ctx = useContextMenu();
457
783
  const size = propSize ?? ctx?.size ?? "md";
458
784
  const sizing = theme.sizing.contextMenu(size);
@@ -480,7 +806,7 @@ var HeadingCell = ({
480
806
  paddingBottom: sizing.itemPaddingVertical
481
807
  },
482
808
  children: [
483
- /* @__PURE__ */ jsx(
809
+ /* @__PURE__ */ jsx2(
484
810
  Typography,
485
811
  {
486
812
  variant: variants.headingAccent,
@@ -489,7 +815,7 @@ var HeadingCell = ({
489
815
  children: label
490
816
  }
491
817
  ),
492
- description !== void 0 && /* @__PURE__ */ jsx(
818
+ description !== void 0 && /* @__PURE__ */ jsx2(
493
819
  Typography,
494
820
  {
495
821
  variant: variants.description,
@@ -504,6 +830,7 @@ var HeadingCell = ({
504
830
  var SearchCell = ({
505
831
  size: propSize,
506
832
  value,
833
+ onChange,
507
834
  onValueChange,
508
835
  placeholder = "Search",
509
836
  autoFocus,
@@ -513,7 +840,11 @@ var SearchCell = ({
513
840
  themeMode,
514
841
  themeProductContext
515
842
  }) => {
516
- const { theme } = useResolvedTheme({ themeMode, themeProductContext });
843
+ const { theme: rawTheme } = useResolvedTheme({
844
+ themeMode,
845
+ themeProductContext
846
+ });
847
+ const theme = rawTheme;
517
848
  const ctx = useContextMenu();
518
849
  const size = propSize ?? ctx?.size ?? "md";
519
850
  const sizing = theme.sizing.contextMenu(size);
@@ -525,7 +856,7 @@ var SearchCell = ({
525
856
  registerCell(id, { type: "search" });
526
857
  return () => unregisterCell(id);
527
858
  }, [registerCell, unregisterCell, id]);
528
- return /* @__PURE__ */ jsx(
859
+ return /* @__PURE__ */ jsx2(
529
860
  "div",
530
861
  {
531
862
  style: {
@@ -546,7 +877,7 @@ var SearchCell = ({
546
877
  borderBottom: `1px solid ${theme.colors.border.secondary}`
547
878
  },
548
879
  children: [
549
- /* @__PURE__ */ jsx(
880
+ /* @__PURE__ */ jsx2(
550
881
  Search,
551
882
  {
552
883
  variant: "line",
@@ -555,16 +886,19 @@ var SearchCell = ({
555
886
  "aria-hidden": true
556
887
  }
557
888
  ),
558
- /* @__PURE__ */ jsx(
889
+ /* @__PURE__ */ jsx2(
559
890
  "input",
560
891
  {
561
892
  type: "search",
562
893
  role: "searchbox",
563
894
  "aria-label": ariaLabel,
564
895
  placeholder,
565
- value,
896
+ value: value ?? "",
566
897
  autoFocus,
567
- onChange: (e) => onValueChange(e.target.value),
898
+ onChange: (e) => {
899
+ onChange?.(e);
900
+ onValueChange?.(e.target.value);
901
+ },
568
902
  "data-testid": testId || testID,
569
903
  style: {
570
904
  flex: 1,
@@ -587,7 +921,11 @@ var SearchCell = ({
587
921
  );
588
922
  };
589
923
  var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) => {
590
- const { theme } = useResolvedTheme({ themeMode, themeProductContext });
924
+ const { theme: rawTheme } = useResolvedTheme({
925
+ themeMode,
926
+ themeProductContext
927
+ });
928
+ const theme = rawTheme;
591
929
  const ctx = useContextMenu();
592
930
  const id = useId();
593
931
  const registerCell = ctx?.registerCell;
@@ -597,7 +935,7 @@ var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) =>
597
935
  registerCell(id, { type: "divider" });
598
936
  return () => unregisterCell(id);
599
937
  }, [registerCell, unregisterCell, id]);
600
- return /* @__PURE__ */ jsx(
938
+ return /* @__PURE__ */ jsx2(
601
939
  "div",
602
940
  {
603
941
  role: "separator",
@@ -611,8 +949,156 @@ var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) =>
611
949
  );
612
950
  };
613
951
 
952
+ // src/ContextMenuSubmenu.tsx
953
+ import {
954
+ useState as useState2,
955
+ useRef,
956
+ useEffect as useEffect2,
957
+ useLayoutEffect as useLayoutEffect2,
958
+ useCallback
959
+ } from "react";
960
+ import { useDesignSystem } from "@xsolla/xui-core";
961
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
962
+ var SUBMENU_GAP2 = 4;
963
+ var OPEN_DELAY_MS = 200;
964
+ var CLOSE_GRACE_MS = 100;
965
+ var ContextMenuSubmenu = ({
966
+ label,
967
+ icon,
968
+ disabled,
969
+ children,
970
+ size: propSize,
971
+ "data-testid": testId = "context-menu-submenu"
972
+ }) => {
973
+ const { theme } = useDesignSystem();
974
+ const xuiTheme = theme;
975
+ const context = useContextMenu();
976
+ const size = propSize || context?.size || "md";
977
+ const sizeStyles = xuiTheme.sizing.contextMenu(size);
978
+ const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
979
+ const [isOpen, setIsOpen] = useState2(false);
980
+ const [visible, setVisible] = useState2(false);
981
+ const [openLeft, setOpenLeft] = useState2(false);
982
+ const [topOffset, setTopOffset] = useState2(0);
983
+ const triggerRef = useRef(null);
984
+ const submenuRef = useRef(null);
985
+ const openTimerRef = useRef(null);
986
+ const closeTimerRef = useRef(null);
987
+ const clearTimers = useCallback(() => {
988
+ if (openTimerRef.current) clearTimeout(openTimerRef.current);
989
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
990
+ }, []);
991
+ const calculatePlacement = useCallback(() => {
992
+ if (!triggerRef.current) return;
993
+ const triggerRect = triggerRef.current.getBoundingClientRect();
994
+ const estimatedWidth = sizeStyles.minWidth + 32;
995
+ const wouldOverflowRight = triggerRect.right + estimatedWidth + SUBMENU_GAP2 > window.innerWidth - 8;
996
+ setOpenLeft(wouldOverflowRight);
997
+ setTopOffset(0);
998
+ }, [sizeStyles.minWidth]);
999
+ useLayoutEffect2(() => {
1000
+ if (!isOpen || !submenuRef.current || !triggerRef.current) return;
1001
+ const submenuRect = submenuRef.current.getBoundingClientRect();
1002
+ const triggerRect = triggerRef.current.getBoundingClientRect();
1003
+ const wouldOverflowRight = triggerRect.right + submenuRect.width + SUBMENU_GAP2 > window.innerWidth - 8;
1004
+ setOpenLeft(wouldOverflowRight);
1005
+ const overflowBottom = triggerRect.top + submenuRect.height - (window.innerHeight - 8);
1006
+ if (overflowBottom > 0) {
1007
+ setTopOffset(-Math.min(overflowBottom, triggerRect.top - 8));
1008
+ } else {
1009
+ setTopOffset(0);
1010
+ }
1011
+ }, [isOpen]);
1012
+ useEffect2(() => {
1013
+ if (!isOpen) {
1014
+ setVisible(false);
1015
+ return;
1016
+ }
1017
+ const raf = requestAnimationFrame(() => setVisible(true));
1018
+ return () => cancelAnimationFrame(raf);
1019
+ }, [isOpen]);
1020
+ useEffect2(() => () => clearTimers(), [clearTimers]);
1021
+ const handleTriggerEnter = () => {
1022
+ if (disabled) return;
1023
+ clearTimers();
1024
+ openTimerRef.current = setTimeout(() => {
1025
+ calculatePlacement();
1026
+ setIsOpen(true);
1027
+ }, OPEN_DELAY_MS);
1028
+ };
1029
+ const handleTriggerLeave = () => {
1030
+ clearTimers();
1031
+ closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
1032
+ };
1033
+ const handleSubmenuEnter = () => clearTimers();
1034
+ const handleSubmenuLeave = () => {
1035
+ clearTimers();
1036
+ closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
1037
+ };
1038
+ const submenuPositionStyle = openLeft ? { right: `calc(100% + ${SUBMENU_GAP2}px)` } : { left: `calc(100% + ${SUBMENU_GAP2}px)` };
1039
+ return /* @__PURE__ */ jsxs2(
1040
+ "div",
1041
+ {
1042
+ ref: triggerRef,
1043
+ style: { position: "relative" },
1044
+ onMouseEnter: handleTriggerEnter,
1045
+ onMouseLeave: handleTriggerLeave,
1046
+ "data-testid": testId,
1047
+ children: [
1048
+ /* @__PURE__ */ jsx3(
1049
+ ContextMenuItem,
1050
+ {
1051
+ type: "option",
1052
+ label,
1053
+ leadingIcon: icon,
1054
+ disabled,
1055
+ hasSubmenu: true,
1056
+ size,
1057
+ "data-testid": `${testId}-trigger`
1058
+ }
1059
+ ),
1060
+ isOpen && /* @__PURE__ */ jsx3(
1061
+ "div",
1062
+ {
1063
+ ref: submenuRef,
1064
+ onMouseEnter: handleSubmenuEnter,
1065
+ onMouseLeave: handleSubmenuLeave,
1066
+ style: {
1067
+ position: "absolute",
1068
+ top: topOffset,
1069
+ ...submenuPositionStyle,
1070
+ zIndex: 1001,
1071
+ opacity: visible ? 1 : 0,
1072
+ transform: visible ? "translateX(0)" : openLeft ? "translateX(4px)" : "translateX(-4px)",
1073
+ transition: "opacity 100ms ease, transform 100ms ease"
1074
+ },
1075
+ "data-testid": `${testId}-content`,
1076
+ children: /* @__PURE__ */ jsx3(
1077
+ Box,
1078
+ {
1079
+ role: "menu",
1080
+ backgroundColor: xuiTheme.colors.background.secondary,
1081
+ borderColor: xuiTheme.colors.border.secondary,
1082
+ borderWidth: 1,
1083
+ borderRadius,
1084
+ paddingVertical: sizeStyles.paddingVertical,
1085
+ style: {
1086
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1087
+ minWidth: sizeStyles.minWidth
1088
+ },
1089
+ children
1090
+ }
1091
+ )
1092
+ }
1093
+ )
1094
+ ]
1095
+ }
1096
+ );
1097
+ };
1098
+ ContextMenuSubmenu.displayName = "ContextMenuSubmenu";
1099
+
614
1100
  // src/hooks/useContextMenuPosition.ts
615
- import { useEffect as useEffect2, useState as useState2 } from "react";
1101
+ import { useEffect as useEffect3, useState as useState3 } from "react";
616
1102
  var splitPlacement = (placement) => {
617
1103
  const [vertical, horizontal] = placement.split("-");
618
1104
  return { vertical, horizontal };
@@ -625,8 +1111,8 @@ var useContextMenuPosition = ({
625
1111
  placement = "bottom-start",
626
1112
  offset = 4
627
1113
  }) => {
628
- const [resolved, setResolved] = useState2();
629
- useEffect2(() => {
1114
+ const [resolved, setResolved] = useState3();
1115
+ useEffect3(() => {
630
1116
  if (!isOpen) {
631
1117
  setResolved(void 0);
632
1118
  return;
@@ -673,30 +1159,19 @@ var useContextMenuPosition = ({
673
1159
  (prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
674
1160
  );
675
1161
  };
676
- let rafId = window.requestAnimationFrame(compute);
1162
+ const rafId = window.requestAnimationFrame(compute);
677
1163
  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
1164
  window.addEventListener("resize", onResize);
688
- window.addEventListener("scroll", onScroll, true);
689
1165
  return () => {
690
1166
  window.cancelAnimationFrame(rafId);
691
1167
  window.removeEventListener("resize", onResize);
692
- window.removeEventListener("scroll", onScroll, true);
693
1168
  };
694
1169
  }, [isOpen, placement, offset, triggerRef, panelRef]);
695
1170
  return resolved;
696
1171
  };
697
1172
 
698
1173
  // src/hooks/useKeyboardNavigation.ts
699
- import { useCallback } from "react";
1174
+ import { useCallback as useCallback2 } from "react";
700
1175
  var isNavigableOption = (meta) => meta.type === "option" && !meta.disabled;
701
1176
  var isTextInputTarget = (target) => {
702
1177
  if (!(target instanceof HTMLElement)) return false;
@@ -711,19 +1186,19 @@ var useKeyboardNavigation = ({
711
1186
  onClose,
712
1187
  triggerRef
713
1188
  }) => {
714
- const findFirstOption = useCallback(() => {
1189
+ const findFirstOption = useCallback2(() => {
715
1190
  for (let i = 0; i < cells.length; i += 1) {
716
1191
  if (isNavigableOption(cells[i].meta)) return i;
717
1192
  }
718
1193
  return -1;
719
1194
  }, [cells]);
720
- const findLastOption = useCallback(() => {
1195
+ const findLastOption = useCallback2(() => {
721
1196
  for (let i = cells.length - 1; i >= 0; i -= 1) {
722
1197
  if (isNavigableOption(cells[i].meta)) return i;
723
1198
  }
724
1199
  return -1;
725
1200
  }, [cells]);
726
- const findNextOption = useCallback(
1201
+ const findNextOption = useCallback2(
727
1202
  (from) => {
728
1203
  const len = cells.length;
729
1204
  if (len === 0) return -1;
@@ -735,7 +1210,7 @@ var useKeyboardNavigation = ({
735
1210
  },
736
1211
  [cells]
737
1212
  );
738
- const findPrevOption = useCallback(
1213
+ const findPrevOption = useCallback2(
739
1214
  (from) => {
740
1215
  const len = cells.length;
741
1216
  if (len === 0) return -1;
@@ -747,7 +1222,7 @@ var useKeyboardNavigation = ({
747
1222
  },
748
1223
  [cells]
749
1224
  );
750
- const handleKeyDown = useCallback(
1225
+ const handleKeyDown = useCallback2(
751
1226
  (event) => {
752
1227
  if (!isOpen) return;
753
1228
  switch (event.key) {
@@ -817,458 +1292,382 @@ var useKeyboardNavigation = ({
817
1292
  };
818
1293
 
819
1294
  // src/ContextMenu.tsx
820
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1295
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1296
+ var DEFAULT_EMPTY_MESSAGE = "No results";
821
1297
  var SEARCH_DEBOUNCE_MS = 200;
822
- var EmptyMessage = ({
823
- children,
824
- color
825
- }) => /* @__PURE__ */ jsx2(
826
- "div",
827
- {
828
- style: {
829
- padding: 12,
830
- color,
831
- fontSize: 14,
832
- textAlign: "center"
833
- },
834
- children
1298
+ var textFromNode = (node) => {
1299
+ if (node === null || node === void 0 || typeof node === "boolean")
1300
+ return "";
1301
+ if (typeof node === "string" || typeof node === "number") return String(node);
1302
+ if (Array.isArray(node)) return node.map(textFromNode).join(" ");
1303
+ if (isValidElement(node)) return textFromNode(node.props.children);
1304
+ return "";
1305
+ };
1306
+ var filterItems = (items, query) => {
1307
+ const normalized = query.trim().toLowerCase();
1308
+ if (!normalized) return [...items];
1309
+ const result = [];
1310
+ let pendingStructural = [];
1311
+ for (const item of items) {
1312
+ if (item.type === "option") {
1313
+ const label = textFromNode(item.label).toLowerCase();
1314
+ if (label.includes(normalized)) {
1315
+ result.push(...pendingStructural, item);
1316
+ pendingStructural = [];
1317
+ }
1318
+ continue;
1319
+ }
1320
+ if (item.type === "heading") {
1321
+ pendingStructural = [item];
1322
+ continue;
1323
+ }
1324
+ if (item.type === "divider") {
1325
+ if (result.length > 0) pendingStructural.push(item);
1326
+ continue;
1327
+ }
835
1328
  }
836
- );
837
- var ContextMenu = (props) => {
838
- const {
839
- type,
840
- items,
1329
+ return result;
1330
+ };
1331
+ var isOption = (item) => item.type === "option";
1332
+ var ContextMenuRoot = forwardRef(
1333
+ ({
841
1334
  children,
1335
+ type = "list",
1336
+ items = [],
842
1337
  size = "md",
843
1338
  searchable,
844
1339
  loading,
845
- emptyMessage,
1340
+ isLoading,
1341
+ emptyMessage = DEFAULT_EMPTY_MESSAGE,
846
1342
  empty,
847
- trigger,
848
- isOpen,
1343
+ isOpen: propIsOpen,
849
1344
  onOpenChange,
850
- closeOnSelect,
851
- width,
852
- maxHeight,
1345
+ trigger,
853
1346
  placement = "bottom-start",
1347
+ position,
1348
+ width,
1349
+ maxHeight = 300,
854
1350
  onSelect,
1351
+ closeOnSelect,
855
1352
  "aria-label": ariaLabel,
856
- "data-testid": testId,
857
1353
  testID,
1354
+ "data-testid": dataTestId,
858
1355
  themeMode,
859
- themeProductContext
860
- } = props;
861
- const { theme } = useResolvedTheme2({ themeMode, themeProductContext });
862
- const isControlled = isOpen !== void 0;
863
- const [internalOpen, setInternalOpen] = useState3(false);
864
- const open = isControlled ? !!isOpen : internalOpen;
865
- const setOpen = useCallback2(
866
- (next) => {
867
- if (!isControlled) setInternalOpen(next);
868
- onOpenChange?.(next);
869
- },
870
- [isControlled, onOpenChange]
871
- );
872
- const [activeIndex, setActiveIndex] = useState3(-1);
873
- const cellsRef = useRef([]);
874
- const [cellsVersion, setCellsVersion] = useState3(0);
875
- const triggerRef = useRef(null);
876
- const panelRef = useRef(null);
877
- const menuId = useId2();
878
- const [query, setQuery] = useState3("");
879
- const [debouncedQuery, setDebouncedQuery] = useState3("");
880
- const debounceTimerRef = useRef(null);
881
- useEffect3(() => {
882
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
883
- debounceTimerRef.current = setTimeout(() => {
884
- setDebouncedQuery(query);
885
- }, SEARCH_DEBOUNCE_MS);
886
- return () => {
887
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
888
- };
889
- }, [query]);
890
- const closeMenu = useCallback2(() => {
891
- setOpen(false);
892
- setActiveIndex(-1);
893
- }, [setOpen]);
894
- const registerCell = useCallback2((id, meta) => {
895
- const existing = cellsRef.current.findIndex((c) => c.id === id);
896
- if (existing === -1) {
1356
+ themeProductContext,
1357
+ style
1358
+ }, ref) => {
1359
+ const { theme } = useDesignSystem2();
1360
+ const xuiTheme = theme;
1361
+ const menuId = useId2();
1362
+ const [internalIsOpen, setInternalIsOpen] = useState4(false);
1363
+ const [activeIndex, setActiveIndex] = useState4(-1);
1364
+ const [cellsVersion, setCellsVersion] = useState4(0);
1365
+ const [query, setQuery] = useState4("");
1366
+ const [debouncedQuery, setDebouncedQuery] = useState4("");
1367
+ const containerRef = useRef2(null);
1368
+ const triggerRef = useRef2(null);
1369
+ const panelRef = useRef2(null);
1370
+ const cellsRef = useRef2([]);
1371
+ const isOpen = propIsOpen !== void 0 ? propIsOpen : internalIsOpen;
1372
+ const sizeStyles = xuiTheme.sizing.contextMenu(size);
1373
+ const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
1374
+ const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
1375
+ const positioned = useContextMenuPosition({
1376
+ triggerRef,
1377
+ panelRef,
1378
+ isOpen: isOpen && !!trigger && !position,
1379
+ placement
1380
+ });
1381
+ const setOpen = useCallback3(
1382
+ (nextOpen) => {
1383
+ if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
1384
+ onOpenChange?.(nextOpen);
1385
+ if (!nextOpen) setActiveIndex(-1);
1386
+ },
1387
+ [propIsOpen, onOpenChange]
1388
+ );
1389
+ const closeMenu = useCallback3(() => {
1390
+ setOpen(false);
1391
+ }, [setOpen]);
1392
+ const toggleMenu = useCallback3(() => {
1393
+ setOpen(!isOpen);
1394
+ }, [isOpen, setOpen]);
1395
+ const registerCell = useCallback3((id, meta) => {
1396
+ const existingIndex = cellsRef.current.findIndex(
1397
+ (cell) => cell.id === id
1398
+ );
1399
+ if (existingIndex >= 0) {
1400
+ cellsRef.current[existingIndex] = { id, meta };
1401
+ setCellsVersion((version) => version + 1);
1402
+ return existingIndex;
1403
+ }
897
1404
  cellsRef.current.push({ id, meta });
898
- setCellsVersion((v) => v + 1);
1405
+ setCellsVersion((version) => version + 1);
899
1406
  return cellsRef.current.length - 1;
900
- }
901
- const prev = cellsRef.current[existing].meta;
902
- cellsRef.current[existing] = { id, meta };
903
- if (prev.disabled !== meta.disabled || prev.type !== meta.type) {
904
- setCellsVersion((v) => v + 1);
905
- }
906
- return existing;
907
- }, []);
908
- const unregisterCell = useCallback2((id) => {
909
- const idx = cellsRef.current.findIndex((c) => c.id === id);
910
- if (idx !== -1) {
911
- cellsRef.current.splice(idx, 1);
912
- setCellsVersion((v) => v + 1);
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,
1407
+ }, []);
1408
+ const unregisterCell = useCallback3((id) => {
1409
+ const index2 = cellsRef.current.findIndex((cell) => cell.id === id);
1410
+ if (index2 >= 0) {
1411
+ cellsRef.current.splice(index2, 1);
1412
+ setCellsVersion((version) => version + 1);
1413
+ }
1414
+ }, []);
1415
+ const getCellIndex = useCallback3((id) => {
1416
+ return cellsRef.current.findIndex((cell) => cell.id === id);
1417
+ }, []);
1418
+ const cells = useMemo(() => [...cellsRef.current], [cellsVersion]);
1419
+ const keyboard = useKeyboardNavigation({
1420
+ isOpen,
1421
+ cells,
928
1422
  activeIndex,
929
1423
  setActiveIndex,
930
- query,
931
- setQuery
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;
1424
+ onClose: closeMenu,
1425
+ triggerRef
1032
1426
  });
1033
- const result = [];
1034
- let pendingHeading = null;
1035
- let lastEmittedWasContent = false;
1036
- let groupHasOption = false;
1037
- for (let i = 0; i < items.length; i += 1) {
1038
- const item = items[i];
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
- }
1427
+ useEffect4(() => {
1428
+ if (!isOpen) {
1429
+ cellsRef.current = [];
1430
+ setCellsVersion((version) => version + 1);
1431
+ setQuery("");
1432
+ setDebouncedQuery("");
1059
1433
  }
1060
- }
1061
- while (result.length > 0 && result[result.length - 1].type === "divider") {
1062
- result.pop();
1063
- }
1064
- void groupHasOption;
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}`
1434
+ }, [isOpen]);
1435
+ useEffect4(() => {
1436
+ const timer = setTimeout(
1437
+ () => setDebouncedQuery(query),
1438
+ SEARCH_DEBOUNCE_MS
1077
1439
  );
1078
- }
1079
- if (item.type === "divider") {
1080
- return /* @__PURE__ */ jsx2(
1440
+ return () => clearTimeout(timer);
1441
+ }, [query]);
1442
+ useEffect4(() => {
1443
+ if (!isOpen || !trigger) return;
1444
+ const onMouseDown = (event) => {
1445
+ const target = event.target;
1446
+ if (!target || containerRef.current?.contains(target)) return;
1447
+ closeMenu();
1448
+ };
1449
+ const onScroll = () => closeMenu();
1450
+ document.addEventListener("mousedown", onMouseDown);
1451
+ window.addEventListener("scroll", onScroll);
1452
+ return () => {
1453
+ document.removeEventListener("mousedown", onMouseDown);
1454
+ window.removeEventListener("scroll", onScroll);
1455
+ };
1456
+ }, [isOpen, trigger, closeMenu]);
1457
+ useLayoutEffect3(() => {
1458
+ if (!isOpen || !panelRef.current) return;
1459
+ const searchbox = panelRef.current.querySelector('[role="searchbox"]');
1460
+ const firstOption = panelRef.current.querySelector(
1461
+ '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
1462
+ );
1463
+ (searchbox ?? firstOption)?.focus();
1464
+ }, [isOpen]);
1465
+ useLayoutEffect3(() => {
1466
+ if (!isOpen || !panelRef.current) return;
1467
+ const activeElement = document.activeElement;
1468
+ if (activeElement && panelRef.current.contains(activeElement)) return;
1469
+ panelRef.current.focus();
1470
+ }, [isOpen, cellsVersion]);
1471
+ useEffect4(() => {
1472
+ if (isOpen) return;
1473
+ triggerRef.current?.focus();
1474
+ }, [isOpen]);
1475
+ const contextValue = useMemo(
1476
+ () => ({
1477
+ size,
1478
+ menuId,
1479
+ closeMenu,
1480
+ activeIndex,
1481
+ setActiveIndex,
1482
+ registerCell,
1483
+ unregisterCell,
1484
+ getCellIndex,
1485
+ cellsVersion,
1486
+ query,
1487
+ setQuery
1488
+ }),
1489
+ [
1490
+ size,
1491
+ menuId,
1492
+ closeMenu,
1493
+ activeIndex,
1494
+ registerCell,
1495
+ unregisterCell,
1496
+ getCellIndex,
1497
+ cellsVersion,
1498
+ query
1499
+ ]
1500
+ );
1501
+ const renderPresetItem = (item, index2) => {
1502
+ if (!isOption(item)) {
1503
+ return /* @__PURE__ */ jsx4(
1504
+ ContextMenuItem,
1505
+ {
1506
+ ...item,
1507
+ themeMode,
1508
+ themeProductContext
1509
+ },
1510
+ `context-menu-item-${index2}`
1511
+ );
1512
+ }
1513
+ const leadingControl = item.leadingControl ?? (type === "checkbox" ? "checkbox" : type === "radio" ? "radio" : void 0);
1514
+ return /* @__PURE__ */ jsx4(
1081
1515
  ContextMenuItem,
1082
1516
  {
1083
1517
  ...item,
1084
- size: item.size ?? size
1518
+ leadingControl,
1519
+ themeMode,
1520
+ themeProductContext,
1521
+ onSelect: () => {
1522
+ item.onSelect?.();
1523
+ onSelect?.(item);
1524
+ if (shouldCloseOnSelect) closeMenu();
1525
+ }
1085
1526
  },
1086
- `d-${key}`
1527
+ `context-menu-item-${index2}`
1087
1528
  );
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
1529
  };
1096
- return /* @__PURE__ */ jsx2(
1097
- ContextMenuItem,
1530
+ const renderedItems = searchable ? filterItems(items, debouncedQuery) : [...items];
1531
+ const renderContent = () => {
1532
+ if (loading || isLoading || type === "loading") {
1533
+ const brandColor = xuiTheme.colors.control.brand.primary.bg;
1534
+ return /* @__PURE__ */ jsx4(
1535
+ Box,
1536
+ {
1537
+ padding: 16,
1538
+ alignItems: "center",
1539
+ justifyContent: "center",
1540
+ minHeight: 60,
1541
+ children: /* @__PURE__ */ jsx4(Spinner, { size: "md", color: brandColor })
1542
+ }
1543
+ );
1544
+ }
1545
+ if (children) return children;
1546
+ const content = renderedItems.map(renderPresetItem);
1547
+ if (searchable) {
1548
+ content.unshift(
1549
+ /* @__PURE__ */ jsx4(
1550
+ "div",
1551
+ {
1552
+ "data-sticky": "top",
1553
+ style: {
1554
+ position: "sticky",
1555
+ top: 0,
1556
+ zIndex: 1,
1557
+ backgroundColor: xuiTheme.colors.background.secondary
1558
+ },
1559
+ children: /* @__PURE__ */ jsx4(
1560
+ ContextMenuItem,
1561
+ {
1562
+ type: "search",
1563
+ value: query,
1564
+ onValueChange: setQuery,
1565
+ placeholder: "Search",
1566
+ autoFocus: true,
1567
+ themeMode,
1568
+ themeProductContext
1569
+ }
1570
+ )
1571
+ },
1572
+ "context-menu-search"
1573
+ )
1574
+ );
1575
+ }
1576
+ if (content.length > (searchable ? 1 : 0)) return content;
1577
+ return empty ?? /* @__PURE__ */ jsx4("div", { style: { padding: 16 }, children: emptyMessage });
1578
+ };
1579
+ const assignPanelRef = (node) => {
1580
+ panelRef.current = node;
1581
+ if (typeof ref === "function") ref(node);
1582
+ else if (ref) ref.current = node;
1583
+ };
1584
+ const assignTriggerRef = (node) => {
1585
+ triggerRef.current = node;
1586
+ };
1587
+ const triggerNode = trigger && isValidElement(trigger) ? cloneElement(trigger, {
1588
+ ref: assignTriggerRef,
1589
+ "aria-haspopup": "menu",
1590
+ "aria-expanded": isOpen ? "true" : "false",
1591
+ onClick: (event) => {
1592
+ trigger.props.onClick?.(event);
1593
+ if (!event.defaultPrevented) toggleMenu();
1594
+ }
1595
+ }) : trigger ? /* @__PURE__ */ jsx4(
1596
+ "span",
1098
1597
  {
1099
- ...composed,
1100
- size: composed.size ?? size,
1101
- onSelect: wrappedSelect
1102
- },
1103
- `o-${key}`
1104
- );
1105
- };
1106
- const isLoadingState = loading;
1107
- let bodyContent = null;
1108
- let isBodyEmpty = false;
1109
- let searchNode = null;
1110
- if (isLoadingState) {
1111
- bodyContent = /* @__PURE__ */ jsx2(
1598
+ ref: assignTriggerRef,
1599
+ role: "button",
1600
+ tabIndex: 0,
1601
+ "aria-haspopup": "menu",
1602
+ "aria-expanded": isOpen ? "true" : "false",
1603
+ onClick: toggleMenu,
1604
+ children: trigger
1605
+ }
1606
+ ) : null;
1607
+ const positionStyle = position ? {
1608
+ position: "fixed",
1609
+ left: position.x,
1610
+ top: position.y
1611
+ } : trigger ? {
1612
+ position: "fixed",
1613
+ left: positioned?.left ?? 0,
1614
+ top: positioned?.top ?? 0
1615
+ } : void 0;
1616
+ return /* @__PURE__ */ jsx4(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
1112
1617
  "div",
1113
1618
  {
1619
+ ref: containerRef,
1114
1620
  style: {
1115
- display: "flex",
1116
- alignItems: "center",
1117
- justifyContent: "center",
1118
- padding: 16
1621
+ position: trigger || position ? "relative" : void 0,
1622
+ display: trigger ? "inline-block" : void 0
1119
1623
  },
1120
- children: /* @__PURE__ */ jsx2(Spinner, { size: size === "xl" ? "lg" : size === "lg" ? "md" : "sm" })
1121
- }
1122
- );
1123
- } else if (children !== void 0 && children !== null) {
1124
- const childArr = React2.Children.toArray(children);
1125
- if (childArr.length === 0) {
1126
- isBodyEmpty = true;
1127
- } else {
1128
- bodyContent = children;
1129
- }
1130
- } else if (type && items) {
1131
- if (searchable) {
1132
- searchNode = /* @__PURE__ */ jsx2(
1133
- ContextMenuItem,
1134
- {
1135
- type: "search",
1136
- value: query,
1137
- onValueChange: setQuery,
1138
- size
1139
- }
1140
- );
1141
- }
1142
- const visible = filteredItems ?? [];
1143
- const optionCount = visible.filter((i) => i.type === "option").length;
1144
- if (optionCount === 0) {
1145
- isBodyEmpty = true;
1146
- } else {
1147
- bodyContent = visible.map((it, idx) => renderPresetItem(it, idx));
1148
- }
1149
- } else {
1150
- isBodyEmpty = true;
1151
- }
1152
- if (isBodyEmpty) {
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
- }
1624
+ children: [
1625
+ triggerNode,
1626
+ isOpen && /* @__PURE__ */ jsx4(
1627
+ Box,
1628
+ {
1629
+ ref: assignPanelRef,
1630
+ role: "menu",
1631
+ "aria-label": ariaLabel,
1632
+ "data-testid": dataTestId ?? testID ?? "context-menu",
1633
+ "data-placement": positioned?.placement ?? placement,
1634
+ backgroundColor: xuiTheme.colors.background.secondary,
1635
+ borderColor: xuiTheme.colors.border.secondary,
1636
+ borderWidth: 1,
1637
+ borderRadius,
1638
+ paddingVertical: sizeStyles.paddingVertical,
1639
+ width,
1640
+ minWidth: sizeStyles.minWidth,
1641
+ tabIndex: -1,
1642
+ onKeyDown: keyboard.handleKeyDown,
1643
+ onMouseLeave: () => setActiveIndex(-1),
1644
+ style: {
1645
+ ...positionStyle,
1646
+ ...style,
1647
+ zIndex: 1e3,
1648
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1649
+ maxHeight,
1650
+ overflowY: "auto",
1651
+ outline: "none"
1652
+ },
1653
+ children: renderContent()
1654
+ }
1655
+ )
1656
+ ]
1206
1657
  }
1207
- skipFocusRestoreRef.current = true;
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 };
1658
+ ) });
1266
1659
  }
1267
- }
1660
+ );
1661
+ ContextMenuRoot.displayName = "ContextMenu";
1662
+ var ContextMenu = Object.assign(ContextMenuRoot, {
1663
+ Item: ContextMenuItem,
1664
+ Submenu: ContextMenuSubmenu
1665
+ });
1268
1666
  export {
1269
1667
  ContextMenu,
1270
1668
  ContextMenuContext,
1271
1669
  ContextMenuItem,
1670
+ ContextMenuSubmenu,
1272
1671
  useContextMenu,
1273
1672
  useContextMenuPosition,
1274
1673
  useContextMenuRequired,