@xsolla/xui-context-menu 0.181.0 → 0.183.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/native/index.d.mts +14 -1
- package/native/index.d.ts +14 -1
- package/native/index.js +92 -22
- package/native/index.js.map +1 -1
- package/native/index.mjs +93 -23
- package/native/index.mjs.map +1 -1
- package/package.json +10 -10
- package/web/index.d.mts +14 -1
- package/web/index.d.ts +14 -1
- package/web/index.js +92 -22
- package/web/index.js.map +1 -1
- package/web/index.mjs +93 -23
- package/web/index.mjs.map +1 -1
package/native/index.d.mts
CHANGED
|
@@ -25,7 +25,7 @@ declare const useKeyboardNavigation: ({ isOpen, cells, activeIndex, setActiveInd
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
type ContextMenuSize = "sm" | "md" | "lg" | "xl";
|
|
28
|
-
type ContextMenuPlacement = "bottom-start" | "top-start" | "bottom-end" | "top-end";
|
|
28
|
+
type ContextMenuPlacement = "bottom-start" | "top-start" | "bottom-end" | "top-end" | "bottom-center" | "top-center";
|
|
29
29
|
type ContextMenuPresetType = "list" | "loading" | "phone" | "checkbox" | "status" | "brandLogo" | "radio" | "avatar";
|
|
30
30
|
interface ContextMenuPosition {
|
|
31
31
|
x: number;
|
|
@@ -87,6 +87,13 @@ interface ContextMenuProps extends ThemeOverrideProps {
|
|
|
87
87
|
emptyMessage?: string;
|
|
88
88
|
empty?: ReactNode;
|
|
89
89
|
trigger?: ReactNode;
|
|
90
|
+
/**
|
|
91
|
+
* Menu placement relative to the trigger element. The `*-center` variants
|
|
92
|
+
* horizontally center the panel on the trigger; every variant auto-flips on
|
|
93
|
+
* the vertical axis (and `start`/`end` flip horizontally) when the menu
|
|
94
|
+
* would clip the viewport. Only applies to trigger-anchored menus (ignored
|
|
95
|
+
* when `position` is set). Defaults to `"bottom-start"`.
|
|
96
|
+
*/
|
|
90
97
|
placement?: ContextMenuPlacement;
|
|
91
98
|
position?: ContextMenuPosition;
|
|
92
99
|
isOpen?: boolean;
|
|
@@ -94,6 +101,12 @@ interface ContextMenuProps extends ThemeOverrideProps {
|
|
|
94
101
|
closeOnSelect?: boolean;
|
|
95
102
|
width?: number | string;
|
|
96
103
|
maxHeight?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Render a directional arrow on the panel edge pointing toward the trigger,
|
|
106
|
+
* mirroring the Tooltip arrow. Only applies to trigger-anchored menus.
|
|
107
|
+
* Defaults to `false`.
|
|
108
|
+
*/
|
|
109
|
+
withArrow?: boolean;
|
|
97
110
|
onSelect?: (item: ContextMenuOptionItemProps) => void;
|
|
98
111
|
"aria-label"?: string;
|
|
99
112
|
testID?: string;
|
package/native/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ declare const useKeyboardNavigation: ({ isOpen, cells, activeIndex, setActiveInd
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
type ContextMenuSize = "sm" | "md" | "lg" | "xl";
|
|
28
|
-
type ContextMenuPlacement = "bottom-start" | "top-start" | "bottom-end" | "top-end";
|
|
28
|
+
type ContextMenuPlacement = "bottom-start" | "top-start" | "bottom-end" | "top-end" | "bottom-center" | "top-center";
|
|
29
29
|
type ContextMenuPresetType = "list" | "loading" | "phone" | "checkbox" | "status" | "brandLogo" | "radio" | "avatar";
|
|
30
30
|
interface ContextMenuPosition {
|
|
31
31
|
x: number;
|
|
@@ -87,6 +87,13 @@ interface ContextMenuProps extends ThemeOverrideProps {
|
|
|
87
87
|
emptyMessage?: string;
|
|
88
88
|
empty?: ReactNode;
|
|
89
89
|
trigger?: ReactNode;
|
|
90
|
+
/**
|
|
91
|
+
* Menu placement relative to the trigger element. The `*-center` variants
|
|
92
|
+
* horizontally center the panel on the trigger; every variant auto-flips on
|
|
93
|
+
* the vertical axis (and `start`/`end` flip horizontally) when the menu
|
|
94
|
+
* would clip the viewport. Only applies to trigger-anchored menus (ignored
|
|
95
|
+
* when `position` is set). Defaults to `"bottom-start"`.
|
|
96
|
+
*/
|
|
90
97
|
placement?: ContextMenuPlacement;
|
|
91
98
|
position?: ContextMenuPosition;
|
|
92
99
|
isOpen?: boolean;
|
|
@@ -94,6 +101,12 @@ interface ContextMenuProps extends ThemeOverrideProps {
|
|
|
94
101
|
closeOnSelect?: boolean;
|
|
95
102
|
width?: number | string;
|
|
96
103
|
maxHeight?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Render a directional arrow on the panel edge pointing toward the trigger,
|
|
106
|
+
* mirroring the Tooltip arrow. Only applies to trigger-anchored menus.
|
|
107
|
+
* Defaults to `false`.
|
|
108
|
+
*/
|
|
109
|
+
withArrow?: boolean;
|
|
97
110
|
onSelect?: (item: ContextMenuOptionItemProps) => void;
|
|
98
111
|
"aria-label"?: string;
|
|
99
112
|
testID?: string;
|
package/native/index.js
CHANGED
|
@@ -1099,7 +1099,11 @@ var useContextMenuPosition = ({
|
|
|
1099
1099
|
const viewportHeight = window.innerHeight;
|
|
1100
1100
|
let { vertical, horizontal } = splitPlacement(placement);
|
|
1101
1101
|
const computeTop = (v) => v === "bottom" ? triggerRect.bottom + offset : triggerRect.top - panelRect.height - offset;
|
|
1102
|
-
const computeLeft = (h) =>
|
|
1102
|
+
const computeLeft = (h) => {
|
|
1103
|
+
if (h === "center")
|
|
1104
|
+
return triggerRect.left + triggerRect.width / 2 - panelRect.width / 2;
|
|
1105
|
+
return h === "start" ? triggerRect.left : triggerRect.right - panelRect.width;
|
|
1106
|
+
};
|
|
1103
1107
|
let top = computeTop(vertical);
|
|
1104
1108
|
const wantedBottom = top + panelRect.height;
|
|
1105
1109
|
if (top < 0 || wantedBottom > viewportHeight) {
|
|
@@ -1112,14 +1116,19 @@ var useContextMenuPosition = ({
|
|
|
1112
1116
|
}
|
|
1113
1117
|
}
|
|
1114
1118
|
let left = computeLeft(horizontal);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
if (
|
|
1121
|
-
|
|
1122
|
-
|
|
1119
|
+
if (horizontal === "center") {
|
|
1120
|
+
const maxLeft = viewportWidth - panelRect.width;
|
|
1121
|
+
left = maxLeft >= 0 ? Math.min(Math.max(left, 0), maxLeft) : 0;
|
|
1122
|
+
} else {
|
|
1123
|
+
const wantedRight = left + panelRect.width;
|
|
1124
|
+
if (left < 0 || wantedRight > viewportWidth) {
|
|
1125
|
+
const flipped = horizontal === "start" ? "end" : "start";
|
|
1126
|
+
const flippedLeft = computeLeft(flipped);
|
|
1127
|
+
const flippedRight = flippedLeft + panelRect.width;
|
|
1128
|
+
if (flippedLeft >= 0 && flippedRight <= viewportWidth) {
|
|
1129
|
+
horizontal = flipped;
|
|
1130
|
+
left = flippedLeft;
|
|
1131
|
+
}
|
|
1123
1132
|
}
|
|
1124
1133
|
}
|
|
1125
1134
|
const next = {
|
|
@@ -1267,6 +1276,10 @@ var useKeyboardNavigation = ({
|
|
|
1267
1276
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1268
1277
|
var DEFAULT_EMPTY_MESSAGE = "No results";
|
|
1269
1278
|
var SEARCH_DEBOUNCE_MS = 200;
|
|
1279
|
+
var ARROW_SIZE = 10;
|
|
1280
|
+
var ARROW_HALF = ARROW_SIZE / 2;
|
|
1281
|
+
var ARROW_EDGE_OFFSET = 12;
|
|
1282
|
+
var PANEL_SHADOW = "0 4px 12px rgba(0,0,0,0.15)";
|
|
1270
1283
|
var textFromNode = (node) => {
|
|
1271
1284
|
if (node === null || node === void 0 || typeof node === "boolean")
|
|
1272
1285
|
return "";
|
|
@@ -1319,6 +1332,7 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
|
|
|
1319
1332
|
position,
|
|
1320
1333
|
width,
|
|
1321
1334
|
maxHeight = 300,
|
|
1335
|
+
withArrow = false,
|
|
1322
1336
|
onSelect,
|
|
1323
1337
|
closeOnSelect,
|
|
1324
1338
|
"aria-label": ariaLabel,
|
|
@@ -1354,6 +1368,33 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
|
|
|
1354
1368
|
isOpen: isOpen && !!trigger && !position,
|
|
1355
1369
|
placement
|
|
1356
1370
|
});
|
|
1371
|
+
const resolvedPlacement = positioned?.placement ?? placement;
|
|
1372
|
+
const getArrowStyle = (arrowPlacement) => {
|
|
1373
|
+
const isTop = arrowPlacement.startsWith("top");
|
|
1374
|
+
const arrowStyle = {
|
|
1375
|
+
position: "absolute",
|
|
1376
|
+
width: ARROW_SIZE,
|
|
1377
|
+
height: ARROW_SIZE,
|
|
1378
|
+
backgroundColor: xuiTheme.colors.background.secondary,
|
|
1379
|
+
borderStyle: "solid",
|
|
1380
|
+
borderColor: xuiTheme.colors.border.secondary,
|
|
1381
|
+
// 1px outline only on the two outward-facing edges of the rotated square
|
|
1382
|
+
borderWidth: isTop ? "0 1px 1px 0" : "1px 0 0 1px",
|
|
1383
|
+
transform: "rotate(45deg)",
|
|
1384
|
+
pointerEvents: "none"
|
|
1385
|
+
};
|
|
1386
|
+
if (isTop) arrowStyle.bottom = -ARROW_HALF;
|
|
1387
|
+
else arrowStyle.top = -ARROW_HALF;
|
|
1388
|
+
if (arrowPlacement.endsWith("center")) {
|
|
1389
|
+
arrowStyle.left = "50%";
|
|
1390
|
+
arrowStyle.marginLeft = -ARROW_HALF;
|
|
1391
|
+
} else if (arrowPlacement.endsWith("end")) {
|
|
1392
|
+
arrowStyle.right = ARROW_EDGE_OFFSET;
|
|
1393
|
+
} else {
|
|
1394
|
+
arrowStyle.left = ARROW_EDGE_OFFSET;
|
|
1395
|
+
}
|
|
1396
|
+
return arrowStyle;
|
|
1397
|
+
};
|
|
1357
1398
|
const setOpen = (0, import_react6.useCallback)(
|
|
1358
1399
|
(nextOpen) => {
|
|
1359
1400
|
if (propIsOpen === void 0) setInternalIsOpen(nextOpen);
|
|
@@ -1564,15 +1605,30 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
|
|
|
1564
1605
|
const assignTriggerRef = (node) => {
|
|
1565
1606
|
triggerRef.current = node;
|
|
1566
1607
|
};
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
onClick: (event) => {
|
|
1572
|
-
trigger.props.onClick?.(event);
|
|
1573
|
-
if (!event.defaultPrevented) toggleMenu();
|
|
1608
|
+
const assignTriggerWrapperRef = (node) => {
|
|
1609
|
+
if (!node) {
|
|
1610
|
+
assignTriggerRef(null);
|
|
1611
|
+
return;
|
|
1574
1612
|
}
|
|
1575
|
-
|
|
1613
|
+
const firstChild = node.firstElementChild;
|
|
1614
|
+
assignTriggerRef(
|
|
1615
|
+
firstChild instanceof HTMLElement ? firstChild : node
|
|
1616
|
+
);
|
|
1617
|
+
};
|
|
1618
|
+
const triggerNode = trigger && (0, import_react6.isValidElement)(trigger) ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1619
|
+
"span",
|
|
1620
|
+
{
|
|
1621
|
+
ref: assignTriggerWrapperRef,
|
|
1622
|
+
onClick: (event) => {
|
|
1623
|
+
if (!event.defaultPrevented) toggleMenu();
|
|
1624
|
+
},
|
|
1625
|
+
style: { display: "inline-flex" },
|
|
1626
|
+
children: (0, import_react6.cloneElement)(trigger, {
|
|
1627
|
+
"aria-haspopup": "menu",
|
|
1628
|
+
"aria-expanded": isOpen ? "true" : "false"
|
|
1629
|
+
})
|
|
1630
|
+
}
|
|
1631
|
+
) : trigger ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1576
1632
|
"span",
|
|
1577
1633
|
{
|
|
1578
1634
|
ref: assignTriggerRef,
|
|
@@ -1610,7 +1666,7 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
|
|
|
1610
1666
|
role: "menu",
|
|
1611
1667
|
"aria-label": ariaLabel,
|
|
1612
1668
|
"data-testid": dataTestId ?? testID ?? "context-menu",
|
|
1613
|
-
"data-placement":
|
|
1669
|
+
"data-placement": resolvedPlacement,
|
|
1614
1670
|
backgroundColor: xuiTheme.colors.background.secondary,
|
|
1615
1671
|
borderColor: xuiTheme.colors.border.secondary,
|
|
1616
1672
|
borderWidth: 1,
|
|
@@ -1625,12 +1681,26 @@ var ContextMenuRoot = (0, import_react6.forwardRef)(
|
|
|
1625
1681
|
...positionStyle,
|
|
1626
1682
|
...style,
|
|
1627
1683
|
zIndex: 1e3,
|
|
1628
|
-
boxShadow:
|
|
1629
|
-
|
|
1630
|
-
|
|
1684
|
+
boxShadow: PANEL_SHADOW,
|
|
1685
|
+
...withArrow ? {
|
|
1686
|
+
overflow: "visible"
|
|
1687
|
+
} : {
|
|
1688
|
+
maxHeight,
|
|
1689
|
+
overflowY: "auto"
|
|
1690
|
+
},
|
|
1631
1691
|
outline: "none"
|
|
1632
1692
|
},
|
|
1633
|
-
children:
|
|
1693
|
+
children: withArrow ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1694
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1695
|
+
"span",
|
|
1696
|
+
{
|
|
1697
|
+
"aria-hidden": "true",
|
|
1698
|
+
"data-testid": "context-menu-arrow",
|
|
1699
|
+
style: getArrowStyle(resolvedPlacement)
|
|
1700
|
+
}
|
|
1701
|
+
),
|
|
1702
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { maxHeight, overflowY: "auto", borderRadius }, children: renderContent() })
|
|
1703
|
+
] }) : renderContent()
|
|
1634
1704
|
}
|
|
1635
1705
|
)
|
|
1636
1706
|
]
|