@xsolla/xui-context-menu 0.140.0-pr246.1776914902 → 0.141.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/README.md +336 -0
- package/native/index.js +11 -5
- package/native/index.js.map +1 -1
- package/native/index.mjs +11 -5
- package/native/index.mjs.map +1 -1
- package/package.json +8 -8
- package/web/index.js +13 -11
- package/web/index.js.map +1 -1
- package/web/index.mjs +13 -11
- package/web/index.mjs.map +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Context Menu
|
|
2
|
+
|
|
3
|
+
A cross-platform React context menu component that can be triggered by a button or right-click, supporting various item types including checkboxes and radio buttons.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @xsolla/xui-context-menu
|
|
9
|
+
# or
|
|
10
|
+
yarn add @xsolla/xui-context-menu
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
### Basic Context Menu
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import * as React from 'react';
|
|
19
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
20
|
+
import { Button } from '@xsolla/xui-button';
|
|
21
|
+
|
|
22
|
+
export default function BasicContextMenu() {
|
|
23
|
+
return (
|
|
24
|
+
<ContextMenu
|
|
25
|
+
trigger={<Button>Open Menu</Button>}
|
|
26
|
+
list={[
|
|
27
|
+
{ id: 'edit', label: 'Edit' },
|
|
28
|
+
{ id: 'duplicate', label: 'Duplicate' },
|
|
29
|
+
{ id: 'delete', label: 'Delete' },
|
|
30
|
+
]}
|
|
31
|
+
onSelect={(item) => console.log('Selected:', item.id)}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Compound Component API
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import * as React from 'react';
|
|
41
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
42
|
+
import { Button } from '@xsolla/xui-button';
|
|
43
|
+
|
|
44
|
+
export default function CompoundAPI() {
|
|
45
|
+
return (
|
|
46
|
+
<ContextMenu trigger={<Button>Actions</Button>}>
|
|
47
|
+
<ContextMenu.Item onPress={() => console.log('Edit')}>Edit</ContextMenu.Item>
|
|
48
|
+
<ContextMenu.Item onPress={() => console.log('Copy')}>Copy</ContextMenu.Item>
|
|
49
|
+
<ContextMenu.Separator />
|
|
50
|
+
<ContextMenu.Item onPress={() => console.log('Delete')}>Delete</ContextMenu.Item>
|
|
51
|
+
</ContextMenu>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### With Groups
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import * as React from 'react';
|
|
60
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
61
|
+
import { Button } from '@xsolla/xui-button';
|
|
62
|
+
|
|
63
|
+
export default function WithGroups() {
|
|
64
|
+
return (
|
|
65
|
+
<ContextMenu
|
|
66
|
+
trigger={<Button>Menu with Groups</Button>}
|
|
67
|
+
groups={[
|
|
68
|
+
{
|
|
69
|
+
id: 'file',
|
|
70
|
+
label: 'File',
|
|
71
|
+
items: [
|
|
72
|
+
{ id: 'new', label: 'New' },
|
|
73
|
+
{ id: 'open', label: 'Open' },
|
|
74
|
+
{ id: 'save', label: 'Save' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'edit',
|
|
79
|
+
label: 'Edit',
|
|
80
|
+
items: [
|
|
81
|
+
{ id: 'cut', label: 'Cut' },
|
|
82
|
+
{ id: 'copy', label: 'Copy' },
|
|
83
|
+
{ id: 'paste', label: 'Paste' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
]}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Anatomy
|
|
93
|
+
|
|
94
|
+
```jsx
|
|
95
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
96
|
+
|
|
97
|
+
<ContextMenu
|
|
98
|
+
trigger={<Button>Menu</Button>} // Trigger element
|
|
99
|
+
isOpen={isOpen} // Controlled open state
|
|
100
|
+
onOpenChange={setIsOpen} // Open state callback
|
|
101
|
+
list={items} // Data-driven items
|
|
102
|
+
groups={groups} // Grouped items
|
|
103
|
+
size="md" // Size variant
|
|
104
|
+
width={200} // Menu width
|
|
105
|
+
maxHeight={300} // Max height with scroll
|
|
106
|
+
closeOnSelect={true} // Close after selection
|
|
107
|
+
onSelect={handleSelect} // Selection callback
|
|
108
|
+
onCheckedChange={handleChecked} // Checkbox/radio callback
|
|
109
|
+
/>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
### Checkbox Items
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import * as React from 'react';
|
|
118
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
119
|
+
import { Button } from '@xsolla/xui-button';
|
|
120
|
+
|
|
121
|
+
export default function CheckboxItems() {
|
|
122
|
+
const [settings, setSettings] = React.useState({
|
|
123
|
+
notifications: true,
|
|
124
|
+
sound: false,
|
|
125
|
+
autoSave: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ContextMenu
|
|
130
|
+
trigger={<Button>Settings</Button>}
|
|
131
|
+
list={[
|
|
132
|
+
{ id: 'notifications', label: 'Notifications', variant: 'checkbox', checked: settings.notifications },
|
|
133
|
+
{ id: 'sound', label: 'Sound', variant: 'checkbox', checked: settings.sound },
|
|
134
|
+
{ id: 'autoSave', label: 'Auto Save', variant: 'checkbox', checked: settings.autoSave },
|
|
135
|
+
]}
|
|
136
|
+
onCheckedChange={(id, checked) => {
|
|
137
|
+
setSettings((prev) => ({ ...prev, [id]: checked }));
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Radio Group
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import * as React from 'react';
|
|
148
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
149
|
+
import { Button } from '@xsolla/xui-button';
|
|
150
|
+
|
|
151
|
+
export default function RadioGroupMenu() {
|
|
152
|
+
const [theme, setTheme] = React.useState('light');
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<ContextMenu trigger={<Button>Theme: {theme}</Button>}>
|
|
156
|
+
<ContextMenu.Group label="Theme">
|
|
157
|
+
<ContextMenu.RadioGroup value={theme} onValueChange={setTheme}>
|
|
158
|
+
<ContextMenu.RadioItem value="light">Light</ContextMenu.RadioItem>
|
|
159
|
+
<ContextMenu.RadioItem value="dark">Dark</ContextMenu.RadioItem>
|
|
160
|
+
<ContextMenu.RadioItem value="system">System</ContextMenu.RadioItem>
|
|
161
|
+
</ContextMenu.RadioGroup>
|
|
162
|
+
</ContextMenu.Group>
|
|
163
|
+
</ContextMenu>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### With Search
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import * as React from 'react';
|
|
172
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
173
|
+
import { Button } from '@xsolla/xui-button';
|
|
174
|
+
|
|
175
|
+
export default function WithSearch() {
|
|
176
|
+
const [search, setSearch] = React.useState('');
|
|
177
|
+
|
|
178
|
+
const allItems = [
|
|
179
|
+
{ id: 'apple', label: 'Apple' },
|
|
180
|
+
{ id: 'banana', label: 'Banana' },
|
|
181
|
+
{ id: 'cherry', label: 'Cherry' },
|
|
182
|
+
{ id: 'date', label: 'Date' },
|
|
183
|
+
{ id: 'elderberry', label: 'Elderberry' },
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const filteredItems = allItems.filter((item) =>
|
|
187
|
+
item.label.toLowerCase().includes(search.toLowerCase())
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<ContextMenu trigger={<Button>Select Fruit</Button>}>
|
|
192
|
+
<ContextMenu.Search
|
|
193
|
+
value={search}
|
|
194
|
+
onValueChange={setSearch}
|
|
195
|
+
placeholder="Search fruits..."
|
|
196
|
+
/>
|
|
197
|
+
{filteredItems.map((item) => (
|
|
198
|
+
<ContextMenu.Item key={item.id}>{item.label}</ContextMenu.Item>
|
|
199
|
+
))}
|
|
200
|
+
</ContextMenu>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### With Icons and Shortcuts
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import * as React from 'react';
|
|
209
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
210
|
+
import { Button } from '@xsolla/xui-button';
|
|
211
|
+
import { Copy, Scissors, Clipboard } from '@xsolla/xui-icons-base';
|
|
212
|
+
|
|
213
|
+
export default function WithIconsAndShortcuts() {
|
|
214
|
+
return (
|
|
215
|
+
<ContextMenu
|
|
216
|
+
trigger={<Button>Edit</Button>}
|
|
217
|
+
list={[
|
|
218
|
+
{ id: 'cut', label: 'Cut', icon: <Scissors />, trailing: { type: 'shortcut', content: 'Cmd+X' } },
|
|
219
|
+
{ id: 'copy', label: 'Copy', icon: <Copy />, trailing: { type: 'shortcut', content: 'Cmd+C' } },
|
|
220
|
+
{ id: 'paste', label: 'Paste', icon: <Clipboard />, trailing: { type: 'shortcut', content: 'Cmd+V' } },
|
|
221
|
+
]}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Right-Click Context Menu
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import * as React from 'react';
|
|
231
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
232
|
+
|
|
233
|
+
export default function RightClickMenu() {
|
|
234
|
+
const [position, setPosition] = React.useState<{ x: number; y: number } | null>(null);
|
|
235
|
+
|
|
236
|
+
const handleContextMenu = (e: React.MouseEvent) => {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
setPosition({ x: e.clientX, y: e.clientY });
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
onContextMenu={handleContextMenu}
|
|
244
|
+
style={{ width: 300, height: 200, background: '#f0f0f0', padding: 16 }}
|
|
245
|
+
>
|
|
246
|
+
Right-click anywhere in this area
|
|
247
|
+
|
|
248
|
+
{position && (
|
|
249
|
+
<ContextMenu
|
|
250
|
+
isOpen={!!position}
|
|
251
|
+
onOpenChange={(open) => !open && setPosition(null)}
|
|
252
|
+
position={position}
|
|
253
|
+
list={[
|
|
254
|
+
{ id: 'inspect', label: 'Inspect' },
|
|
255
|
+
{ id: 'refresh', label: 'Refresh' },
|
|
256
|
+
]}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## API Reference
|
|
265
|
+
|
|
266
|
+
### ContextMenu
|
|
267
|
+
|
|
268
|
+
**ContextMenuProps:**
|
|
269
|
+
|
|
270
|
+
| Prop | Type | Default | Description |
|
|
271
|
+
| :--- | :--- | :------ | :---------- |
|
|
272
|
+
| children | `ReactNode` | - | Compound component children. |
|
|
273
|
+
| trigger | `ReactNode` | - | Element that triggers the menu. |
|
|
274
|
+
| list | `ContextMenuItemData[]` | - | Data-driven item list. |
|
|
275
|
+
| groups | `ContextMenuGroupData[]` | - | Grouped items with labels. |
|
|
276
|
+
| isOpen | `boolean` | - | Controlled open state. |
|
|
277
|
+
| onOpenChange | `(open: boolean) => void` | - | Open state change callback. |
|
|
278
|
+
| position | `{ x: number; y: number }` | - | Fixed position for right-click menus. |
|
|
279
|
+
| size | `"sm" \| "md" \| "lg"` | `"md"` | Menu size variant. |
|
|
280
|
+
| width | `number` | - | Menu width in pixels. |
|
|
281
|
+
| maxHeight | `number` | `300` | Max height before scrolling. |
|
|
282
|
+
| closeOnSelect | `boolean` | `true` | Close menu after item selection. |
|
|
283
|
+
| isLoading | `boolean` | `false` | Show loading spinner. |
|
|
284
|
+
| onSelect | `(item: ContextMenuItemData) => void` | - | Item selection callback. |
|
|
285
|
+
| onCheckedChange | `(id: string, checked: boolean) => void` | - | Checkbox/radio change callback. |
|
|
286
|
+
| aria-label | `string` | - | Accessible menu label. |
|
|
287
|
+
|
|
288
|
+
**ContextMenuItemData:**
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
interface ContextMenuItemData {
|
|
292
|
+
id: string;
|
|
293
|
+
label: string;
|
|
294
|
+
icon?: ReactNode;
|
|
295
|
+
description?: string;
|
|
296
|
+
disabled?: boolean;
|
|
297
|
+
selected?: boolean;
|
|
298
|
+
checked?: boolean;
|
|
299
|
+
variant?: 'default' | 'checkbox' | 'radio';
|
|
300
|
+
trailing?: { type: 'shortcut' | 'content' | 'none'; content?: string | ReactNode };
|
|
301
|
+
children?: ContextMenuItemData[];
|
|
302
|
+
onPress?: () => void;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Compound Components
|
|
307
|
+
|
|
308
|
+
| Component | Description |
|
|
309
|
+
| :-------- | :---------- |
|
|
310
|
+
| `ContextMenu.Item` | Standard menu item. |
|
|
311
|
+
| `ContextMenu.CheckboxItem` | Item with checkbox. |
|
|
312
|
+
| `ContextMenu.RadioGroup` | Container for radio items. |
|
|
313
|
+
| `ContextMenu.RadioItem` | Radio button item. |
|
|
314
|
+
| `ContextMenu.Group` | Group with optional label. |
|
|
315
|
+
| `ContextMenu.Separator` | Visual separator line. |
|
|
316
|
+
| `ContextMenu.Search` | Search input for filtering. |
|
|
317
|
+
|
|
318
|
+
## Keyboard Navigation
|
|
319
|
+
|
|
320
|
+
| Key | Action |
|
|
321
|
+
| :-- | :----- |
|
|
322
|
+
| `ArrowDown` | Move to next item |
|
|
323
|
+
| `ArrowUp` | Move to previous item |
|
|
324
|
+
| `Home` | Move to first item |
|
|
325
|
+
| `End` | Move to last item |
|
|
326
|
+
| `Enter` / `Space` | Select current item |
|
|
327
|
+
| `Escape` | Close menu |
|
|
328
|
+
| `Tab` | Close menu |
|
|
329
|
+
|
|
330
|
+
## Accessibility
|
|
331
|
+
|
|
332
|
+
- Menu has `role="menu"` with proper ARIA attributes
|
|
333
|
+
- Items have `role="menuitem"`, checkboxes have `role="menuitemcheckbox"`
|
|
334
|
+
- Keyboard navigation follows WAI-ARIA menu pattern
|
|
335
|
+
- Focus is trapped within menu when open
|
|
336
|
+
- Escape key closes menu and returns focus to trigger
|
package/native/index.js
CHANGED
|
@@ -49,7 +49,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
49
49
|
// src/ContextMenu.tsx
|
|
50
50
|
var import_react10 = __toESM(require("react"));
|
|
51
51
|
|
|
52
|
-
//
|
|
52
|
+
// ../primitives-native/src/Box.tsx
|
|
53
53
|
var import_react_native = require("react-native");
|
|
54
54
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
55
55
|
var Box = ({
|
|
@@ -223,7 +223,7 @@ var Box = ({
|
|
|
223
223
|
);
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
-
//
|
|
226
|
+
// ../primitives-native/src/Text.tsx
|
|
227
227
|
var import_react_native2 = require("react-native");
|
|
228
228
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
229
229
|
var roleMap = {
|
|
@@ -286,7 +286,7 @@ var Text = ({
|
|
|
286
286
|
);
|
|
287
287
|
};
|
|
288
288
|
|
|
289
|
-
//
|
|
289
|
+
// ../primitives-native/src/Icon.tsx
|
|
290
290
|
var import_react = __toESM(require("react"));
|
|
291
291
|
var import_react_native3 = require("react-native");
|
|
292
292
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
@@ -310,7 +310,7 @@ var Icon = ({ children, color, size }) => {
|
|
|
310
310
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style, children: childrenWithProps });
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
-
//
|
|
313
|
+
// ../primitives-native/src/Input.tsx
|
|
314
314
|
var import_react2 = require("react");
|
|
315
315
|
var import_react_native4 = require("react-native");
|
|
316
316
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
@@ -356,6 +356,7 @@ var InputPrimitive = (0, import_react2.forwardRef)(
|
|
|
356
356
|
style,
|
|
357
357
|
color,
|
|
358
358
|
fontSize,
|
|
359
|
+
fontFamily,
|
|
359
360
|
placeholderTextColor,
|
|
360
361
|
maxLength,
|
|
361
362
|
type,
|
|
@@ -386,6 +387,10 @@ var InputPrimitive = (0, import_react2.forwardRef)(
|
|
|
386
387
|
};
|
|
387
388
|
const keyboardType = inputMode ? inputModeToKeyboardType[inputMode] || "default" : type ? keyboardTypeMap[type] || "default" : "default";
|
|
388
389
|
const textContentType = autoComplete ? autoCompleteToTextContentType[autoComplete] : void 0;
|
|
390
|
+
let resolvedFontFamily = fontFamily ? fontFamily.split(",")[0].replace(/['"]/g, "").trim() : void 0;
|
|
391
|
+
if (resolvedFontFamily === "Pilat Wide" || resolvedFontFamily === "Pilat Wide Bold" || resolvedFontFamily === "Aktiv Grotesk") {
|
|
392
|
+
resolvedFontFamily = void 0;
|
|
393
|
+
}
|
|
389
394
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
390
395
|
import_react_native4.TextInput,
|
|
391
396
|
{
|
|
@@ -412,6 +417,7 @@ var InputPrimitive = (0, import_react2.forwardRef)(
|
|
|
412
417
|
{
|
|
413
418
|
color,
|
|
414
419
|
fontSize: typeof fontSize === "number" ? fontSize : void 0,
|
|
420
|
+
fontFamily: resolvedFontFamily,
|
|
415
421
|
flex: 1,
|
|
416
422
|
padding: 0,
|
|
417
423
|
textAlign: style?.textAlign || "left"
|
|
@@ -1124,7 +1130,7 @@ ContextMenuSeparator.displayName = "ContextMenuSeparator";
|
|
|
1124
1130
|
var import_react9 = __toESM(require("react"));
|
|
1125
1131
|
var import_xui_core6 = require("@xsolla/xui-core");
|
|
1126
1132
|
|
|
1127
|
-
//
|
|
1133
|
+
// ../icons-base/dist/web/index.mjs
|
|
1128
1134
|
var import_styled_components = __toESM(require("styled-components"), 1);
|
|
1129
1135
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1130
1136
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|