@ceed/ads 1.20.0 → 1.20.1-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ProfileMenu/ProfileMenu.d.ts +1 -1
- package/dist/components/data-display/Markdown.md +832 -0
- package/dist/components/feedback/Dialog.md +605 -3
- package/dist/components/feedback/Modal.md +656 -24
- package/dist/components/feedback/llms.txt +1 -1
- package/dist/components/inputs/Autocomplete.md +734 -2
- package/dist/components/inputs/Calendar.md +655 -1
- package/dist/components/inputs/DatePicker.md +699 -3
- package/dist/components/inputs/DateRangePicker.md +815 -1
- package/dist/components/inputs/MonthPicker.md +626 -4
- package/dist/components/inputs/MonthRangePicker.md +682 -4
- package/dist/components/inputs/Select.md +600 -0
- package/dist/components/layout/Container.md +507 -0
- package/dist/components/navigation/Breadcrumbs.md +582 -0
- package/dist/components/navigation/IconMenuButton.md +693 -0
- package/dist/components/navigation/InsetDrawer.md +1150 -3
- package/dist/components/navigation/Link.md +526 -0
- package/dist/components/navigation/MenuButton.md +632 -0
- package/dist/components/navigation/NavigationGroup.md +401 -1
- package/dist/components/navigation/NavigationItem.md +311 -0
- package/dist/components/navigation/Navigator.md +373 -0
- package/dist/components/navigation/Pagination.md +521 -0
- package/dist/components/navigation/ProfileMenu.md +605 -0
- package/dist/components/navigation/Tabs.md +609 -7
- package/dist/components/surfaces/Accordions.md +947 -3
- package/dist/index.cjs +3 -1
- package/dist/index.js +3 -1
- package/dist/llms.txt +1 -1
- package/framer/index.js +1 -1
- package/package.json +3 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Introduction
|
|
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.
|
|
6
|
+
|
|
5
7
|
```tsx
|
|
6
8
|
<MenuButton
|
|
7
9
|
buttonText="Dashboard..."
|
|
@@ -32,8 +34,638 @@
|
|
|
32
34
|
| color | — | "primary" |
|
|
33
35
|
| size | — | — |
|
|
34
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
|
+
|
|
35
46
|
## Usage
|
|
36
47
|
|
|
37
48
|
```tsx
|
|
38
49
|
import { MenuButton } from '@ceed/ads';
|
|
50
|
+
|
|
51
|
+
function UserMenu() {
|
|
52
|
+
return (
|
|
53
|
+
<MenuButton
|
|
54
|
+
buttonText="Account"
|
|
55
|
+
items={[
|
|
56
|
+
{ text: 'Profile', onClick: () => navigate('/profile') },
|
|
57
|
+
{ text: 'Settings', onClick: () => navigate('/settings') },
|
|
58
|
+
{ text: 'Logout', onClick: handleLogout },
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
### Playground
|
|
68
|
+
|
|
69
|
+
Interactive example with all controls.
|
|
70
|
+
|
|
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
|
+
```
|
|
86
|
+
|
|
87
|
+
### Standalone (Without Icon)
|
|
88
|
+
|
|
89
|
+
Use as a simple navigation link without the dropdown icon.
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
<MenuButton
|
|
93
|
+
buttonText="Dashboard"
|
|
94
|
+
items={[{
|
|
95
|
+
text: 'Profile'
|
|
96
|
+
}, {
|
|
97
|
+
text: 'My account'
|
|
98
|
+
}, {
|
|
99
|
+
text: 'Logout'
|
|
100
|
+
}]}
|
|
101
|
+
showIcon={false}
|
|
102
|
+
variant="solid"
|
|
103
|
+
color="primary"
|
|
104
|
+
buttonComponent={Link}
|
|
105
|
+
buttonComponentProps={{
|
|
106
|
+
to: '/'
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### With End Decorator
|
|
112
|
+
|
|
113
|
+
Add badges, chips, or icons at the end of the button.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<MenuButton
|
|
117
|
+
buttonText="Dashboard"
|
|
118
|
+
items={[{
|
|
119
|
+
text: 'Profile'
|
|
120
|
+
}, {
|
|
121
|
+
text: 'My account'
|
|
122
|
+
}, {
|
|
123
|
+
text: 'Logout'
|
|
124
|
+
}]}
|
|
125
|
+
showIcon
|
|
126
|
+
variant="solid"
|
|
127
|
+
color="primary"
|
|
128
|
+
endDecorator={<Chip color="primary">New</Chip>}
|
|
129
|
+
/>
|
|
39
130
|
```
|
|
131
|
+
|
|
132
|
+
### Placement: Bottom Start
|
|
133
|
+
|
|
134
|
+
Menu aligns to the left edge of the button.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<MenuButton
|
|
138
|
+
buttonText="Hi"
|
|
139
|
+
items={[{
|
|
140
|
+
text: 'Profile'
|
|
141
|
+
}, {
|
|
142
|
+
text: 'My account'
|
|
143
|
+
}, {
|
|
144
|
+
text: 'Logout'
|
|
145
|
+
}]}
|
|
146
|
+
showIcon
|
|
147
|
+
variant="solid"
|
|
148
|
+
color="primary"
|
|
149
|
+
placement="bottom-start"
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Placement: Bottom
|
|
154
|
+
|
|
155
|
+
Menu centers below the button.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<MenuButton
|
|
159
|
+
buttonText="Hi"
|
|
160
|
+
items={[{
|
|
161
|
+
text: 'Profile'
|
|
162
|
+
}, {
|
|
163
|
+
text: 'My account'
|
|
164
|
+
}, {
|
|
165
|
+
text: 'Logout'
|
|
166
|
+
}]}
|
|
167
|
+
showIcon
|
|
168
|
+
variant="solid"
|
|
169
|
+
color="primary"
|
|
170
|
+
placement="bottom"
|
|
171
|
+
/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Placement: Bottom End
|
|
175
|
+
|
|
176
|
+
Menu aligns to the right edge of the button.
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
<MenuButton
|
|
180
|
+
buttonText="Hi"
|
|
181
|
+
items={[{
|
|
182
|
+
text: 'Profile'
|
|
183
|
+
}, {
|
|
184
|
+
text: 'My account'
|
|
185
|
+
}, {
|
|
186
|
+
text: 'Logout'
|
|
187
|
+
}]}
|
|
188
|
+
showIcon
|
|
189
|
+
variant="solid"
|
|
190
|
+
color="primary"
|
|
191
|
+
placement="bottom-end"
|
|
192
|
+
/>
|
|
193
|
+
```
|
|
194
|
+
|
|
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
|
+
## Common Use Cases
|
|
215
|
+
|
|
216
|
+
### User Account Menu
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
function AccountMenu({ user, onLogout }) {
|
|
220
|
+
return (
|
|
221
|
+
<MenuButton
|
|
222
|
+
buttonText={user.name}
|
|
223
|
+
startDecorator={<Avatar src={user.avatar} size="sm" />}
|
|
224
|
+
variant="plain"
|
|
225
|
+
color="neutral"
|
|
226
|
+
items={[
|
|
227
|
+
{ text: 'Profile', onClick: () => navigate('/profile') },
|
|
228
|
+
{ text: 'Account Settings', onClick: () => navigate('/settings') },
|
|
229
|
+
{ text: 'Billing', onClick: () => navigate('/billing') },
|
|
230
|
+
{ text: 'Logout', onClick: onLogout },
|
|
231
|
+
]}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Export Options Menu
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
function ExportMenu({ data, onExport }) {
|
|
241
|
+
return (
|
|
242
|
+
<MenuButton
|
|
243
|
+
buttonText="Export"
|
|
244
|
+
variant="outlined"
|
|
245
|
+
startDecorator={<DownloadIcon />}
|
|
246
|
+
items={[
|
|
247
|
+
{ text: 'Export as CSV', onClick: () => onExport('csv') },
|
|
248
|
+
{ text: 'Export as Excel', onClick: () => onExport('xlsx') },
|
|
249
|
+
{ text: 'Export as PDF', onClick: () => onExport('pdf') },
|
|
250
|
+
{ text: 'Print', onClick: () => window.print() },
|
|
251
|
+
]}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Actions Menu in Toolbar
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
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
|
+
return (
|
|
289
|
+
<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"
|
|
367
|
+
color="neutral"
|
|
368
|
+
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
|
+
},
|
|
381
|
+
]}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
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
|
+
## Best Practices
|
|
534
|
+
|
|
535
|
+
### ✅ Do
|
|
536
|
+
|
|
537
|
+
1. **Use clear, action-oriented labels**: Button text should indicate what the menu contains
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
// ✅ Good: Clear menu purpose
|
|
541
|
+
<MenuButton buttonText="Export Options" items={exportItems} />
|
|
542
|
+
<MenuButton buttonText="User Actions" items={userItems} />
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
2. **Group related actions**: Keep menu items logically related
|
|
546
|
+
|
|
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
|
+
```
|
|
559
|
+
|
|
560
|
+
3. **Use appropriate variants**: Match the button style to its context
|
|
561
|
+
|
|
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
|
+
```
|
|
567
|
+
|
|
568
|
+
4. **Limit menu items**: Keep menus scannable (5-7 items max)
|
|
569
|
+
|
|
570
|
+
### ❌ Don't
|
|
571
|
+
|
|
572
|
+
1. **Don't use for form inputs**: Use Select for value selection
|
|
573
|
+
|
|
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
|
+
```
|
|
670
|
+
|
|
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.
|