@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.mjs CHANGED
@@ -1152,7 +1152,11 @@ var useContextMenuPosition = ({
1152
1152
  const viewportHeight = window.innerHeight;
1153
1153
  let { vertical, horizontal } = splitPlacement(placement);
1154
1154
  const computeTop = (v) => v === "bottom" ? triggerRect.bottom + offset : triggerRect.top - panelRect.height - offset;
1155
- const computeLeft = (h) => h === "start" ? triggerRect.left : triggerRect.right - panelRect.width;
1155
+ const computeLeft = (h) => {
1156
+ if (h === "center")
1157
+ return triggerRect.left + triggerRect.width / 2 - panelRect.width / 2;
1158
+ return h === "start" ? triggerRect.left : triggerRect.right - panelRect.width;
1159
+ };
1156
1160
  let top = computeTop(vertical);
1157
1161
  const wantedBottom = top + panelRect.height;
1158
1162
  if (top < 0 || wantedBottom > viewportHeight) {
@@ -1165,14 +1169,19 @@ var useContextMenuPosition = ({
1165
1169
  }
1166
1170
  }
1167
1171
  let left = computeLeft(horizontal);
1168
- const wantedRight = left + panelRect.width;
1169
- if (left < 0 || wantedRight > viewportWidth) {
1170
- const flipped = horizontal === "start" ? "end" : "start";
1171
- const flippedLeft = computeLeft(flipped);
1172
- const flippedRight = flippedLeft + panelRect.width;
1173
- if (flippedLeft >= 0 && flippedRight <= viewportWidth) {
1174
- horizontal = flipped;
1175
- left = flippedLeft;
1172
+ if (horizontal === "center") {
1173
+ const maxLeft = viewportWidth - panelRect.width;
1174
+ left = maxLeft >= 0 ? Math.min(Math.max(left, 0), maxLeft) : 0;
1175
+ } else {
1176
+ const wantedRight = left + panelRect.width;
1177
+ if (left < 0 || wantedRight > viewportWidth) {
1178
+ const flipped = horizontal === "start" ? "end" : "start";
1179
+ const flippedLeft = computeLeft(flipped);
1180
+ const flippedRight = flippedLeft + panelRect.width;
1181
+ if (flippedLeft >= 0 && flippedRight <= viewportWidth) {
1182
+ horizontal = flipped;
1183
+ left = flippedLeft;
1184
+ }
1176
1185
  }
1177
1186
  }
1178
1187
  const next = {
@@ -1317,9 +1326,13 @@ var useKeyboardNavigation = ({
1317
1326
  };
1318
1327
 
1319
1328
  // src/ContextMenu.tsx
1320
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1329
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1321
1330
  var DEFAULT_EMPTY_MESSAGE = "No results";
1322
1331
  var SEARCH_DEBOUNCE_MS = 200;
1332
+ var ARROW_SIZE = 10;
1333
+ var ARROW_HALF = ARROW_SIZE / 2;
1334
+ var ARROW_EDGE_OFFSET = 12;
1335
+ var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
1323
1336
  var textFromNode = (node) => {
1324
1337
  if (node === null || node === void 0 || typeof node === "boolean")
1325
1338
  return "";
@@ -1372,6 +1385,7 @@ var ContextMenuRoot = forwardRef(
1372
1385
  position,
1373
1386
  width,
1374
1387
  maxHeight = 300,
1388
+ withArrow = false,
1375
1389
  onSelect,
1376
1390
  closeOnSelect,
1377
1391
  "aria-label": ariaLabel,
@@ -1407,6 +1421,33 @@ var ContextMenuRoot = forwardRef(
1407
1421
  isOpen: isOpen && !!trigger && !position,
1408
1422
  placement
1409
1423
  });
1424
+ const resolvedPlacement = positioned?.placement ?? placement;
1425
+ const getArrowStyle = (arrowPlacement) => {
1426
+ const isTop = arrowPlacement.startsWith("top");
1427
+ const arrowStyle = {
1428
+ position: "absolute",
1429
+ width: ARROW_SIZE,
1430
+ height: ARROW_SIZE,
1431
+ backgroundColor: xuiTheme.colors.background.secondary,
1432
+ borderStyle: "solid",
1433
+ borderColor: xuiTheme.colors.border.secondary,
1434
+ // 1px outline only on the two outward-facing edges of the rotated square
1435
+ borderWidth: isTop ? "0 1px 1px 0" : "1px 0 0 1px",
1436
+ transform: "rotate(45deg)",
1437
+ pointerEvents: "none"
1438
+ };
1439
+ if (isTop) arrowStyle.bottom = -ARROW_HALF;
1440
+ else arrowStyle.top = -ARROW_HALF;
1441
+ if (arrowPlacement.endsWith("center")) {
1442
+ arrowStyle.left = "50%";
1443
+ arrowStyle.marginLeft = -ARROW_HALF;
1444
+ } else if (arrowPlacement.endsWith("end")) {
1445
+ arrowStyle.right = ARROW_EDGE_OFFSET;
1446
+ } else {
1447
+ arrowStyle.left = ARROW_EDGE_OFFSET;
1448
+ }
1449
+ return arrowStyle;
1450
+ };
1410
1451
  const setOpen = useCallback3(
1411
1452
  (nextOpen) => {
1412
1453
  if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
@@ -1617,15 +1658,30 @@ var ContextMenuRoot = forwardRef(
1617
1658
  const assignTriggerRef = (node) => {
1618
1659
  triggerRef.current = node;
1619
1660
  };
1620
- const triggerNode = trigger && isValidElement(trigger) ? cloneElement(trigger, {
1621
- ref: assignTriggerRef,
1622
- "aria-haspopup": "menu",
1623
- "aria-expanded": isOpen ? "true" : "false",
1624
- onClick: (event) => {
1625
- trigger.props.onClick?.(event);
1626
- if (!event.defaultPrevented) toggleMenu();
1661
+ const assignTriggerWrapperRef = (node) => {
1662
+ if (!node) {
1663
+ assignTriggerRef(null);
1664
+ return;
1627
1665
  }
1628
- }) : trigger ? /* @__PURE__ */ jsx4(
1666
+ const firstChild = node.firstElementChild;
1667
+ assignTriggerRef(
1668
+ firstChild instanceof HTMLElement ? firstChild : node
1669
+ );
1670
+ };
1671
+ const triggerNode = trigger && isValidElement(trigger) ? /* @__PURE__ */ jsx4(
1672
+ "span",
1673
+ {
1674
+ ref: assignTriggerWrapperRef,
1675
+ onClick: (event) => {
1676
+ if (!event.defaultPrevented) toggleMenu();
1677
+ },
1678
+ style: { display: "inline-flex" },
1679
+ children: cloneElement(trigger, {
1680
+ "aria-haspopup": "menu",
1681
+ "aria-expanded": isOpen ? "true" : "false"
1682
+ })
1683
+ }
1684
+ ) : trigger ? /* @__PURE__ */ jsx4(
1629
1685
  "span",
1630
1686
  {
1631
1687
  ref: assignTriggerRef,
@@ -1663,7 +1719,7 @@ var ContextMenuRoot = forwardRef(
1663
1719
  role: "menu",
1664
1720
  "aria-label": ariaLabel,
1665
1721
  "data-testid": dataTestId ?? testID ?? "context-menu",
1666
- "data-placement": positioned?.placement ?? placement,
1722
+ "data-placement": resolvedPlacement,
1667
1723
  backgroundColor: xuiTheme.colors.background.secondary,
1668
1724
  borderColor: xuiTheme.colors.border.secondary,
1669
1725
  borderWidth: 1,
@@ -1678,12 +1734,26 @@ var ContextMenuRoot = forwardRef(
1678
1734
  ...positionStyle,
1679
1735
  ...style,
1680
1736
  zIndex: 1e3,
1681
- boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1682
- maxHeight,
1683
- overflowY: "auto",
1737
+ boxShadow: PANEL_SHADOW,
1738
+ ...withArrow ? {
1739
+ overflow: "visible"
1740
+ } : {
1741
+ maxHeight,
1742
+ overflowY: "auto"
1743
+ },
1684
1744
  outline: "none"
1685
1745
  },
1686
- children: renderContent()
1746
+ children: withArrow ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1747
+ /* @__PURE__ */ jsx4(
1748
+ "span",
1749
+ {
1750
+ "aria-hidden": "true",
1751
+ "data-testid": "context-menu-arrow",
1752
+ style: getArrowStyle(resolvedPlacement)
1753
+ }
1754
+ ),
1755
+ /* @__PURE__ */ jsx4("div", { style: { maxHeight, overflowY: "auto", borderRadius }, children: renderContent() })
1756
+ ] }) : renderContent()
1687
1757
  }
1688
1758
  )
1689
1759
  ]