@xsolla/xui-context-menu 0.172.2 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/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,20 +42,272 @@ __export(index_exports, {
41
42
  module.exports = __toCommonJS(index_exports);
42
43
 
43
44
  // src/ContextMenu.tsx
44
- var import_react5 = __toESM(require("react"));
45
- var import_react_dom2 = require("react-dom");
46
- var import_xui_core2 = require("@xsolla/xui-core");
45
+ var import_react8 = require("react");
46
+
47
+ // ../../foundation/primitives-web/src/Box.tsx
48
+ var import_react2 = __toESM(require("react"));
49
+ var import_styled_components = __toESM(require("styled-components"));
50
+
51
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
52
+ var import_react = __toESM(require("react"));
53
+
54
+ // ../../../node_modules/@emotion/memoize/dist/memoize.esm.js
55
+ function memoize(fn) {
56
+ var cache = {};
57
+ return function(arg) {
58
+ if (cache[arg] === void 0) cache[arg] = fn(arg);
59
+ return cache[arg];
60
+ };
61
+ }
62
+ var memoize_esm_default = memoize;
63
+
64
+ // ../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.esm.js
65
+ 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)-.*))$/;
66
+ var index = memoize_esm_default(
67
+ function(prop) {
68
+ return reactPropsRegex.test(prop) || prop.charCodeAt(0) === 111 && prop.charCodeAt(1) === 110 && prop.charCodeAt(2) < 91;
69
+ }
70
+ /* Z+1 */
71
+ );
72
+ var is_prop_valid_esm_default = index;
73
+
74
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
75
+ var ADDITIONAL_BLOCKED_PROPS = /* @__PURE__ */ new Set([
76
+ // RN-only event handlers (pass isPropValid's on* pattern)
77
+ "onPress",
78
+ "onChangeText",
79
+ "onLayout",
80
+ "onMoveShouldSetResponder",
81
+ "onResponderGrant",
82
+ "onResponderMove",
83
+ "onResponderRelease",
84
+ "onResponderTerminate",
85
+ // SVG attributes that pass isPropValid
86
+ "strokeWidth",
87
+ // CSS properties that pass isPropValid but are used as component props
88
+ "overflow",
89
+ "cursor",
90
+ "fontSize",
91
+ "fontWeight",
92
+ "fontFamily",
93
+ "textDecoration"
94
+ ]);
95
+ function shouldForwardProp(key) {
96
+ if (ADDITIONAL_BLOCKED_PROPS.has(key)) return false;
97
+ return is_prop_valid_esm_default(key);
98
+ }
99
+ function createFilteredElement(defaultTag) {
100
+ const Component = import_react.default.forwardRef(
101
+ ({ children, elementType, ...props }, ref) => {
102
+ const Tag = elementType || defaultTag;
103
+ const htmlProps = {};
104
+ for (const key of Object.keys(props)) {
105
+ if (shouldForwardProp(key)) {
106
+ htmlProps[key] = props[key];
107
+ }
108
+ }
109
+ return import_react.default.createElement(
110
+ Tag,
111
+ { ref, ...htmlProps },
112
+ children
113
+ );
114
+ }
115
+ );
116
+ Component.displayName = `Filtered(${defaultTag})`;
117
+ return Component;
118
+ }
119
+
120
+ // ../../foundation/primitives-web/src/Box.tsx
121
+ var import_jsx_runtime = require("react/jsx-runtime");
122
+ var FilteredDiv = createFilteredElement("div");
123
+ var StyledBox = (0, import_styled_components.default)(FilteredDiv)`
124
+ display: flex;
125
+ box-sizing: border-box;
126
+ background-color: ${(props) => props.backgroundColor || "transparent"};
127
+ border-color: ${(props) => props.borderColor || "transparent"};
128
+ border-width: ${(props) => typeof props.borderWidth === "number" ? `${props.borderWidth}px` : props.borderWidth || 0};
129
+
130
+ ${(props) => props.borderBottomWidth !== void 0 && `
131
+ border-bottom-width: ${typeof props.borderBottomWidth === "number" ? `${props.borderBottomWidth}px` : props.borderBottomWidth};
132
+ border-bottom-color: ${props.borderBottomColor || props.borderColor || "transparent"};
133
+ border-bottom-style: solid;
134
+ `}
135
+ ${(props) => props.borderTopWidth !== void 0 && `
136
+ border-top-width: ${typeof props.borderTopWidth === "number" ? `${props.borderTopWidth}px` : props.borderTopWidth};
137
+ border-top-color: ${props.borderTopColor || props.borderColor || "transparent"};
138
+ border-top-style: solid;
139
+ `}
140
+ ${(props) => props.borderLeftWidth !== void 0 && `
141
+ border-left-width: ${typeof props.borderLeftWidth === "number" ? `${props.borderLeftWidth}px` : props.borderLeftWidth};
142
+ border-left-color: ${props.borderLeftColor || props.borderColor || "transparent"};
143
+ border-left-style: solid;
144
+ `}
145
+ ${(props) => props.borderRightWidth !== void 0 && `
146
+ border-right-width: ${typeof props.borderRightWidth === "number" ? `${props.borderRightWidth}px` : props.borderRightWidth};
147
+ border-right-color: ${props.borderRightColor || props.borderColor || "transparent"};
148
+ border-right-style: solid;
149
+ `}
150
+
151
+ border-style: ${(props) => props.borderStyle || (props.borderWidth || props.borderBottomWidth || props.borderTopWidth || props.borderLeftWidth || props.borderRightWidth ? "solid" : "none")};
152
+ border-radius: ${(props) => typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius || 0};
153
+ height: ${(props) => typeof props.height === "number" ? `${props.height}px` : props.height || "auto"};
154
+ width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width || "auto"};
155
+ min-width: ${(props) => typeof props.minWidth === "number" ? `${props.minWidth}px` : props.minWidth || "auto"};
156
+ min-height: ${(props) => typeof props.minHeight === "number" ? `${props.minHeight}px` : props.minHeight || "auto"};
157
+ max-width: ${(props) => typeof props.maxWidth === "number" ? `${props.maxWidth}px` : props.maxWidth || "none"};
158
+ max-height: ${(props) => typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight || "none"};
159
+
160
+ padding: ${(props) => typeof props.padding === "number" ? `${props.padding}px` : props.padding || 0};
161
+ ${(props) => props.paddingHorizontal && `
162
+ padding-left: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
163
+ padding-right: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
164
+ `}
165
+ ${(props) => props.paddingVertical && `
166
+ padding-top: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
167
+ padding-bottom: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
168
+ `}
169
+ ${(props) => props.paddingTop !== void 0 && `padding-top: ${typeof props.paddingTop === "number" ? `${props.paddingTop}px` : props.paddingTop};`}
170
+ ${(props) => props.paddingBottom !== void 0 && `padding-bottom: ${typeof props.paddingBottom === "number" ? `${props.paddingBottom}px` : props.paddingBottom};`}
171
+ ${(props) => props.paddingLeft !== void 0 && `padding-left: ${typeof props.paddingLeft === "number" ? `${props.paddingLeft}px` : props.paddingLeft};`}
172
+ ${(props) => props.paddingRight !== void 0 && `padding-right: ${typeof props.paddingRight === "number" ? `${props.paddingRight}px` : props.paddingRight};`}
173
+
174
+ margin: ${(props) => typeof props.margin === "number" ? `${props.margin}px` : props.margin || 0};
175
+ ${(props) => props.marginTop !== void 0 && `margin-top: ${typeof props.marginTop === "number" ? `${props.marginTop}px` : props.marginTop};`}
176
+ ${(props) => props.marginBottom !== void 0 && `margin-bottom: ${typeof props.marginBottom === "number" ? `${props.marginBottom}px` : props.marginBottom};`}
177
+ ${(props) => props.marginLeft !== void 0 && `margin-left: ${typeof props.marginLeft === "number" ? `${props.marginLeft}px` : props.marginLeft};`}
178
+ ${(props) => props.marginRight !== void 0 && `margin-right: ${typeof props.marginRight === "number" ? `${props.marginRight}px` : props.marginRight};`}
179
+
180
+ flex-direction: ${(props) => props.flexDirection || "column"};
181
+ flex-wrap: ${(props) => props.flexWrap || "nowrap"};
182
+ align-items: ${(props) => props.alignItems || "stretch"};
183
+ justify-content: ${(props) => props.justifyContent || "flex-start"};
184
+ cursor: ${(props) => props.cursor ? props.cursor : props.onClick || props.onPress ? "pointer" : "inherit"};
185
+ position: ${(props) => props.position || "static"};
186
+ top: ${(props) => typeof props.top === "number" ? `${props.top}px` : props.top};
187
+ bottom: ${(props) => typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom};
188
+ left: ${(props) => typeof props.left === "number" ? `${props.left}px` : props.left};
189
+ right: ${(props) => typeof props.right === "number" ? `${props.right}px` : props.right};
190
+ flex: ${(props) => props.flex};
191
+ flex-shrink: ${(props) => props.flexShrink ?? 1};
192
+ gap: ${(props) => typeof props.gap === "number" ? `${props.gap}px` : props.gap || 0};
193
+ align-self: ${(props) => props.alignSelf || "auto"};
194
+ overflow: ${(props) => props.overflow || "visible"};
195
+ overflow-x: ${(props) => props.overflowX || "visible"};
196
+ overflow-y: ${(props) => props.overflowY || "visible"};
197
+ z-index: ${(props) => props.zIndex};
198
+ opacity: ${(props) => props.disabled ? 0.5 : 1};
199
+ pointer-events: ${(props) => props.disabled ? "none" : "auto"};
200
+
201
+ &:hover {
202
+ ${(props) => props.hoverStyle?.backgroundColor && `background-color: ${props.hoverStyle.backgroundColor};`}
203
+ ${(props) => props.hoverStyle?.borderColor && `border-color: ${props.hoverStyle.borderColor};`}
204
+ }
205
+
206
+ &:active {
207
+ ${(props) => props.pressStyle?.backgroundColor && `background-color: ${props.pressStyle.backgroundColor};`}
208
+ }
209
+ `;
210
+ var Box = import_react2.default.forwardRef(
211
+ ({
212
+ children,
213
+ onPress,
214
+ onKeyDown,
215
+ onKeyUp,
216
+ role,
217
+ "aria-label": ariaLabel,
218
+ "aria-labelledby": ariaLabelledBy,
219
+ "aria-current": ariaCurrent,
220
+ "aria-disabled": ariaDisabled,
221
+ "aria-live": ariaLive,
222
+ "aria-busy": ariaBusy,
223
+ "aria-describedby": ariaDescribedBy,
224
+ "aria-expanded": ariaExpanded,
225
+ "aria-haspopup": ariaHasPopup,
226
+ "aria-pressed": ariaPressed,
227
+ "aria-controls": ariaControls,
228
+ tabIndex,
229
+ as,
230
+ src,
231
+ alt,
232
+ onError,
233
+ onLoad,
234
+ type,
235
+ disabled,
236
+ id,
237
+ testID,
238
+ "data-testid": dataTestId,
239
+ ...props
240
+ }, ref) => {
241
+ if (as === "img" && src) {
242
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
243
+ "img",
244
+ {
245
+ src,
246
+ alt: alt || "",
247
+ onError,
248
+ onLoad,
249
+ style: {
250
+ display: "block",
251
+ objectFit: "cover",
252
+ width: typeof props.width === "number" ? `${props.width}px` : props.width,
253
+ height: typeof props.height === "number" ? `${props.height}px` : props.height,
254
+ borderRadius: typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius,
255
+ position: props.position,
256
+ top: typeof props.top === "number" ? `${props.top}px` : props.top,
257
+ left: typeof props.left === "number" ? `${props.left}px` : props.left,
258
+ right: typeof props.right === "number" ? `${props.right}px` : props.right,
259
+ bottom: typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom,
260
+ ...props.style
261
+ }
262
+ }
263
+ );
264
+ }
265
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
266
+ StyledBox,
267
+ {
268
+ ref,
269
+ elementType: as,
270
+ id,
271
+ type: as === "button" ? type || "button" : void 0,
272
+ disabled: as === "button" ? disabled : void 0,
273
+ onClick: onPress,
274
+ onKeyDown,
275
+ onKeyUp,
276
+ role,
277
+ "aria-label": ariaLabel,
278
+ "aria-labelledby": ariaLabelledBy,
279
+ "aria-current": ariaCurrent,
280
+ "aria-disabled": ariaDisabled,
281
+ "aria-busy": ariaBusy,
282
+ "aria-describedby": ariaDescribedBy,
283
+ "aria-expanded": ariaExpanded,
284
+ "aria-haspopup": ariaHasPopup,
285
+ "aria-pressed": ariaPressed,
286
+ "aria-controls": ariaControls,
287
+ "aria-live": ariaLive,
288
+ tabIndex: tabIndex !== void 0 ? tabIndex : void 0,
289
+ "data-testid": dataTestId || testID,
290
+ ...props,
291
+ children
292
+ }
293
+ );
294
+ }
295
+ );
296
+ Box.displayName = "Box";
297
+
298
+ // src/ContextMenu.tsx
299
+ var import_xui_core3 = require("@xsolla/xui-core");
47
300
  var import_xui_spinner = require("@xsolla/xui-spinner");
48
301
 
49
302
  // src/ContextMenuContext.tsx
50
- var import_react = require("react");
51
- var ContextMenuContext = (0, import_react.createContext)(void 0);
303
+ var import_react3 = require("react");
304
+ var ContextMenuContext = (0, import_react3.createContext)(void 0);
52
305
  var useContextMenu = () => {
53
- const context = (0, import_react.useContext)(ContextMenuContext);
306
+ const context = (0, import_react3.useContext)(ContextMenuContext);
54
307
  return context;
55
308
  };
56
309
  var useContextMenuRequired = () => {
57
- const context = (0, import_react.useContext)(ContextMenuContext);
310
+ const context = (0, import_react3.useContext)(ContextMenuContext);
58
311
  if (!context) {
59
312
  throw new Error(
60
313
  "useContextMenuRequired must be used within a ContextMenu component"
@@ -64,14 +317,14 @@ var useContextMenuRequired = () => {
64
317
  };
65
318
 
66
319
  // src/ContextMenuItem.tsx
67
- var import_react2 = __toESM(require("react"));
320
+ var import_react4 = __toESM(require("react"));
68
321
  var import_react_dom = require("react-dom");
69
322
  var import_xui_core = require("@xsolla/xui-core");
70
323
  var import_xui_typography = require("@xsolla/xui-typography");
71
324
  var import_xui_checkbox = require("@xsolla/xui-checkbox");
72
325
  var import_xui_radio = require("@xsolla/xui-radio");
73
326
  var import_xui_icons_base = require("@xsolla/xui-icons-base");
74
- var import_jsx_runtime = require("react/jsx-runtime");
327
+ var import_jsx_runtime2 = require("react/jsx-runtime");
75
328
  var sizeToVariants = {
76
329
  xl: { label: "bodyLg", description: "bodyLg", headingAccent: "bodyLgAccent" },
77
330
  lg: { label: "bodyLg", description: "bodyMd", headingAccent: "bodyMdAccent" },
@@ -81,18 +334,51 @@ var sizeToVariants = {
81
334
  var sizeLabelOverride = {
82
335
  xl: { fontSize: 20, lineHeight: "26px" }
83
336
  };
337
+ var SUBMENU_GAP = 8;
338
+ var SUBMENU_VIEWPORT_PADDING = 8;
339
+ var clipsOverflow = (style) => {
340
+ return /(auto|scroll|hidden|clip)/.test(
341
+ `${style.overflow}${style.overflowX}${style.overflowY}`
342
+ );
343
+ };
344
+ var getSubmenuBoundary = (node) => {
345
+ const viewport = {
346
+ top: SUBMENU_VIEWPORT_PADDING,
347
+ right: window.innerWidth - SUBMENU_VIEWPORT_PADDING,
348
+ bottom: window.innerHeight - SUBMENU_VIEWPORT_PADDING,
349
+ left: SUBMENU_VIEWPORT_PADDING
350
+ };
351
+ let current = node.parentElement;
352
+ while (current && current !== document.body) {
353
+ const style = window.getComputedStyle(current);
354
+ if (clipsOverflow(style) && current.getAttribute("role") !== "menu") {
355
+ const rect = current.getBoundingClientRect();
356
+ return {
357
+ top: Math.max(viewport.top, rect.top + SUBMENU_VIEWPORT_PADDING),
358
+ right: Math.min(viewport.right, rect.right - SUBMENU_VIEWPORT_PADDING),
359
+ bottom: Math.min(
360
+ viewport.bottom,
361
+ rect.bottom - SUBMENU_VIEWPORT_PADDING
362
+ ),
363
+ left: Math.max(viewport.left, rect.left + SUBMENU_VIEWPORT_PADDING)
364
+ };
365
+ }
366
+ current = current.parentElement;
367
+ }
368
+ return viewport;
369
+ };
84
370
  var ContextMenuItem = (props) => {
85
- if (props.type === "option") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OptionCell, { ...props });
86
- if (props.type === "heading") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HeadingCell, { ...props });
87
- if (props.type === "divider") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DividerCell, { ...props });
88
- if (props.type === "search") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchCell, { ...props });
371
+ if (props.type === "option") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OptionCell, { ...props });
372
+ if (props.type === "heading") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HeadingCell, { ...props });
373
+ if (props.type === "divider") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DividerCell, { ...props });
374
+ if (props.type === "search") return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SearchCell, { ...props });
89
375
  return null;
90
376
  };
91
377
  ContextMenuItem.displayName = "ContextMenuItem";
92
378
  var SubmenuChevron = ({
93
379
  color,
94
380
  size
95
- }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
381
+ }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
96
382
  "span",
97
383
  {
98
384
  "data-testid": "ctxmenu-submenu-chevron",
@@ -104,7 +390,7 @@ var SubmenuChevron = ({
104
390
  width: size,
105
391
  height: size
106
392
  },
107
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
393
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
108
394
  "svg",
109
395
  {
110
396
  width: size,
@@ -112,7 +398,7 @@ var SubmenuChevron = ({
112
398
  viewBox: "0 0 24 24",
113
399
  fill: "none",
114
400
  xmlns: "http://www.w3.org/2000/svg",
115
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
401
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
116
402
  "path",
117
403
  {
118
404
  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 +420,7 @@ var OptionCell = ({
134
420
  leadingIcon,
135
421
  status,
136
422
  iconWrapper,
423
+ slot,
137
424
  slotContent,
138
425
  value,
139
426
  hint,
@@ -142,12 +429,17 @@ var OptionCell = ({
142
429
  hasSubmenu,
143
430
  submenu,
144
431
  onSelect,
432
+ onCheckedChange,
145
433
  testID,
146
434
  themeMode,
147
435
  themeProductContext,
148
436
  "data-testid": testId
149
437
  }) => {
150
- const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
438
+ const { theme: rawTheme } = (0, import_xui_core.useResolvedTheme)({
439
+ themeMode,
440
+ themeProductContext
441
+ });
442
+ const theme = rawTheme;
151
443
  const ctx = useContextMenu();
152
444
  const size = propSize ?? ctx?.size ?? "md";
153
445
  const sizing = theme.sizing.contextMenu(size);
@@ -156,12 +448,12 @@ var OptionCell = ({
156
448
  const registerCell = ctx?.registerCell;
157
449
  const unregisterCell = ctx?.unregisterCell;
158
450
  const getCellIndex = ctx?.getCellIndex;
159
- const [isHovered, setIsHovered] = (0, import_react2.useState)(false);
160
- const [submenuOpen, setSubmenuOpen] = (0, import_react2.useState)(false);
161
- const [submenuPos, setSubmenuPos] = (0, import_react2.useState)(null);
162
- const optionRef = import_react2.default.useRef(null);
163
- const submenuWrapperRef = import_react2.default.useRef(null);
164
- const closeTimerRef = import_react2.default.useRef(
451
+ const [isHovered, setIsHovered] = (0, import_react4.useState)(false);
452
+ const [submenuOpen, setSubmenuOpen] = (0, import_react4.useState)(false);
453
+ const [submenuPos, setSubmenuPos] = (0, import_react4.useState)(null);
454
+ const optionRef = import_react4.default.useRef(null);
455
+ const submenuWrapperRef = import_react4.default.useRef(null);
456
+ const closeTimerRef = import_react4.default.useRef(
165
457
  null
166
458
  );
167
459
  const cancelClose = () => {
@@ -174,8 +466,8 @@ var OptionCell = ({
174
466
  cancelClose();
175
467
  closeTimerRef.current = setTimeout(() => setSubmenuOpen(false), 120);
176
468
  };
177
- (0, import_react2.useEffect)(() => () => cancelClose(), []);
178
- (0, import_react2.useEffect)(() => {
469
+ (0, import_react4.useEffect)(() => () => cancelClose(), []);
470
+ (0, import_react4.useEffect)(() => {
179
471
  if (!hasSubmenu || !submenuOpen) return;
180
472
  const onMouseDown = (e) => {
181
473
  const target = e.target;
@@ -195,7 +487,7 @@ var OptionCell = ({
195
487
  document.addEventListener("mousedown", onMouseDown);
196
488
  return () => document.removeEventListener("mousedown", onMouseDown);
197
489
  }, [hasSubmenu, submenuOpen]);
198
- (0, import_react2.useLayoutEffect)(() => {
490
+ (0, import_react4.useLayoutEffect)(() => {
199
491
  if (!hasSubmenu || !submenuOpen) {
200
492
  setSubmenuPos(null);
201
493
  return;
@@ -204,7 +496,32 @@ var OptionCell = ({
204
496
  const node = optionRef.current;
205
497
  if (!node) return;
206
498
  const rect = node.getBoundingClientRect();
207
- setSubmenuPos({ top: rect.top, left: rect.right });
499
+ const submenuRect = submenuWrapperRef.current?.getBoundingClientRect();
500
+ const submenuWidth = submenuRect?.width ?? 0;
501
+ const submenuHeight = submenuRect?.height ?? 0;
502
+ const boundary = getSubmenuBoundary(node);
503
+ const rightSideLeft = rect.right + SUBMENU_GAP;
504
+ const leftSideLeft = rect.left - SUBMENU_GAP - submenuWidth;
505
+ const opensLeft = submenuWidth > 0 && rightSideLeft + submenuWidth > boundary.right;
506
+ let left = opensLeft ? leftSideLeft : rightSideLeft;
507
+ if (submenuWidth > 0) {
508
+ if (opensLeft) {
509
+ left = Math.max(SUBMENU_VIEWPORT_PADDING, left);
510
+ } else {
511
+ left = Math.min(
512
+ Math.max(boundary.left, left),
513
+ Math.max(boundary.left, boundary.right - submenuWidth)
514
+ );
515
+ }
516
+ }
517
+ let top = rect.top;
518
+ if (submenuHeight > 0 && top + submenuHeight > boundary.bottom) {
519
+ top = boundary.bottom - submenuHeight;
520
+ }
521
+ top = Math.max(boundary.top, top);
522
+ setSubmenuPos(
523
+ (prev) => prev?.top === top && prev.left === left ? prev : { top, left }
524
+ );
208
525
  };
209
526
  update();
210
527
  window.addEventListener("scroll", update, true);
@@ -213,10 +530,10 @@ var OptionCell = ({
213
530
  window.removeEventListener("scroll", update, true);
214
531
  window.removeEventListener("resize", update);
215
532
  };
216
- }, [hasSubmenu, submenuOpen]);
217
- const onSelectRef = import_react2.default.useRef(onSelect);
533
+ });
534
+ const onSelectRef = import_react4.default.useRef(onSelect);
218
535
  onSelectRef.current = onSelect;
219
- (0, import_react2.useEffect)(() => {
536
+ (0, import_react4.useEffect)(() => {
220
537
  if (!registerCell || !unregisterCell) return;
221
538
  registerCell(id, {
222
539
  type: "option",
@@ -224,7 +541,7 @@ var OptionCell = ({
224
541
  });
225
542
  return () => unregisterCell(id);
226
543
  }, [registerCell, unregisterCell, id]);
227
- (0, import_react2.useEffect)(() => {
544
+ (0, import_react4.useEffect)(() => {
228
545
  if (!registerCell) return;
229
546
  registerCell(id, {
230
547
  type: "option",
@@ -232,12 +549,12 @@ var OptionCell = ({
232
549
  onSelect: () => onSelectRef.current?.()
233
550
  });
234
551
  }, [registerCell, id, disabled]);
235
- const index = getCellIndex ? getCellIndex(id) : -1;
236
- const isActive = ctx ? index >= 0 && ctx.activeIndex === index : false;
552
+ const index2 = getCellIndex ? getCellIndex(id) : -1;
553
+ const isActive = ctx ? index2 >= 0 && ctx.activeIndex === index2 : false;
237
554
  const inHoverState = isActive || !ctx && isHovered || hasSubmenu && submenuOpen;
238
555
  const handleEnter = () => {
239
556
  if (disabled) return;
240
- if (ctx && index >= 0) ctx.setActiveIndex(index);
557
+ if (ctx && index2 >= 0) ctx.setActiveIndex(index2);
241
558
  if (!ctx) setIsHovered(true);
242
559
  if (hasSubmenu) setSubmenuOpen(true);
243
560
  };
@@ -247,7 +564,7 @@ var OptionCell = ({
247
564
  };
248
565
  const labelColor = disabled ? theme.colors.control.input.textDisable : destructive ? theme.colors.content.alert.primary : theme.colors.content.primary;
249
566
  const bg = inHoverState ? theme.colors.control.input.bgHover : "transparent";
250
- const role = !hasSubmenu && checked !== void 0 ? "menuitemcheckbox" : "menuitem";
567
+ const role = !hasSubmenu && checked !== void 0 ? leadingControl === "radio" ? "menuitemradio" : "menuitemcheckbox" : "menuitem";
251
568
  const ariaChecked = !hasSubmenu && checked !== void 0 ? checked ? "true" : "false" : void 0;
252
569
  const handleClick = () => {
253
570
  if (disabled) return;
@@ -255,6 +572,9 @@ var OptionCell = ({
255
572
  setSubmenuOpen(true);
256
573
  return;
257
574
  }
575
+ if (checked !== void 0) {
576
+ onCheckedChange?.(!checked);
577
+ }
258
578
  onSelect?.();
259
579
  };
260
580
  const closeSubmenuAndFocus = () => {
@@ -284,10 +604,13 @@ var OptionCell = ({
284
604
  }
285
605
  if (e.key === "Enter" || e.key === " ") {
286
606
  e.preventDefault();
607
+ if (checked !== void 0) {
608
+ onCheckedChange?.(!checked);
609
+ }
287
610
  onSelect?.();
288
611
  }
289
612
  };
290
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
613
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
291
614
  "div",
292
615
  {
293
616
  ref: optionRef,
@@ -327,13 +650,13 @@ var OptionCell = ({
327
650
  outline: "none"
328
651
  },
329
652
  children: [
330
- leadingControl === "checkbox" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
653
+ leadingControl === "checkbox" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
331
654
  "span",
332
655
  {
333
656
  "data-testid": "ctxmenu-leading-checkbox",
334
657
  "aria-hidden": "true",
335
658
  style: { pointerEvents: "none", display: "inline-flex" },
336
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
659
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
337
660
  import_xui_checkbox.Checkbox,
338
661
  {
339
662
  size,
@@ -345,13 +668,13 @@ var OptionCell = ({
345
668
  )
346
669
  }
347
670
  ),
348
- leadingControl === "radio" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
671
+ leadingControl === "radio" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
349
672
  "span",
350
673
  {
351
674
  "data-testid": "ctxmenu-leading-radio",
352
675
  "aria-hidden": "true",
353
676
  style: { pointerEvents: "none", display: "inline-flex" },
354
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
677
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
355
678
  import_xui_radio.Radio,
356
679
  {
357
680
  size,
@@ -366,8 +689,9 @@ var OptionCell = ({
366
689
  leadingIcon,
367
690
  status,
368
691
  iconWrapper,
692
+ slot,
369
693
  slotContent,
370
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
694
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
371
695
  "span",
372
696
  {
373
697
  style: {
@@ -378,7 +702,7 @@ var OptionCell = ({
378
702
  minWidth: 0
379
703
  },
380
704
  children: [
381
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
705
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
382
706
  import_xui_typography.Typography,
383
707
  {
384
708
  variant: variants.label,
@@ -391,7 +715,7 @@ var OptionCell = ({
391
715
  children: label
392
716
  }
393
717
  ),
394
- description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
718
+ description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
395
719
  import_xui_typography.Typography,
396
720
  {
397
721
  variant: variants.description,
@@ -402,7 +726,7 @@ var OptionCell = ({
402
726
  ]
403
727
  }
404
728
  ),
405
- (value !== void 0 || hint !== void 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
729
+ (value !== void 0 || hint !== void 0) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
406
730
  "span",
407
731
  {
408
732
  style: {
@@ -411,7 +735,7 @@ var OptionCell = ({
411
735
  alignItems: "flex-end"
412
736
  },
413
737
  children: [
414
- value !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
738
+ value !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
415
739
  import_xui_typography.Typography,
416
740
  {
417
741
  variant: variants.label,
@@ -420,7 +744,7 @@ var OptionCell = ({
420
744
  children: value
421
745
  }
422
746
  ),
423
- hint !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
747
+ hint !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
424
748
  import_xui_typography.Typography,
425
749
  {
426
750
  variant: variants.description,
@@ -431,7 +755,7 @@ var OptionCell = ({
431
755
  ]
432
756
  }
433
757
  ),
434
- keyboardShortcut && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
758
+ keyboardShortcut && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
435
759
  import_xui_typography.Typography,
436
760
  {
437
761
  as: "kbd",
@@ -440,7 +764,7 @@ var OptionCell = ({
440
764
  children: keyboardShortcut
441
765
  }
442
766
  ),
443
- hasSubmenu && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
767
+ hasSubmenu && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
444
768
  SubmenuChevron,
445
769
  {
446
770
  color: theme.colors.content.tertiary,
@@ -449,7 +773,7 @@ var OptionCell = ({
449
773
  ),
450
774
  trailingIcon,
451
775
  hasSubmenu && submenuOpen && submenu && submenuPos && typeof document !== "undefined" && (0, import_react_dom.createPortal)(
452
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
453
777
  "div",
454
778
  {
455
779
  ref: submenuWrapperRef,
@@ -480,7 +804,11 @@ var HeadingCell = ({
480
804
  themeProductContext,
481
805
  "data-testid": testId
482
806
  }) => {
483
- const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
807
+ const { theme: rawTheme } = (0, import_xui_core.useResolvedTheme)({
808
+ themeMode,
809
+ themeProductContext
810
+ });
811
+ const theme = rawTheme;
484
812
  const ctx = useContextMenu();
485
813
  const size = propSize ?? ctx?.size ?? "md";
486
814
  const sizing = theme.sizing.contextMenu(size);
@@ -488,12 +816,12 @@ var HeadingCell = ({
488
816
  const id = (0, import_xui_core.useId)();
489
817
  const registerCell = ctx?.registerCell;
490
818
  const unregisterCell = ctx?.unregisterCell;
491
- (0, import_react2.useEffect)(() => {
819
+ (0, import_react4.useEffect)(() => {
492
820
  if (!registerCell || !unregisterCell) return;
493
821
  registerCell(id, { type: "heading" });
494
822
  return () => unregisterCell(id);
495
823
  }, [registerCell, unregisterCell, id]);
496
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
824
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
497
825
  "div",
498
826
  {
499
827
  role: "presentation",
@@ -508,7 +836,7 @@ var HeadingCell = ({
508
836
  paddingBottom: sizing.itemPaddingVertical
509
837
  },
510
838
  children: [
511
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
839
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
512
840
  import_xui_typography.Typography,
513
841
  {
514
842
  variant: variants.headingAccent,
@@ -517,7 +845,7 @@ var HeadingCell = ({
517
845
  children: label
518
846
  }
519
847
  ),
520
- description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
848
+ description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
521
849
  import_xui_typography.Typography,
522
850
  {
523
851
  variant: variants.description,
@@ -532,6 +860,7 @@ var HeadingCell = ({
532
860
  var SearchCell = ({
533
861
  size: propSize,
534
862
  value,
863
+ onChange,
535
864
  onValueChange,
536
865
  placeholder = "Search",
537
866
  autoFocus,
@@ -541,19 +870,23 @@ var SearchCell = ({
541
870
  themeMode,
542
871
  themeProductContext
543
872
  }) => {
544
- const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
873
+ const { theme: rawTheme } = (0, import_xui_core.useResolvedTheme)({
874
+ themeMode,
875
+ themeProductContext
876
+ });
877
+ const theme = rawTheme;
545
878
  const ctx = useContextMenu();
546
879
  const size = propSize ?? ctx?.size ?? "md";
547
880
  const sizing = theme.sizing.contextMenu(size);
548
881
  const id = (0, import_xui_core.useId)();
549
882
  const registerCell = ctx?.registerCell;
550
883
  const unregisterCell = ctx?.unregisterCell;
551
- (0, import_react2.useEffect)(() => {
884
+ (0, import_react4.useEffect)(() => {
552
885
  if (!registerCell || !unregisterCell) return;
553
886
  registerCell(id, { type: "search" });
554
887
  return () => unregisterCell(id);
555
888
  }, [registerCell, unregisterCell, id]);
556
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
889
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
557
890
  "div",
558
891
  {
559
892
  style: {
@@ -563,7 +896,7 @@ var SearchCell = ({
563
896
  paddingTop: sizing.searchPaddingVertical,
564
897
  paddingBottom: sizing.searchPaddingVertical
565
898
  },
566
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
899
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
567
900
  "div",
568
901
  {
569
902
  style: {
@@ -574,7 +907,7 @@ var SearchCell = ({
574
907
  borderBottom: `1px solid ${theme.colors.border.secondary}`
575
908
  },
576
909
  children: [
577
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
910
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
578
911
  import_xui_icons_base.Search,
579
912
  {
580
913
  variant: "line",
@@ -583,16 +916,19 @@ var SearchCell = ({
583
916
  "aria-hidden": true
584
917
  }
585
918
  ),
586
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
919
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
587
920
  "input",
588
921
  {
589
922
  type: "search",
590
923
  role: "searchbox",
591
924
  "aria-label": ariaLabel,
592
925
  placeholder,
593
- value,
926
+ value: value ?? "",
594
927
  autoFocus,
595
- onChange: (e) => onValueChange(e.target.value),
928
+ onChange: (e) => {
929
+ onChange?.(e);
930
+ onValueChange?.(e.target.value);
931
+ },
596
932
  "data-testid": testId || testID,
597
933
  style: {
598
934
  flex: 1,
@@ -615,17 +951,21 @@ var SearchCell = ({
615
951
  );
616
952
  };
617
953
  var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) => {
618
- const { theme } = (0, import_xui_core.useResolvedTheme)({ themeMode, themeProductContext });
954
+ const { theme: rawTheme } = (0, import_xui_core.useResolvedTheme)({
955
+ themeMode,
956
+ themeProductContext
957
+ });
958
+ const theme = rawTheme;
619
959
  const ctx = useContextMenu();
620
960
  const id = (0, import_xui_core.useId)();
621
961
  const registerCell = ctx?.registerCell;
622
962
  const unregisterCell = ctx?.unregisterCell;
623
- (0, import_react2.useEffect)(() => {
963
+ (0, import_react4.useEffect)(() => {
624
964
  if (!registerCell || !unregisterCell) return;
625
965
  registerCell(id, { type: "divider" });
626
966
  return () => unregisterCell(id);
627
967
  }, [registerCell, unregisterCell, id]);
628
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
968
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
629
969
  "div",
630
970
  {
631
971
  role: "separator",
@@ -639,8 +979,150 @@ var DividerCell = ({ themeMode, themeProductContext, "data-testid": testId }) =>
639
979
  );
640
980
  };
641
981
 
982
+ // src/ContextMenuSubmenu.tsx
983
+ var import_react5 = require("react");
984
+ var import_xui_core2 = require("@xsolla/xui-core");
985
+ var import_jsx_runtime3 = require("react/jsx-runtime");
986
+ var SUBMENU_GAP2 = 4;
987
+ var OPEN_DELAY_MS = 200;
988
+ var CLOSE_GRACE_MS = 100;
989
+ var ContextMenuSubmenu = ({
990
+ label,
991
+ icon,
992
+ disabled,
993
+ children,
994
+ size: propSize,
995
+ "data-testid": testId = "context-menu-submenu"
996
+ }) => {
997
+ const { theme } = (0, import_xui_core2.useDesignSystem)();
998
+ const xuiTheme = theme;
999
+ const context = useContextMenu();
1000
+ const size = propSize || context?.size || "md";
1001
+ const sizeStyles = xuiTheme.sizing.contextMenu(size);
1002
+ const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
1003
+ const [isOpen, setIsOpen] = (0, import_react5.useState)(false);
1004
+ const [visible, setVisible] = (0, import_react5.useState)(false);
1005
+ const [openLeft, setOpenLeft] = (0, import_react5.useState)(false);
1006
+ const [topOffset, setTopOffset] = (0, import_react5.useState)(0);
1007
+ const triggerRef = (0, import_react5.useRef)(null);
1008
+ const submenuRef = (0, import_react5.useRef)(null);
1009
+ const openTimerRef = (0, import_react5.useRef)(null);
1010
+ const closeTimerRef = (0, import_react5.useRef)(null);
1011
+ const clearTimers = (0, import_react5.useCallback)(() => {
1012
+ if (openTimerRef.current) clearTimeout(openTimerRef.current);
1013
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
1014
+ }, []);
1015
+ const calculatePlacement = (0, import_react5.useCallback)(() => {
1016
+ if (!triggerRef.current) return;
1017
+ const triggerRect = triggerRef.current.getBoundingClientRect();
1018
+ const estimatedWidth = sizeStyles.minWidth + 32;
1019
+ const wouldOverflowRight = triggerRect.right + estimatedWidth + SUBMENU_GAP2 > window.innerWidth - 8;
1020
+ setOpenLeft(wouldOverflowRight);
1021
+ setTopOffset(0);
1022
+ }, [sizeStyles.minWidth]);
1023
+ (0, import_react5.useLayoutEffect)(() => {
1024
+ if (!isOpen || !submenuRef.current || !triggerRef.current) return;
1025
+ const submenuRect = submenuRef.current.getBoundingClientRect();
1026
+ const triggerRect = triggerRef.current.getBoundingClientRect();
1027
+ const wouldOverflowRight = triggerRect.right + submenuRect.width + SUBMENU_GAP2 > window.innerWidth - 8;
1028
+ setOpenLeft(wouldOverflowRight);
1029
+ const overflowBottom = triggerRect.top + submenuRect.height - (window.innerHeight - 8);
1030
+ if (overflowBottom > 0) {
1031
+ setTopOffset(-Math.min(overflowBottom, triggerRect.top - 8));
1032
+ } else {
1033
+ setTopOffset(0);
1034
+ }
1035
+ }, [isOpen]);
1036
+ (0, import_react5.useEffect)(() => {
1037
+ if (!isOpen) {
1038
+ setVisible(false);
1039
+ return;
1040
+ }
1041
+ const raf = requestAnimationFrame(() => setVisible(true));
1042
+ return () => cancelAnimationFrame(raf);
1043
+ }, [isOpen]);
1044
+ (0, import_react5.useEffect)(() => () => clearTimers(), [clearTimers]);
1045
+ const handleTriggerEnter = () => {
1046
+ if (disabled) return;
1047
+ clearTimers();
1048
+ openTimerRef.current = setTimeout(() => {
1049
+ calculatePlacement();
1050
+ setIsOpen(true);
1051
+ }, OPEN_DELAY_MS);
1052
+ };
1053
+ const handleTriggerLeave = () => {
1054
+ clearTimers();
1055
+ closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
1056
+ };
1057
+ const handleSubmenuEnter = () => clearTimers();
1058
+ const handleSubmenuLeave = () => {
1059
+ clearTimers();
1060
+ closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_GRACE_MS);
1061
+ };
1062
+ const submenuPositionStyle = openLeft ? { right: `calc(100% + ${SUBMENU_GAP2}px)` } : { left: `calc(100% + ${SUBMENU_GAP2}px)` };
1063
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1064
+ "div",
1065
+ {
1066
+ ref: triggerRef,
1067
+ style: { position: "relative" },
1068
+ onMouseEnter: handleTriggerEnter,
1069
+ onMouseLeave: handleTriggerLeave,
1070
+ "data-testid": testId,
1071
+ children: [
1072
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1073
+ ContextMenuItem,
1074
+ {
1075
+ type: "option",
1076
+ label,
1077
+ leadingIcon: icon,
1078
+ disabled,
1079
+ hasSubmenu: true,
1080
+ size,
1081
+ "data-testid": `${testId}-trigger`
1082
+ }
1083
+ ),
1084
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1085
+ "div",
1086
+ {
1087
+ ref: submenuRef,
1088
+ onMouseEnter: handleSubmenuEnter,
1089
+ onMouseLeave: handleSubmenuLeave,
1090
+ style: {
1091
+ position: "absolute",
1092
+ top: topOffset,
1093
+ ...submenuPositionStyle,
1094
+ zIndex: 1001,
1095
+ opacity: visible ? 1 : 0,
1096
+ transform: visible ? "translateX(0)" : openLeft ? "translateX(4px)" : "translateX(-4px)",
1097
+ transition: "opacity 100ms ease, transform 100ms ease"
1098
+ },
1099
+ "data-testid": `${testId}-content`,
1100
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1101
+ Box,
1102
+ {
1103
+ role: "menu",
1104
+ backgroundColor: xuiTheme.colors.background.secondary,
1105
+ borderColor: xuiTheme.colors.border.secondary,
1106
+ borderWidth: 1,
1107
+ borderRadius,
1108
+ paddingVertical: sizeStyles.paddingVertical,
1109
+ style: {
1110
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1111
+ minWidth: sizeStyles.minWidth
1112
+ },
1113
+ children
1114
+ }
1115
+ )
1116
+ }
1117
+ )
1118
+ ]
1119
+ }
1120
+ );
1121
+ };
1122
+ ContextMenuSubmenu.displayName = "ContextMenuSubmenu";
1123
+
642
1124
  // src/hooks/useContextMenuPosition.ts
643
- var import_react3 = require("react");
1125
+ var import_react6 = require("react");
644
1126
  var splitPlacement = (placement) => {
645
1127
  const [vertical, horizontal] = placement.split("-");
646
1128
  return { vertical, horizontal };
@@ -653,8 +1135,8 @@ var useContextMenuPosition = ({
653
1135
  placement = "bottom-start",
654
1136
  offset = 4
655
1137
  }) => {
656
- const [resolved, setResolved] = (0, import_react3.useState)();
657
- (0, import_react3.useEffect)(() => {
1138
+ const [resolved, setResolved] = (0, import_react6.useState)();
1139
+ (0, import_react6.useEffect)(() => {
658
1140
  if (!isOpen) {
659
1141
  setResolved(void 0);
660
1142
  return;
@@ -701,30 +1183,19 @@ var useContextMenuPosition = ({
701
1183
  (prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
702
1184
  );
703
1185
  };
704
- let rafId = window.requestAnimationFrame(compute);
1186
+ const rafId = window.requestAnimationFrame(compute);
705
1187
  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
1188
  window.addEventListener("resize", onResize);
716
- window.addEventListener("scroll", onScroll, true);
717
1189
  return () => {
718
1190
  window.cancelAnimationFrame(rafId);
719
1191
  window.removeEventListener("resize", onResize);
720
- window.removeEventListener("scroll", onScroll, true);
721
1192
  };
722
1193
  }, [isOpen, placement, offset, triggerRef, panelRef]);
723
1194
  return resolved;
724
1195
  };
725
1196
 
726
1197
  // src/hooks/useKeyboardNavigation.ts
727
- var import_react4 = require("react");
1198
+ var import_react7 = require("react");
728
1199
  var isNavigableOption = (meta) => meta.type === "option" && !meta.disabled;
729
1200
  var isTextInputTarget = (target) => {
730
1201
  if (!(target instanceof HTMLElement)) return false;
@@ -739,19 +1210,19 @@ var useKeyboardNavigation = ({
739
1210
  onClose,
740
1211
  triggerRef
741
1212
  }) => {
742
- const findFirstOption = (0, import_react4.useCallback)(() => {
1213
+ const findFirstOption = (0, import_react7.useCallback)(() => {
743
1214
  for (let i = 0; i < cells.length; i += 1) {
744
1215
  if (isNavigableOption(cells[i].meta)) return i;
745
1216
  }
746
1217
  return -1;
747
1218
  }, [cells]);
748
- const findLastOption = (0, import_react4.useCallback)(() => {
1219
+ const findLastOption = (0, import_react7.useCallback)(() => {
749
1220
  for (let i = cells.length - 1; i >= 0; i -= 1) {
750
1221
  if (isNavigableOption(cells[i].meta)) return i;
751
1222
  }
752
1223
  return -1;
753
1224
  }, [cells]);
754
- const findNextOption = (0, import_react4.useCallback)(
1225
+ const findNextOption = (0, import_react7.useCallback)(
755
1226
  (from) => {
756
1227
  const len = cells.length;
757
1228
  if (len === 0) return -1;
@@ -763,7 +1234,7 @@ var useKeyboardNavigation = ({
763
1234
  },
764
1235
  [cells]
765
1236
  );
766
- const findPrevOption = (0, import_react4.useCallback)(
1237
+ const findPrevOption = (0, import_react7.useCallback)(
767
1238
  (from) => {
768
1239
  const len = cells.length;
769
1240
  if (len === 0) return -1;
@@ -775,7 +1246,7 @@ var useKeyboardNavigation = ({
775
1246
  },
776
1247
  [cells]
777
1248
  );
778
- const handleKeyDown = (0, import_react4.useCallback)(
1249
+ const handleKeyDown = (0, import_react7.useCallback)(
779
1250
  (event) => {
780
1251
  if (!isOpen) return;
781
1252
  switch (event.key) {
@@ -845,459 +1316,383 @@ var useKeyboardNavigation = ({
845
1316
  };
846
1317
 
847
1318
  // src/ContextMenu.tsx
848
- var import_jsx_runtime2 = require("react/jsx-runtime");
1319
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1320
+ var DEFAULT_EMPTY_MESSAGE = "No results";
849
1321
  var SEARCH_DEBOUNCE_MS = 200;
850
- var EmptyMessage = ({
851
- children,
852
- color
853
- }) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
854
- "div",
855
- {
856
- style: {
857
- padding: 12,
858
- color,
859
- fontSize: 14,
860
- textAlign: "center"
861
- },
862
- children
1322
+ var textFromNode = (node) => {
1323
+ if (node === null || node === void 0 || typeof node === "boolean")
1324
+ return "";
1325
+ if (typeof node === "string" || typeof node === "number") return String(node);
1326
+ if (Array.isArray(node)) return node.map(textFromNode).join(" ");
1327
+ if ((0, import_react8.isValidElement)(node)) return textFromNode(node.props.children);
1328
+ return "";
1329
+ };
1330
+ var filterItems = (items, query) => {
1331
+ const normalized = query.trim().toLowerCase();
1332
+ if (!normalized) return [...items];
1333
+ const result = [];
1334
+ let pendingStructural = [];
1335
+ for (const item of items) {
1336
+ if (item.type === "option") {
1337
+ const label = textFromNode(item.label).toLowerCase();
1338
+ if (label.includes(normalized)) {
1339
+ result.push(...pendingStructural, item);
1340
+ pendingStructural = [];
1341
+ }
1342
+ continue;
1343
+ }
1344
+ if (item.type === "heading") {
1345
+ pendingStructural = [item];
1346
+ continue;
1347
+ }
1348
+ if (item.type === "divider") {
1349
+ if (result.length > 0) pendingStructural.push(item);
1350
+ continue;
1351
+ }
863
1352
  }
864
- );
865
- var ContextMenu = (props) => {
866
- const {
867
- type,
868
- items,
1353
+ return result;
1354
+ };
1355
+ var isOption = (item) => item.type === "option";
1356
+ var ContextMenuRoot = (0, import_react8.forwardRef)(
1357
+ ({
869
1358
  children,
1359
+ type = "list",
1360
+ items = [],
870
1361
  size = "md",
871
1362
  searchable,
872
1363
  loading,
873
- emptyMessage,
1364
+ isLoading,
1365
+ emptyMessage = DEFAULT_EMPTY_MESSAGE,
874
1366
  empty,
875
- trigger,
876
- isOpen,
1367
+ isOpen: propIsOpen,
877
1368
  onOpenChange,
878
- closeOnSelect,
879
- width,
880
- maxHeight,
1369
+ trigger,
881
1370
  placement = "bottom-start",
1371
+ position,
1372
+ width,
1373
+ maxHeight = 300,
882
1374
  onSelect,
1375
+ closeOnSelect,
883
1376
  "aria-label": ariaLabel,
884
- "data-testid": testId,
885
1377
  testID,
1378
+ "data-testid": dataTestId,
886
1379
  themeMode,
887
- themeProductContext
888
- } = props;
889
- const { theme } = (0, import_xui_core2.useResolvedTheme)({ themeMode, themeProductContext });
890
- const isControlled = isOpen !== void 0;
891
- const [internalOpen, setInternalOpen] = (0, import_react5.useState)(false);
892
- const open = isControlled ? !!isOpen : internalOpen;
893
- const setOpen = (0, import_react5.useCallback)(
894
- (next) => {
895
- if (!isControlled) setInternalOpen(next);
896
- onOpenChange?.(next);
897
- },
898
- [isControlled, onOpenChange]
899
- );
900
- const [activeIndex, setActiveIndex] = (0, import_react5.useState)(-1);
901
- const cellsRef = (0, import_react5.useRef)([]);
902
- const [cellsVersion, setCellsVersion] = (0, import_react5.useState)(0);
903
- const triggerRef = (0, import_react5.useRef)(null);
904
- const panelRef = (0, import_react5.useRef)(null);
905
- const menuId = (0, import_xui_core2.useId)();
906
- const [query, setQuery] = (0, import_react5.useState)("");
907
- const [debouncedQuery, setDebouncedQuery] = (0, import_react5.useState)("");
908
- const debounceTimerRef = (0, import_react5.useRef)(null);
909
- (0, import_react5.useEffect)(() => {
910
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
911
- debounceTimerRef.current = setTimeout(() => {
912
- setDebouncedQuery(query);
913
- }, SEARCH_DEBOUNCE_MS);
914
- return () => {
915
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
916
- };
917
- }, [query]);
918
- const closeMenu = (0, import_react5.useCallback)(() => {
919
- setOpen(false);
920
- setActiveIndex(-1);
921
- }, [setOpen]);
922
- const registerCell = (0, import_react5.useCallback)((id, meta) => {
923
- const existing = cellsRef.current.findIndex((c) => c.id === id);
924
- if (existing === -1) {
1380
+ themeProductContext,
1381
+ style
1382
+ }, ref) => {
1383
+ const { theme } = (0, import_xui_core3.useDesignSystem)();
1384
+ const xuiTheme = theme;
1385
+ const menuId = (0, import_xui_core3.useId)();
1386
+ const [internalIsOpen, setInternalIsOpen] = (0, import_react8.useState)(false);
1387
+ const [activeIndex, setActiveIndex] = (0, import_react8.useState)(-1);
1388
+ const [cellsVersion, setCellsVersion] = (0, import_react8.useState)(0);
1389
+ const [query, setQuery] = (0, import_react8.useState)("");
1390
+ const [debouncedQuery, setDebouncedQuery] = (0, import_react8.useState)("");
1391
+ const containerRef = (0, import_react8.useRef)(null);
1392
+ const triggerRef = (0, import_react8.useRef)(null);
1393
+ const panelRef = (0, import_react8.useRef)(null);
1394
+ const cellsRef = (0, import_react8.useRef)([]);
1395
+ const isOpen = propIsOpen !== void 0 ? propIsOpen : internalIsOpen;
1396
+ const sizeStyles = xuiTheme.sizing.contextMenu(size);
1397
+ const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? xuiTheme.radius?.button ?? 8;
1398
+ const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
1399
+ const positioned = useContextMenuPosition({
1400
+ triggerRef,
1401
+ panelRef,
1402
+ isOpen: isOpen && !!trigger && !position,
1403
+ placement
1404
+ });
1405
+ const setOpen = (0, import_react8.useCallback)(
1406
+ (nextOpen) => {
1407
+ if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
1408
+ onOpenChange?.(nextOpen);
1409
+ if (!nextOpen) setActiveIndex(-1);
1410
+ },
1411
+ [propIsOpen, onOpenChange]
1412
+ );
1413
+ const closeMenu = (0, import_react8.useCallback)(() => {
1414
+ setOpen(false);
1415
+ }, [setOpen]);
1416
+ const toggleMenu = (0, import_react8.useCallback)(() => {
1417
+ setOpen(!isOpen);
1418
+ }, [isOpen, setOpen]);
1419
+ const registerCell = (0, import_react8.useCallback)((id, meta) => {
1420
+ const existingIndex = cellsRef.current.findIndex(
1421
+ (cell) => cell.id === id
1422
+ );
1423
+ if (existingIndex >= 0) {
1424
+ cellsRef.current[existingIndex] = { id, meta };
1425
+ setCellsVersion((version) => version + 1);
1426
+ return existingIndex;
1427
+ }
925
1428
  cellsRef.current.push({ id, meta });
926
- setCellsVersion((v) => v + 1);
1429
+ setCellsVersion((version) => version + 1);
927
1430
  return cellsRef.current.length - 1;
928
- }
929
- const prev = cellsRef.current[existing].meta;
930
- cellsRef.current[existing] = { id, meta };
931
- if (prev.disabled !== meta.disabled || prev.type !== meta.type) {
932
- setCellsVersion((v) => v + 1);
933
- }
934
- return existing;
935
- }, []);
936
- const unregisterCell = (0, import_react5.useCallback)((id) => {
937
- const idx = cellsRef.current.findIndex((c) => c.id === id);
938
- if (idx !== -1) {
939
- cellsRef.current.splice(idx, 1);
940
- setCellsVersion((v) => v + 1);
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,
1431
+ }, []);
1432
+ const unregisterCell = (0, import_react8.useCallback)((id) => {
1433
+ const index2 = cellsRef.current.findIndex((cell) => cell.id === id);
1434
+ if (index2 >= 0) {
1435
+ cellsRef.current.splice(index2, 1);
1436
+ setCellsVersion((version) => version + 1);
1437
+ }
1438
+ }, []);
1439
+ const getCellIndex = (0, import_react8.useCallback)((id) => {
1440
+ return cellsRef.current.findIndex((cell) => cell.id === id);
1441
+ }, []);
1442
+ const cells = (0, import_react8.useMemo)(() => [...cellsRef.current], [cellsVersion]);
1443
+ const keyboard = useKeyboardNavigation({
1444
+ isOpen,
1445
+ cells,
956
1446
  activeIndex,
957
1447
  setActiveIndex,
958
- query,
959
- setQuery
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;
1448
+ onClose: closeMenu,
1449
+ triggerRef
1060
1450
  });
1061
- const result = [];
1062
- let pendingHeading = null;
1063
- let lastEmittedWasContent = false;
1064
- let groupHasOption = false;
1065
- for (let i = 0; i < items.length; i += 1) {
1066
- const item = items[i];
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
- }
1451
+ (0, import_react8.useEffect)(() => {
1452
+ if (!isOpen) {
1453
+ cellsRef.current = [];
1454
+ setCellsVersion((version) => version + 1);
1455
+ setQuery("");
1456
+ setDebouncedQuery("");
1087
1457
  }
1088
- }
1089
- while (result.length > 0 && result[result.length - 1].type === "divider") {
1090
- result.pop();
1091
- }
1092
- void groupHasOption;
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}`
1458
+ }, [isOpen]);
1459
+ (0, import_react8.useEffect)(() => {
1460
+ const timer = setTimeout(
1461
+ () => setDebouncedQuery(query),
1462
+ SEARCH_DEBOUNCE_MS
1105
1463
  );
1106
- }
1107
- if (item.type === "divider") {
1108
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1464
+ return () => clearTimeout(timer);
1465
+ }, [query]);
1466
+ (0, import_react8.useEffect)(() => {
1467
+ if (!isOpen || !trigger) return;
1468
+ const onMouseDown = (event) => {
1469
+ const target = event.target;
1470
+ if (!target || containerRef.current?.contains(target)) return;
1471
+ closeMenu();
1472
+ };
1473
+ const onScroll = () => closeMenu();
1474
+ document.addEventListener("mousedown", onMouseDown);
1475
+ window.addEventListener("scroll", onScroll);
1476
+ return () => {
1477
+ document.removeEventListener("mousedown", onMouseDown);
1478
+ window.removeEventListener("scroll", onScroll);
1479
+ };
1480
+ }, [isOpen, trigger, closeMenu]);
1481
+ (0, import_react8.useLayoutEffect)(() => {
1482
+ if (!isOpen || !panelRef.current) return;
1483
+ const searchbox = panelRef.current.querySelector('[role="searchbox"]');
1484
+ const firstOption = panelRef.current.querySelector(
1485
+ '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]'
1486
+ );
1487
+ (searchbox ?? firstOption)?.focus();
1488
+ }, [isOpen]);
1489
+ (0, import_react8.useLayoutEffect)(() => {
1490
+ if (!isOpen || !panelRef.current) return;
1491
+ const activeElement = document.activeElement;
1492
+ if (activeElement && panelRef.current.contains(activeElement)) return;
1493
+ panelRef.current.focus();
1494
+ }, [isOpen, cellsVersion]);
1495
+ (0, import_react8.useEffect)(() => {
1496
+ if (isOpen) return;
1497
+ triggerRef.current?.focus();
1498
+ }, [isOpen]);
1499
+ const contextValue = (0, import_react8.useMemo)(
1500
+ () => ({
1501
+ size,
1502
+ menuId,
1503
+ closeMenu,
1504
+ activeIndex,
1505
+ setActiveIndex,
1506
+ registerCell,
1507
+ unregisterCell,
1508
+ getCellIndex,
1509
+ cellsVersion,
1510
+ query,
1511
+ setQuery
1512
+ }),
1513
+ [
1514
+ size,
1515
+ menuId,
1516
+ closeMenu,
1517
+ activeIndex,
1518
+ registerCell,
1519
+ unregisterCell,
1520
+ getCellIndex,
1521
+ cellsVersion,
1522
+ query
1523
+ ]
1524
+ );
1525
+ const renderPresetItem = (item, index2) => {
1526
+ if (!isOption(item)) {
1527
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1528
+ ContextMenuItem,
1529
+ {
1530
+ ...item,
1531
+ themeMode,
1532
+ themeProductContext
1533
+ },
1534
+ `context-menu-item-${index2}`
1535
+ );
1536
+ }
1537
+ const leadingControl = item.leadingControl ?? (type === "checkbox" ? "checkbox" : type === "radio" ? "radio" : void 0);
1538
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1109
1539
  ContextMenuItem,
1110
1540
  {
1111
1541
  ...item,
1112
- size: item.size ?? size
1542
+ leadingControl,
1543
+ themeMode,
1544
+ themeProductContext,
1545
+ onSelect: () => {
1546
+ item.onSelect?.();
1547
+ onSelect?.(item);
1548
+ if (shouldCloseOnSelect) closeMenu();
1549
+ }
1113
1550
  },
1114
- `d-${key}`
1551
+ `context-menu-item-${index2}`
1115
1552
  );
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
1553
  };
1124
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1125
- ContextMenuItem,
1554
+ const renderedItems = searchable ? filterItems(items, debouncedQuery) : [...items];
1555
+ const renderContent = () => {
1556
+ if (loading || isLoading || type === "loading") {
1557
+ const brandColor = xuiTheme.colors.control.brand.primary.bg;
1558
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1559
+ Box,
1560
+ {
1561
+ padding: 16,
1562
+ alignItems: "center",
1563
+ justifyContent: "center",
1564
+ minHeight: 60,
1565
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_xui_spinner.Spinner, { size: "md", color: brandColor })
1566
+ }
1567
+ );
1568
+ }
1569
+ if (children) return children;
1570
+ const content = renderedItems.map(renderPresetItem);
1571
+ if (searchable) {
1572
+ content.unshift(
1573
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1574
+ "div",
1575
+ {
1576
+ "data-sticky": "top",
1577
+ style: {
1578
+ position: "sticky",
1579
+ top: 0,
1580
+ zIndex: 1,
1581
+ backgroundColor: xuiTheme.colors.background.secondary
1582
+ },
1583
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1584
+ ContextMenuItem,
1585
+ {
1586
+ type: "search",
1587
+ value: query,
1588
+ onValueChange: setQuery,
1589
+ placeholder: "Search",
1590
+ autoFocus: true,
1591
+ themeMode,
1592
+ themeProductContext
1593
+ }
1594
+ )
1595
+ },
1596
+ "context-menu-search"
1597
+ )
1598
+ );
1599
+ }
1600
+ if (content.length > (searchable ? 1 : 0)) return content;
1601
+ return empty ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { padding: 16 }, children: emptyMessage });
1602
+ };
1603
+ const assignPanelRef = (node) => {
1604
+ panelRef.current = node;
1605
+ if (typeof ref === "function") ref(node);
1606
+ else if (ref) ref.current = node;
1607
+ };
1608
+ const assignTriggerRef = (node) => {
1609
+ triggerRef.current = node;
1610
+ };
1611
+ const triggerNode = trigger && (0, import_react8.isValidElement)(trigger) ? (0, import_react8.cloneElement)(trigger, {
1612
+ ref: assignTriggerRef,
1613
+ "aria-haspopup": "menu",
1614
+ "aria-expanded": isOpen ? "true" : "false",
1615
+ onClick: (event) => {
1616
+ trigger.props.onClick?.(event);
1617
+ if (!event.defaultPrevented) toggleMenu();
1618
+ }
1619
+ }) : trigger ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1620
+ "span",
1126
1621
  {
1127
- ...composed,
1128
- size: composed.size ?? size,
1129
- onSelect: wrappedSelect
1130
- },
1131
- `o-${key}`
1132
- );
1133
- };
1134
- const isLoadingState = loading;
1135
- let bodyContent = null;
1136
- let isBodyEmpty = false;
1137
- let searchNode = null;
1138
- if (isLoadingState) {
1139
- bodyContent = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1622
+ ref: assignTriggerRef,
1623
+ role: "button",
1624
+ tabIndex: 0,
1625
+ "aria-haspopup": "menu",
1626
+ "aria-expanded": isOpen ? "true" : "false",
1627
+ onClick: toggleMenu,
1628
+ children: trigger
1629
+ }
1630
+ ) : null;
1631
+ const positionStyle = position ? {
1632
+ position: "fixed",
1633
+ left: position.x,
1634
+ top: position.y
1635
+ } : trigger ? {
1636
+ position: "fixed",
1637
+ left: positioned?.left ?? 0,
1638
+ top: positioned?.top ?? 0
1639
+ } : void 0;
1640
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1140
1641
  "div",
1141
1642
  {
1643
+ ref: containerRef,
1142
1644
  style: {
1143
- display: "flex",
1144
- alignItems: "center",
1145
- justifyContent: "center",
1146
- padding: 16
1645
+ position: trigger || position ? "relative" : void 0,
1646
+ display: trigger ? "inline-block" : void 0
1147
1647
  },
1148
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_xui_spinner.Spinner, { size: size === "xl" ? "lg" : size === "lg" ? "md" : "sm" })
1149
- }
1150
- );
1151
- } else if (children !== void 0 && children !== null) {
1152
- const childArr = import_react5.default.Children.toArray(children);
1153
- if (childArr.length === 0) {
1154
- isBodyEmpty = true;
1155
- } else {
1156
- bodyContent = children;
1157
- }
1158
- } else if (type && items) {
1159
- if (searchable) {
1160
- searchNode = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1161
- ContextMenuItem,
1162
- {
1163
- type: "search",
1164
- value: query,
1165
- onValueChange: setQuery,
1166
- size
1167
- }
1168
- );
1169
- }
1170
- const visible = filteredItems ?? [];
1171
- const optionCount = visible.filter((i) => i.type === "option").length;
1172
- if (optionCount === 0) {
1173
- isBodyEmpty = true;
1174
- } else {
1175
- bodyContent = visible.map((it, idx) => renderPresetItem(it, idx));
1176
- }
1177
- } else {
1178
- isBodyEmpty = true;
1179
- }
1180
- if (isBodyEmpty) {
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
- }
1648
+ children: [
1649
+ triggerNode,
1650
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1651
+ Box,
1652
+ {
1653
+ ref: assignPanelRef,
1654
+ role: "menu",
1655
+ "aria-label": ariaLabel,
1656
+ "data-testid": dataTestId ?? testID ?? "context-menu",
1657
+ "data-placement": positioned?.placement ?? placement,
1658
+ backgroundColor: xuiTheme.colors.background.secondary,
1659
+ borderColor: xuiTheme.colors.border.secondary,
1660
+ borderWidth: 1,
1661
+ borderRadius,
1662
+ paddingVertical: sizeStyles.paddingVertical,
1663
+ width,
1664
+ minWidth: sizeStyles.minWidth,
1665
+ tabIndex: -1,
1666
+ onKeyDown: keyboard.handleKeyDown,
1667
+ onMouseLeave: () => setActiveIndex(-1),
1668
+ style: {
1669
+ ...positionStyle,
1670
+ ...style,
1671
+ zIndex: 1e3,
1672
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1673
+ maxHeight,
1674
+ overflowY: "auto",
1675
+ outline: "none"
1676
+ },
1677
+ children: renderContent()
1678
+ }
1679
+ )
1680
+ ]
1234
1681
  }
1235
- skipFocusRestoreRef.current = true;
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 };
1682
+ ) });
1294
1683
  }
1295
- }
1684
+ );
1685
+ ContextMenuRoot.displayName = "ContextMenu";
1686
+ var ContextMenu = Object.assign(ContextMenuRoot, {
1687
+ Item: ContextMenuItem,
1688
+ Submenu: ContextMenuSubmenu
1689
+ });
1296
1690
  // Annotate the CommonJS export names for ESM import in node:
1297
1691
  0 && (module.exports = {
1298
1692
  ContextMenu,
1299
1693
  ContextMenuContext,
1300
1694
  ContextMenuItem,
1695
+ ContextMenuSubmenu,
1301
1696
  useContextMenu,
1302
1697
  useContextMenuPosition,
1303
1698
  useContextMenuRequired,