@xsolla/xui-context-menu 0.183.0 → 0.185.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.
@@ -87,6 +87,11 @@ interface ContextMenuProps extends ThemeOverrideProps {
87
87
  emptyMessage?: string;
88
88
  empty?: ReactNode;
89
89
  trigger?: ReactNode;
90
+ /**
91
+ * When `true`, the trigger fills the full width of its parent instead of
92
+ * shrinking to its content width. Defaults to `false`.
93
+ */
94
+ triggerFullWidth?: boolean;
90
95
  /**
91
96
  * Menu placement relative to the trigger element. The `*-center` variants
92
97
  * horizontally center the panel on the trigger; every variant auto-flips on
@@ -95,6 +100,13 @@ interface ContextMenuProps extends ThemeOverrideProps {
95
100
  * when `position` is set). Defaults to `"bottom-start"`.
96
101
  */
97
102
  placement?: ContextMenuPlacement;
103
+ /**
104
+ * Gap in pixels between the trigger and the menu. When `withArrow` is set
105
+ * this is measured to the arrow's tip; otherwise to the panel edge. Only
106
+ * applies to trigger-anchored menus (ignored when `position` is set).
107
+ * Defaults to `4`.
108
+ */
109
+ offset?: number;
98
110
  position?: ContextMenuPosition;
99
111
  isOpen?: boolean;
100
112
  onOpenChange?: (open: boolean) => void;
@@ -174,6 +186,7 @@ declare const useContextMenuRequired: () => ContextMenuContextValue;
174
186
  interface UseContextMenuPositionOptions {
175
187
  triggerRef: RefObject<HTMLElement | null>;
176
188
  panelRef: RefObject<HTMLElement | null>;
189
+ containerRef: RefObject<HTMLElement | null>;
177
190
  isOpen: boolean;
178
191
  placement?: ContextMenuPlacement;
179
192
  offset?: number;
@@ -183,6 +196,6 @@ interface ResolvedPosition {
183
196
  left: number;
184
197
  placement: ContextMenuPlacement;
185
198
  }
186
- declare const useContextMenuPosition: ({ triggerRef, panelRef, isOpen, placement, offset, }: UseContextMenuPositionOptions) => ResolvedPosition | undefined;
199
+ declare const useContextMenuPosition: ({ triggerRef, panelRef, containerRef, isOpen, placement, offset, }: UseContextMenuPositionOptions) => ResolvedPosition | undefined;
187
200
 
188
201
  export { ContextMenu, ContextMenuContext, type ContextMenuContextValue, type ContextMenuDividerItemProps, type ContextMenuHeadingItemProps, ContextMenuItem, type ContextMenuItemProps, type ContextMenuOptionItemProps, type ContextMenuPlacement, type ContextMenuPosition, type ContextMenuPresetType, type ContextMenuProps, type ContextMenuSearchItemProps, type ContextMenuSize, type ContextMenuSizing, ContextMenuSubmenu, type ContextMenuSubmenuProps, useContextMenu, useContextMenuPosition, useContextMenuRequired, useKeyboardNavigation };
package/native/index.d.ts CHANGED
@@ -87,6 +87,11 @@ interface ContextMenuProps extends ThemeOverrideProps {
87
87
  emptyMessage?: string;
88
88
  empty?: ReactNode;
89
89
  trigger?: ReactNode;
90
+ /**
91
+ * When `true`, the trigger fills the full width of its parent instead of
92
+ * shrinking to its content width. Defaults to `false`.
93
+ */
94
+ triggerFullWidth?: boolean;
90
95
  /**
91
96
  * Menu placement relative to the trigger element. The `*-center` variants
92
97
  * horizontally center the panel on the trigger; every variant auto-flips on
@@ -95,6 +100,13 @@ interface ContextMenuProps extends ThemeOverrideProps {
95
100
  * when `position` is set). Defaults to `"bottom-start"`.
96
101
  */
97
102
  placement?: ContextMenuPlacement;
103
+ /**
104
+ * Gap in pixels between the trigger and the menu. When `withArrow` is set
105
+ * this is measured to the arrow's tip; otherwise to the panel edge. Only
106
+ * applies to trigger-anchored menus (ignored when `position` is set).
107
+ * Defaults to `4`.
108
+ */
109
+ offset?: number;
98
110
  position?: ContextMenuPosition;
99
111
  isOpen?: boolean;
100
112
  onOpenChange?: (open: boolean) => void;
@@ -174,6 +186,7 @@ declare const useContextMenuRequired: () => ContextMenuContextValue;
174
186
  interface UseContextMenuPositionOptions {
175
187
  triggerRef: RefObject<HTMLElement | null>;
176
188
  panelRef: RefObject<HTMLElement | null>;
189
+ containerRef: RefObject<HTMLElement | null>;
177
190
  isOpen: boolean;
178
191
  placement?: ContextMenuPlacement;
179
192
  offset?: number;
@@ -183,6 +196,6 @@ interface ResolvedPosition {
183
196
  left: number;
184
197
  placement: ContextMenuPlacement;
185
198
  }
186
- declare const useContextMenuPosition: ({ triggerRef, panelRef, isOpen, placement, offset, }: UseContextMenuPositionOptions) => ResolvedPosition | undefined;
199
+ declare const useContextMenuPosition: ({ triggerRef, panelRef, containerRef, isOpen, placement, offset, }: UseContextMenuPositionOptions) => ResolvedPosition | undefined;
187
200
 
188
201
  export { ContextMenu, ContextMenuContext, type ContextMenuContextValue, type ContextMenuDividerItemProps, type ContextMenuHeadingItemProps, ContextMenuItem, type ContextMenuItemProps, type ContextMenuOptionItemProps, type ContextMenuPlacement, type ContextMenuPosition, type ContextMenuPresetType, type ContextMenuProps, type ContextMenuSearchItemProps, type ContextMenuSize, type ContextMenuSizing, ContextMenuSubmenu, type ContextMenuSubmenuProps, useContextMenu, useContextMenuPosition, useContextMenuRequired, useKeyboardNavigation };
package/native/index.js CHANGED
@@ -1079,12 +1079,13 @@ var joinPlacement = (vertical, horizontal) => `${vertical}-${horizontal}`;
1079
1079
  var useContextMenuPosition = ({
1080
1080
  triggerRef,
1081
1081
  panelRef,
1082
+ containerRef,
1082
1083
  isOpen,
1083
1084
  placement = "bottom-start",
1084
1085
  offset = 4
1085
1086
  }) => {
1086
1087
  const [resolved, setResolved] = (0, import_react4.useState)();
1087
- (0, import_react4.useEffect)(() => {
1088
+ (0, import_react4.useLayoutEffect)(() => {
1088
1089
  if (!isOpen) {
1089
1090
  setResolved(void 0);
1090
1091
  return;
@@ -1092,6 +1093,7 @@ var useContextMenuPosition = ({
1092
1093
  const compute = () => {
1093
1094
  const trigger = triggerRef.current;
1094
1095
  const panel = panelRef.current;
1096
+ const container = containerRef.current;
1095
1097
  if (!trigger || !panel) return;
1096
1098
  const triggerRect = trigger.getBoundingClientRect();
1097
1099
  const panelRect = panel.getBoundingClientRect();
@@ -1131,6 +1133,15 @@ var useContextMenuPosition = ({
1131
1133
  }
1132
1134
  }
1133
1135
  }
1136
+ if (container) {
1137
+ const containerRect = container.getBoundingClientRect();
1138
+ const cOffsetW = container.offsetWidth;
1139
+ const cOffsetH = container.offsetHeight;
1140
+ const scaleX = cOffsetW > 0 ? containerRect.width / cOffsetW : 1;
1141
+ const scaleY = cOffsetH > 0 ? containerRect.height / cOffsetH : 1;
1142
+ left = (left - containerRect.left) / (scaleX || 1);
1143
+ top = (top - containerRect.top) / (scaleY || 1);
1144
+ }
1134
1145
  const next = {
1135
1146
  top,
1136
1147
  left,
@@ -1140,14 +1151,13 @@ var useContextMenuPosition = ({
1140
1151
  (prev) => prev && prev.top === next.top && prev.left === next.left && prev.placement === next.placement ? prev : next
1141
1152
  );
1142
1153
  };
1143
- const rafId = window.requestAnimationFrame(compute);
1154
+ compute();
1144
1155
  const onResize = () => compute();
1145
1156
  window.addEventListener("resize", onResize);
1146
1157
  return () => {
1147
- window.cancelAnimationFrame(rafId);
1148
1158
  window.removeEventListener("resize", onResize);
1149
1159
  };
1150
- }, [isOpen, placement, offset, triggerRef, panelRef]);
1160
+ }, [isOpen, placement, offset, triggerRef, panelRef, containerRef]);
1151
1161
  return resolved;
1152
1162
  };
1153
1163
 
@@ -1279,6 +1289,7 @@ var SEARCH_DEBOUNCE_MS = 200;
1279
1289
  var ARROW_SIZE = 10;
1280
1290
  var ARROW_HALF = ARROW_SIZE / 2;
1281
1291
  var ARROW_EDGE_OFFSET = 12;
1292
+ var ARROW_TIP_PROTRUSION = ARROW_HALF * Math.SQRT2;
1282
1293
  var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
1283
1294
  var textFromNode = (node) => {
1284
1295
  if (node === null || node === void 0 || typeof node === "boolean")
@@ -1328,7 +1339,9 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
1328
1339
  isOpen: propIsOpen,
1329
1340
  onOpenChange,
1330
1341
  trigger,
1342
+ triggerFullWidth = false,
1331
1343
  placement = "bottom-start",
1344
+ offset = 4,
1332
1345
  position,
1333
1346
  width,
1334
1347
  maxHeight = 300,
@@ -1362,11 +1375,14 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
1362
1375
  const sizeStyles = xuiTheme.sizing.contextMenu(size);
1363
1376
  const borderRadius = xuiTheme.shape?.contextMenu?.[size]?.borderRadius ?? 8;
1364
1377
  const shouldCloseOnSelect = closeOnSelect ?? (type === "checkbox" ? false : true);
1378
+ const positionOffset = withArrow ? offset + ARROW_TIP_PROTRUSION : offset;
1365
1379
  const positioned = useContextMenuPosition({
1366
1380
  triggerRef,
1367
1381
  panelRef,
1382
+ containerRef,
1368
1383
  isOpen: isOpen && !!trigger && !position,
1369
- placement
1384
+ placement,
1385
+ offset: positionOffset
1370
1386
  });
1371
1387
  const resolvedPlacement = positioned?.placement ?? placement;
1372
1388
  const getArrowStyle = (arrowPlacement) => {
@@ -1622,7 +1638,10 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
1622
1638
  onClick: (event) => {
1623
1639
  if (!event.defaultPrevented) toggleMenu();
1624
1640
  },
1625
- style: { display: "inline-flex" },
1641
+ style: {
1642
+ display: "inline-flex",
1643
+ width: triggerFullWidth ? "100%" : "auto"
1644
+ },
1626
1645
  children: (0, import_react6.cloneElement)(trigger, {
1627
1646
  "aria-haspopup": "menu",
1628
1647
  "aria-expanded": isOpen ? "true" : "false"
@@ -1644,10 +1663,13 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
1644
1663
  position: "fixed",
1645
1664
  left: position.x,
1646
1665
  top: position.y
1647
- } : trigger ? {
1648
- position: "fixed",
1649
- left: positioned?.left ?? 0,
1650
- top: positioned?.top ?? 0
1666
+ } : trigger ? positioned ? {
1667
+ position: "absolute",
1668
+ left: positioned.left,
1669
+ top: positioned.top
1670
+ } : {
1671
+ position: "absolute",
1672
+ visibility: "hidden"
1651
1673
  } : void 0;
1652
1674
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ContextMenuContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1653
1675
  "div",