@xsolla/xui-context-menu 0.99.0 → 0.101.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 +316 -40
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,60 +1,336 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Context Menu
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
npm install @xsolla/xui-context-menu
|
|
9
|
+
# or
|
|
8
10
|
yarn add @xsolla/xui-context-menu
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
##
|
|
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
|
|
12
115
|
|
|
13
116
|
```tsx
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
ContextMenuItem,
|
|
17
|
-
ContextMenuGroup,
|
|
18
|
-
ContextMenuSeparator,
|
|
19
|
-
} from '@xsolla/xui-context-menu';
|
|
117
|
+
import * as React from 'react';
|
|
118
|
+
import { ContextMenu } from '@xsolla/xui-context-menu';
|
|
20
119
|
import { Button } from '@xsolla/xui-button';
|
|
21
120
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
}
|
|
32
142
|
```
|
|
33
143
|
|
|
34
|
-
|
|
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');
|
|
35
153
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
```
|
|
43
167
|
|
|
44
|
-
|
|
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
|
|
45
265
|
|
|
46
266
|
### ContextMenu
|
|
47
267
|
|
|
268
|
+
**ContextMenuProps:**
|
|
269
|
+
|
|
48
270
|
| Prop | Type | Default | Description |
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsolla/xui-context-menu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.0",
|
|
4
4
|
"main": "./web/index.js",
|
|
5
5
|
"module": "./web/index.mjs",
|
|
6
6
|
"types": "./web/index.d.ts",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"test:coverage": "vitest run --coverage"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@xsolla/xui-checkbox": "0.
|
|
17
|
-
"@xsolla/xui-core": "0.
|
|
18
|
-
"@xsolla/xui-divider": "0.
|
|
19
|
-
"@xsolla/xui-icons": "0.
|
|
20
|
-
"@xsolla/xui-primitives-core": "0.
|
|
21
|
-
"@xsolla/xui-radio": "0.
|
|
22
|
-
"@xsolla/xui-spinner": "0.
|
|
16
|
+
"@xsolla/xui-checkbox": "0.101.0",
|
|
17
|
+
"@xsolla/xui-core": "0.101.0",
|
|
18
|
+
"@xsolla/xui-divider": "0.101.0",
|
|
19
|
+
"@xsolla/xui-icons": "0.101.0",
|
|
20
|
+
"@xsolla/xui-primitives-core": "0.101.0",
|
|
21
|
+
"@xsolla/xui-radio": "0.101.0",
|
|
22
|
+
"@xsolla/xui-spinner": "0.101.0"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=16.8.0",
|