@xsolla/xui-context-menu 0.181.0 → 0.182.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
@@ -1176,7 +1176,11 @@ var useContextMenuPosition = ({
1176
1176
  const viewportHeight = window.innerHeight;
1177
1177
  let { vertical, horizontal } = splitPlacement(placement);
1178
1178
  const computeTop = (v) => v === "bottom" ? triggerRect.bottom + offset : triggerRect.top - panelRect.height - offset;
1179
- const computeLeft = (h) => h === "start" ? triggerRect.left : triggerRect.right - panelRect.width;
1179
+ const computeLeft = (h) => {
1180
+ if (h === "center")
1181
+ return triggerRect.left + triggerRect.width / 2 - panelRect.width / 2;
1182
+ return h === "start" ? triggerRect.left : triggerRect.right - panelRect.width;
1183
+ };
1180
1184
  let top = computeTop(vertical);
1181
1185
  const wantedBottom = top + panelRect.height;
1182
1186
  if (top < 0 || wantedBottom > viewportHeight) {
@@ -1189,14 +1193,19 @@ var useContextMenuPosition = ({
1189
1193
  }
1190
1194
  }
1191
1195
  let left = computeLeft(horizontal);
1192
- const wantedRight = left + panelRect.width;
1193
- if (left < 0 || wantedRight > viewportWidth) {
1194
- const flipped = horizontal === "start" ? "end" : "start";
1195
- const flippedLeft = computeLeft(flipped);
1196
- const flippedRight = flippedLeft + panelRect.width;
1197
- if (flippedLeft >= 0 && flippedRight <= viewportWidth) {
1198
- horizontal = flipped;
1199
- left = flippedLeft;
1196
+ if (horizontal === "center") {
1197
+ const maxLeft = viewportWidth - panelRect.width;
1198
+ left = maxLeft >= 0 ? Math.min(Math.max(left, 0), maxLeft) : 0;
1199
+ } else {
1200
+ const wantedRight = left + panelRect.width;
1201
+ if (left < 0 || wantedRight > viewportWidth) {
1202
+ const flipped = horizontal === "start" ? "end" : "start";
1203
+ const flippedLeft = computeLeft(flipped);
1204
+ const flippedRight = flippedLeft + panelRect.width;
1205
+ if (flippedLeft >= 0 && flippedRight <= viewportWidth) {
1206
+ horizontal = flipped;
1207
+ left = flippedLeft;
1208
+ }
1200
1209
  }
1201
1210
  }
1202
1211
  const next = {
@@ -1344,6 +1353,10 @@ var useKeyboardNavigation = ({
1344
1353
  var import_jsx_runtime4 = require("react/jsx-runtime");
1345
1354
  var DEFAULT_EMPTY_MESSAGE = "No results";
1346
1355
  var SEARCH_DEBOUNCE_MS = 200;
1356
+ var ARROW_SIZE = 10;
1357
+ var ARROW_HALF = ARROW_SIZE / 2;
1358
+ var ARROW_EDGE_OFFSET = 12;
1359
+ var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
1347
1360
  var textFromNode = (node) => {
1348
1361
  if (node === null || node === void 0 || typeof node === "boolean")
1349
1362
  return "";
@@ -1396,6 +1409,7 @@ var ContextMenuRoot = (0, import_react8.forwardRef)(
1396
1409
  position,
1397
1410
  width,
1398
1411
  maxHeight = 300,
1412
+ withArrow = false,
1399
1413
  onSelect,
1400
1414
  closeOnSelect,
1401
1415
  "aria-label": ariaLabel,
@@ -1431,6 +1445,33 @@ var ContextMenuRoot = (0, import_react8.forwardRef)(
1431
1445
  isOpen: isOpen && !!trigger && !position,
1432
1446
  placement
1433
1447
  });
1448
+ const resolvedPlacement = positioned?.placement ?? placement;
1449
+ const getArrowStyle = (arrowPlacement) => {
1450
+ const isTop = arrowPlacement.startsWith("top");
1451
+ const arrowStyle = {
1452
+ position: "absolute",
1453
+ width: ARROW_SIZE,
1454
+ height: ARROW_SIZE,
1455
+ backgroundColor: xuiTheme.colors.background.secondary,
1456
+ borderStyle: "solid",
1457
+ borderColor: xuiTheme.colors.border.secondary,
1458
+ // 1px outline only on the two outward-facing edges of the rotated square
1459
+ borderWidth: isTop ? "0 1px 1px 0" : "1px 0 0 1px",
1460
+ transform: "rotate(45deg)",
1461
+ pointerEvents: "none"
1462
+ };
1463
+ if (isTop) arrowStyle.bottom = -ARROW_HALF;
1464
+ else arrowStyle.top = -ARROW_HALF;
1465
+ if (arrowPlacement.endsWith("center")) {
1466
+ arrowStyle.left = "50%";
1467
+ arrowStyle.marginLeft = -ARROW_HALF;
1468
+ } else if (arrowPlacement.endsWith("end")) {
1469
+ arrowStyle.right = ARROW_EDGE_OFFSET;
1470
+ } else {
1471
+ arrowStyle.left = ARROW_EDGE_OFFSET;
1472
+ }
1473
+ return arrowStyle;
1474
+ };
1434
1475
  const setOpen = (0, import_react8.useCallback)(
1435
1476
  (nextOpen) => {
1436
1477
  if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
@@ -1641,15 +1682,30 @@ var ContextMenuRoot = (0, import_react8.forwardRef)(
1641
1682
  const assignTriggerRef = (node) => {
1642
1683
  triggerRef.current = node;
1643
1684
  };
1644
- const triggerNode = trigger && (0, import_react8.isValidElement)(trigger) ? (0, import_react8.cloneElement)(trigger, {
1645
- ref: assignTriggerRef,
1646
- "aria-haspopup": "menu",
1647
- "aria-expanded": isOpen ? "true" : "false",
1648
- onClick: (event) => {
1649
- trigger.props.onClick?.(event);
1650
- if (!event.defaultPrevented) toggleMenu();
1685
+ const assignTriggerWrapperRef = (node) => {
1686
+ if (!node) {
1687
+ assignTriggerRef(null);
1688
+ return;
1651
1689
  }
1652
- }) : trigger ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1690
+ const firstChild = node.firstElementChild;
1691
+ assignTriggerRef(
1692
+ firstChild instanceof HTMLElement ? firstChild : node
1693
+ );
1694
+ };
1695
+ const triggerNode = trigger && (0, import_react8.isValidElement)(trigger) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1696
+ "span",
1697
+ {
1698
+ ref: assignTriggerWrapperRef,
1699
+ onClick: (event) => {
1700
+ if (!event.defaultPrevented) toggleMenu();
1701
+ },
1702
+ style: { display: "inline-flex" },
1703
+ children: (0, import_react8.cloneElement)(trigger, {
1704
+ "aria-haspopup": "menu",
1705
+ "aria-expanded": isOpen ? "true" : "false"
1706
+ })
1707
+ }
1708
+ ) : trigger ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1653
1709
  "span",
1654
1710
  {
1655
1711
  ref: assignTriggerRef,
@@ -1687,7 +1743,7 @@ var ContextMenuRoot = (0, import_react8.forwardRef)(
1687
1743
  role: "menu",
1688
1744
  "aria-label": ariaLabel,
1689
1745
  "data-testid": dataTestId ?? testID ?? "context-menu",
1690
- "data-placement": positioned?.placement ?? placement,
1746
+ "data-placement": resolvedPlacement,
1691
1747
  backgroundColor: xuiTheme.colors.background.secondary,
1692
1748
  borderColor: xuiTheme.colors.border.secondary,
1693
1749
  borderWidth: 1,
@@ -1702,12 +1758,26 @@ var ContextMenuRoot = (0, import_react8.forwardRef)(
1702
1758
  ...positionStyle,
1703
1759
  ...style,
1704
1760
  zIndex: 1e3,
1705
- boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1706
- maxHeight,
1707
- overflowY: "auto",
1761
+ boxShadow: PANEL_SHADOW,
1762
+ ...withArrow ? {
1763
+ overflow: "visible"
1764
+ } : {
1765
+ maxHeight,
1766
+ overflowY: "auto"
1767
+ },
1708
1768
  outline: "none"
1709
1769
  },
1710
- children: renderContent()
1770
+ children: withArrow ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1771
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1772
+ "span",
1773
+ {
1774
+ "aria-hidden": "true",
1775
+ "data-testid": "context-menu-arrow",
1776
+ style: getArrowStyle(resolvedPlacement)
1777
+ }
1778
+ ),
1779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { maxHeight, overflowY: "auto", borderRadius }, children: renderContent() })
1780
+ ] }) : renderContent()
1711
1781
  }
1712
1782
  )
1713
1783
  ]