@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/web/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
@@ -426,6 +426,7 @@ var OptionCell = ({
426
426
  const closeTimerRef = React3.useRef(
427
427
  null
428
428
  );
429
+ const ownsSubmenu = Boolean(hasSubmenu && submenu != null);
429
430
  const cancelClose = () => {
430
431
  if (closeTimerRef.current) {
431
432
  clearTimeout(closeTimerRef.current);
@@ -434,9 +435,22 @@ var OptionCell = ({
434
435
  };
435
436
  const scheduleClose = () => {
436
437
  cancelClose();
437
- closeTimerRef.current = setTimeout(() => setSubmenuOpen(false), 120);
438
+ closeTimerRef.current = setTimeout(() => {
439
+ setSubmenuOpen(false);
440
+ if (ownsSubmenu && ctx?.activeSubmenuId === id) {
441
+ ctx.setActiveSubmenuId?.(null);
442
+ }
443
+ }, 120);
438
444
  };
439
445
  useEffect(() => () => cancelClose(), []);
446
+ useEffect(() => {
447
+ if (!ownsSubmenu || !submenuOpen) return;
448
+ const activeId = ctx?.activeSubmenuId;
449
+ if (activeId != null && activeId !== id) {
450
+ cancelClose();
451
+ setSubmenuOpen(false);
452
+ }
453
+ }, [ctx?.activeSubmenuId, ownsSubmenu, submenuOpen, id]);
440
454
  useEffect(() => {
441
455
  if (!hasSubmenu || !submenuOpen) return;
442
456
  const onMouseDown = (e) => {
@@ -527,6 +541,7 @@ var OptionCell = ({
527
541
  if (ctx && index2 >= 0) ctx.setActiveIndex(index2);
528
542
  if (!ctx) setIsHovered(true);
529
543
  if (hasSubmenu) setSubmenuOpen(true);
544
+ if (ownsSubmenu) ctx?.setActiveSubmenuId?.(id);
530
545
  };
531
546
  const handleLeave = () => {
532
547
  if (!ctx) setIsHovered(false);
@@ -1123,7 +1138,7 @@ var ContextMenuSubmenu = ({
1123
1138
  ContextMenuSubmenu.displayName = "ContextMenuSubmenu";
1124
1139
 
1125
1140
  // src/hooks/useContextMenuPosition.ts
1126
- import { useEffect as useEffect3, useState as useState3 } from "react";
1141
+ import { useLayoutEffect as useLayoutEffect3, useState as useState3 } from "react";
1127
1142
  var splitPlacement = (placement) => {
1128
1143
  const [vertical, horizontal] = placement.split("-");
1129
1144
  return { vertical, horizontal };
@@ -1132,12 +1147,13 @@ var joinPlacement = (vertical, horizontal) => `${vertical}-${horizontal}`;
1132
1147
  var useContextMenuPosition = ({
1133
1148
  triggerRef,
1134
1149
  panelRef,
1150
+ containerRef,
1135
1151
  isOpen,
1136
1152
  placement = "bottom-start",
1137
1153
  offset = 4
1138
1154
  }) => {
1139
1155
  const [resolved, setResolved] = useState3();
1140
- useEffect3(() => {
1156
+ useLayoutEffect3(() => {
1141
1157
  if (!isOpen) {
1142
1158
  setResolved(void 0);
1143
1159
  return;
@@ -1145,6 +1161,7 @@ var useContextMenuPosition = ({
1145
1161
  const compute = () => {
1146
1162
  const trigger = triggerRef.current;
1147
1163
  const panel = panelRef.current;
1164
+ const container = containerRef.current;
1148
1165
  if (!trigger || !panel) return;
1149
1166
  const triggerRect = trigger.getBoundingClientRect();
1150
1167
  const panelRect = panel.getBoundingClientRect();
@@ -1184,6 +1201,15 @@ var useContextMenuPosition = ({
1184
1201
  }
1185
1202
  }
1186
1203
  }
1204
+ if (container) {
1205
+ const containerRect = container.getBoundingClientRect();
1206
+ const cOffsetW = container.offsetWidth;
1207
+ const cOffsetH = container.offsetHeight;
1208
+ const scaleX = cOffsetW > 0 ? containerRect.width / cOffsetW : 1;
1209
+ const scaleY = cOffsetH > 0 ? containerRect.height / cOffsetH : 1;
1210
+ left = (left - containerRect.left) / (scaleX || 1);
1211
+ top = (top - containerRect.top) / (scaleY || 1);
1212
+ }
1187
1213
  const next = {
1188
1214
  top,
1189
1215
  left,
@@ -1193,14 +1219,13 @@ var useContextMenuPosition = ({
1193
1219
  (prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
1194
1220
  );
1195
1221
  };
1196
- const rafId = window.requestAnimationFrame(compute);
1222
+ compute();
1197
1223
  const onResize = () => compute();
1198
1224
  window.addEventListener("resize", onResize);
1199
1225
  return () => {
1200
- window.cancelAnimationFrame(rafId);
1201
1226
  window.removeEventListener("resize", onResize);
1202
1227
  };
1203
- }, [isOpen, placement, offset, triggerRef, panelRef]);
1228
+ }, [isOpen, placement, offset, triggerRef, panelRef, containerRef]);
1204
1229
  return resolved;
1205
1230
  };
1206
1231
 
@@ -1332,6 +1357,7 @@ var SEARCH_DEBOUNCE_MS = 200;
1332
1357
  var ARROW_SIZE = 10;
1333
1358
  var ARROW_HALF = ARROW_SIZE / 2;
1334
1359
  var ARROW_EDGE_OFFSET = 12;
1360
+ var ARROW_TIP_PROTRUSION = ARROW_HALF * Math.SQRT2;
1335
1361
  var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
1336
1362
  var textFromNode = (node) => {
1337
1363
  if (node === null || node === void 0 || typeof node === "boolean")
@@ -1381,7 +1407,9 @@ var ContextMenuRoot = forwardRef(
1381
1407
  isOpen: propIsOpen,
1382
1408
  onOpenChange,
1383
1409
  trigger,
1410
+ triggerFullWidth = false,
1384
1411
  placement = "bottom-start",
1412
+ offset = 4,
1385
1413
  position,
1386
1414
  width,
1387
1415
  maxHeight = 300,
@@ -1415,11 +1443,14 @@ var ContextMenuRoot = forwardRef(
1415
1443
  const sizeStyles = xuiTheme.sizing.contextMenu(size);
1416
1444
  const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? 8;
1417
1445
  const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
1446
+ const positionOffset = withArrow ? offset + ARROW_TIP_PROTRUSION : offset;
1418
1447
  const positioned = useContextMenuPosition({
1419
1448
  triggerRef,
1420
1449
  panelRef,
1450
+ containerRef,
1421
1451
  isOpen: isOpen && !!trigger && !position,
1422
- placement
1452
+ placement,
1453
+ offset: positionOffset
1423
1454
  });
1424
1455
  const resolvedPlacement = positioned?.placement ?? placement;
1425
1456
  const getArrowStyle = (arrowPlacement) => {
@@ -1494,7 +1525,7 @@ var ContextMenuRoot = forwardRef(
1494
1525
  onClose: closeMenu,
1495
1526
  triggerRef
1496
1527
  });
1497
- useEffect4(() => {
1528
+ useEffect3(() => {
1498
1529
  if (!isOpen) {
1499
1530
  cellsRef.current = [];
1500
1531
  setCellsVersion((version) => version + 1);
@@ -1503,14 +1534,14 @@ var ContextMenuRoot = forwardRef(
1503
1534
  setActiveSubmenuId(null);
1504
1535
  }
1505
1536
  }, [isOpen]);
1506
- useEffect4(() => {
1537
+ useEffect3(() => {
1507
1538
  const timer = setTimeout(
1508
1539
  () => setDebouncedQuery(query),
1509
1540
  SEARCH_DEBOUNCE_MS
1510
1541
  );
1511
1542
  return () => clearTimeout(timer);
1512
1543
  }, [query]);
1513
- useEffect4(() => {
1544
+ useEffect3(() => {
1514
1545
  if (!isOpen || !trigger) return;
1515
1546
  const onMouseDown = (event) => {
1516
1547
  const target = event.target;
@@ -1525,7 +1556,7 @@ var ContextMenuRoot = forwardRef(
1525
1556
  window.removeEventListener("scroll", onScroll);
1526
1557
  };
1527
1558
  }, [isOpen, trigger, closeMenu]);
1528
- useLayoutEffect3(() => {
1559
+ useLayoutEffect4(() => {
1529
1560
  if (!isOpen || !panelRef.current) return;
1530
1561
  const searchbox = panelRef.current.querySelector('[role="searchbox"]');
1531
1562
  const firstOption = panelRef.current.querySelector(
@@ -1533,13 +1564,13 @@ var ContextMenuRoot = forwardRef(
1533
1564
  );
1534
1565
  (searchbox ?? firstOption)?.focus();
1535
1566
  }, [isOpen]);
1536
- useLayoutEffect3(() => {
1567
+ useLayoutEffect4(() => {
1537
1568
  if (!isOpen || !panelRef.current) return;
1538
1569
  const activeElement = document.activeElement;
1539
1570
  if (activeElement && panelRef.current.contains(activeElement)) return;
1540
1571
  panelRef.current.focus();
1541
1572
  }, [isOpen, cellsVersion]);
1542
- useEffect4(() => {
1573
+ useEffect3(() => {
1543
1574
  if (isOpen) return;
1544
1575
  triggerRef.current?.focus();
1545
1576
  }, [isOpen]);
@@ -1675,7 +1706,10 @@ var ContextMenuRoot = forwardRef(
1675
1706
  onClick: (event) => {
1676
1707
  if (!event.defaultPrevented) toggleMenu();
1677
1708
  },
1678
- style: { display: "inline-flex" },
1709
+ style: {
1710
+ display: "inline-flex",
1711
+ width: triggerFullWidth ? "100%" : "auto"
1712
+ },
1679
1713
  children: cloneElement(trigger, {
1680
1714
  "aria-haspopup": "menu",
1681
1715
  "aria-expanded": isOpen ? "true" : "false"
@@ -1697,10 +1731,13 @@ var ContextMenuRoot = forwardRef(
1697
1731
  position: "fixed",
1698
1732
  left: position.x,
1699
1733
  top: position.y
1700
- } : trigger ? {
1701
- position: "fixed",
1702
- left: positioned?.left ?? 0,
1703
- top: positioned?.top ?? 0
1734
+ } : trigger ? positioned ? {
1735
+ position: "absolute",
1736
+ left: positioned.left,
1737
+ top: positioned.top
1738
+ } : {
1739
+ position: "absolute",
1740
+ visibility: "hidden"
1704
1741
  } : void 0;
1705
1742
  return /* @__PURE__ */ jsx4(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
1706
1743
  "div",