@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.
Files changed (2) hide show
  1. package/README.md +316 -40
  2. package/package.json +8 -8
package/README.md CHANGED
@@ -1,60 +1,336 @@
1
- # @xsolla/xui-context-menu
1
+ # Context Menu
2
2
 
3
- Accessible dropdown and right-click menu with compound components, keyboard navigation, and a data-driven API.
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
- ## Usage
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
- ContextMenu,
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
- const Example = () => (
23
- <ContextMenu trigger={<Button>Options</Button>} size="md">
24
- <ContextMenuGroup label="Actions">
25
- <ContextMenuItem onPress={() => console.log('copy')}>Copy</ContextMenuItem>
26
- <ContextMenuItem onPress={() => console.log('delete')} disabled>Delete</ContextMenuItem>
27
- </ContextMenuGroup>
28
- <ContextMenuSeparator />
29
- <ContextMenuItem onPress={() => console.log('settings')}>Settings</ContextMenuItem>
30
- </ContextMenu>
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
- ## Components
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
- - `ContextMenu` — root container; accepts `trigger`, `list`, or `groups` props
37
- - `ContextMenuItem` — standard menu item
38
- - `ContextMenuCheckboxItem` — item with a checkbox
39
- - `ContextMenuRadioGroup` + `ContextMenuRadioItem` — single-select group
40
- - `ContextMenuGroup` — labelled section wrapper
41
- - `ContextMenuSeparator` — horizontal divider
42
- - `ContextMenuSearch` — inline search input for filtering items
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
- ## Props
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
- | `trigger` | `ReactNode` | | Element that opens the menu on click |
51
- | `list` | `ContextMenuItemData[]` | | Data-driven item list (alternative to children) |
52
- | `groups` | `ContextMenuGroupData[]` | | Data-driven grouped items |
53
- | `size` | `"sm" \| "md" \| "lg" \| "xl"` | | Size of the menu and its items |
54
- | `isOpen` | `boolean` | | Controlled open state |
55
- | `onOpenChange` | `(open: boolean) => void` | | Called when open state changes |
56
- | `position` | `{ x: number; y: number }` | | Anchor position for right-click mode |
57
- | `width` | `number \| string` | | Menu width |
58
- | `maxHeight` | `number` | | Maximum height before scrolling |
59
- | `onSelect` | `(item: ContextMenuItemData) => void` | | Called when an item is selected |
60
- | `closeOnSelect` | `boolean` | | Close menu after item selection |
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.99.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.99.0",
17
- "@xsolla/xui-core": "0.99.0",
18
- "@xsolla/xui-divider": "0.99.0",
19
- "@xsolla/xui-icons": "0.99.0",
20
- "@xsolla/xui-primitives-core": "0.99.0",
21
- "@xsolla/xui-radio": "0.99.0",
22
- "@xsolla/xui-spinner": "0.99.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",