@ceed/ads 1.23.2 → 1.23.4

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 (45) hide show
  1. package/dist/components/data-display/Badge.md +71 -39
  2. package/dist/components/data-display/InfoSign.md +74 -98
  3. package/dist/components/data-display/Typography.md +310 -61
  4. package/dist/components/feedback/CircularProgress.md +257 -0
  5. package/dist/components/feedback/Dialog.md +8 -4
  6. package/dist/components/feedback/Modal.md +7 -3
  7. package/dist/components/feedback/Skeleton.md +280 -0
  8. package/dist/components/feedback/llms.txt +2 -0
  9. package/dist/components/inputs/ButtonGroup.md +115 -106
  10. package/dist/components/inputs/Calendar.md +98 -459
  11. package/dist/components/inputs/CurrencyInput.md +181 -8
  12. package/dist/components/inputs/DatePicker.md +108 -436
  13. package/dist/components/inputs/DateRangePicker.md +130 -496
  14. package/dist/components/inputs/FilterMenu.md +169 -19
  15. package/dist/components/inputs/FilterableCheckboxGroup.md +119 -24
  16. package/dist/components/inputs/FormControl.md +368 -0
  17. package/dist/components/inputs/IconButton.md +137 -88
  18. package/dist/components/inputs/MonthPicker.md +95 -427
  19. package/dist/components/inputs/MonthRangePicker.md +89 -471
  20. package/dist/components/inputs/PercentageInput.md +183 -19
  21. package/dist/components/inputs/RadioButton.md +163 -35
  22. package/dist/components/inputs/RadioList.md +241 -0
  23. package/dist/components/inputs/RadioTileGroup.md +146 -62
  24. package/dist/components/inputs/Select.md +219 -328
  25. package/dist/components/inputs/Slider.md +334 -0
  26. package/dist/components/inputs/Switch.md +136 -376
  27. package/dist/components/inputs/Textarea.md +209 -11
  28. package/dist/components/inputs/Uploader/Uploader.md +145 -66
  29. package/dist/components/inputs/llms.txt +3 -0
  30. package/dist/components/navigation/Breadcrumbs.md +80 -322
  31. package/dist/components/navigation/Dropdown.md +92 -221
  32. package/dist/components/navigation/IconMenuButton.md +40 -502
  33. package/dist/components/navigation/InsetDrawer.md +68 -738
  34. package/dist/components/navigation/Link.md +39 -298
  35. package/dist/components/navigation/Menu.md +92 -285
  36. package/dist/components/navigation/MenuButton.md +55 -448
  37. package/dist/components/navigation/Pagination.md +47 -338
  38. package/dist/components/navigation/ProfileMenu.md +45 -268
  39. package/dist/components/navigation/Stepper.md +160 -28
  40. package/dist/components/navigation/Tabs.md +57 -316
  41. package/dist/components/surfaces/Sheet.md +150 -333
  42. package/dist/guides/ThemeProvider.md +116 -0
  43. package/dist/guides/llms.txt +9 -0
  44. package/dist/llms.txt +8 -0
  45. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  # IconMenuButton
2
2
 
3
- ## Introduction
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 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.
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
- ## 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
- ```
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 Menu
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: 'Add to Cart', onClick: () => onAction('cart', product.id) },
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
- ### 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
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
- <FormatBoldIcon />
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
- ### 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
- ```
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
- 3. **Don't use for primary actions**: Important actions should be visible
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
- ```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
240
+ {/* ❌ Bad: No accessible name */}
241
+ <IconMenuButton icon={<MoreVertIcon />} items={items} />
242
+ ```
668
243
 
669
- ## Performance Considerations
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
- ### Memoize Items Array
246
+ ```tsx
247
+ {/* ✅ Good: Standard kebab/meatball icons */}
248
+ <IconMenuButton icon={<MoreVertIcon />} />
249
+ <IconMenuButton icon={<MoreHorizIcon />} />
672
250
 
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
- );
251
+ {/* ❌ Avoid: Non-standard icon for a menu trigger */}
252
+ <IconMenuButton icon={<StarIcon />} />
253
+ ```
698
254
 
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
- ```
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
- ### Avoid Inline Object Creation
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
- ```tsx
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
- // ✅ Good: Stable props object
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
- 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.
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.