@xsolla/xui-context-menu 0.154.1-pr250.1778744843 → 0.154.2

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 CHANGED
@@ -1,175 +1,334 @@
1
1
  # Context Menu
2
2
 
3
- ## Overview
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
- `ContextMenu` is an anchored panel of selectable cells. The component is built around two primitives: a single `ContextMenuItem` whose `type` prop switches between cell variants (`option`, `search`, `heading`, `divider`), and a panel that supports a preset shorthand (`type="list" | "phone" | "checkbox" | "radio" | "status" | "brandLogo" | "avatar" | "loading"`) plus a fully custom composition path via `children`.
5
+ ## Installation
6
6
 
7
- ## When to use
7
+ ```bash
8
+ npm install @xsolla/xui-context-menu
9
+ ```
8
10
 
9
- - A primary action menu attached to a trigger (e.g. a button's overflow actions).
10
- - A selection control with checkbox or radio cells (multi-select or single-select).
11
- - A lightweight picker for status, country, or brand-logo lists.
11
+ ## Demo
12
12
 
13
- ## When not to use
13
+ ### Basic Context Menu
14
14
 
15
- - For form fields driven by validation — use `Select`, `Autocomplete` or a plain `Input` instead.
16
- - For navigational menus or app-level navigation — use the appropriate navigation primitives.
17
- - For a single destructive confirmation — use a `Dialog`.
15
+ ```tsx
16
+ import * as React from 'react';
17
+ import { ContextMenu } from '@xsolla/xui-context-menu';
18
+ import { Button } from '@xsolla/xui-button';
19
+
20
+ export default function BasicContextMenu() {
21
+ return (
22
+ <ContextMenu
23
+ trigger={<Button>Open Menu</Button>}
24
+ list={[
25
+ { id: 'edit', label: 'Edit' },
26
+ { id: 'duplicate', label: 'Duplicate' },
27
+ { id: 'delete', label: 'Delete' },
28
+ ]}
29
+ onSelect={(item) => console.log('Selected:', item.id)}
30
+ />
31
+ );
32
+ }
33
+ ```
18
34
 
19
- ## Installation
35
+ ### Compound Component API
20
36
 
21
- ```bash
22
- yarn add @xsolla/xui-context-menu
37
+ ```tsx
38
+ import * as React from 'react';
39
+ import { ContextMenu } from '@xsolla/xui-context-menu';
40
+ import { Button } from '@xsolla/xui-button';
41
+
42
+ export default function CompoundAPI() {
43
+ return (
44
+ <ContextMenu trigger={<Button>Actions</Button>}>
45
+ <ContextMenu.Item onPress={() => console.log('Edit')}>Edit</ContextMenu.Item>
46
+ <ContextMenu.Item onPress={() => console.log('Copy')}>Copy</ContextMenu.Item>
47
+ <ContextMenu.Separator />
48
+ <ContextMenu.Item onPress={() => console.log('Delete')}>Delete</ContextMenu.Item>
49
+ </ContextMenu>
50
+ );
51
+ }
23
52
  ```
24
53
 
25
- ## Two API paths
54
+ ### With Groups
26
55
 
27
- ### Preset path
56
+ ```tsx
57
+ import * as React from 'react';
58
+ import { ContextMenu } from '@xsolla/xui-context-menu';
59
+ import { Button } from '@xsolla/xui-button';
60
+
61
+ export default function WithGroups() {
62
+ return (
63
+ <ContextMenu
64
+ trigger={<Button>Menu with Groups</Button>}
65
+ groups={[
66
+ {
67
+ id: 'file',
68
+ label: 'File',
69
+ items: [
70
+ { id: 'new', label: 'New' },
71
+ { id: 'open', label: 'Open' },
72
+ { id: 'save', label: 'Save' },
73
+ ],
74
+ },
75
+ {
76
+ id: 'edit',
77
+ label: 'Edit',
78
+ items: [
79
+ { id: 'cut', label: 'Cut' },
80
+ { id: 'copy', label: 'Copy' },
81
+ { id: 'paste', label: 'Paste' },
82
+ ],
83
+ },
84
+ ]}
85
+ />
86
+ );
87
+ }
88
+ ```
28
89
 
29
- Pass `type` and `items`. The panel renders the preset's chrome and composes each option with the right control or slot.
90
+ ## Anatomy
30
91
 
31
- ```tsx
32
- import { ContextMenu } from "@xsolla/xui-context-menu";
92
+ ```jsx
93
+ import { ContextMenu } from '@xsolla/xui-context-menu';
33
94
 
34
95
  <ContextMenu
35
- type="list"
36
- trigger={<Button>Open</Button>}
37
- items={[
38
- { type: "option", label: "Edit" },
39
- { type: "option", label: "Duplicate" },
40
- { type: "option", label: "Delete", destructive: true },
41
- ]}
42
- />;
96
+ trigger={<Button>Menu</Button>} // Trigger element
97
+ isOpen={isOpen} // Controlled open state
98
+ onOpenChange={setIsOpen} // Open state callback
99
+ list={items} // Data-driven items
100
+ groups={groups} // Grouped items
101
+ size="md" // Size variant
102
+ width={200} // Menu width
103
+ maxHeight={300} // Max height with scroll
104
+ closeOnSelect={true} // Close after selection
105
+ onSelect={handleSelect} // Selection callback
106
+ onCheckedChange={handleChecked} // Checkbox/radio callback
107
+ />
43
108
  ```
44
109
 
45
- ### Custom path
110
+ ## Examples
111
+
112
+ ### Checkbox Items
46
113
 
47
- Compose cells as children when you need full control of the layout, want to mix headings and dividers freely, or render a slot the preset path doesn't cover.
114
+ ```tsx
115
+ import * as React from 'react';
116
+ import { ContextMenu } from '@xsolla/xui-context-menu';
117
+ import { Button } from '@xsolla/xui-button';
118
+
119
+ export default function CheckboxItems() {
120
+ const [settings, setSettings] = React.useState({
121
+ notifications: true,
122
+ sound: false,
123
+ autoSave: true,
124
+ });
125
+
126
+ return (
127
+ <ContextMenu
128
+ trigger={<Button>Settings</Button>}
129
+ list={[
130
+ { id: 'notifications', label: 'Notifications', variant: 'checkbox', checked: settings.notifications },
131
+ { id: 'sound', label: 'Sound', variant: 'checkbox', checked: settings.sound },
132
+ { id: 'autoSave', label: 'Auto Save', variant: 'checkbox', checked: settings.autoSave },
133
+ ]}
134
+ onCheckedChange={(id, checked) => {
135
+ setSettings((prev) => ({ ...prev, [id]: checked }));
136
+ }}
137
+ />
138
+ );
139
+ }
140
+ ```
141
+
142
+ ### Radio Group
48
143
 
49
144
  ```tsx
50
- import { ContextMenu, ContextMenuItem } from "@xsolla/xui-context-menu";
51
-
52
- <ContextMenu trigger={<Button>Open</Button>} aria-label="Actions">
53
- <ContextMenuItem type="heading" label="Workspace" />
54
- <ContextMenuItem type="option" label="Personal" />
55
- <ContextMenuItem type="option" label="Acme Inc." />
56
- <ContextMenuItem type="divider" />
57
- <ContextMenuItem type="option" label="Sign out" destructive />
58
- </ContextMenu>;
145
+ import * as React from 'react';
146
+ import { ContextMenu } from '@xsolla/xui-context-menu';
147
+ import { Button } from '@xsolla/xui-button';
148
+
149
+ export default function RadioGroupMenu() {
150
+ const [theme, setTheme] = React.useState('light');
151
+
152
+ return (
153
+ <ContextMenu trigger={<Button>Theme: {theme}</Button>}>
154
+ <ContextMenu.Group label="Theme">
155
+ <ContextMenu.RadioGroup value={theme} onValueChange={setTheme}>
156
+ <ContextMenu.RadioItem value="light">Light</ContextMenu.RadioItem>
157
+ <ContextMenu.RadioItem value="dark">Dark</ContextMenu.RadioItem>
158
+ <ContextMenu.RadioItem value="system">System</ContextMenu.RadioItem>
159
+ </ContextMenu.RadioGroup>
160
+ </ContextMenu.Group>
161
+ </ContextMenu>
162
+ );
163
+ }
59
164
  ```
60
165
 
61
- Choose the preset path for typical menus where the data is uniform; choose the custom path when cells differ structurally or when you need to drop in bespoke nodes between cells.
62
-
63
- ## `ContextMenuItem` reference
64
-
65
- `ContextMenuItem` is a discriminated union on `type`. All cell types accept `size`, `data-testid` and theme-override props.
66
-
67
- ### `type="option"`
68
-
69
- | Prop | Type | Purpose |
70
- | --- | --- | --- |
71
- | `label` | `ReactNode` | Primary cell text (required). |
72
- | `description` | `ReactNode` | Secondary line beneath the label. |
73
- | `leadingControl` | `"checkbox" \| "radio"` | Renders a `Checkbox` or `Radio` at the start. |
74
- | `leadingIcon` | `ReactNode` | Icon node before the label group. |
75
- | `status` | `ReactNode` | Status indicator slot (e.g. `<Status>`). |
76
- | `iconWrapper` | `ReactNode` | Wrapped icon / avatar slot. |
77
- | `slot` / `slotContent` | `ReactNode` | Generic slot before the label. |
78
- | `value` | `ReactNode` | Right-side primary text (e.g. shortcut value, dial code). |
79
- | `hint` | `ReactNode` | Right-side secondary text below `value`. |
80
- | `trailingIcon` | `ReactNode` | Trailing icon at the end of the cell. |
81
- | `keyboardShortcut` | `string` | Display-only shortcut rendered as `<kbd>` and exposed via `aria-keyshortcuts`. |
82
- | `hasSubmenu` | `boolean` | Marks the cell as a submenu trigger and renders a chevron. |
83
- | `submenu` | `ReactNode` | A nested `<ContextMenu>` opened on hover/`ArrowRight`/`Enter`. |
84
- | `checked` | `boolean` | Fully controlled checked state. |
85
- | `disabled` | `boolean` | Disables interaction and applies the disabled style. |
86
- | `destructive` | `boolean` | Applies the destructive content colour. |
87
- | `onSelect` | `() => void` | Fires on activation (click, `Enter`, `Space`). |
88
- | `onCheckedChange` | `(checked: boolean) => void` | Optional change callback for controls. |
89
-
90
- Render order (left → right): `leadingControl`, `leadingIcon`, `status`, `iconWrapper`, `slotContent`, `label` (with optional `description` below), `value` (with optional `hint` below), `keyboardShortcut`, submenu chevron, `trailingIcon`.
91
-
92
- ### `type="search"`
93
-
94
- | Prop | Type | Purpose |
95
- | --- | --- | --- |
96
- | `value` | `string` | Controlled value (required). |
97
- | `onValueChange` | `(value: string) => void` | Change callback (required). |
98
- | `placeholder` | `string` | Defaults to `"Search"`. |
99
- | `autoFocus` | `boolean` | Focuses the input on mount. |
100
- | `aria-label` | `string` | Defaults to `"Search options"`. |
101
-
102
- ### `type="heading"`
103
-
104
- | Prop | Type | Purpose |
105
- | --- | --- | --- |
106
- | `label` | `ReactNode` | Section title (uppercase styling). |
107
- | `description` | `ReactNode` | Optional helper line beneath. |
108
-
109
- ### `type="divider"`
110
-
111
- A horizontal rule with `role="separator"`. No content props.
112
-
113
- ## `ContextMenu` reference
114
-
115
- | Prop | Type | Purpose |
116
- | --- | --- | --- |
117
- | `type` | `"list" \| "loading" \| "phone" \| "checkbox" \| "status" \| "brandLogo" \| "radio" \| "avatar"` | Panel preset; works with `items`. |
118
- | `items` | `ReadonlyArray<Option \| Heading \| Divider>` | Data-driven cells for the preset path. |
119
- | `children` | `ReactNode` | Custom-composition cells (alternative to `items`). |
120
- | `size` | `"sm" \| "md" \| "lg" \| "xl"` | Controls cell sizing across the panel. Default `md`. |
121
- | `searchable` | `boolean` | Auto-renders a sticky search cell and filters options. |
122
- | `loading` | `boolean` | Renders a centred spinner instead of the cell list. |
123
- | `emptyMessage` | `string` | Custom message for the default empty state. |
124
- | `empty` | `ReactNode` | Replace the empty state entirely. |
125
- | `trigger` | `ReactNode` | Element that toggles the panel; receives `aria-haspopup` / `aria-expanded`. |
126
- | `placement` | `"bottom-start" \| "top-start" \| "bottom-end" \| "top-end"` | Initial placement. Auto-flips when clipped. |
127
- | `isOpen` | `boolean` | Controlled open state. |
128
- | `onOpenChange` | `(open: boolean) => void` | Open-state callback. |
129
- | `closeOnSelect` | `boolean` | Override the per-preset default. |
130
- | `width` | `number` | Forces panel width (px). |
131
- | `maxHeight` | `number` | Caps panel height (px); body scrolls and search stays sticky. |
132
- | `onSelect` | `(item: ContextMenuOptionItemProps) => void` | Fires for the preset path on option activation. |
133
- | `aria-label` | `string` | Accessible name for the menu container. |
134
- | `data-testid` | `string` | Testing handle. |
135
-
136
- ## Behaviour & accessibility
137
-
138
- - The panel root is `role="menu"` (or hosts `role="menuitemcheckbox"` / `role="menuitemradio"` cells when `checked` is provided). Headings render as `role="presentation"`, dividers as `role="separator"`, and the search cell as `role="searchbox"`.
139
- - `closeOnSelect` defaults to `true` for every preset except `checkbox`, where multi-select keeps the panel open.
140
- - On open, focus moves to the search input when present, otherwise to the first option.
141
- - On close, focus returns to the trigger.
142
- - The trigger element receives `aria-haspopup="menu"` and `aria-expanded` synced to the open state.
143
-
144
- ## Keyboard reference
166
+ ### With Search
167
+
168
+ ```tsx
169
+ import * as React from 'react';
170
+ import { ContextMenu } from '@xsolla/xui-context-menu';
171
+ import { Button } from '@xsolla/xui-button';
172
+
173
+ export default function WithSearch() {
174
+ const [search, setSearch] = React.useState('');
175
+
176
+ const allItems = [
177
+ { id: 'apple', label: 'Apple' },
178
+ { id: 'banana', label: 'Banana' },
179
+ { id: 'cherry', label: 'Cherry' },
180
+ { id: 'date', label: 'Date' },
181
+ { id: 'elderberry', label: 'Elderberry' },
182
+ ];
183
+
184
+ const filteredItems = allItems.filter((item) =>
185
+ item.label.toLowerCase().includes(search.toLowerCase())
186
+ );
187
+
188
+ return (
189
+ <ContextMenu trigger={<Button>Select Fruit</Button>}>
190
+ <ContextMenu.Search
191
+ value={search}
192
+ onValueChange={setSearch}
193
+ placeholder="Search fruits..."
194
+ />
195
+ {filteredItems.map((item) => (
196
+ <ContextMenu.Item key={item.id}>{item.label}</ContextMenu.Item>
197
+ ))}
198
+ </ContextMenu>
199
+ );
200
+ }
201
+ ```
202
+
203
+ ### With Icons and Shortcuts
204
+
205
+ ```tsx
206
+ import * as React from 'react';
207
+ import { ContextMenu } from '@xsolla/xui-context-menu';
208
+ import { Button } from '@xsolla/xui-button';
209
+ import { Copy, Scissors, Clipboard } from '@xsolla/xui-icons-base';
210
+
211
+ export default function WithIconsAndShortcuts() {
212
+ return (
213
+ <ContextMenu
214
+ trigger={<Button>Edit</Button>}
215
+ list={[
216
+ { id: 'cut', label: 'Cut', icon: <Scissors />, trailing: { type: 'shortcut', content: 'Cmd+X' } },
217
+ { id: 'copy', label: 'Copy', icon: <Copy />, trailing: { type: 'shortcut', content: 'Cmd+C' } },
218
+ { id: 'paste', label: 'Paste', icon: <Clipboard />, trailing: { type: 'shortcut', content: 'Cmd+V' } },
219
+ ]}
220
+ />
221
+ );
222
+ }
223
+ ```
224
+
225
+ ### Right-Click Context Menu
226
+
227
+ ```tsx
228
+ import * as React from 'react';
229
+ import { ContextMenu } from '@xsolla/xui-context-menu';
230
+
231
+ export default function RightClickMenu() {
232
+ const [position, setPosition] = React.useState<{ x: number; y: number } | null>(null);
233
+
234
+ const handleContextMenu = (e: React.MouseEvent) => {
235
+ e.preventDefault();
236
+ setPosition({ x: e.clientX, y: e.clientY });
237
+ };
238
+
239
+ return (
240
+ <div
241
+ onContextMenu={handleContextMenu}
242
+ style={{ width: 300, height: 200, background: '#f0f0f0', padding: 16 }}
243
+ >
244
+ Right-click anywhere in this area
245
+
246
+ {position && (
247
+ <ContextMenu
248
+ isOpen={!!position}
249
+ onOpenChange={(open) => !open && setPosition(null)}
250
+ position={position}
251
+ list={[
252
+ { id: 'inspect', label: 'Inspect' },
253
+ { id: 'refresh', label: 'Refresh' },
254
+ ]}
255
+ />
256
+ )}
257
+ </div>
258
+ );
259
+ }
260
+ ```
261
+
262
+ ## API Reference
263
+
264
+ ### ContextMenu
265
+
266
+ **ContextMenuProps:**
267
+
268
+ | Prop | Type | Default | Description |
269
+ | :--- | :--- | :------ | :---------- |
270
+ | children | `ReactNode` | - | Compound component children. |
271
+ | trigger | `ReactNode` | - | Element that triggers the menu. |
272
+ | list | `ContextMenuItemData[]` | - | Data-driven item list. |
273
+ | groups | `ContextMenuGroupData[]` | - | Grouped items with labels. |
274
+ | isOpen | `boolean` | - | Controlled open state. |
275
+ | onOpenChange | `(open: boolean) => void` | - | Open state change callback. |
276
+ | position | `{ x: number; y: number }` | - | Fixed position for right-click menus. |
277
+ | size | `"sm" \| "md" \| "lg"` | `"md"` | Menu size variant. |
278
+ | width | `number` | - | Menu width in pixels. |
279
+ | maxHeight | `number` | `300` | Max height before scrolling. |
280
+ | closeOnSelect | `boolean` | `true` | Close menu after item selection. |
281
+ | isLoading | `boolean` | `false` | Show loading spinner. |
282
+ | onSelect | `(item: ContextMenuItemData) => void` | - | Item selection callback. |
283
+ | onCheckedChange | `(id: string, checked: boolean) => void` | - | Checkbox/radio change callback. |
284
+ | aria-label | `string` | - | Accessible menu label. |
285
+
286
+ **ContextMenuItemData:**
287
+
288
+ ```typescript
289
+ interface ContextMenuItemData {
290
+ id: string;
291
+ label: string;
292
+ icon?: ReactNode;
293
+ description?: string;
294
+ disabled?: boolean;
295
+ selected?: boolean;
296
+ checked?: boolean;
297
+ variant?: 'default' | 'checkbox' | 'radio';
298
+ trailing?: { type: 'shortcut' | 'content' | 'none'; content?: string | ReactNode };
299
+ children?: ContextMenuItemData[];
300
+ onPress?: () => void;
301
+ }
302
+ ```
303
+
304
+ ### Compound Components
305
+
306
+ | Component | Description |
307
+ | :-------- | :---------- |
308
+ | `ContextMenu.Item` | Standard menu item. |
309
+ | `ContextMenu.CheckboxItem` | Item with checkbox. |
310
+ | `ContextMenu.RadioGroup` | Container for radio items. |
311
+ | `ContextMenu.RadioItem` | Radio button item. |
312
+ | `ContextMenu.Group` | Group with optional label. |
313
+ | `ContextMenu.Separator` | Visual separator line. |
314
+ | `ContextMenu.Search` | Search input for filtering. |
315
+
316
+ ## Keyboard Navigation
145
317
 
146
318
  | Key | Action |
147
- | --- | --- |
148
- | `↑` / `↓` | Move active option up/down (skips heading/divider). |
149
- | `Enter` / `Space` | Activate the focused option. |
150
- | `Esc` | Close the menu and return focus to the trigger. |
151
- | `Tab` | Close the menu and continue natural focus order. |
152
- | `Home` / `End` | Jump to the first/last option. |
153
- | `→` / `Enter` | Open a submenu when on a `hasSubmenu` option. |
154
- | `←` / `Esc` | Close the submenu and return focus to its parent option. |
155
-
156
- ## Content guidelines
157
-
158
- - Use short, imperative labels ("Edit", "Duplicate", "Sign out").
159
- - Use sentence case for option labels.
160
- - Use uppercase for headings the heading cell already applies the visual treatment.
161
- - Place destructive options at the bottom of the list, ideally separated by a divider.
162
- - Prefer specific empty messages ("No countries match") over the generic default.
163
- - Aim for seven options or fewer per panel; group with headings or split into submenus when longer.
164
-
165
- ## Migration from prior API
166
-
167
- | Old | New |
168
- | --- | --- |
169
- | `ContextMenuCheckboxItem` | `<ContextMenuItem type="option" leadingControl="checkbox" />` |
170
- | `ContextMenuRadioItem` | `<ContextMenuItem type="option" leadingControl="radio" />` |
171
- | `ContextMenuRadioGroup` | `type="radio"` panel preset with shared selected state |
172
- | `ContextMenuGroup` | `<ContextMenuItem type="heading" />` |
173
- | `ContextMenuSeparator` | `<ContextMenuItem type="divider" />` |
174
- | `ContextMenuSearch` | `<ContextMenuItem type="search" />` (or set `searchable: true` on the panel for auto-render) |
175
- | Size scale `s` / `m` / `l` / `xl` | `sm` / `md` / `lg` / `xl` |
319
+ | :-- | :----- |
320
+ | `ArrowDown` | Move to next item |
321
+ | `ArrowUp` | Move to previous item |
322
+ | `Home` | Move to first item |
323
+ | `End` | Move to last item |
324
+ | `Enter` / `Space` | Select current item |
325
+ | `Escape` | Close menu |
326
+ | `Tab` | Close menu |
327
+
328
+ ## Accessibility
329
+
330
+ - Menu has `role="menu"` with proper ARIA attributes
331
+ - Items have `role="menuitem"`, checkboxes have `role="menuitemcheckbox"`
332
+ - Keyboard navigation follows WAI-ARIA menu pattern
333
+ - Focus is trapped within menu when open
334
+ - Escape key closes menu and returns focus to trigger