0
# Navigation Components
1
2
Menu systems and navigation components including dropdown menus and contextual navigation. These components provide user interface patterns for organizing and accessing application functionality.
3
4
## Capabilities
5
6
### Material Menu
7
8
Dropdown menu component with positioning, keyboard navigation, and smooth animations.
9
10
```javascript { .api }
11
/**
12
* Material Design menu component
13
* CSS Class: mdl-js-menu
14
* Widget: true
15
*/
16
interface MaterialMenu {
17
/**
18
* Display the menu at calculated position
19
* @param evt - Optional event object for positioning context
20
*/
21
show(evt?: Event): void;
22
23
/** Hide the menu with animation */
24
hide(): void;
25
26
/**
27
* Toggle menu visibility
28
* @param evt - Optional event object for positioning context
29
*/
30
toggle(evt?: Event): void;
31
}
32
```
33
34
**HTML Structure:**
35
36
```html
37
<!-- Basic menu -->
38
<button id="demo-menu-lower-left"
39
class="mdl-button mdl-js-button mdl-button--icon">
40
<i class="material-icons">more_vert</i>
41
</button>
42
43
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"
44
for="demo-menu-lower-left">
45
<li class="mdl-menu__item">Some Action</li>
46
<li class="mdl-menu__item">Another Action</li>
47
<li class="mdl-menu__item" disabled>Disabled Action</li>
48
<li class="mdl-menu__item">Yet Another Action</li>
49
</ul>
50
51
<!-- Menu with icons -->
52
<button id="demo-menu-with-icons"
53
class="mdl-button mdl-js-button mdl-button--icon">
54
<i class="material-icons">settings</i>
55
</button>
56
57
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu"
58
for="demo-menu-with-icons">
59
<li class="mdl-menu__item">
60
<i class="material-icons">edit</i>Edit
61
</li>
62
<li class="mdl-menu__item">
63
<i class="material-icons">delete</i>Delete
64
</li>
65
<li class="mdl-menu__item">
66
<i class="material-icons">share</i>Share
67
</li>
68
</ul>
69
```
70
71
**Menu Positioning:**
72
73
```html
74
<!-- Top positioning -->
75
<ul class="mdl-menu mdl-menu--top-left mdl-js-menu">
76
<ul class="mdl-menu mdl-menu--top-right mdl-js-menu">
77
78
<!-- Bottom positioning -->
79
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu">
80
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu">
81
82
<!-- Unaligned (centered) -->
83
<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu">
84
```
85
86
**Usage Examples:**
87
88
```javascript
89
// Access menu instance
90
const menuButton = document.querySelector('#demo-menu-lower-left');
91
const menu = document.querySelector('[for="demo-menu-lower-left"]').MaterialMenu;
92
93
// Show menu programmatically
94
menu.show();
95
96
// Hide menu programmatically
97
menu.hide();
98
99
// Toggle menu visibility
100
menuButton.addEventListener('click', (event) => {
101
menu.toggle(event);
102
});
103
104
// Handle menu item clicks
105
document.addEventListener('click', (event) => {
106
if (event.target.matches('.mdl-menu__item')) {
107
const menuItem = event.target;
108
const menuContainer = menuItem.closest('.mdl-js-menu');
109
110
console.log('Menu item clicked:', menuItem.textContent);
111
112
// Hide menu after selection
113
menuContainer.MaterialMenu.hide();
114
115
// Perform action based on menu item
116
handleMenuItemAction(menuItem);
117
}
118
});
119
120
function handleMenuItemAction(menuItem) {
121
const action = menuItem.textContent.trim();
122
123
switch (action) {
124
case 'Edit':
125
editItem();
126
break;
127
case 'Delete':
128
deleteItem();
129
break;
130
case 'Share':
131
shareItem();
132
break;
133
}
134
}
135
```
136
137
### Dynamic Menu Management
138
139
```javascript
140
// Create menu dynamically
141
function createMenu(buttonId, items) {
142
const button = document.getElementById(buttonId);
143
144
// Create menu element
145
const menu = document.createElement('ul');
146
menu.className = 'mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect';
147
menu.setAttribute('for', buttonId);
148
149
// Add menu items
150
items.forEach(item => {
151
const li = document.createElement('li');
152
li.className = 'mdl-menu__item';
153
li.textContent = item.text;
154
155
if (item.disabled) {
156
li.setAttribute('disabled', '');
157
}
158
159
if (item.icon) {
160
const icon = document.createElement('i');
161
icon.className = 'material-icons';
162
icon.textContent = item.icon;
163
li.insertBefore(icon, li.firstChild);
164
}
165
166
menu.appendChild(li);
167
});
168
169
// Insert menu after button
170
button.parentNode.insertBefore(menu, button.nextSibling);
171
172
// Upgrade the menu
173
componentHandler.upgradeElement(menu);
174
175
return menu.MaterialMenu;
176
}
177
178
// Usage
179
const dynamicMenu = createMenu('my-button', [
180
{ text: 'Edit', icon: 'edit' },
181
{ text: 'Delete', icon: 'delete', disabled: true },
182
{ text: 'Share', icon: 'share' }
183
]);
184
```
185
186
### Keyboard Navigation
187
188
Menus support full keyboard navigation:
189
190
```javascript
191
// Keyboard navigation is automatic, but you can listen for events
192
document.addEventListener('keydown', (event) => {
193
const activeMenu = document.querySelector('.mdl-menu.is-visible');
194
195
if (activeMenu) {
196
const menu = activeMenu.MaterialMenu;
197
198
switch (event.key) {
199
case 'Escape':
200
menu.hide();
201
event.preventDefault();
202
break;
203
204
case 'ArrowUp':
205
navigateMenuUp(activeMenu);
206
event.preventDefault();
207
break;
208
209
case 'ArrowDown':
210
navigateMenuDown(activeMenu);
211
event.preventDefault();
212
break;
213
214
case 'Enter':
215
case ' ':
216
const focusedItem = activeMenu.querySelector('.mdl-menu__item:focus');
217
if (focusedItem && !focusedItem.hasAttribute('disabled')) {
218
focusedItem.click();
219
}
220
event.preventDefault();
221
break;
222
}
223
}
224
});
225
226
function navigateMenuUp(menu) {
227
const items = Array.from(menu.querySelectorAll('.mdl-menu__item:not([disabled])'));
228
const currentIndex = items.indexOf(document.activeElement);
229
const nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
230
items[nextIndex].focus();
231
}
232
233
function navigateMenuDown(menu) {
234
const items = Array.from(menu.querySelectorAll('.mdl-menu__item:not([disabled])'));
235
const currentIndex = items.indexOf(document.activeElement);
236
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
237
items[nextIndex].focus();
238
}
239
```
240
241
## Menu Constants
242
243
```javascript { .api }
244
/**
245
* Material Menu constants and configuration
246
*/
247
interface MenuConstants {
248
/** Total transition duration in seconds */
249
TRANSITION_DURATION_SECONDS: 0.3;
250
251
/** Fraction of transition used for main animation */
252
TRANSITION_DURATION_FRACTION: 0.8;
253
254
/** Timeout before closing menu after selection */
255
CLOSE_TIMEOUT: 150;
256
}
257
258
/**
259
* Keyboard codes used by menu navigation
260
*/
261
interface MenuKeyCodes {
262
ENTER: 13;
263
ESCAPE: 27;
264
SPACE: 32;
265
UP_ARROW: 38;
266
DOWN_ARROW: 40;
267
}
268
269
/**
270
* Menu positioning classes
271
*/
272
interface MenuPositions {
273
BOTTOM_LEFT: 'mdl-menu--bottom-left';
274
BOTTOM_RIGHT: 'mdl-menu--bottom-right';
275
TOP_LEFT: 'mdl-menu--top-left';
276
TOP_RIGHT: 'mdl-menu--top-right';
277
UNALIGNED: 'mdl-menu--unaligned';
278
}
279
```
280
281
## Context Menus
282
283
Create context menus that appear on right-click:
284
285
```javascript
286
// Context menu implementation
287
function createContextMenu(targetSelector, menuItems) {
288
let contextMenu = null;
289
290
document.addEventListener('contextmenu', (event) => {
291
if (event.target.matches(targetSelector)) {
292
event.preventDefault();
293
294
// Remove existing context menu
295
if (contextMenu) {
296
contextMenu.remove();
297
}
298
299
// Create new context menu
300
contextMenu = document.createElement('ul');
301
contextMenu.className = 'mdl-menu mdl-js-menu mdl-menu--unaligned';
302
contextMenu.style.position = 'fixed';
303
contextMenu.style.left = event.clientX + 'px';
304
contextMenu.style.top = event.clientY + 'px';
305
306
// Add menu items
307
menuItems.forEach(item => {
308
const li = document.createElement('li');
309
li.className = 'mdl-menu__item';
310
li.textContent = item.text;
311
li.addEventListener('click', () => {
312
item.action(event.target);
313
contextMenu.MaterialMenu.hide();
314
});
315
contextMenu.appendChild(li);
316
});
317
318
document.body.appendChild(contextMenu);
319
componentHandler.upgradeElement(contextMenu);
320
321
// Show menu
322
contextMenu.MaterialMenu.show();
323
}
324
});
325
326
// Hide context menu on regular click
327
document.addEventListener('click', () => {
328
if (contextMenu) {
329
contextMenu.MaterialMenu.hide();
330
}
331
});
332
}
333
334
// Usage
335
createContextMenu('.data-item', [
336
{
337
text: 'Edit',
338
action: (target) => editDataItem(target)
339
},
340
{
341
text: 'Delete',
342
action: (target) => deleteDataItem(target)
343
},
344
{
345
text: 'Duplicate',
346
action: (target) => duplicateDataItem(target)
347
}
348
]);
349
```
350
351
## Menu State Management
352
353
```javascript
354
// Track menu states across the application
355
class MenuManager {
356
constructor() {
357
this.openMenus = new Set();
358
this.setupGlobalListeners();
359
}
360
361
setupGlobalListeners() {
362
// Track menu opening/closing
363
document.addEventListener('click', (event) => {
364
if (event.target.matches('.mdl-menu__item')) {
365
const menu = event.target.closest('.mdl-js-menu');
366
this.closeMenu(menu);
367
}
368
});
369
370
// Close all menus on escape
371
document.addEventListener('keydown', (event) => {
372
if (event.key === 'Escape') {
373
this.closeAllMenus();
374
}
375
});
376
377
// Close menus when clicking outside
378
document.addEventListener('click', (event) => {
379
if (!event.target.closest('.mdl-menu') &&
380
!event.target.matches('[class*="mdl-button"]')) {
381
this.closeAllMenus();
382
}
383
});
384
}
385
386
openMenu(menu) {
387
this.openMenus.add(menu);
388
menu.MaterialMenu.show();
389
}
390
391
closeMenu(menu) {
392
this.openMenus.delete(menu);
393
menu.MaterialMenu.hide();
394
}
395
396
closeAllMenus() {
397
this.openMenus.forEach(menu => {
398
menu.MaterialMenu.hide();
399
});
400
this.openMenus.clear();
401
}
402
403
toggleMenu(menu, event) {
404
if (this.openMenus.has(menu)) {
405
this.closeMenu(menu);
406
} else {
407
this.closeAllMenus(); // Close other menus first
408
this.openMenu(menu);
409
}
410
}
411
}
412
413
// Global menu manager instance
414
const menuManager = new MenuManager();
415
416
// Use with buttons
417
document.addEventListener('click', (event) => {
418
if (event.target.matches('[data-menu-trigger]')) {
419
const menuId = event.target.getAttribute('data-menu-trigger');
420
const menu = document.getElementById(menuId);
421
if (menu) {
422
menuManager.toggleMenu(menu, event);
423
}
424
}
425
});
426
```
427
428
## Menu Customization
429
430
```javascript
431
// Custom menu themes and styles
432
function applyMenuTheme(menu, theme) {
433
const themes = {
434
dark: {
435
backgroundColor: '#333',
436
color: '#fff',
437
itemHover: '#555'
438
},
439
light: {
440
backgroundColor: '#fff',
441
color: '#333',
442
itemHover: '#f5f5f5'
443
},
444
colored: {
445
backgroundColor: '#3f51b5',
446
color: '#fff',
447
itemHover: '#5c6bc0'
448
}
449
};
450
451
const themeConfig = themes[theme];
452
if (!themeConfig) return;
453
454
menu.style.backgroundColor = themeConfig.backgroundColor;
455
menu.style.color = themeConfig.color;
456
457
const items = menu.querySelectorAll('.mdl-menu__item');
458
items.forEach(item => {
459
item.addEventListener('mouseenter', () => {
460
item.style.backgroundColor = themeConfig.itemHover;
461
});
462
463
item.addEventListener('mouseleave', () => {
464
item.style.backgroundColor = '';
465
});
466
});
467
}
468
469
// Animation customization
470
function customMenuAnimation(menu) {
471
// Override default animation
472
menu.addEventListener('mdl-componentupgraded', () => {
473
const menuInstance = menu.MaterialMenu;
474
475
// Custom show animation
476
const originalShow = menuInstance.show;
477
menuInstance.show = function(evt) {
478
originalShow.call(this, evt);
479
480
// Add custom animation class
481
menu.classList.add('custom-menu-animation');
482
483
setTimeout(() => {
484
menu.classList.add('custom-menu-visible');
485
}, 10);
486
};
487
488
// Custom hide animation
489
const originalHide = menuInstance.hide;
490
menuInstance.hide = function() {
491
menu.classList.remove('custom-menu-visible');
492
493
setTimeout(() => {
494
originalHide.call(this);
495
menu.classList.remove('custom-menu-animation');
496
}, 200);
497
};
498
});
499
}
500
```