@ceed/ads 1.29.1 → 1.30.0-next.1
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/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
- package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
- package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/SearchBar/SearchBar.d.ts +21 -0
- package/dist/components/SearchBar/index.d.ts +3 -0
- package/dist/components/data-display/Badge.md +39 -71
- package/dist/components/data-display/DataTable.md +1 -1
- package/dist/components/data-display/InfoSign.md +98 -74
- package/dist/components/data-display/Typography.md +97 -363
- package/dist/components/feedback/Dialog.md +62 -76
- package/dist/components/feedback/Modal.md +44 -259
- package/dist/components/feedback/llms.txt +0 -2
- package/dist/components/index.d.ts +2 -0
- package/dist/components/inputs/Autocomplete.md +107 -356
- package/dist/components/inputs/ButtonGroup.md +106 -115
- package/dist/components/inputs/Calendar.md +459 -98
- package/dist/components/inputs/CurrencyInput.md +5 -183
- package/dist/components/inputs/DatePicker.md +431 -108
- package/dist/components/inputs/DateRangePicker.md +492 -131
- package/dist/components/inputs/FilterMenu.md +19 -169
- package/dist/components/inputs/FilterableCheckboxGroup.md +23 -123
- package/dist/components/inputs/IconButton.md +88 -137
- package/dist/components/inputs/Input.md +0 -5
- package/dist/components/inputs/MonthPicker.md +422 -95
- package/dist/components/inputs/MonthRangePicker.md +466 -89
- package/dist/components/inputs/PercentageInput.md +16 -185
- package/dist/components/inputs/RadioButton.md +35 -163
- package/dist/components/inputs/RadioTileGroup.md +61 -150
- package/dist/components/inputs/SearchBar.md +44 -0
- package/dist/components/inputs/Select.md +326 -222
- package/dist/components/inputs/Switch.md +376 -136
- package/dist/components/inputs/Textarea.md +10 -213
- package/dist/components/inputs/Uploader/Uploader.md +66 -145
- package/dist/components/inputs/llms.txt +1 -3
- package/dist/components/navigation/Breadcrumbs.md +322 -80
- package/dist/components/navigation/Dropdown.md +221 -92
- package/dist/components/navigation/IconMenuButton.md +502 -40
- package/dist/components/navigation/InsetDrawer.md +738 -68
- package/dist/components/navigation/Link.md +298 -39
- package/dist/components/navigation/Menu.md +285 -92
- package/dist/components/navigation/MenuButton.md +448 -55
- package/dist/components/navigation/Pagination.md +338 -47
- package/dist/components/navigation/ProfileMenu.md +268 -45
- package/dist/components/navigation/Stepper.md +28 -160
- package/dist/components/navigation/Tabs.md +316 -57
- package/dist/components/surfaces/Sheet.md +334 -151
- package/dist/index.browser.js +15 -13
- package/dist/index.browser.js.map +4 -4
- package/dist/index.cjs +289 -288
- package/dist/index.d.ts +1 -1
- package/dist/index.js +426 -369
- package/dist/llms.txt +1 -8
- package/framer/index.js +1 -1
- package/package.json +16 -15
- package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
- package/dist/components/feedback/CircularProgress.md +0 -257
- package/dist/components/feedback/Skeleton.md +0 -280
- package/dist/components/inputs/FormControl.md +0 -361
- package/dist/components/inputs/RadioList.md +0 -241
- package/dist/components/inputs/Slider.md +0 -334
- package/dist/guides/ThemeProvider.md +0 -116
- package/dist/guides/llms.txt +0 -9
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# IconMenuButton
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Introduction
|
|
4
4
|
|
|
5
|
-
IconMenuButton is
|
|
5
|
+
IconMenuButton is a compact menu trigger component that displays an icon-only button to reveal a dropdown menu of actions. Unlike MenuButton which shows text, IconMenuButton uses minimal screen space by displaying only an icon (commonly a "more" or kebab icon). It's ideal for table row actions, card menus, and any context where space is limited but multiple actions need to be accessible.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
<IconMenuButton
|
|
@@ -30,6 +30,15 @@ IconMenuButton is part of the **menu component family** alongside Dropdown, Menu
|
|
|
30
30
|
| color | — | "primary" |
|
|
31
31
|
| size | — | — |
|
|
32
32
|
|
|
33
|
+
> ⚠️ **Usage Warning** ⚠️
|
|
34
|
+
>
|
|
35
|
+
> Accessibility consideration for icon-only buttons:
|
|
36
|
+
>
|
|
37
|
+
> - **Always provide `aria-label`**: Screen readers need text context
|
|
38
|
+
> - **Use familiar icons**: MoreVert (⋮), MoreHoriz (⋯) are commonly recognized
|
|
39
|
+
> - **Consider MenuButton**: When space allows, text labels improve clarity
|
|
40
|
+
> - **Tooltip support**: Consider adding tooltips for sighted users
|
|
41
|
+
|
|
33
42
|
## Usage
|
|
34
43
|
|
|
35
44
|
```tsx
|
|
@@ -50,11 +59,30 @@ function RowActions({ onEdit, onDelete }) {
|
|
|
50
59
|
}
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
##
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
### Playground
|
|
65
|
+
|
|
66
|
+
Interactive example with all controls.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<IconMenuButton
|
|
70
|
+
icon={<MoreVert />}
|
|
71
|
+
items={[{
|
|
72
|
+
text: 'Profile'
|
|
73
|
+
}, {
|
|
74
|
+
text: 'My account'
|
|
75
|
+
}, {
|
|
76
|
+
text: 'Logout'
|
|
77
|
+
}]}
|
|
78
|
+
variant="solid"
|
|
79
|
+
color="primary"
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
54
82
|
|
|
55
83
|
### Standalone
|
|
56
84
|
|
|
57
|
-
Icon button
|
|
85
|
+
Icon button without dropdown functionality.
|
|
58
86
|
|
|
59
87
|
```tsx
|
|
60
88
|
<IconMenuButton
|
|
@@ -77,7 +105,7 @@ Icon button rendered without dropdown menu functionality, useful as a custom lin
|
|
|
77
105
|
|
|
78
106
|
### Placement: Bottom Start
|
|
79
107
|
|
|
80
|
-
Menu aligns to the left edge of the
|
|
108
|
+
Menu aligns to the left edge of the button.
|
|
81
109
|
|
|
82
110
|
```tsx
|
|
83
111
|
<IconMenuButton
|
|
@@ -100,7 +128,7 @@ Menu aligns to the left edge of the icon button.
|
|
|
100
128
|
|
|
101
129
|
### Placement: Bottom
|
|
102
130
|
|
|
103
|
-
Menu centers below the
|
|
131
|
+
Menu centers below the button.
|
|
104
132
|
|
|
105
133
|
```tsx
|
|
106
134
|
<IconMenuButton
|
|
@@ -123,7 +151,7 @@ Menu centers below the icon button.
|
|
|
123
151
|
|
|
124
152
|
### Placement: Bottom End
|
|
125
153
|
|
|
126
|
-
Menu aligns to the right edge of the
|
|
154
|
+
Menu aligns to the right edge of the button.
|
|
127
155
|
|
|
128
156
|
```tsx
|
|
129
157
|
<IconMenuButton
|
|
@@ -144,6 +172,25 @@ Menu aligns to the right edge of the icon button. Recommended when the button is
|
|
|
144
172
|
/>
|
|
145
173
|
```
|
|
146
174
|
|
|
175
|
+
## When to Use
|
|
176
|
+
|
|
177
|
+
### ✅ Good Use Cases
|
|
178
|
+
|
|
179
|
+
- **Table row actions**: Edit, delete, view details actions for each row
|
|
180
|
+
- **Card menus**: Action menus on cards or list items
|
|
181
|
+
- **Toolbar overflow**: Secondary actions in space-constrained toolbars
|
|
182
|
+
- **Mobile interfaces**: Compact action triggers on mobile screens
|
|
183
|
+
- **Grid item actions**: Actions for items in a grid layout
|
|
184
|
+
- **List item actions**: Contextual actions for list entries
|
|
185
|
+
|
|
186
|
+
### ❌ When Not to Use
|
|
187
|
+
|
|
188
|
+
- **Primary actions**: Important actions should be visible buttons
|
|
189
|
+
- **When space allows text**: Use MenuButton for clearer labeling
|
|
190
|
+
- **Form submissions**: Use standard buttons for form actions
|
|
191
|
+
- **Navigation**: Use proper navigation components for routing
|
|
192
|
+
- **Single action**: Use IconButton for single-action scenarios
|
|
193
|
+
|
|
147
194
|
## Common Use Cases
|
|
148
195
|
|
|
149
196
|
### Table Row Actions
|
|
@@ -154,12 +201,12 @@ function DataTableRow({ row, onEdit, onDelete, onDuplicate }) {
|
|
|
154
201
|
<tr>
|
|
155
202
|
<td>{row.name}</td>
|
|
156
203
|
<td>{row.status}</td>
|
|
204
|
+
<td>{row.date}</td>
|
|
157
205
|
<td>
|
|
158
206
|
<IconMenuButton
|
|
159
207
|
icon={<MoreVertIcon />}
|
|
160
208
|
variant="plain"
|
|
161
209
|
color="neutral"
|
|
162
|
-
size="sm"
|
|
163
210
|
buttonComponentProps={{ 'aria-label': `Actions for ${row.name}` }}
|
|
164
211
|
items={[
|
|
165
212
|
{ text: 'Edit', onClick: () => onEdit(row.id) },
|
|
@@ -173,12 +220,17 @@ function DataTableRow({ row, onEdit, onDelete, onDuplicate }) {
|
|
|
173
220
|
}
|
|
174
221
|
```
|
|
175
222
|
|
|
176
|
-
### Card Actions
|
|
223
|
+
### Card Actions Menu
|
|
177
224
|
|
|
178
225
|
```tsx
|
|
179
226
|
function ProductCard({ product, onAction }) {
|
|
180
227
|
return (
|
|
181
228
|
<Card>
|
|
229
|
+
<CardOverflow>
|
|
230
|
+
<AspectRatio>
|
|
231
|
+
<img src={product.image} alt={product.name} />
|
|
232
|
+
</AspectRatio>
|
|
233
|
+
</CardOverflow>
|
|
182
234
|
<CardContent>
|
|
183
235
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
184
236
|
<Typography level="title-md">{product.name}</Typography>
|
|
@@ -190,24 +242,159 @@ function ProductCard({ product, onAction }) {
|
|
|
190
242
|
items={[
|
|
191
243
|
{ text: 'View Details', onClick: () => onAction('view', product.id) },
|
|
192
244
|
{ text: 'Edit', onClick: () => onAction('edit', product.id) },
|
|
193
|
-
{ text: '
|
|
245
|
+
{ text: 'Add to Cart', onClick: () => onAction('cart', product.id) },
|
|
246
|
+
{ text: 'Share', onClick: () => onAction('share', product.id) },
|
|
194
247
|
]}
|
|
195
248
|
/>
|
|
196
249
|
</Box>
|
|
250
|
+
<Typography level="body-sm">${product.price}</Typography>
|
|
197
251
|
</CardContent>
|
|
198
252
|
</Card>
|
|
199
253
|
);
|
|
200
254
|
}
|
|
201
255
|
```
|
|
202
256
|
|
|
203
|
-
###
|
|
257
|
+
### Comment Actions
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
function Comment({ comment, currentUserId, onAction }) {
|
|
261
|
+
const isOwner = comment.authorId === currentUserId;
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
265
|
+
<Avatar src={comment.authorAvatar} />
|
|
266
|
+
<Box sx={{ flex: 1 }}>
|
|
267
|
+
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
|
|
268
|
+
<Box>
|
|
269
|
+
<Typography level="title-sm">{comment.authorName}</Typography>
|
|
270
|
+
<Typography level="body-xs" color="neutral">
|
|
271
|
+
{comment.timestamp}
|
|
272
|
+
</Typography>
|
|
273
|
+
</Box>
|
|
274
|
+
<IconMenuButton
|
|
275
|
+
icon={<MoreHorizIcon />}
|
|
276
|
+
variant="plain"
|
|
277
|
+
color="neutral"
|
|
278
|
+
size="sm"
|
|
279
|
+
buttonComponentProps={{ 'aria-label': 'Comment actions' }}
|
|
280
|
+
items={[
|
|
281
|
+
{ text: 'Reply', onClick: () => onAction('reply', comment.id) },
|
|
282
|
+
...(isOwner
|
|
283
|
+
? [
|
|
284
|
+
{ text: 'Edit', onClick: () => onAction('edit', comment.id) },
|
|
285
|
+
{ text: 'Delete', onClick: () => onAction('delete', comment.id) },
|
|
286
|
+
]
|
|
287
|
+
: [{ text: 'Report', onClick: () => onAction('report', comment.id) }]),
|
|
288
|
+
]}
|
|
289
|
+
/>
|
|
290
|
+
</Stack>
|
|
291
|
+
<Typography level="body-sm" sx={{ mt: 1 }}>
|
|
292
|
+
{comment.content}
|
|
293
|
+
</Typography>
|
|
294
|
+
</Box>
|
|
295
|
+
</Box>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Notification Item Actions
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
function NotificationItem({ notification, onAction }) {
|
|
304
|
+
return (
|
|
305
|
+
<ListItem
|
|
306
|
+
sx={{
|
|
307
|
+
bgcolor: notification.read ? 'transparent' : 'primary.softBg',
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<ListItemDecorator>
|
|
311
|
+
<Avatar src={notification.avatar} />
|
|
312
|
+
</ListItemDecorator>
|
|
313
|
+
<ListItemContent>
|
|
314
|
+
<Typography level="title-sm">{notification.title}</Typography>
|
|
315
|
+
<Typography level="body-xs">{notification.time}</Typography>
|
|
316
|
+
</ListItemContent>
|
|
317
|
+
<IconMenuButton
|
|
318
|
+
icon={<MoreVertIcon />}
|
|
319
|
+
variant="plain"
|
|
320
|
+
color="neutral"
|
|
321
|
+
size="sm"
|
|
322
|
+
buttonComponentProps={{ 'aria-label': 'Notification actions' }}
|
|
323
|
+
items={[
|
|
324
|
+
{
|
|
325
|
+
text: notification.read ? 'Mark as Unread' : 'Mark as Read',
|
|
326
|
+
onClick: () => onAction('toggleRead', notification.id),
|
|
327
|
+
},
|
|
328
|
+
{ text: 'Dismiss', onClick: () => onAction('dismiss', notification.id) },
|
|
329
|
+
{ text: 'Turn Off', onClick: () => onAction('turnOff', notification.type) },
|
|
330
|
+
]}
|
|
331
|
+
/>
|
|
332
|
+
</ListItem>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### File Item Actions
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
function FileItem({ file, onAction }) {
|
|
341
|
+
const getFileIcon = () => {
|
|
342
|
+
switch (file.type) {
|
|
343
|
+
case 'folder':
|
|
344
|
+
return <FolderIcon />;
|
|
345
|
+
case 'image':
|
|
346
|
+
return <ImageIcon />;
|
|
347
|
+
case 'document':
|
|
348
|
+
return <DescriptionIcon />;
|
|
349
|
+
default:
|
|
350
|
+
return <InsertDriveFileIcon />;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<ListItem>
|
|
356
|
+
<ListItemDecorator>{getFileIcon()}</ListItemDecorator>
|
|
357
|
+
<ListItemContent>
|
|
358
|
+
<Typography level="title-sm">{file.name}</Typography>
|
|
359
|
+
<Typography level="body-xs" color="neutral">
|
|
360
|
+
{file.size} • Modified {file.modified}
|
|
361
|
+
</Typography>
|
|
362
|
+
</ListItemContent>
|
|
363
|
+
<IconMenuButton
|
|
364
|
+
icon={<MoreVertIcon />}
|
|
365
|
+
variant="plain"
|
|
366
|
+
color="neutral"
|
|
367
|
+
size="sm"
|
|
368
|
+
placement="bottom-end"
|
|
369
|
+
buttonComponentProps={{ 'aria-label': `Actions for ${file.name}` }}
|
|
370
|
+
items={[
|
|
371
|
+
{ text: 'Download', onClick: () => onAction('download', file.id) },
|
|
372
|
+
{ text: 'Share', onClick: () => onAction('share', file.id) },
|
|
373
|
+
{ text: 'Rename', onClick: () => onAction('rename', file.id) },
|
|
374
|
+
{ text: 'Move', onClick: () => onAction('move', file.id) },
|
|
375
|
+
{ text: 'Delete', onClick: () => onAction('delete', file.id) },
|
|
376
|
+
]}
|
|
377
|
+
/>
|
|
378
|
+
</ListItem>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Toolbar Overflow Menu
|
|
204
384
|
|
|
205
385
|
```tsx
|
|
206
386
|
function EditorToolbar({ onAction }) {
|
|
207
387
|
return (
|
|
208
388
|
<Stack direction="row" gap={1} alignItems="center">
|
|
209
|
-
<IconButton onClick={() => onAction('bold')}
|
|
210
|
-
|
|
389
|
+
<IconButton onClick={() => onAction('bold')}>
|
|
390
|
+
<FormatBoldIcon />
|
|
391
|
+
</IconButton>
|
|
392
|
+
<IconButton onClick={() => onAction('italic')}>
|
|
393
|
+
<FormatItalicIcon />
|
|
394
|
+
</IconButton>
|
|
395
|
+
<IconButton onClick={() => onAction('underline')}>
|
|
396
|
+
<FormatUnderlinedIcon />
|
|
397
|
+
</IconButton>
|
|
211
398
|
<Divider orientation="vertical" />
|
|
212
399
|
<IconMenuButton
|
|
213
400
|
icon={<MoreHorizIcon />}
|
|
@@ -217,7 +404,9 @@ function EditorToolbar({ onAction }) {
|
|
|
217
404
|
items={[
|
|
218
405
|
{ text: 'Strikethrough', onClick: () => onAction('strikethrough') },
|
|
219
406
|
{ text: 'Superscript', onClick: () => onAction('superscript') },
|
|
407
|
+
{ text: 'Subscript', onClick: () => onAction('subscript') },
|
|
220
408
|
{ text: 'Code', onClick: () => onAction('code') },
|
|
409
|
+
{ text: 'Clear Formatting', onClick: () => onAction('clear') },
|
|
221
410
|
]}
|
|
222
411
|
/>
|
|
223
412
|
</Stack>
|
|
@@ -225,42 +414,315 @@ function EditorToolbar({ onAction }) {
|
|
|
225
414
|
}
|
|
226
415
|
```
|
|
227
416
|
|
|
228
|
-
|
|
417
|
+
### User List Item
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
function UserListItem({ user, onAction }) {
|
|
421
|
+
return (
|
|
422
|
+
<ListItem>
|
|
423
|
+
<ListItemDecorator>
|
|
424
|
+
<Avatar src={user.avatar}>{user.initials}</Avatar>
|
|
425
|
+
</ListItemDecorator>
|
|
426
|
+
<ListItemContent>
|
|
427
|
+
<Typography level="title-sm">{user.name}</Typography>
|
|
428
|
+
<Typography level="body-xs">{user.email}</Typography>
|
|
429
|
+
</ListItemContent>
|
|
430
|
+
<Chip size="sm" color={user.active ? 'success' : 'neutral'}>
|
|
431
|
+
{user.active ? 'Active' : 'Inactive'}
|
|
432
|
+
</Chip>
|
|
433
|
+
<IconMenuButton
|
|
434
|
+
icon={<MoreVertIcon />}
|
|
435
|
+
variant="plain"
|
|
436
|
+
color="neutral"
|
|
437
|
+
size="sm"
|
|
438
|
+
buttonComponentProps={{ 'aria-label': `Actions for ${user.name}` }}
|
|
439
|
+
items={[
|
|
440
|
+
{ text: 'View Profile', onClick: () => onAction('view', user.id) },
|
|
441
|
+
{ text: 'Send Message', onClick: () => onAction('message', user.id) },
|
|
442
|
+
{
|
|
443
|
+
text: user.active ? 'Deactivate' : 'Activate',
|
|
444
|
+
onClick: () => onAction('toggleActive', user.id),
|
|
445
|
+
},
|
|
446
|
+
{ text: 'Remove', onClick: () => onAction('remove', user.id) },
|
|
447
|
+
]}
|
|
448
|
+
/>
|
|
449
|
+
</ListItem>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Props and Customization
|
|
455
|
+
|
|
456
|
+
### Key Props
|
|
457
|
+
|
|
458
|
+
| Prop | Type | Default | Description |
|
|
459
|
+
| ---------------------- | -------------------------------------------------------------- | ---------------- | -------------------------------------------------- |
|
|
460
|
+
| `icon` | `ReactNode` | - | Icon element to display (e.g., `<MoreVertIcon />`) |
|
|
461
|
+
| `items` | `Array<{ text: string; onClick?: () => void }>` | - | Menu items to display |
|
|
462
|
+
| `variant` | `'solid' \| 'soft' \| 'outlined' \| 'plain'` | `'solid'` | Button style variant |
|
|
463
|
+
| `color` | `'primary' \| 'neutral' \| 'danger' \| 'success' \| 'warning'` | `'primary'` | Button color |
|
|
464
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
|
|
465
|
+
| `placement` | `'bottom-start' \| 'bottom' \| 'bottom-end'` | `'bottom-start'` | Menu position |
|
|
466
|
+
| `buttonComponent` | `React.ElementType` | `IconButton` | Custom button component |
|
|
467
|
+
| `buttonComponentProps` | `object` | - | Props for button (include `aria-label`) |
|
|
468
|
+
|
|
469
|
+
### Common Icon Options
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import MoreVertIcon from '@mui/icons-material/MoreVert'; // Vertical dots (⋮)
|
|
473
|
+
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; // Horizontal dots (⋯)
|
|
474
|
+
import SettingsIcon from '@mui/icons-material/Settings'; // Gear icon
|
|
475
|
+
import MenuIcon from '@mui/icons-material/Menu'; // Hamburger menu
|
|
476
|
+
|
|
477
|
+
// Vertical dots (most common)
|
|
478
|
+
<IconMenuButton icon={<MoreVertIcon />} items={items} />
|
|
479
|
+
|
|
480
|
+
// Horizontal dots
|
|
481
|
+
<IconMenuButton icon={<MoreHorizIcon />} items={items} />
|
|
229
482
|
|
|
230
|
-
|
|
483
|
+
// Custom icon
|
|
484
|
+
<IconMenuButton icon={<SettingsIcon />} items={settingsItems} />
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Variant and Color Options
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
// Solid (default) - high emphasis
|
|
491
|
+
<IconMenuButton icon={<MoreVertIcon />} variant="solid" color="primary" />
|
|
492
|
+
|
|
493
|
+
// Plain - minimal emphasis (recommended for tables/lists)
|
|
494
|
+
<IconMenuButton icon={<MoreVertIcon />} variant="plain" color="neutral" />
|
|
231
495
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<IconMenuButton
|
|
235
|
-
icon={<MoreVertIcon />}
|
|
236
|
-
buttonComponentProps={{ 'aria-label': `Actions for ${item.name}` }}
|
|
237
|
-
items={items}
|
|
238
|
-
/>
|
|
496
|
+
// Soft - subtle background
|
|
497
|
+
<IconMenuButton icon={<MoreVertIcon />} variant="soft" color="primary" />
|
|
239
498
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
499
|
+
// Outlined - bordered
|
|
500
|
+
<IconMenuButton icon={<MoreVertIcon />} variant="outlined" color="neutral" />
|
|
501
|
+
```
|
|
243
502
|
|
|
244
|
-
|
|
503
|
+
### Size Options
|
|
245
504
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
<IconMenuButton icon={<MoreHorizIcon />} />
|
|
505
|
+
```tsx
|
|
506
|
+
// Small - for compact lists
|
|
507
|
+
<IconMenuButton icon={<MoreVertIcon />} size="sm" items={items} />
|
|
250
508
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
```
|
|
509
|
+
// Medium (default)
|
|
510
|
+
<IconMenuButton icon={<MoreVertIcon />} size="md" items={items} />
|
|
254
511
|
|
|
255
|
-
|
|
512
|
+
// Large - for prominent actions
|
|
513
|
+
<IconMenuButton icon={<MoreVertIcon />} size="lg" items={items} />
|
|
514
|
+
```
|
|
256
515
|
|
|
257
|
-
|
|
516
|
+
### Accessibility Props
|
|
258
517
|
|
|
259
|
-
|
|
518
|
+
```tsx
|
|
519
|
+
// Always provide aria-label for accessibility
|
|
520
|
+
<IconMenuButton
|
|
521
|
+
icon={<MoreVertIcon />}
|
|
522
|
+
buttonComponentProps={{
|
|
523
|
+
'aria-label': 'More actions for item',
|
|
524
|
+
}}
|
|
525
|
+
items={items}
|
|
526
|
+
/>
|
|
527
|
+
|
|
528
|
+
// Include context in aria-label
|
|
529
|
+
<IconMenuButton
|
|
530
|
+
icon={<MoreVertIcon />}
|
|
531
|
+
buttonComponentProps={{
|
|
532
|
+
'aria-label': `Actions for ${item.name}`,
|
|
533
|
+
}}
|
|
534
|
+
items={items}
|
|
535
|
+
/>
|
|
536
|
+
```
|
|
260
537
|
|
|
261
538
|
## Accessibility
|
|
262
539
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
540
|
+
IconMenuButton requires additional accessibility consideration since it lacks visible text:
|
|
541
|
+
|
|
542
|
+
### ARIA Requirements
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
// ✅ Required: Always provide aria-label
|
|
546
|
+
<IconMenuButton
|
|
547
|
+
icon={<MoreVertIcon />}
|
|
548
|
+
buttonComponentProps={{ 'aria-label': 'More actions' }}
|
|
549
|
+
items={items}
|
|
550
|
+
/>
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Keyboard Navigation
|
|
554
|
+
|
|
555
|
+
- **Enter/Space**: Open menu when button is focused
|
|
556
|
+
- **Arrow Down**: Move to next menu item
|
|
557
|
+
- **Arrow Up**: Move to previous menu item
|
|
558
|
+
- **Escape**: Close menu and return focus to button
|
|
559
|
+
- **Tab**: Close menu and move to next element
|
|
560
|
+
|
|
561
|
+
### Screen Reader Announcements
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
// Button announces: "More actions, menu button"
|
|
565
|
+
<IconMenuButton
|
|
566
|
+
icon={<MoreVertIcon />}
|
|
567
|
+
buttonComponentProps={{ 'aria-label': 'More actions' }}
|
|
568
|
+
/>
|
|
569
|
+
|
|
570
|
+
// Contextual label: "Actions for Product XYZ, menu button"
|
|
571
|
+
<IconMenuButton
|
|
572
|
+
icon={<MoreVertIcon />}
|
|
573
|
+
buttonComponentProps={{ 'aria-label': `Actions for ${product.name}` }}
|
|
574
|
+
/>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Focus Visibility
|
|
578
|
+
|
|
579
|
+
- Clear focus ring on button when focused
|
|
580
|
+
- Menu items have visible focus indicators
|
|
581
|
+
- Focus is properly managed between button and menu
|
|
582
|
+
|
|
583
|
+
## Best Practices
|
|
584
|
+
|
|
585
|
+
### ✅ Do
|
|
586
|
+
|
|
587
|
+
1. **Always include `aria-label`**: Essential for screen readers
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
// ✅ Good: Descriptive aria-label
|
|
591
|
+
<IconMenuButton
|
|
592
|
+
icon={<MoreVertIcon />}
|
|
593
|
+
buttonComponentProps={{ 'aria-label': 'File actions' }}
|
|
594
|
+
items={items}
|
|
595
|
+
/>
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
2. **Use familiar icons**: Stick to recognizable patterns
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
// ✅ Good: Widely recognized menu icons
|
|
602
|
+
<IconMenuButton icon={<MoreVertIcon />} /> // Vertical dots
|
|
603
|
+
<IconMenuButton icon={<MoreHorizIcon />} /> // Horizontal dots
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
3. **Use plain variant in lists/tables**: Reduces visual clutter
|
|
607
|
+
|
|
608
|
+
```tsx
|
|
609
|
+
// ✅ Good: Subtle appearance in data rows
|
|
610
|
+
<IconMenuButton
|
|
611
|
+
icon={<MoreVertIcon />}
|
|
612
|
+
variant="plain"
|
|
613
|
+
color="neutral"
|
|
614
|
+
size="sm"
|
|
615
|
+
/>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
4. **Position menus appropriately**: Prevent overflow issues
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
// ✅ Good: Right-aligned menu for right-side buttons
|
|
622
|
+
<IconMenuButton
|
|
623
|
+
icon={<MoreVertIcon />}
|
|
624
|
+
placement="bottom-end"
|
|
625
|
+
/>
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### ❌ Don't
|
|
629
|
+
|
|
630
|
+
1. **Don't skip aria-label**: Makes button inaccessible
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
// ❌ Bad: No accessible name
|
|
634
|
+
<IconMenuButton icon={<MoreVertIcon />} items={items} />
|
|
635
|
+
|
|
636
|
+
// ✅ Good: Has aria-label
|
|
637
|
+
<IconMenuButton
|
|
638
|
+
icon={<MoreVertIcon />}
|
|
639
|
+
buttonComponentProps={{ 'aria-label': 'Actions' }}
|
|
640
|
+
items={items}
|
|
641
|
+
/>
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
2. **Don't use unfamiliar icons**: Users may not recognize them
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
// ❌ Bad: Unclear what icon means
|
|
648
|
+
<IconMenuButton icon={<StarIcon />} items={items} />
|
|
649
|
+
|
|
650
|
+
// ✅ Good: Standard menu icon
|
|
651
|
+
<IconMenuButton icon={<MoreVertIcon />} items={items} />
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
3. **Don't use for primary actions**: Important actions should be visible
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
// ❌ Bad: Hiding primary action in icon menu
|
|
658
|
+
<IconMenuButton
|
|
659
|
+
icon={<MoreVertIcon />}
|
|
660
|
+
items={[{ text: 'Submit Form', onClick: handleSubmit }]}
|
|
661
|
+
/>
|
|
662
|
+
|
|
663
|
+
// ✅ Good: Primary action is visible
|
|
664
|
+
<Button onClick={handleSubmit}>Submit</Button>
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
4. **Don't use too-small sizes**: Must be easily clickable
|
|
668
|
+
|
|
669
|
+
## Performance Considerations
|
|
670
|
+
|
|
671
|
+
### Memoize Items Array
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
const rowActions = useMemo(
|
|
675
|
+
() => [
|
|
676
|
+
{ text: 'Edit', onClick: () => onEdit(row.id) },
|
|
677
|
+
{ text: 'Delete', onClick: () => onDelete(row.id) },
|
|
678
|
+
],
|
|
679
|
+
[row.id, onEdit, onDelete]
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
<IconMenuButton icon={<MoreVertIcon />} items={rowActions} />
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Memoize in List Renders
|
|
686
|
+
|
|
687
|
+
When rendering many IconMenuButtons in lists/tables:
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
const ActionMenu = memo(({ item, onAction }) => {
|
|
691
|
+
const items = useMemo(
|
|
692
|
+
() => [
|
|
693
|
+
{ text: 'Edit', onClick: () => onAction('edit', item.id) },
|
|
694
|
+
{ text: 'Delete', onClick: () => onAction('delete', item.id) },
|
|
695
|
+
],
|
|
696
|
+
[item.id, onAction]
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
<IconMenuButton
|
|
701
|
+
icon={<MoreVertIcon />}
|
|
702
|
+
variant="plain"
|
|
703
|
+
color="neutral"
|
|
704
|
+
size="sm"
|
|
705
|
+
buttonComponentProps={{ 'aria-label': `Actions for ${item.name}` }}
|
|
706
|
+
items={items}
|
|
707
|
+
/>
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Avoid Inline Object Creation
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
// ❌ Bad: Creates new object every render
|
|
716
|
+
<IconMenuButton
|
|
717
|
+
buttonComponentProps={{ 'aria-label': 'Actions' }}
|
|
718
|
+
/>
|
|
719
|
+
|
|
720
|
+
// ✅ Good: Stable props object
|
|
721
|
+
const buttonProps = useMemo(
|
|
722
|
+
() => ({ 'aria-label': `Actions for ${item.name}` }),
|
|
723
|
+
[item.name]
|
|
724
|
+
);
|
|
725
|
+
<IconMenuButton buttonComponentProps={buttonProps} />
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
IconMenuButton provides a compact way to offer contextual actions in space-constrained interfaces. Always include proper accessibility attributes and use familiar icons to ensure all users can understand and interact with the menu trigger effectively.
|