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