@xsolla/xui-context-menu 0.149.1 → 0.150.0-pr250.1778061864

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,336 +1,175 @@
1
1
  # Context Menu
2
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.
3
+ ## Overview
4
4
 
5
- ## Installation
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`.
6
6
 
7
- ```bash
8
- npm install @xsolla/xui-context-menu
9
- # or
10
- yarn add @xsolla/xui-context-menu
11
- ```
7
+ ## When to use
12
8
 
13
- ## Demo
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.
14
12
 
15
- ### Basic Context Menu
13
+ ## When not to use
16
14
 
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
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`.
38
18
 
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';
19
+ ## Installation
96
20
 
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
- />
21
+ ```bash
22
+ yarn add @xsolla/xui-context-menu
110
23
  ```
111
24
 
112
- ## Examples
25
+ ## Two API paths
113
26
 
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
- ```
27
+ ### Preset path
143
28
 
144
- ### Radio Group
29
+ Pass `type` and `items`. The panel renders the preset's chrome and composes each option with the right control or slot.
145
30
 
146
31
  ```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
- ```
32
+ import { ContextMenu } from "@xsolla/xui-context-menu";
167
33
 
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
- }
34
+ <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
+ />;
203
43
  ```
204
44
 
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
- ```
45
+ ### Custom path
226
46
 
227
- ### Right-Click Context Menu
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.
228
48
 
229
49
  ```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
- }
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>;
262
59
  ```
263
60
 
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
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
319
145
 
320
146
  | 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
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` |