torch-glare 2.1.7 → 2.2.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/apps/lib/components/BadgeField.tsx +131 -12
- package/apps/lib/components/ContextMenu.tsx +524 -0
- package/apps/lib/components/DropdownMenu.tsx +254 -102
- package/apps/lib/components/SearchableSelect.tsx +308 -0
- package/apps/lib/components/SearchableTable.tsx +363 -0
- package/apps/lib/components/Table.tsx +6 -6
- package/docs/components/context-menu.md +455 -0
- package/docs/components/dropdown-menu.md +37 -34
- package/docs/components/searchable-select.md +359 -0
- package/docs/components/searchable-table.md +419 -0
- package/docs/reference/tailwind-plugins.md +21 -1
- package/docs/tutorials/getting-started.md +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ContextMenu
|
|
3
|
+
description: Right-click (or long-press) menu that opens at the pointer, with submenus, checkboxes, radio groups, and keyboard navigation
|
|
4
|
+
group: Overlays & Dialogs
|
|
5
|
+
keywords: [context-menu, right-click, menu, radix-ui, submenu, checkbox, contextmenu]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# ContextMenu
|
|
9
|
+
|
|
10
|
+
> A right-click / long-press menu that opens at the pointer. Wrap any zone in a `ContextMenuTrigger` and the menu appears where the user clicks — same surface as DropdownMenu (items, groups, the boxed look, auto-grouping), built on `@radix-ui/react-context-menu`.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @radix-ui/react-context-menu
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Import
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
ContextMenu,
|
|
23
|
+
ContextMenuTrigger,
|
|
24
|
+
ContextMenuContent,
|
|
25
|
+
ContextMenuItem,
|
|
26
|
+
ContextMenuGroup,
|
|
27
|
+
ContextMenuRadioGroup,
|
|
28
|
+
ContextMenuCheckboxItem,
|
|
29
|
+
ContextMenuRadioItem,
|
|
30
|
+
ContextMenuLabel,
|
|
31
|
+
ContextMenuShortcut,
|
|
32
|
+
ContextMenuSub,
|
|
33
|
+
ContextMenuSubTrigger,
|
|
34
|
+
ContextMenuSubContent,
|
|
35
|
+
ContextMenuPortal,
|
|
36
|
+
} from '@torch-ui/components'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Examples
|
|
40
|
+
|
|
41
|
+
### Basic Menu
|
|
42
|
+
|
|
43
|
+
Wrap the right-click zone in `ContextMenuTrigger`. The menu opens at the pointer.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem } from '@torch-ui/components'
|
|
47
|
+
|
|
48
|
+
function Example() {
|
|
49
|
+
return (
|
|
50
|
+
<ContextMenu>
|
|
51
|
+
<ContextMenuTrigger asChild>
|
|
52
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
53
|
+
Right-click here
|
|
54
|
+
</div>
|
|
55
|
+
</ContextMenuTrigger>
|
|
56
|
+
<ContextMenuContent>
|
|
57
|
+
<ContextMenuItem>Profile</ContextMenuItem>
|
|
58
|
+
<ContextMenuItem>Settings</ContextMenuItem>
|
|
59
|
+
<ContextMenuItem>Logout</ContextMenuItem>
|
|
60
|
+
</ContextMenuContent>
|
|
61
|
+
</ContextMenu>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### With Icons, Shortcuts, and a Negative Item
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuShortcut } from '@torch-ui/components'
|
|
70
|
+
|
|
71
|
+
function ActionsMenu() {
|
|
72
|
+
return (
|
|
73
|
+
<ContextMenu>
|
|
74
|
+
<ContextMenuTrigger asChild>
|
|
75
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
76
|
+
Right-click the canvas
|
|
77
|
+
</div>
|
|
78
|
+
</ContextMenuTrigger>
|
|
79
|
+
<ContextMenuContent>
|
|
80
|
+
<ContextMenuItem>
|
|
81
|
+
<i className="ri-edit-line" />
|
|
82
|
+
<span>Edit</span>
|
|
83
|
+
<ContextMenuShortcut>⌘E</ContextMenuShortcut>
|
|
84
|
+
</ContextMenuItem>
|
|
85
|
+
<ContextMenuItem>
|
|
86
|
+
<i className="ri-share-line" />
|
|
87
|
+
<span>Share</span>
|
|
88
|
+
<ContextMenuShortcut>⌘⇧S</ContextMenuShortcut>
|
|
89
|
+
</ContextMenuItem>
|
|
90
|
+
<ContextMenuItem variant="Negative">
|
|
91
|
+
<i className="ri-delete-bin-line" />
|
|
92
|
+
<span>Delete</span>
|
|
93
|
+
<ContextMenuShortcut>⌫</ContextMenuShortcut>
|
|
94
|
+
</ContextMenuItem>
|
|
95
|
+
</ContextMenuContent>
|
|
96
|
+
</ContextMenu>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### With Checkboxes
|
|
102
|
+
|
|
103
|
+
> Clicking a checkbox item keeps the menu open — `onSelect` calls `preventDefault()` internally so Radix does not auto-close. Toggle several options without the menu dismissing.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuCheckboxItem } from '@torch-ui/components'
|
|
107
|
+
import { useState } from 'react'
|
|
108
|
+
|
|
109
|
+
function CheckboxMenu() {
|
|
110
|
+
const [showStatusBar, setShowStatusBar] = useState(true)
|
|
111
|
+
const [showActivityBar, setShowActivityBar] = useState(false)
|
|
112
|
+
const [showPanel, setShowPanel] = useState(false)
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ContextMenu>
|
|
116
|
+
<ContextMenuTrigger asChild>
|
|
117
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
118
|
+
Right-click to toggle view
|
|
119
|
+
</div>
|
|
120
|
+
</ContextMenuTrigger>
|
|
121
|
+
<ContextMenuContent>
|
|
122
|
+
<ContextMenuCheckboxItem
|
|
123
|
+
checked={showStatusBar}
|
|
124
|
+
onCheckedChange={setShowStatusBar}
|
|
125
|
+
>
|
|
126
|
+
Status Bar
|
|
127
|
+
</ContextMenuCheckboxItem>
|
|
128
|
+
<ContextMenuCheckboxItem
|
|
129
|
+
checked={showActivityBar}
|
|
130
|
+
onCheckedChange={setShowActivityBar}
|
|
131
|
+
>
|
|
132
|
+
Activity Bar
|
|
133
|
+
</ContextMenuCheckboxItem>
|
|
134
|
+
<ContextMenuCheckboxItem
|
|
135
|
+
checked={showPanel}
|
|
136
|
+
onCheckedChange={setShowPanel}
|
|
137
|
+
>
|
|
138
|
+
Panel
|
|
139
|
+
</ContextMenuCheckboxItem>
|
|
140
|
+
</ContextMenuContent>
|
|
141
|
+
</ContextMenu>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### With Radio Group
|
|
147
|
+
|
|
148
|
+
> Like checkboxes, selecting a radio item keeps the menu open (`onSelect` `preventDefault` is built in).
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuRadioGroup, ContextMenuRadioItem } from '@torch-ui/components'
|
|
152
|
+
import { useState } from 'react'
|
|
153
|
+
|
|
154
|
+
function RadioMenu() {
|
|
155
|
+
const [position, setPosition] = useState('bottom')
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<ContextMenu>
|
|
159
|
+
<ContextMenuTrigger asChild>
|
|
160
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
161
|
+
Right-click to pick a position
|
|
162
|
+
</div>
|
|
163
|
+
</ContextMenuTrigger>
|
|
164
|
+
<ContextMenuContent>
|
|
165
|
+
<ContextMenuRadioGroup value={position} onValueChange={setPosition}>
|
|
166
|
+
<ContextMenuRadioItem value="top">Top</ContextMenuRadioItem>
|
|
167
|
+
<ContextMenuRadioItem value="bottom">Bottom</ContextMenuRadioItem>
|
|
168
|
+
<ContextMenuRadioItem value="right">Right</ContextMenuRadioItem>
|
|
169
|
+
</ContextMenuRadioGroup>
|
|
170
|
+
</ContextMenuContent>
|
|
171
|
+
</ContextMenu>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### With Submenu
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent } from '@torch-ui/components'
|
|
180
|
+
|
|
181
|
+
function SubmenuExample() {
|
|
182
|
+
return (
|
|
183
|
+
<ContextMenu>
|
|
184
|
+
<ContextMenuTrigger asChild>
|
|
185
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
186
|
+
Right-click for more
|
|
187
|
+
</div>
|
|
188
|
+
</ContextMenuTrigger>
|
|
189
|
+
<ContextMenuContent>
|
|
190
|
+
<ContextMenuItem>New Tab</ContextMenuItem>
|
|
191
|
+
<ContextMenuItem>New Window</ContextMenuItem>
|
|
192
|
+
|
|
193
|
+
<ContextMenuSub>
|
|
194
|
+
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
|
|
195
|
+
<ContextMenuSubContent>
|
|
196
|
+
<ContextMenuItem>Developer Tools</ContextMenuItem>
|
|
197
|
+
<ContextMenuItem>Task Manager</ContextMenuItem>
|
|
198
|
+
<ContextMenuItem>Extensions</ContextMenuItem>
|
|
199
|
+
</ContextMenuSubContent>
|
|
200
|
+
</ContextMenuSub>
|
|
201
|
+
|
|
202
|
+
<ContextMenuItem>Print</ContextMenuItem>
|
|
203
|
+
</ContextMenuContent>
|
|
204
|
+
</ContextMenu>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### RTL
|
|
210
|
+
|
|
211
|
+
Set `dir="rtl"` on the Root and the menu, items, and submenu arrows mirror automatically.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuShortcut } from '@torch-ui/components'
|
|
215
|
+
|
|
216
|
+
function RtlMenu() {
|
|
217
|
+
return (
|
|
218
|
+
<ContextMenu dir="rtl">
|
|
219
|
+
<ContextMenuTrigger asChild>
|
|
220
|
+
<div className="flex h-40 w-72 items-center justify-center rounded-md border border-dashed">
|
|
221
|
+
انقر بزر الفأرة الأيمن
|
|
222
|
+
</div>
|
|
223
|
+
</ContextMenuTrigger>
|
|
224
|
+
<ContextMenuContent>
|
|
225
|
+
<ContextMenuItem>
|
|
226
|
+
تحرير
|
|
227
|
+
<ContextMenuShortcut>⌘E</ContextMenuShortcut>
|
|
228
|
+
</ContextMenuItem>
|
|
229
|
+
<ContextMenuItem variant="Negative">حذف</ContextMenuItem>
|
|
230
|
+
</ContextMenuContent>
|
|
231
|
+
</ContextMenu>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## API Reference
|
|
237
|
+
|
|
238
|
+
### ContextMenu (Root)
|
|
239
|
+
|
|
240
|
+
A controlled wrapper around the Radix root. It tracks the open state internally (so a second right-click can dismiss the menu) while still forwarding `open` / `onOpenChange` when you control it.
|
|
241
|
+
|
|
242
|
+
| Prop | Type | Default | Description |
|
|
243
|
+
|------|------|---------|-------------|
|
|
244
|
+
| `open` | `boolean` | - | Controlled open state |
|
|
245
|
+
| `onOpenChange` | `(open: boolean) => void` | - | Callback when state changes |
|
|
246
|
+
| `dir` | `'ltr' \| 'rtl'` | - | Reading direction; mirrors layout and arrows |
|
|
247
|
+
| `modal` | `boolean` | `true` | Whether to block outside interactions |
|
|
248
|
+
| `children` | `React.ReactNode` | - | Trigger and content |
|
|
249
|
+
|
|
250
|
+
### ContextMenuTrigger
|
|
251
|
+
|
|
252
|
+
The right-click zone. Wrap it around the element the menu should open from.
|
|
253
|
+
|
|
254
|
+
| Prop | Type | Default | Description |
|
|
255
|
+
|------|------|---------|-------------|
|
|
256
|
+
| `asChild` | `boolean` | `false` | Merge props onto the child element instead of rendering a wrapper |
|
|
257
|
+
| `disabled` | `boolean` | `false` | Disables opening on right-click |
|
|
258
|
+
|
|
259
|
+
### ContextMenuContent
|
|
260
|
+
|
|
261
|
+
| Prop | Type | Default | Description |
|
|
262
|
+
|------|------|---------|-------------|
|
|
263
|
+
| `variant` | `'PresentationStyle'` | `'PresentationStyle'` | Visual style variant |
|
|
264
|
+
| `theme` | `'dark' \| 'light' \| 'default'` | - | Theme variant (applied as `data-theme`) |
|
|
265
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
266
|
+
| `collisionPadding` | `number` | `8` | Min distance kept from the viewport edge |
|
|
267
|
+
| `autoGroup` | `boolean` | `true` | Auto-wrap loose items in a Boxed group (see Behavior notes) |
|
|
268
|
+
|
|
269
|
+
### ContextMenuItem
|
|
270
|
+
|
|
271
|
+
| Prop | Type | Default | Description |
|
|
272
|
+
|------|------|---------|-------------|
|
|
273
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Item style variant |
|
|
274
|
+
| `size` | `'S' \| 'M'` | `'M'` | Item size |
|
|
275
|
+
| `active` | `boolean` | `false` | Active (selected) state |
|
|
276
|
+
| `disabled` | `boolean` | `false` | Disabled state (still shows but is not selectable) |
|
|
277
|
+
| `onSelect` | `(event: Event) => void` | - | Select handler; closes the menu by default |
|
|
278
|
+
|
|
279
|
+
### ContextMenuCheckboxItem
|
|
280
|
+
|
|
281
|
+
| Prop | Type | Default | Description |
|
|
282
|
+
|------|------|---------|-------------|
|
|
283
|
+
| `checked` | `boolean \| 'indeterminate'` | `false` | Checked state |
|
|
284
|
+
| `onCheckedChange` | `(checked: boolean) => void` | - | Change handler |
|
|
285
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Style variant |
|
|
286
|
+
| `size` | `'S' \| 'M'` | `'M'` | Item size |
|
|
287
|
+
|
|
288
|
+
> Selecting a checkbox item keeps the menu open — `onSelect` `preventDefault` is built in.
|
|
289
|
+
|
|
290
|
+
### ContextMenuRadioGroup
|
|
291
|
+
|
|
292
|
+
| Prop | Type | Default | Description |
|
|
293
|
+
|------|------|---------|-------------|
|
|
294
|
+
| `value` | `string` | - | Selected radio value |
|
|
295
|
+
| `onValueChange` | `(value: string) => void` | - | Change handler |
|
|
296
|
+
| `variant` | `'Boxed' \| 'Plain'` | `'Boxed'` | `Boxed` renders a bordered container; `Plain` is semantic grouping only |
|
|
297
|
+
|
|
298
|
+
### ContextMenuRadioItem
|
|
299
|
+
|
|
300
|
+
| Prop | Type | Default | Description |
|
|
301
|
+
|------|------|---------|-------------|
|
|
302
|
+
| `value` | `string` | Required | Radio option value |
|
|
303
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Style variant |
|
|
304
|
+
| `size` | `'S' \| 'M'` | `'M'` | Item size |
|
|
305
|
+
|
|
306
|
+
> Selecting a radio item keeps the menu open — `onSelect` `preventDefault` is built in.
|
|
307
|
+
|
|
308
|
+
### ContextMenuSubTrigger
|
|
309
|
+
|
|
310
|
+
| Prop | Type | Default | Description |
|
|
311
|
+
|------|------|---------|-------------|
|
|
312
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Style variant |
|
|
313
|
+
| `size` | `'S' \| 'M'` | `'M'` | Item size |
|
|
314
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
315
|
+
|
|
316
|
+
Renders a trailing chevron (`ri-arrow-right-s-line`) that mirrors in RTL.
|
|
317
|
+
|
|
318
|
+
### ContextMenuLabel
|
|
319
|
+
|
|
320
|
+
| Prop | Type | Default | Description |
|
|
321
|
+
|------|------|---------|-------------|
|
|
322
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
323
|
+
|
|
324
|
+
A non-interactive section heading. Acts as a boundary for auto-grouping.
|
|
325
|
+
|
|
326
|
+
### ContextMenuShortcut
|
|
327
|
+
|
|
328
|
+
| Prop | Type | Default | Description |
|
|
329
|
+
|------|------|---------|-------------|
|
|
330
|
+
| `className` | `string` | - | Additional CSS classes |
|
|
331
|
+
|
|
332
|
+
A right-aligned (RTL-aware) span for keyboard hints inside an item.
|
|
333
|
+
|
|
334
|
+
## TypeScript
|
|
335
|
+
|
|
336
|
+
### Key Interfaces
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
|
|
340
|
+
|
|
341
|
+
// Root — controlled wrapper
|
|
342
|
+
type ContextMenuProps = React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Root>
|
|
343
|
+
// { open?, onOpenChange?, dir?, modal?, children, ... }
|
|
344
|
+
|
|
345
|
+
export const ContextMenu: React.FC<ContextMenuProps>
|
|
346
|
+
|
|
347
|
+
// Content
|
|
348
|
+
interface ContextMenuContentProps {
|
|
349
|
+
variant?: 'PresentationStyle'
|
|
350
|
+
theme?: 'dark' | 'light' | 'default'
|
|
351
|
+
className?: string
|
|
352
|
+
collisionPadding?: number // default 8
|
|
353
|
+
autoGroup?: boolean // default true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const ContextMenuContent: React.ForwardRefExoticComponent<ContextMenuContentProps>
|
|
357
|
+
|
|
358
|
+
// Item
|
|
359
|
+
interface ContextMenuItemProps {
|
|
360
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
361
|
+
size?: 'S' | 'M'
|
|
362
|
+
active?: boolean
|
|
363
|
+
disabled?: boolean
|
|
364
|
+
onSelect?: (event: Event) => void
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export const ContextMenuItem: React.ForwardRefExoticComponent<ContextMenuItemProps>
|
|
368
|
+
|
|
369
|
+
// CheckboxItem
|
|
370
|
+
interface ContextMenuCheckboxItemProps {
|
|
371
|
+
checked?: boolean | 'indeterminate'
|
|
372
|
+
onCheckedChange?: (checked: boolean) => void
|
|
373
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
374
|
+
size?: 'S' | 'M'
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const ContextMenuCheckboxItem: React.ForwardRefExoticComponent<ContextMenuCheckboxItemProps>
|
|
378
|
+
|
|
379
|
+
// RadioGroup
|
|
380
|
+
interface ContextMenuRadioGroupProps {
|
|
381
|
+
value?: string
|
|
382
|
+
onValueChange?: (value: string) => void
|
|
383
|
+
variant?: 'Boxed' | 'Plain'
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const ContextMenuRadioGroup: React.ForwardRefExoticComponent<ContextMenuRadioGroupProps>
|
|
387
|
+
|
|
388
|
+
// RadioItem
|
|
389
|
+
interface ContextMenuRadioItemProps {
|
|
390
|
+
value: string
|
|
391
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
392
|
+
size?: 'S' | 'M'
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const ContextMenuRadioItem: React.ForwardRefExoticComponent<ContextMenuRadioItemProps>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Behavior Notes
|
|
399
|
+
|
|
400
|
+
- **Opens at the pointer**: the menu opens on right-click (`contextmenu`) at the exact cursor position, not anchored to a fixed trigger button.
|
|
401
|
+
- **Second right-click closes it**: the Root is made controlled and tracks `open` in context. The Trigger listens in the capture phase, and when the menu is already open it `preventDefault()` / `stopPropagation()` and closes — so a second right-click dismisses instead of re-anchoring (which Radix handles unreliably).
|
|
402
|
+
- **Auto-grouping**: by default (`autoGroup` on `ContextMenuContent`, default `true`) consecutive loose items (`ContextMenuItem`, `ContextMenuCheckboxItem`, `ContextMenuRadioItem`, and `ContextMenuSub`) are automatically wrapped in a `Boxed` `ContextMenuGroup`, so they render inside a boxed container like DropdownMenu even when you do not write a group. Labels and explicit groups act as boundaries and pass through unchanged. Set `autoGroup={false}` to render children verbatim.
|
|
403
|
+
- **Checkbox / radio keep the menu open**: `ContextMenuCheckboxItem` and `ContextMenuRadioItem` call `event.preventDefault()` inside `onSelect`, stopping Radix's default auto-close so users can toggle multiple options in one pass.
|
|
404
|
+
- **Open-only animation**: only the open (enter) state animates (`fade-in`). There is intentionally no exit animation — holding the old DOM node during close breaks close/reposition on a second right-click, so it is omitted to keep repositioning reliable.
|
|
405
|
+
- **Submenus and RTL**: nested `ContextMenuSub` / `ContextMenuSubTrigger` / `ContextMenuSubContent` are supported, and `dir="rtl"` on the Root mirrors the layout (including the submenu chevron).
|
|
406
|
+
|
|
407
|
+
## Accessibility
|
|
408
|
+
|
|
409
|
+
- **Keyboard Support**:
|
|
410
|
+
- Shift+F10 or the Menu (context) key: open the menu from the focused trigger
|
|
411
|
+
- Arrow Down / Arrow Up: move between items
|
|
412
|
+
- Arrow Right: open submenu (Arrow Left to close) — mirrored in RTL
|
|
413
|
+
- Enter / Space: select item
|
|
414
|
+
- Escape: close menu
|
|
415
|
+
- **Touch**: long-press on the trigger opens the menu on touch devices.
|
|
416
|
+
- **ARIA Attributes**: roles and states are applied automatically by Radix UI.
|
|
417
|
+
- **Focus Management**: focus is trapped within the open menu and restored on close.
|
|
418
|
+
- **Screen Readers**: menu structure, checked/selected states, and submenus are announced.
|
|
419
|
+
|
|
420
|
+
## Best Practices
|
|
421
|
+
|
|
422
|
+
1. **Use a clear right-click zone**
|
|
423
|
+
```typescript
|
|
424
|
+
<ContextMenuTrigger asChild>
|
|
425
|
+
<div className="rounded-md border border-dashed">Right-click here</div>
|
|
426
|
+
</ContextMenuTrigger>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
2. **Use labels to separate sections** — they also break auto-grouping into distinct boxed runs
|
|
430
|
+
```typescript
|
|
431
|
+
<ContextMenuLabel>Edit</ContextMenuLabel>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
3. **Show keyboard shortcuts**
|
|
435
|
+
```typescript
|
|
436
|
+
<ContextMenuItem>
|
|
437
|
+
Save
|
|
438
|
+
<ContextMenuShortcut>⌘S</ContextMenuShortcut>
|
|
439
|
+
</ContextMenuItem>
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
4. **Use the Negative variant for destructive actions**
|
|
443
|
+
```typescript
|
|
444
|
+
<ContextMenuItem variant="Negative">Delete</ContextMenuItem>
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
5. **Let checkbox/radio toggles stay open**: rely on the built-in behavior so users can adjust several settings without reopening.
|
|
448
|
+
6. **Avoid deeply nested submenus**: 2 levels max.
|
|
449
|
+
7. **Keep item labels concise**: short, action-oriented text.
|
|
450
|
+
|
|
451
|
+
## Related Components
|
|
452
|
+
|
|
453
|
+
- [DropdownMenu](./dropdown-menu.md) - Button-anchored menu
|
|
454
|
+
- [Popover](./popover.md) - Non-menu popover
|
|
455
|
+
- [Select](./select.md) - Form select field
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: DropdownMenu
|
|
3
|
-
description: Comprehensive dropdown menu component with rich features including sub-menus, checkboxes, radio groups, and keyboard navigation
|
|
3
|
+
description: Comprehensive dropdown menu component with rich features including sub-menus, checkboxes, radio groups, auto-grouping, and keyboard navigation
|
|
4
4
|
group: Overlays & Dialogs
|
|
5
|
-
keywords: [dropdown-menu, menu,
|
|
5
|
+
keywords: [dropdown-menu, menu, radix-ui, submenu, checkbox, radio, auto-group]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# DropdownMenu
|
|
9
9
|
|
|
10
|
-
> A feature-rich dropdown menu component with support for nested submenus, checkbox items, radio groups,
|
|
10
|
+
> A feature-rich dropdown menu component with support for nested submenus, checkbox items, radio groups, auto-grouped boxed sections, and keyboard shortcuts. Perfect for application menus and complex action lists. For right-click menus, see [ContextMenu](./context-menu.md).
|
|
11
11
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
DropdownMenuRadioGroup,
|
|
28
28
|
DropdownMenuRadioItem,
|
|
29
29
|
DropdownMenuLabel,
|
|
30
|
-
DropdownMenuSeparator,
|
|
31
30
|
DropdownMenuShortcut,
|
|
32
31
|
DropdownMenuSub,
|
|
33
32
|
DropdownMenuSubTrigger,
|
|
@@ -36,6 +35,8 @@ import {
|
|
|
36
35
|
} from '@torch-ui/components'
|
|
37
36
|
```
|
|
38
37
|
|
|
38
|
+
> **Auto-grouping:** Loose items placed directly in `DropdownMenuContent` (or `DropdownMenuSubContent`) are automatically wrapped in a boxed group, so they render inside a rounded container without you writing `DropdownMenuGroup` yourself. A `DropdownMenuLabel`, an explicit `DropdownMenuGroup`, or a `DropdownMenuRadioGroup` acts as a boundary that starts a new box. Disable this with `autoGroup={false}` on the content. (There is no `DropdownMenuSeparator` — separation comes from labels and the boxed groups.)
|
|
39
|
+
|
|
39
40
|
## Quick Examples
|
|
40
41
|
|
|
41
42
|
### Basic Menu
|
|
@@ -122,10 +123,12 @@ function ShortcutMenu() {
|
|
|
122
123
|
}
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
### With Labels and
|
|
126
|
+
### With Labels and Grouping
|
|
127
|
+
|
|
128
|
+
Labels act as section boundaries. The loose items between labels are automatically wrapped in boxed groups — no separator component needed.
|
|
126
129
|
|
|
127
130
|
```typescript
|
|
128
|
-
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel
|
|
131
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel } from '@torch-ui/components'
|
|
129
132
|
|
|
130
133
|
function OrganizedMenu() {
|
|
131
134
|
return (
|
|
@@ -138,14 +141,10 @@ function OrganizedMenu() {
|
|
|
138
141
|
<DropdownMenuItem>Profile</DropdownMenuItem>
|
|
139
142
|
<DropdownMenuItem>Billing</DropdownMenuItem>
|
|
140
143
|
|
|
141
|
-
<DropdownMenuSeparator />
|
|
142
|
-
|
|
143
144
|
<DropdownMenuLabel>Settings</DropdownMenuLabel>
|
|
144
145
|
<DropdownMenuItem>Preferences</DropdownMenuItem>
|
|
145
146
|
<DropdownMenuItem>Keyboard Shortcuts</DropdownMenuItem>
|
|
146
147
|
|
|
147
|
-
<DropdownMenuSeparator />
|
|
148
|
-
|
|
149
148
|
<DropdownMenuItem variant="Negative">Logout</DropdownMenuItem>
|
|
150
149
|
</DropdownMenuContent>
|
|
151
150
|
</DropdownMenu>
|
|
@@ -155,8 +154,10 @@ function OrganizedMenu() {
|
|
|
155
154
|
|
|
156
155
|
### With Checkboxes
|
|
157
156
|
|
|
157
|
+
Checkbox and radio items keep the menu **open** when toggled (the built-in `onSelect` calls `preventDefault`), so users can change several options without the menu closing each time.
|
|
158
|
+
|
|
158
159
|
```typescript
|
|
159
|
-
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem
|
|
160
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem } from '@torch-ui/components'
|
|
160
161
|
import { useState } from 'react'
|
|
161
162
|
|
|
162
163
|
function CheckboxMenu() {
|
|
@@ -295,26 +296,25 @@ function SystemMenu() {
|
|
|
295
296
|
}
|
|
296
297
|
```
|
|
297
298
|
|
|
298
|
-
###
|
|
299
|
+
### Right-click Menu
|
|
300
|
+
|
|
301
|
+
For a true right-click (context) menu, use the dedicated [ContextMenu](./context-menu.md) component instead of DropdownMenu — it opens at the pointer on right-click / long-press.
|
|
299
302
|
|
|
300
303
|
```typescript
|
|
301
|
-
import {
|
|
304
|
+
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem } from '@torch-ui/components'
|
|
302
305
|
|
|
303
|
-
function
|
|
306
|
+
function Example() {
|
|
304
307
|
return (
|
|
305
|
-
<
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
<DropdownMenuItem>Properties</DropdownMenuItem>
|
|
316
|
-
</DropdownMenuContent>
|
|
317
|
-
</DropdownMenu>
|
|
308
|
+
<ContextMenu>
|
|
309
|
+
<ContextMenuTrigger className="rounded-md border border-dashed p-8">
|
|
310
|
+
Right-click here
|
|
311
|
+
</ContextMenuTrigger>
|
|
312
|
+
<ContextMenuContent>
|
|
313
|
+
<ContextMenuItem>View</ContextMenuItem>
|
|
314
|
+
<ContextMenuItem>Edit</ContextMenuItem>
|
|
315
|
+
<ContextMenuItem variant="Negative">Delete</ContextMenuItem>
|
|
316
|
+
</ContextMenuContent>
|
|
317
|
+
</ContextMenu>
|
|
318
318
|
)
|
|
319
319
|
}
|
|
320
320
|
```
|
|
@@ -344,13 +344,15 @@ function ContextMenu({ x, y }: { x: number; y: number }) {
|
|
|
344
344
|
| `theme` | `'dark' \| 'light' \| 'default'` | - | Theme variant |
|
|
345
345
|
| `className` | `string` | - | Additional CSS classes |
|
|
346
346
|
| `sideOffset` | `number` | `4` | Distance from trigger |
|
|
347
|
+
| `collisionPadding` | `number` | `8` | Gap kept from viewport edges when flipping/shifting |
|
|
347
348
|
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Alignment |
|
|
349
|
+
| `autoGroup` | `boolean` | `true` | Auto-wrap loose items in boxed groups |
|
|
348
350
|
|
|
349
351
|
### DropdownMenuItem
|
|
350
352
|
|
|
351
353
|
| Prop | Type | Default | Description |
|
|
352
354
|
|------|------|---------|-------------|
|
|
353
|
-
| `variant` | `'Default' \| '
|
|
355
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Item style variant |
|
|
354
356
|
| `size` | `'S' \| 'M'` | `'M'` | Item size |
|
|
355
357
|
| `disabled` | `boolean` | `false` | Disabled state |
|
|
356
358
|
| `active` | `boolean` | `false` | Active state |
|
|
@@ -362,7 +364,7 @@ function ContextMenu({ x, y }: { x: number; y: number }) {
|
|
|
362
364
|
|------|------|---------|-------------|
|
|
363
365
|
| `checked` | `boolean \| 'indeterminate'` | `false` | Checked state |
|
|
364
366
|
| `onCheckedChange` | `(checked: boolean) => void` | - | Change handler |
|
|
365
|
-
| `variant` | `'Default' \| '
|
|
367
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Style variant |
|
|
366
368
|
|
|
367
369
|
### DropdownMenuRadioGroup
|
|
368
370
|
|
|
@@ -376,7 +378,7 @@ function ContextMenu({ x, y }: { x: number; y: number }) {
|
|
|
376
378
|
| Prop | Type | Default | Description |
|
|
377
379
|
|------|------|---------|-------------|
|
|
378
380
|
| `value` | `string` | Required | Radio option value |
|
|
379
|
-
| `variant` | `'Default' \| '
|
|
381
|
+
| `variant` | `'Default' \| 'info' \| 'Negative'` | `'Default'` | Style variant |
|
|
380
382
|
|
|
381
383
|
### DropdownMenuLabel
|
|
382
384
|
|
|
@@ -384,10 +386,11 @@ function ContextMenu({ x, y }: { x: number; y: number }) {
|
|
|
384
386
|
|------|------|---------|-------------|
|
|
385
387
|
| `className` | `string` | - | Additional CSS classes |
|
|
386
388
|
|
|
387
|
-
###
|
|
389
|
+
### DropdownMenuGroup
|
|
388
390
|
|
|
389
391
|
| Prop | Type | Default | Description |
|
|
390
392
|
|------|------|---------|-------------|
|
|
393
|
+
| `variant` | `'Boxed' \| 'Plain'` | `'Boxed'` | Boxed renders a bordered container; Plain is semantic-only |
|
|
391
394
|
| `className` | `string` | - | Additional CSS classes |
|
|
392
395
|
|
|
393
396
|
### DropdownMenuShortcut
|
|
@@ -427,7 +430,7 @@ export const DropdownMenuContent: React.ForwardRefExoticComponent<DropdownMenuCo
|
|
|
427
430
|
|
|
428
431
|
// Item
|
|
429
432
|
interface DropdownMenuItemProps {
|
|
430
|
-
variant?: 'Default' | '
|
|
433
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
431
434
|
size?: 'S' | 'M'
|
|
432
435
|
disabled?: boolean
|
|
433
436
|
active?: boolean
|
|
@@ -440,7 +443,7 @@ export const DropdownMenuItem: React.ForwardRefExoticComponent<DropdownMenuItemP
|
|
|
440
443
|
interface DropdownMenuCheckboxItemProps {
|
|
441
444
|
checked?: boolean | 'indeterminate'
|
|
442
445
|
onCheckedChange?: (checked: boolean) => void
|
|
443
|
-
variant?: 'Default' | '
|
|
446
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
444
447
|
}
|
|
445
448
|
|
|
446
449
|
export const DropdownMenuCheckboxItem: React.ForwardRefExoticComponent<DropdownMenuCheckboxItemProps>
|
|
@@ -448,7 +451,7 @@ export const DropdownMenuCheckboxItem: React.ForwardRefExoticComponent<DropdownM
|
|
|
448
451
|
// RadioItem
|
|
449
452
|
interface DropdownMenuRadioItemProps {
|
|
450
453
|
value: string
|
|
451
|
-
variant?: 'Default' | '
|
|
454
|
+
variant?: 'Default' | 'info' | 'Negative'
|
|
452
455
|
}
|
|
453
456
|
|
|
454
457
|
export const DropdownMenuRadioItem: React.ForwardRefExoticComponent<DropdownMenuRadioItemProps>
|