@ceed/ads 1.23.3 → 1.23.5
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/data-display/Badge.md +71 -39
- package/dist/components/data-display/InfoSign.md +74 -98
- package/dist/components/data-display/Typography.md +310 -61
- package/dist/components/feedback/CircularProgress.md +257 -0
- package/dist/components/feedback/Skeleton.md +280 -0
- package/dist/components/feedback/llms.txt +2 -0
- package/dist/components/inputs/ButtonGroup.md +115 -106
- package/dist/components/inputs/Calendar.md +98 -459
- package/dist/components/inputs/CurrencyInput.md +181 -8
- package/dist/components/inputs/DatePicker.md +108 -436
- package/dist/components/inputs/DateRangePicker.md +130 -496
- package/dist/components/inputs/FilterMenu.md +169 -19
- package/dist/components/inputs/FilterableCheckboxGroup.md +119 -24
- package/dist/components/inputs/FormControl.md +361 -0
- package/dist/components/inputs/IconButton.md +137 -88
- package/dist/components/inputs/MonthPicker.md +95 -427
- package/dist/components/inputs/MonthRangePicker.md +89 -471
- package/dist/components/inputs/PercentageInput.md +183 -19
- package/dist/components/inputs/RadioButton.md +163 -35
- package/dist/components/inputs/RadioList.md +241 -0
- package/dist/components/inputs/RadioTileGroup.md +146 -62
- package/dist/components/inputs/Select.md +219 -328
- package/dist/components/inputs/Slider.md +334 -0
- package/dist/components/inputs/Switch.md +136 -376
- package/dist/components/inputs/Textarea.md +209 -11
- package/dist/components/inputs/Uploader/Uploader.md +145 -66
- package/dist/components/inputs/llms.txt +3 -0
- package/dist/components/navigation/Breadcrumbs.md +80 -322
- package/dist/components/navigation/Dropdown.md +92 -221
- package/dist/components/navigation/IconMenuButton.md +40 -502
- package/dist/components/navigation/InsetDrawer.md +68 -738
- package/dist/components/navigation/Link.md +39 -298
- package/dist/components/navigation/Menu.md +92 -285
- package/dist/components/navigation/MenuButton.md +55 -448
- package/dist/components/navigation/Pagination.md +47 -338
- package/dist/components/navigation/ProfileMenu.md +45 -268
- package/dist/components/navigation/Stepper.md +160 -28
- package/dist/components/navigation/Tabs.md +57 -316
- package/dist/components/surfaces/Sheet.md +150 -333
- package/dist/guides/ThemeProvider.md +116 -0
- package/dist/guides/llms.txt +9 -0
- package/dist/llms.txt +8 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# IconMenuButton
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
IconMenuButton is a compact menu trigger that displays only an icon (typically a "more" or kebab icon) to reveal a dropdown list of actions. It shares the same prop-driven API as MenuButton -- pass an `items` array and the component handles the rest -- but it takes up minimal screen space by omitting visible text. This makes it ideal for table rows, card actions, list items, and toolbars where space is limited.
|
|
4
4
|
|
|
5
|
-
IconMenuButton is
|
|
5
|
+
IconMenuButton is part of the **menu component family** alongside Dropdown, Menu, MenuItem, and MenuButton. When space allows, prefer MenuButton for its clearer labeling. When you need custom content inside the menu (dividers, headers, icons on items), use the lower-level Dropdown + Menu composition instead.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
<IconMenuButton
|
|
@@ -30,15 +30,6 @@ IconMenuButton is a compact menu trigger component that displays an icon-only bu
|
|
|
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
|
-
|
|
42
33
|
## Usage
|
|
43
34
|
|
|
44
35
|
```tsx
|
|
@@ -59,30 +50,11 @@ function RowActions({ onEdit, onDelete }) {
|
|
|
59
50
|
}
|
|
60
51
|
```
|
|
61
52
|
|
|
62
|
-
##
|
|
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
|
-
```
|
|
53
|
+
## Features
|
|
82
54
|
|
|
83
55
|
### Standalone
|
|
84
56
|
|
|
85
|
-
Icon button without dropdown functionality.
|
|
57
|
+
Icon button rendered without dropdown menu functionality, useful as a custom link trigger.
|
|
86
58
|
|
|
87
59
|
```tsx
|
|
88
60
|
<IconMenuButton
|
|
@@ -105,7 +77,7 @@ Icon button without dropdown functionality.
|
|
|
105
77
|
|
|
106
78
|
### Placement: Bottom Start
|
|
107
79
|
|
|
108
|
-
Menu aligns to the left edge of the button.
|
|
80
|
+
Menu aligns to the left edge of the icon button.
|
|
109
81
|
|
|
110
82
|
```tsx
|
|
111
83
|
<IconMenuButton
|
|
@@ -128,7 +100,7 @@ Menu aligns to the left edge of the button.
|
|
|
128
100
|
|
|
129
101
|
### Placement: Bottom
|
|
130
102
|
|
|
131
|
-
Menu centers below the button.
|
|
103
|
+
Menu centers below the icon button.
|
|
132
104
|
|
|
133
105
|
```tsx
|
|
134
106
|
<IconMenuButton
|
|
@@ -151,7 +123,7 @@ Menu centers below the button.
|
|
|
151
123
|
|
|
152
124
|
### Placement: Bottom End
|
|
153
125
|
|
|
154
|
-
Menu aligns to the right edge of the button.
|
|
126
|
+
Menu aligns to the right edge of the icon button. Recommended when the button is positioned on the right side of a container to prevent the menu from overflowing the viewport.
|
|
155
127
|
|
|
156
128
|
```tsx
|
|
157
129
|
<IconMenuButton
|
|
@@ -172,25 +144,6 @@ Menu aligns to the right edge of the button.
|
|
|
172
144
|
/>
|
|
173
145
|
```
|
|
174
146
|
|
|
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
|
-
|
|
194
147
|
## Common Use Cases
|
|
195
148
|
|
|
196
149
|
### Table Row Actions
|
|
@@ -201,12 +154,12 @@ function DataTableRow({ row, onEdit, onDelete, onDuplicate }) {
|
|
|
201
154
|
<tr>
|
|
202
155
|
<td>{row.name}</td>
|
|
203
156
|
<td>{row.status}</td>
|
|
204
|
-
<td>{row.date}</td>
|
|
205
157
|
<td>
|
|
206
158
|
<IconMenuButton
|
|
207
159
|
icon={<MoreVertIcon />}
|
|
208
160
|
variant="plain"
|
|
209
161
|
color="neutral"
|
|
162
|
+
size="sm"
|
|
210
163
|
buttonComponentProps={{ 'aria-label': `Actions for ${row.name}` }}
|
|
211
164
|
items={[
|
|
212
165
|
{ text: 'Edit', onClick: () => onEdit(row.id) },
|
|
@@ -220,17 +173,12 @@ function DataTableRow({ row, onEdit, onDelete, onDuplicate }) {
|
|
|
220
173
|
}
|
|
221
174
|
```
|
|
222
175
|
|
|
223
|
-
### Card Actions
|
|
176
|
+
### Card Actions
|
|
224
177
|
|
|
225
178
|
```tsx
|
|
226
179
|
function ProductCard({ product, onAction }) {
|
|
227
180
|
return (
|
|
228
181
|
<Card>
|
|
229
|
-
<CardOverflow>
|
|
230
|
-
<AspectRatio>
|
|
231
|
-
<img src={product.image} alt={product.name} />
|
|
232
|
-
</AspectRatio>
|
|
233
|
-
</CardOverflow>
|
|
234
182
|
<CardContent>
|
|
235
183
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
236
184
|
<Typography level="title-md">{product.name}</Typography>
|
|
@@ -242,159 +190,24 @@ function ProductCard({ product, onAction }) {
|
|
|
242
190
|
items={[
|
|
243
191
|
{ text: 'View Details', onClick: () => onAction('view', product.id) },
|
|
244
192
|
{ text: 'Edit', onClick: () => onAction('edit', product.id) },
|
|
245
|
-
{ text: '
|
|
246
|
-
{ text: 'Share', onClick: () => onAction('share', product.id) },
|
|
193
|
+
{ text: 'Delete', onClick: () => onAction('delete', product.id) },
|
|
247
194
|
]}
|
|
248
195
|
/>
|
|
249
196
|
</Box>
|
|
250
|
-
<Typography level="body-sm">${product.price}</Typography>
|
|
251
197
|
</CardContent>
|
|
252
198
|
</Card>
|
|
253
199
|
);
|
|
254
200
|
}
|
|
255
201
|
```
|
|
256
202
|
|
|
257
|
-
###
|
|
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
|
|
203
|
+
### Toolbar Overflow
|
|
384
204
|
|
|
385
205
|
```tsx
|
|
386
206
|
function EditorToolbar({ onAction }) {
|
|
387
207
|
return (
|
|
388
208
|
<Stack direction="row" gap={1} alignItems="center">
|
|
389
|
-
<IconButton onClick={() => onAction('bold')}>
|
|
390
|
-
|
|
391
|
-
</IconButton>
|
|
392
|
-
<IconButton onClick={() => onAction('italic')}>
|
|
393
|
-
<FormatItalicIcon />
|
|
394
|
-
</IconButton>
|
|
395
|
-
<IconButton onClick={() => onAction('underline')}>
|
|
396
|
-
<FormatUnderlinedIcon />
|
|
397
|
-
</IconButton>
|
|
209
|
+
<IconButton onClick={() => onAction('bold')}><FormatBoldIcon /></IconButton>
|
|
210
|
+
<IconButton onClick={() => onAction('italic')}><FormatItalicIcon /></IconButton>
|
|
398
211
|
<Divider orientation="vertical" />
|
|
399
212
|
<IconMenuButton
|
|
400
213
|
icon={<MoreHorizIcon />}
|
|
@@ -404,9 +217,7 @@ function EditorToolbar({ onAction }) {
|
|
|
404
217
|
items={[
|
|
405
218
|
{ text: 'Strikethrough', onClick: () => onAction('strikethrough') },
|
|
406
219
|
{ text: 'Superscript', onClick: () => onAction('superscript') },
|
|
407
|
-
{ text: 'Subscript', onClick: () => onAction('subscript') },
|
|
408
220
|
{ text: 'Code', onClick: () => onAction('code') },
|
|
409
|
-
{ text: 'Clear Formatting', onClick: () => onAction('clear') },
|
|
410
221
|
]}
|
|
411
222
|
/>
|
|
412
223
|
</Stack>
|
|
@@ -414,315 +225,42 @@ function EditorToolbar({ onAction }) {
|
|
|
414
225
|
}
|
|
415
226
|
```
|
|
416
227
|
|
|
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} />
|
|
482
|
-
|
|
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" />
|
|
495
|
-
|
|
496
|
-
// Soft - subtle background
|
|
497
|
-
<IconMenuButton icon={<MoreVertIcon />} variant="soft" color="primary" />
|
|
498
|
-
|
|
499
|
-
// Outlined - bordered
|
|
500
|
-
<IconMenuButton icon={<MoreVertIcon />} variant="outlined" color="neutral" />
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
### Size Options
|
|
504
|
-
|
|
505
|
-
```tsx
|
|
506
|
-
// Small - for compact lists
|
|
507
|
-
<IconMenuButton icon={<MoreVertIcon />} size="sm" items={items} />
|
|
508
|
-
|
|
509
|
-
// Medium (default)
|
|
510
|
-
<IconMenuButton icon={<MoreVertIcon />} size="md" items={items} />
|
|
511
|
-
|
|
512
|
-
// Large - for prominent actions
|
|
513
|
-
<IconMenuButton icon={<MoreVertIcon />} size="lg" items={items} />
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
### Accessibility Props
|
|
517
|
-
|
|
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
|
-
```
|
|
537
|
-
|
|
538
|
-
## Accessibility
|
|
539
|
-
|
|
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
228
|
## Best Practices
|
|
584
229
|
|
|
585
|
-
|
|
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
|
-
```
|
|
230
|
+
- **Always provide `aria-label`.** Since IconMenuButton has no visible text, screen readers rely entirely on the `aria-label` to announce the button's purpose. Include context when possible.
|
|
653
231
|
|
|
654
|
-
|
|
232
|
+
```tsx
|
|
233
|
+
{/* ✅ Good: Descriptive, contextual label */}
|
|
234
|
+
<IconMenuButton
|
|
235
|
+
icon={<MoreVertIcon />}
|
|
236
|
+
buttonComponentProps={{ 'aria-label': `Actions for ${item.name}` }}
|
|
237
|
+
items={items}
|
|
238
|
+
/>
|
|
655
239
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
|
240
|
+
{/* ❌ Bad: No accessible name */}
|
|
241
|
+
<IconMenuButton icon={<MoreVertIcon />} items={items} />
|
|
242
|
+
```
|
|
668
243
|
|
|
669
|
-
|
|
244
|
+
- **Use familiar icons.** Stick to universally recognized menu icons like MoreVert or MoreHoriz. Unfamiliar icons confuse users about what will happen when they click.
|
|
670
245
|
|
|
671
|
-
|
|
246
|
+
```tsx
|
|
247
|
+
{/* ✅ Good: Standard kebab/meatball icons */}
|
|
248
|
+
<IconMenuButton icon={<MoreVertIcon />} />
|
|
249
|
+
<IconMenuButton icon={<MoreHorizIcon />} />
|
|
672
250
|
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
);
|
|
251
|
+
{/* ❌ Avoid: Non-standard icon for a menu trigger */}
|
|
252
|
+
<IconMenuButton icon={<StarIcon />} />
|
|
253
|
+
```
|
|
698
254
|
|
|
699
|
-
|
|
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
|
-
```
|
|
255
|
+
- **Prefer `variant="plain"` in data-dense contexts.** In tables, lists, and cards, a plain icon button reduces visual noise while remaining discoverable on hover.
|
|
711
256
|
|
|
712
|
-
|
|
257
|
+
- **Position menus to avoid overflow.** Use `placement="bottom-end"` for buttons near the right edge of a container, and `placement="bottom-start"` for buttons near the left edge.
|
|
713
258
|
|
|
714
|
-
|
|
715
|
-
// ❌ Bad: Creates new object every render
|
|
716
|
-
<IconMenuButton
|
|
717
|
-
buttonComponentProps={{ 'aria-label': 'Actions' }}
|
|
718
|
-
/>
|
|
259
|
+
- **Do not hide primary actions.** Important actions like "Save" or "Submit" should always be visible as standalone buttons. Reserve IconMenuButton for secondary or contextual actions.
|
|
719
260
|
|
|
720
|
-
|
|
721
|
-
const buttonProps = useMemo(
|
|
722
|
-
() => ({ 'aria-label': `Actions for ${item.name}` }),
|
|
723
|
-
[item.name]
|
|
724
|
-
);
|
|
725
|
-
<IconMenuButton buttonComponentProps={buttonProps} />
|
|
726
|
-
```
|
|
261
|
+
## Accessibility
|
|
727
262
|
|
|
728
|
-
|
|
263
|
+
- **ARIA requirements**: Always pass `'aria-label'` through `buttonComponentProps`. The button automatically receives `aria-haspopup="true"` and `aria-expanded` reflecting the menu state.
|
|
264
|
+
- **Keyboard navigation**: Enter or Space opens the menu. Arrow Down/Up moves between items. Escape closes the menu and returns focus to the icon button. Tab closes the menu and advances focus.
|
|
265
|
+
- **Focus visibility**: The icon button displays a visible focus ring when focused via keyboard, and menu items have clear focus indicators for keyboard navigation.
|
|
266
|
+
- **Touch targets**: Ensure the button size is at least 44x44 CSS pixels for comfortable touch interaction. Avoid using `size="sm"` in mobile-primary interfaces unless the surrounding layout provides adequate spacing.
|