@xsolla/xui-context-menu 0.184.0 → 0.185.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/native/index.mjs CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  forwardRef,
5
5
  isValidElement,
6
6
  useCallback as useCallback3,
7
- useEffect as useEffect4,
8
- useLayoutEffect as useLayoutEffect3,
7
+ useEffect as useEffect3,
8
+ useLayoutEffect as useLayoutEffect4,
9
9
  useMemo,
10
10
  useRef as useRef2,
11
11
  useState as useState4
@@ -353,6 +353,7 @@ var OptionCell = ({
353
353
  const closeTimerRef = React.useRef(
354
354
  null
355
355
  );
356
+ const ownsSubmenu = Boolean(hasSubmenu && submenu != null);
356
357
  const cancelClose = () => {
357
358
  if (closeTimerRef.current) {
358
359
  clearTimeout(closeTimerRef.current);
@@ -361,9 +362,22 @@ var OptionCell = ({
361
362
  };
362
363
  const scheduleClose = () => {
363
364
  cancelClose();
364
- closeTimerRef.current = setTimeout(() => setSubmenuOpen(false), 120);
365
+ closeTimerRef.current = setTimeout(() => {
366
+ setSubmenuOpen(false);
367
+ if (ownsSubmenu && ctx?.activeSubmenuId === id) {
368
+ ctx.setActiveSubmenuId?.(null);
369
+ }
370
+ }, 120);
365
371
  };
366
372
  useEffect(() => () => cancelClose(), []);
373
+ useEffect(() => {
374
+ if (!ownsSubmenu || !submenuOpen) return;
375
+ const activeId = ctx?.activeSubmenuId;
376
+ if (activeId != null && activeId !== id) {
377
+ cancelClose();
378
+ setSubmenuOpen(false);
379
+ }
380
+ }, [ctx?.activeSubmenuId, ownsSubmenu, submenuOpen, id]);
367
381
  useEffect(() => {
368
382
  if (!hasSubmenu || !submenuOpen) return;
369
383
  const onMouseDown = (e) => {
@@ -454,6 +468,7 @@ var OptionCell = ({
454
468
  if (ctx && index >= 0) ctx.setActiveIndex(index);
455
469
  if (!ctx) setIsHovered(true);
456
470
  if (hasSubmenu) setSubmenuOpen(true);
471
+ if (ownsSubmenu) ctx?.setActiveSubmenuId?.(id);
457
472
  };
458
473
  const handleLeave = () => {
459
474
  if (!ctx) setIsHovered(false);
@@ -1050,7 +1065,7 @@ var ContextMenuSubmenu = ({
1050
1065
  ContextMenuSubmenu.displayName = "ContextMenuSubmenu";
1051
1066
 
1052
1067
  // src/hooks/useContextMenuPosition.ts
1053
- import { useEffect as useEffect3, useState as useState3 } from "react";
1068
+ import { useLayoutEffect as useLayoutEffect3, useState as useState3 } from "react";
1054
1069
  var splitPlacement = (placement) => {
1055
1070
  const [vertical, horizontal] = placement.split("-");
1056
1071
  return { vertical, horizontal };
@@ -1059,12 +1074,13 @@ var joinPlacement = (vertical, horizontal) => `${vertical}-${horizontal}`;
1059
1074
  var useContextMenuPosition = ({
1060
1075
  triggerRef,
1061
1076
  panelRef,
1077
+ containerRef,
1062
1078
  isOpen,
1063
1079
  placement = "bottom-start",
1064
1080
  offset = 4
1065
1081
  }) => {
1066
1082
  const [resolved, setResolved] = useState3();
1067
- useEffect3(() => {
1083
+ useLayoutEffect3(() => {
1068
1084
  if (!isOpen) {
1069
1085
  setResolved(void 0);
1070
1086
  return;
@@ -1072,6 +1088,7 @@ var useContextMenuPosition = ({
1072
1088
  const compute = () => {
1073
1089
  const trigger = triggerRef.current;
1074
1090
  const panel = panelRef.current;
1091
+ const container = containerRef.current;
1075
1092
  if (!trigger || !panel) return;
1076
1093
  const triggerRect = trigger.getBoundingClientRect();
1077
1094
  const panelRect = panel.getBoundingClientRect();
@@ -1111,6 +1128,15 @@ var useContextMenuPosition = ({
1111
1128
  }
1112
1129
  }
1113
1130
  }
1131
+ if (container) {
1132
+ const containerRect = container.getBoundingClientRect();
1133
+ const cOffsetW = container.offsetWidth;
1134
+ const cOffsetH = container.offsetHeight;
1135
+ const scaleX = cOffsetW > 0 ? containerRect.width / cOffsetW : 1;
1136
+ const scaleY = cOffsetH > 0 ? containerRect.height / cOffsetH : 1;
1137
+ left = (left - containerRect.left) / (scaleX || 1);
1138
+ top = (top - containerRect.top) / (scaleY || 1);
1139
+ }
1114
1140
  const next = {
1115
1141
  top,
1116
1142
  left,
@@ -1120,14 +1146,13 @@ var useContextMenuPosition = ({
1120
1146
  (prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
1121
1147
  );
1122
1148
  };
1123
- const rafId = window.requestAnimationFrame(compute);
1149
+ compute();
1124
1150
  const onResize = () => compute();
1125
1151
  window.addEventListener("resize", onResize);
1126
1152
  return () => {
1127
- window.cancelAnimationFrame(rafId);
1128
1153
  window.removeEventListener("resize", onResize);
1129
1154
  };
1130
- }, [isOpen, placement, offset, triggerRef, panelRef]);
1155
+ }, [isOpen, placement, offset, triggerRef, panelRef, containerRef]);
1131
1156
  return resolved;
1132
1157
  };
1133
1158
 
@@ -1259,6 +1284,7 @@ var SEARCH_DEBOUNCE_MS = 200;
1259
1284
  var ARROW_SIZE = 10;
1260
1285
  var ARROW_HALF = ARROW_SIZE / 2;
1261
1286
  var ARROW_EDGE_OFFSET = 12;
1287
+ var ARROW_TIP_PROTRUSION = ARROW_HALF * Math.SQRT2;
1262
1288
  var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
1263
1289
  var textFromNode = (node) => {
1264
1290
  if (node === null || node === void 0 || typeof node === "boolean")
@@ -1308,7 +1334,9 @@ var ContextMenuRoot = forwardRef(
1308
1334
  isOpen: propIsOpen,
1309
1335
  onOpenChange,
1310
1336
  trigger,
1337
+ triggerFullWidth = false,
1311
1338
  placement = "bottom-start",
1339
+ offset = 4,
1312
1340
  position,
1313
1341
  width,
1314
1342
  maxHeight = 300,
@@ -1342,11 +1370,14 @@ var ContextMenuRoot = forwardRef(
1342
1370
  const sizeStyles = xuiTheme.sizing.contextMenu(size);
1343
1371
  const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? 8;
1344
1372
  const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
1373
+ const positionOffset = withArrow ? offset + ARROW_TIP_PROTRUSION : offset;
1345
1374
  const positioned = useContextMenuPosition({
1346
1375
  triggerRef,
1347
1376
  panelRef,
1377
+ containerRef,
1348
1378
  isOpen: isOpen && !!trigger && !position,
1349
- placement
1379
+ placement,
1380
+ offset: positionOffset
1350
1381
  });
1351
1382
  const resolvedPlacement = positioned?.placement ?? placement;
1352
1383
  const getArrowStyle = (arrowPlacement) => {
@@ -1421,7 +1452,7 @@ var ContextMenuRoot = forwardRef(
1421
1452
  onClose: closeMenu,
1422
1453
  triggerRef
1423
1454
  });
1424
- useEffect4(() => {
1455
+ useEffect3(() => {
1425
1456
  if (!isOpen) {
1426
1457
  cellsRef.current = [];
1427
1458
  setCellsVersion((version) => version + 1);
@@ -1430,14 +1461,14 @@ var ContextMenuRoot = forwardRef(
1430
1461
  setActiveSubmenuId(null);
1431
1462
  }
1432
1463
  }, [isOpen]);
1433
- useEffect4(() => {
1464
+ useEffect3(() => {
1434
1465
  const timer = setTimeout(
1435
1466
  () => setDebouncedQuery(query),
1436
1467
  SEARCH_DEBOUNCE_MS
1437
1468
  );
1438
1469
  return () => clearTimeout(timer);
1439
1470
  }, [query]);
1440
- useEffect4(() => {
1471
+ useEffect3(() => {
1441
1472
  if (!isOpen || !trigger) return;
1442
1473
  const onMouseDown = (event) => {
1443
1474
  const target = event.target;
@@ -1452,7 +1483,7 @@ var ContextMenuRoot = forwardRef(
1452
1483
  window.removeEventListener("scroll", onScroll);
1453
1484
  };
1454
1485
  }, [isOpen, trigger, closeMenu]);
1455
- useLayoutEffect3(() => {
1486
+ useLayoutEffect4(() => {
1456
1487
  if (!isOpen || !panelRef.current) return;
1457
1488
  const searchbox = panelRef.current.querySelector('[role="searchbox"]');
1458
1489
  const firstOption = panelRef.current.querySelector(
@@ -1460,13 +1491,13 @@ var ContextMenuRoot = forwardRef(
1460
1491
  );
1461
1492
  (searchbox ?? firstOption)?.focus();
1462
1493
  }, [isOpen]);
1463
- useLayoutEffect3(() => {
1494
+ useLayoutEffect4(() => {
1464
1495
  if (!isOpen || !panelRef.current) return;
1465
1496
  const activeElement = document.activeElement;
1466
1497
  if (activeElement && panelRef.current.contains(activeElement)) return;
1467
1498
  panelRef.current.focus();
1468
1499
  }, [isOpen, cellsVersion]);
1469
- useEffect4(() => {
1500
+ useEffect3(() => {
1470
1501
  if (isOpen) return;
1471
1502
  triggerRef.current?.focus();
1472
1503
  }, [isOpen]);
@@ -1602,7 +1633,10 @@ var ContextMenuRoot = forwardRef(
1602
1633
  onClick: (event) => {
1603
1634
  if (!event.defaultPrevented) toggleMenu();
1604
1635
  },
1605
- style: { display: "inline-flex" },
1636
+ style: {
1637
+ display: "inline-flex",
1638
+ width: triggerFullWidth ? "100%" : "auto"
1639
+ },
1606
1640
  children: cloneElement(trigger, {
1607
1641
  "aria-haspopup": "menu",
1608
1642
  "aria-expanded": isOpen ? "true" : "false"
@@ -1624,10 +1658,13 @@ var ContextMenuRoot = forwardRef(
1624
1658
  position: "fixed",
1625
1659
  left: position.x,
1626
1660
  top: position.y
1627
- } : trigger ? {
1628
- position: "fixed",
1629
- left: positioned?.left ?? 0,
1630
- top: positioned?.top ?? 0
1661
+ } : trigger ? positioned ? {
1662
+ position: "absolute",
1663
+ left: positioned.left,
1664
+ top: positioned.top
1665
+ } : {
1666
+ position: "absolute",
1667
+ visibility: "hidden"
1631
1668
  } : void 0;
1632
1669
  return /* @__PURE__ */ jsx4(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
1633
1670
  "div",