@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.
Files changed (43) 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/Skeleton.md +280 -0
  6. package/dist/components/feedback/llms.txt +2 -0
  7. package/dist/components/inputs/ButtonGroup.md +115 -106
  8. package/dist/components/inputs/Calendar.md +98 -459
  9. package/dist/components/inputs/CurrencyInput.md +181 -8
  10. package/dist/components/inputs/DatePicker.md +108 -436
  11. package/dist/components/inputs/DateRangePicker.md +130 -496
  12. package/dist/components/inputs/FilterMenu.md +169 -19
  13. package/dist/components/inputs/FilterableCheckboxGroup.md +119 -24
  14. package/dist/components/inputs/FormControl.md +361 -0
  15. package/dist/components/inputs/IconButton.md +137 -88
  16. package/dist/components/inputs/MonthPicker.md +95 -427
  17. package/dist/components/inputs/MonthRangePicker.md +89 -471
  18. package/dist/components/inputs/PercentageInput.md +183 -19
  19. package/dist/components/inputs/RadioButton.md +163 -35
  20. package/dist/components/inputs/RadioList.md +241 -0
  21. package/dist/components/inputs/RadioTileGroup.md +146 -62
  22. package/dist/components/inputs/Select.md +219 -328
  23. package/dist/components/inputs/Slider.md +334 -0
  24. package/dist/components/inputs/Switch.md +136 -376
  25. package/dist/components/inputs/Textarea.md +209 -11
  26. package/dist/components/inputs/Uploader/Uploader.md +145 -66
  27. package/dist/components/inputs/llms.txt +3 -0
  28. package/dist/components/navigation/Breadcrumbs.md +80 -322
  29. package/dist/components/navigation/Dropdown.md +92 -221
  30. package/dist/components/navigation/IconMenuButton.md +40 -502
  31. package/dist/components/navigation/InsetDrawer.md +68 -738
  32. package/dist/components/navigation/Link.md +39 -298
  33. package/dist/components/navigation/Menu.md +92 -285
  34. package/dist/components/navigation/MenuButton.md +55 -448
  35. package/dist/components/navigation/Pagination.md +47 -338
  36. package/dist/components/navigation/ProfileMenu.md +45 -268
  37. package/dist/components/navigation/Stepper.md +160 -28
  38. package/dist/components/navigation/Tabs.md +57 -316
  39. package/dist/components/surfaces/Sheet.md +150 -333
  40. package/dist/guides/ThemeProvider.md +116 -0
  41. package/dist/guides/llms.txt +9 -0
  42. package/dist/llms.txt +8 -0
  43. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  # MenuButton
2
2
 
3
- ## Introduction
3
+ MenuButton is a high-level compound component that combines a labeled button trigger with a dropdown menu in a single, prop-driven API. Pass `buttonText` and an `items` array and the component handles the Dropdown, Menu, and MenuItem composition internally. It is the recommended choice when you need a simple text-triggered menu without custom content inside the menu panel.
4
4
 
5
- MenuButton is a compound component that combines a button with a dropdown menu, allowing users to trigger a list of actions or navigation options from a single interactive element. It displays a button with text and an optional dropdown indicator icon that, when clicked, reveals a menu of selectable items. MenuButton is commonly used for action menus, navigation shortcuts, and grouped operations in toolbars and headers.
5
+ MenuButton belongs to the **menu component family** alongside Dropdown, Menu, MenuItem, and IconMenuButton. For icon-only triggers, use IconMenuButton. For menus that need custom content (headers, dividers, mixed elements), use the lower-level Dropdown + Menu composition instead.
6
6
 
7
7
  ```tsx
8
8
  <MenuButton
@@ -34,15 +34,6 @@ MenuButton is a compound component that combines a button with a dropdown menu,
34
34
  | color | — | "primary" |
35
35
  | size | — | — |
36
36
 
37
- > ⚠️ **Usage Warning** ⚠️
38
- >
39
- > Choose the right component for your use case:
40
- >
41
- > - **MenuButton**: For action menus triggered by a labeled button
42
- > - **IconMenuButton**: For action menus triggered by an icon-only button
43
- > - **Dropdown**: For more complex menu structures with custom triggers
44
- > - **Select**: For form inputs where user selects a value (not actions)
45
-
46
37
  ## Usage
47
38
 
48
39
  ```tsx
@@ -62,31 +53,19 @@ function UserMenu() {
62
53
  }
63
54
  ```
64
55
 
65
- ## Examples
56
+ ## When to Use Which
66
57
 
67
- ### Playground
58
+ | Component | Trigger | Content | Use when |
59
+ | ------------------- | ----------- | ---------------- | ------------------------------------------------------------ |
60
+ | **MenuButton** | Text button | Simple item list | You need a labeled trigger with a flat list of text options |
61
+ | **IconMenuButton** | Icon button | Simple item list | Space is limited (tables, cards, toolbars) |
62
+ | **Dropdown + Menu** | Any element | Rich content | You need dividers, headers, icons in items, or custom layout |
68
63
 
69
- Interactive example with all controls.
64
+ ## Features
70
65
 
71
- ```tsx
72
- <MenuButton
73
- buttonText="Dashboard..."
74
- items={[{
75
- text: 'Profile'
76
- }, {
77
- text: 'My account'
78
- }, {
79
- text: 'Logout'
80
- }]}
81
- showIcon
82
- variant="solid"
83
- color="primary"
84
- />
85
- ```
66
+ ### Standalone (Without Dropdown Icon)
86
67
 
87
- ### Standalone (Without Icon)
88
-
89
- Use as a simple navigation link without the dropdown icon.
68
+ Use `showIcon={false}` and a custom `buttonComponent` to render MenuButton as a standalone navigation link without the dropdown indicator.
90
69
 
91
70
  ```tsx
92
71
  <MenuButton
@@ -110,7 +89,7 @@ Use as a simple navigation link without the dropdown icon.
110
89
 
111
90
  ### With End Decorator
112
91
 
113
- Add badges, chips, or icons at the end of the button.
92
+ Add badges, chips, or icons after the button text using `endDecorator`.
114
93
 
115
94
  ```tsx
116
95
  <MenuButton
@@ -131,7 +110,7 @@ Add badges, chips, or icons at the end of the button.
131
110
 
132
111
  ### Placement: Bottom Start
133
112
 
134
- Menu aligns to the left edge of the button.
113
+ Menu aligns to the left edge of the trigger button.
135
114
 
136
115
  ```tsx
137
116
  <MenuButton
@@ -152,7 +131,7 @@ Menu aligns to the left edge of the button.
152
131
 
153
132
  ### Placement: Bottom
154
133
 
155
- Menu centers below the button.
134
+ Menu centers below the trigger button.
156
135
 
157
136
  ```tsx
158
137
  <MenuButton
@@ -173,7 +152,7 @@ Menu centers below the button.
173
152
 
174
153
  ### Placement: Bottom End
175
154
 
176
- Menu aligns to the right edge of the button.
155
+ Menu aligns to the right edge of the trigger button.
177
156
 
178
157
  ```tsx
179
158
  <MenuButton
@@ -192,25 +171,6 @@ Menu aligns to the right edge of the button.
192
171
  />
193
172
  ```
194
173
 
195
- ## When to Use
196
-
197
- ### ✅ Good Use Cases
198
-
199
- - **User account menus**: Profile, settings, logout actions
200
- - **Action grouping**: Related actions in a toolbar
201
- - **Navigation shortcuts**: Quick access to related pages
202
- - **Overflow menus**: When space is limited
203
- - **Context actions**: Page or section-specific actions
204
- - **Export options**: Download, export, or share actions
205
-
206
- ### ❌ When Not to Use
207
-
208
- - **Form value selection**: Use Select for picking values in forms
209
- - **Single action**: Use Button for single actions
210
- - **Icon-only trigger**: Use IconMenuButton for minimal space
211
- - **Complex menus**: Use Dropdown for nested menus or custom content
212
- - **Primary navigation**: Use Navigator or Tabs for main navigation
213
-
214
174
  ## Common Use Cases
215
175
 
216
176
  ### User Account Menu
@@ -226,7 +186,6 @@ function AccountMenu({ user, onLogout }) {
226
186
  items={[
227
187
  { text: 'Profile', onClick: () => navigate('/profile') },
228
188
  { text: 'Account Settings', onClick: () => navigate('/settings') },
229
- { text: 'Billing', onClick: () => navigate('/billing') },
230
189
  { text: 'Logout', onClick: onLogout },
231
190
  ]}
232
191
  />
@@ -234,10 +193,10 @@ function AccountMenu({ user, onLogout }) {
234
193
  }
235
194
  ```
236
195
 
237
- ### Export Options Menu
196
+ ### Export Options
238
197
 
239
198
  ```tsx
240
- function ExportMenu({ data, onExport }) {
199
+ function ExportMenu({ onExport }) {
241
200
  return (
242
201
  <MenuButton
243
202
  buttonText="Export"
@@ -247,425 +206,73 @@ function ExportMenu({ data, onExport }) {
247
206
  { text: 'Export as CSV', onClick: () => onExport('csv') },
248
207
  { text: 'Export as Excel', onClick: () => onExport('xlsx') },
249
208
  { text: 'Export as PDF', onClick: () => onExport('pdf') },
250
- { text: 'Print', onClick: () => window.print() },
251
209
  ]}
252
210
  />
253
211
  );
254
212
  }
255
213
  ```
256
214
 
257
- ### Actions Menu in Toolbar
215
+ ### Toolbar Actions
258
216
 
259
217
  ```tsx
260
218
  function ToolbarActions({ selectedItems, onAction }) {
261
- const hasSelection = selectedItems.length > 0;
262
-
263
- return (
264
- <Stack direction="row" gap={1}>
265
- <Button onClick={() => onAction('create')}>Create New</Button>
266
- <MenuButton
267
- buttonText="Actions"
268
- variant="outlined"
269
- color="neutral"
270
- disabled={!hasSelection}
271
- items={[
272
- { text: `Edit (${selectedItems.length})`, onClick: () => onAction('edit') },
273
- { text: 'Duplicate', onClick: () => onAction('duplicate') },
274
- { text: 'Move to Folder', onClick: () => onAction('move') },
275
- { text: 'Archive', onClick: () => onAction('archive') },
276
- { text: 'Delete', onClick: () => onAction('delete') },
277
- ]}
278
- />
279
- </Stack>
280
- );
281
- }
282
- ```
283
-
284
- ### Navigation with Dropdown
285
-
286
- ```tsx
287
- function ProductsMenu() {
288
219
  return (
289
220
  <MenuButton
290
- buttonText="Products"
291
- variant="plain"
292
- showIcon={true}
293
- items={[
294
- { text: 'All Products', onClick: () => navigate('/products') },
295
- { text: 'Categories', onClick: () => navigate('/products/categories') },
296
- { text: 'Inventory', onClick: () => navigate('/products/inventory') },
297
- { text: 'Price Lists', onClick: () => navigate('/products/prices') },
298
- ]}
299
- />
300
- );
301
- }
302
- ```
303
-
304
- ### Status Change Menu
305
-
306
- ```tsx
307
- function StatusMenu({ currentStatus, onStatusChange }) {
308
- const statuses = [
309
- { value: 'draft', label: 'Draft', color: 'neutral' },
310
- { value: 'pending', label: 'Pending Review', color: 'warning' },
311
- { value: 'approved', label: 'Approved', color: 'success' },
312
- { value: 'rejected', label: 'Rejected', color: 'danger' },
313
- ];
314
-
315
- const current = statuses.find((s) => s.value === currentStatus);
316
-
317
- return (
318
- <MenuButton
319
- buttonText={current?.label || 'Set Status'}
320
- variant="soft"
321
- color={current?.color || 'neutral'}
322
- items={statuses.map((status) => ({
323
- text: status.label,
324
- onClick: () => onStatusChange(status.value),
325
- }))}
326
- />
327
- );
328
- }
329
- ```
330
-
331
- ### Menu with Decorators
332
-
333
- ```tsx
334
- function NotificationsMenu({ notifications }) {
335
- const unreadCount = notifications.filter((n) => !n.read).length;
336
-
337
- return (
338
- <MenuButton
339
- buttonText="Notifications"
340
- startDecorator={<NotificationsIcon />}
341
- endDecorator={
342
- unreadCount > 0 && (
343
- <Chip size="sm" color="danger">
344
- {unreadCount}
345
- </Chip>
346
- )
347
- }
348
- variant="soft"
349
- items={notifications.slice(0, 5).map((n) => ({
350
- text: n.title,
351
- onClick: () => markAsRead(n.id),
352
- }))}
353
- />
354
- );
355
- }
356
- ```
357
-
358
- ### Quick Settings Menu
359
-
360
- ```tsx
361
- function QuickSettings({ settings, onSettingChange }) {
362
- return (
363
- <MenuButton
364
- buttonText="Settings"
365
- startDecorator={<SettingsIcon />}
366
- variant="plain"
221
+ buttonText="Actions"
222
+ variant="outlined"
367
223
  color="neutral"
224
+ disabled={selectedItems.length === 0}
368
225
  items={[
369
- {
370
- text: settings.darkMode ? '☀️ Light Mode' : '🌙 Dark Mode',
371
- onClick: () => onSettingChange('darkMode', !settings.darkMode),
372
- },
373
- {
374
- text: settings.compactView ? '📐 Normal View' : '📏 Compact View',
375
- onClick: () => onSettingChange('compactView', !settings.compactView),
376
- },
377
- {
378
- text: 'Language: ' + settings.language.toUpperCase(),
379
- onClick: () => openLanguageModal(),
380
- },
226
+ { text: `Edit (${selectedItems.length})`, onClick: () => onAction('edit') },
227
+ { text: 'Duplicate', onClick: () => onAction('duplicate') },
228
+ { text: 'Archive', onClick: () => onAction('archive') },
229
+ { text: 'Delete', onClick: () => onAction('delete') },
381
230
  ]}
382
231
  />
383
232
  );
384
233
  }
385
234
  ```
386
235
 
387
- ## Props and Customization
388
-
389
- ### Key Props
390
-
391
- | Prop | Type | Default | Description |
392
- | ---------------------- | -------------------------------------------------------------- | ---------------- | --------------------------------- |
393
- | `buttonText` | `string` | - | Text displayed on the button |
394
- | `items` | `Array<{ text: string; onClick?: () => void }>` | - | Menu items to display |
395
- | `showIcon` | `boolean` | `true` | Show dropdown indicator icon |
396
- | `variant` | `'solid' \| 'soft' \| 'outlined' \| 'plain'` | `'solid'` | Button style variant |
397
- | `color` | `'primary' \| 'neutral' \| 'danger' \| 'success' \| 'warning'` | `'primary'` | Button color |
398
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
399
- | `placement` | `'bottom-start' \| 'bottom' \| 'bottom-end'` | `'bottom-start'` | Menu position |
400
- | `startDecorator` | `ReactNode` | - | Content before button text |
401
- | `endDecorator` | `ReactNode` | - | Content after button text |
402
- | `buttonComponent` | `React.ElementType` | `Button` | Custom button component |
403
- | `buttonComponentProps` | `object` | - | Props for custom button component |
404
-
405
- ### Menu Item Structure
406
-
407
- ```tsx
408
- interface MenuItem {
409
- text: string; // Display text
410
- onClick?: () => void; // Click handler
411
- // Additional props can be passed based on implementation
412
- }
413
-
414
- // Example items array
415
- const items = [
416
- { text: 'Edit', onClick: handleEdit },
417
- { text: 'Delete', onClick: handleDelete },
418
- ];
419
- ```
420
-
421
- ### Variant and Color Options
422
-
423
- ```tsx
424
- // Primary solid (default)
425
- <MenuButton buttonText="Actions" variant="solid" color="primary" />
426
-
427
- // Subtle outlined
428
- <MenuButton buttonText="Actions" variant="outlined" color="neutral" />
429
-
430
- // Soft background
431
- <MenuButton buttonText="Actions" variant="soft" color="primary" />
432
-
433
- // Plain/text only
434
- <MenuButton buttonText="Actions" variant="plain" color="neutral" />
435
-
436
- // Danger color
437
- <MenuButton buttonText="Delete" variant="soft" color="danger" />
438
- ```
439
-
440
- ### Size Options
441
-
442
- ```tsx
443
- // Small
444
- <MenuButton buttonText="Small" size="sm" items={items} />
445
-
446
- // Medium (default)
447
- <MenuButton buttonText="Medium" size="md" items={items} />
448
-
449
- // Large
450
- <MenuButton buttonText="Large" size="lg" items={items} />
451
- ```
452
-
453
- ### Custom Button Component
454
-
455
- ```tsx
456
- // Use with custom components like Link
457
- import { Link } from 'react-router-dom';
458
-
459
- <MenuButton
460
- buttonText="Dashboard"
461
- buttonComponent={Link}
462
- buttonComponentProps={{ to: '/dashboard' }}
463
- showIcon={false}
464
- items={items}
465
- />
466
- ```
467
-
468
- ### Decorators
469
-
470
- ```tsx
471
- // Start decorator (icon before text)
472
- <MenuButton
473
- buttonText="Export"
474
- startDecorator={<DownloadIcon />}
475
- items={exportItems}
476
- />
477
-
478
- // End decorator (badge after text)
479
- <MenuButton
480
- buttonText="Inbox"
481
- endDecorator={<Chip size="sm">5</Chip>}
482
- items={inboxItems}
483
- />
484
-
485
- // Both decorators
486
- <MenuButton
487
- buttonText="User"
488
- startDecorator={<Avatar size="sm" />}
489
- endDecorator={<ChevronDownIcon />}
490
- showIcon={false}
491
- items={userItems}
492
- />
493
- ```
494
-
495
- ## Accessibility
496
-
497
- MenuButton includes built-in accessibility features:
498
-
499
- ### ARIA Attributes
500
-
501
- - Button has `aria-haspopup="true"` indicating it opens a menu
502
- - Button has `aria-expanded` reflecting menu open state
503
- - Menu has proper `role="menu"` and `role="menuitem"` structure
504
- - Focus is managed between button and menu items
505
-
506
- ### Keyboard Navigation
507
-
508
- - **Enter/Space**: Open menu when button is focused
509
- - **Arrow Down**: Move to next menu item
510
- - **Arrow Up**: Move to previous menu item
511
- - **Escape**: Close menu and return focus to button
512
- - **Tab**: Close menu and move to next focusable element
513
- - **Enter**: Select focused menu item
514
-
515
- ### Screen Reader Support
516
-
517
- ```tsx
518
- // Button announces: "Account, menu button, expanded/collapsed"
519
- <MenuButton
520
- buttonText="Account"
521
- items={[...]}
522
- />
523
-
524
- // Menu items are announced as: "Profile, menu item, 1 of 3"
525
- ```
526
-
527
- ### Focus Management
528
-
529
- - Focus moves to first menu item when menu opens
530
- - Focus returns to button when menu closes
531
- - Menu items are focusable with arrow keys
532
-
533
236
  ## Best Practices
534
237
 
535
- ### Do
238
+ - **Use descriptive button text.** The button label should clearly indicate what kind of options the menu contains. Avoid vague labels like "More" or "Options" -- prefer specific labels like "Export Options" or "User Actions".
536
239
 
537
- 1. **Use clear, action-oriented labels**: Button text should indicate what the menu contains
240
+ ```tsx
241
+ {/* ✅ Good: Clear purpose */}
242
+ <MenuButton buttonText="Export Options" items={exportItems} />
538
243
 
539
- ```tsx
540
- // Good: Clear menu purpose
541
- <MenuButton buttonText="Export Options" items={exportItems} />
542
- <MenuButton buttonText="User Actions" items={userItems} />
543
- ```
244
+ {/* ❌ Avoid: Vague label */}
245
+ <MenuButton buttonText="More" items={items} />
246
+ ```
544
247
 
545
- 2. **Group related actions**: Keep menu items logically related
248
+ - **Keep the items list short and focused.** Aim for 3-7 items that are logically related. If you need more items or mixed content, switch to the Dropdown + Menu composition pattern.
546
249
 
547
- ```tsx
548
- // ✅ Good: Related file actions
549
- <MenuButton
550
- buttonText="File"
551
- items={[
552
- { text: 'New', onClick: handleNew },
553
- { text: 'Open', onClick: handleOpen },
554
- { text: 'Save', onClick: handleSave },
555
- { text: 'Save As...', onClick: handleSaveAs },
556
- ]}
557
- />
558
- ```
250
+ - **Match variant to context.** Use `variant="solid"` for primary action menus, `variant="outlined"` or `variant="plain"` for secondary or navigation menus. Be consistent within the same toolbar or header.
559
251
 
560
- 3. **Use appropriate variants**: Match the button style to its context
252
+ ```tsx
253
+ {/* ✅ Good: Appropriate variants */}
254
+ <MenuButton buttonText="Create" variant="solid" color="primary" items={createItems} />
255
+ <MenuButton buttonText="Settings" variant="plain" color="neutral" items={settingsItems} />
256
+ ```
561
257
 
562
- ```tsx
563
- // ✅ Good: Primary for main actions, plain for navigation
564
- <MenuButton buttonText="Create" variant="solid" color="primary" />
565
- <MenuButton buttonText="Settings" variant="plain" color="neutral" />
566
- ```
258
+ - **Do not use MenuButton for form value selection.** MenuButton triggers actions -- it is not a Select replacement. For form inputs where the user picks a value, use the Select component.
567
259
 
568
- 4. **Limit menu items**: Keep menus scannable (5-7 items max)
260
+ ```tsx
261
+ {/* ❌ Bad: Form selection */}
262
+ <MenuButton buttonText={selectedValue} items={options} />
569
263
 
570
- ### Don't
264
+ {/* Good: Use Select for forms */}
265
+ <Select value={selectedValue} onChange={handleChange}>
266
+ <Option value="a">Option A</Option>
267
+ <Option value="b">Option B</Option>
268
+ </Select>
269
+ ```
571
270
 
572
- 1. **Don't use for form inputs**: Use Select for value selection
271
+ - **Use `placement` to prevent overflow.** When the trigger is near the right edge of the viewport, use `placement="bottom-end"` so the menu does not extend beyond the screen.
573
272
 
574
- ```tsx
575
- // ❌ Bad: Using MenuButton for form selection
576
- <MenuButton buttonText={selectedOption} items={options} />
577
-
578
- // ✅ Good: Use Select for forms
579
- <Select value={selectedOption} onChange={handleChange}>
580
- {options.map((opt) => <Option key={opt.value} value={opt.value}>{opt.label}</Option>)}
581
- </Select>
582
- ```
583
-
584
- 2. **Don't use vague labels**: Be specific about menu contents
585
-
586
- ```tsx
587
- // ❌ Bad: Unclear what "More" contains
588
- <MenuButton buttonText="More" items={items} />
589
-
590
- // ✅ Good: Clear menu purpose
591
- <MenuButton buttonText="More Actions" items={actionItems} />
592
- ```
593
-
594
- 3. **Don't overload menus**: Split large menus into categories
595
-
596
- ```tsx
597
- // ❌ Bad: Too many unrelated items
598
- <MenuButton
599
- buttonText="Options"
600
- items={[
601
- { text: 'Edit' },
602
- { text: 'Delete' },
603
- { text: 'Share' },
604
- { text: 'Settings' },
605
- { text: 'Help' },
606
- { text: 'About' },
607
- // ... many more items
608
- ]}
609
- />
610
- ```
611
-
612
- 4. **Don't hide primary actions**: Important actions should be visible
613
-
614
- ## Performance Considerations
615
-
616
- ### Memoize Items Array
617
-
618
- Prevent unnecessary re-renders by memoizing the items array:
619
-
620
- ```tsx
621
- const menuItems = useMemo(
622
- () => [
623
- { text: 'Edit', onClick: handleEdit },
624
- { text: 'Delete', onClick: handleDelete },
625
- ],
626
- [handleEdit, handleDelete]
627
- );
628
-
629
- <MenuButton buttonText="Actions" items={menuItems} />
630
- ```
631
-
632
- ### Memoize Click Handlers
633
-
634
- ```tsx
635
- const handleExport = useCallback(
636
- (format: string) => {
637
- exportData(data, format);
638
- },
639
- [data]
640
- );
641
-
642
- const exportItems = useMemo(
643
- () => [
644
- { text: 'CSV', onClick: () => handleExport('csv') },
645
- { text: 'Excel', onClick: () => handleExport('xlsx') },
646
- ],
647
- [handleExport]
648
- );
649
- ```
650
-
651
- ### Lazy Load Heavy Actions
652
-
653
- For actions that trigger heavy operations:
654
-
655
- ```tsx
656
- const items = useMemo(
657
- () => [
658
- {
659
- text: 'Generate Report',
660
- onClick: async () => {
661
- setLoading(true);
662
- await generateReport();
663
- setLoading(false);
664
- },
665
- },
666
- ],
667
- []
668
- );
669
- ```
273
+ ## Accessibility
670
274
 
671
- MenuButton provides a convenient way to group related actions behind a single button trigger. Use it for account menus, action groups, and navigation shortcuts while keeping the menu contents focused and scannable. For icon-only triggers in space-constrained areas, consider IconMenuButton instead.
275
+ - **ARIA attributes**: The trigger button automatically receives `aria-haspopup="true"` and `aria-expanded` that reflects the open/closed state. The menu panel has `role="menu"` and each item has `role="menuitem"`.
276
+ - **Keyboard navigation**: Enter or Space opens the menu. Arrow Down/Up moves between items. Escape closes the menu and returns focus to the button. Tab closes the menu and moves focus to the next element.
277
+ - **Screen reader support**: The button is announced as "\[buttonText], menu button, expanded/collapsed". Menu items are announced with their position (e.g., "Profile, menu item, 1 of 3").
278
+ - **Focus management**: Focus moves to the first menu item when the menu opens and returns to the trigger button when the menu closes.