@ceed/ads 1.29.0 → 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.
Files changed (64) hide show
  1. package/dist/components/CurrencyInput/CurrencyInput.d.ts +1 -1
  2. package/dist/components/CurrencyInput/hooks/use-currency-setting.d.ts +2 -2
  3. package/dist/components/DataTable/hooks.d.ts +2 -1
  4. package/dist/components/DataTable/utils.d.ts +1 -0
  5. package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
  6. package/dist/components/SearchBar/SearchBar.d.ts +21 -0
  7. package/dist/components/SearchBar/index.d.ts +3 -0
  8. package/dist/components/data-display/Badge.md +39 -71
  9. package/dist/components/data-display/DataTable.md +1 -1
  10. package/dist/components/data-display/InfoSign.md +98 -74
  11. package/dist/components/data-display/Typography.md +97 -363
  12. package/dist/components/feedback/Dialog.md +62 -76
  13. package/dist/components/feedback/Modal.md +44 -259
  14. package/dist/components/feedback/llms.txt +0 -2
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/components/inputs/Autocomplete.md +107 -356
  17. package/dist/components/inputs/ButtonGroup.md +106 -115
  18. package/dist/components/inputs/Calendar.md +459 -98
  19. package/dist/components/inputs/CurrencyInput.md +5 -183
  20. package/dist/components/inputs/DatePicker.md +431 -108
  21. package/dist/components/inputs/DateRangePicker.md +492 -131
  22. package/dist/components/inputs/FilterMenu.md +19 -169
  23. package/dist/components/inputs/FilterableCheckboxGroup.md +23 -123
  24. package/dist/components/inputs/IconButton.md +88 -137
  25. package/dist/components/inputs/Input.md +0 -5
  26. package/dist/components/inputs/MonthPicker.md +422 -95
  27. package/dist/components/inputs/MonthRangePicker.md +466 -89
  28. package/dist/components/inputs/PercentageInput.md +16 -185
  29. package/dist/components/inputs/RadioButton.md +35 -163
  30. package/dist/components/inputs/RadioTileGroup.md +61 -150
  31. package/dist/components/inputs/SearchBar.md +44 -0
  32. package/dist/components/inputs/Select.md +326 -222
  33. package/dist/components/inputs/Switch.md +376 -136
  34. package/dist/components/inputs/Textarea.md +10 -213
  35. package/dist/components/inputs/Uploader/Uploader.md +66 -145
  36. package/dist/components/inputs/llms.txt +1 -3
  37. package/dist/components/navigation/Breadcrumbs.md +322 -80
  38. package/dist/components/navigation/Dropdown.md +221 -92
  39. package/dist/components/navigation/IconMenuButton.md +502 -40
  40. package/dist/components/navigation/InsetDrawer.md +738 -68
  41. package/dist/components/navigation/Link.md +298 -39
  42. package/dist/components/navigation/Menu.md +285 -92
  43. package/dist/components/navigation/MenuButton.md +448 -55
  44. package/dist/components/navigation/Pagination.md +338 -47
  45. package/dist/components/navigation/ProfileMenu.md +268 -45
  46. package/dist/components/navigation/Stepper.md +28 -160
  47. package/dist/components/navigation/Tabs.md +316 -57
  48. package/dist/components/surfaces/Sheet.md +334 -151
  49. package/dist/index.browser.js +15 -13
  50. package/dist/index.browser.js.map +4 -4
  51. package/dist/index.cjs +313 -291
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +450 -372
  54. package/dist/llms.txt +1 -8
  55. package/framer/index.js +1 -1
  56. package/package.json +16 -15
  57. package/dist/chunks/rehype-accent-FZRUD7VI.js +0 -39
  58. package/dist/components/feedback/CircularProgress.md +0 -257
  59. package/dist/components/feedback/Skeleton.md +0 -280
  60. package/dist/components/inputs/FormControl.md +0 -361
  61. package/dist/components/inputs/RadioList.md +0 -241
  62. package/dist/components/inputs/Slider.md +0 -334
  63. package/dist/guides/ThemeProvider.md +0 -116
  64. package/dist/guides/llms.txt +0 -9
@@ -1,8 +1,8 @@
1
1
  # IconMenuButton
2
2
 
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.
3
+ ## Introduction
4
4
 
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.
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
- ## Features
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 rendered without dropdown menu functionality, useful as a custom link trigger.
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 icon button.
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 icon button.
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 icon button. Recommended when the button is positioned on the right side of a container to prevent the menu from overflowing the viewport.
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: 'Delete', onClick: () => onAction('delete', product.id) },
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
- ### Toolbar Overflow
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')}><FormatBoldIcon /></IconButton>
210
- <IconButton onClick={() => onAction('italic')}><FormatItalicIcon /></IconButton>
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
- ## Best Practices
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
- - **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.
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
- ```tsx
233
- {/* Good: Descriptive, contextual label */}
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
- {/* Bad: No accessible name */}
241
- <IconMenuButton icon={<MoreVertIcon />} items={items} />
242
- ```
499
+ // Outlined - bordered
500
+ <IconMenuButton icon={<MoreVertIcon />} variant="outlined" color="neutral" />
501
+ ```
243
502
 
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.
503
+ ### Size Options
245
504
 
246
- ```tsx
247
- {/* Good: Standard kebab/meatball icons */}
248
- <IconMenuButton icon={<MoreVertIcon />} />
249
- <IconMenuButton icon={<MoreHorizIcon />} />
505
+ ```tsx
506
+ // Small - for compact lists
507
+ <IconMenuButton icon={<MoreVertIcon />} size="sm" items={items} />
250
508
 
251
- {/* Avoid: Non-standard icon for a menu trigger */}
252
- <IconMenuButton icon={<StarIcon />} />
253
- ```
509
+ // Medium (default)
510
+ <IconMenuButton icon={<MoreVertIcon />} size="md" items={items} />
254
511
 
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.
512
+ // Large - for prominent actions
513
+ <IconMenuButton icon={<MoreVertIcon />} size="lg" items={items} />
514
+ ```
256
515
 
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.
516
+ ### Accessibility Props
258
517
 
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.
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
- - **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.
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.