Material Design Components in CSS, JS and HTML providing a comprehensive implementation of Google's Material Design specification for web applications.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Menu systems and navigation components including dropdown menus and contextual navigation. These components provide user interface patterns for organizing and accessing application functionality.
Dropdown menu component with positioning, keyboard navigation, and smooth animations.
/**
* Material Design menu component
* CSS Class: mdl-js-menu
* Widget: true
*/
interface MaterialMenu {
/**
* Display the menu at calculated position
* @param evt - Optional event object for positioning context
*/
show(evt?: Event): void;
/** Hide the menu with animation */
hide(): void;
/**
* Toggle menu visibility
* @param evt - Optional event object for positioning context
*/
toggle(evt?: Event): void;
}HTML Structure:
<!-- Basic menu -->
<button id="demo-menu-lower-left"
class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">more_vert</i>
</button>
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"
for="demo-menu-lower-left">
<li class="mdl-menu__item">Some Action</li>
<li class="mdl-menu__item">Another Action</li>
<li class="mdl-menu__item" disabled>Disabled Action</li>
<li class="mdl-menu__item">Yet Another Action</li>
</ul>
<!-- Menu with icons -->
<button id="demo-menu-with-icons"
class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">settings</i>
</button>
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu"
for="demo-menu-with-icons">
<li class="mdl-menu__item">
<i class="material-icons">edit</i>Edit
</li>
<li class="mdl-menu__item">
<i class="material-icons">delete</i>Delete
</li>
<li class="mdl-menu__item">
<i class="material-icons">share</i>Share
</li>
</ul>Menu Positioning:
<!-- Top positioning -->
<ul class="mdl-menu mdl-menu--top-left mdl-js-menu">
<ul class="mdl-menu mdl-menu--top-right mdl-js-menu">
<!-- Bottom positioning -->
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu">
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu">
<!-- Unaligned (centered) -->
<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu">Usage Examples:
// Access menu instance
const menuButton = document.querySelector('#demo-menu-lower-left');
const menu = document.querySelector('[for="demo-menu-lower-left"]').MaterialMenu;
// Show menu programmatically
menu.show();
// Hide menu programmatically
menu.hide();
// Toggle menu visibility
menuButton.addEventListener('click', (event) => {
menu.toggle(event);
});
// Handle menu item clicks
document.addEventListener('click', (event) => {
if (event.target.matches('.mdl-menu__item')) {
const menuItem = event.target;
const menuContainer = menuItem.closest('.mdl-js-menu');
console.log('Menu item clicked:', menuItem.textContent);
// Hide menu after selection
menuContainer.MaterialMenu.hide();
// Perform action based on menu item
handleMenuItemAction(menuItem);
}
});
function handleMenuItemAction(menuItem) {
const action = menuItem.textContent.trim();
switch (action) {
case 'Edit':
editItem();
break;
case 'Delete':
deleteItem();
break;
case 'Share':
shareItem();
break;
}
}// Create menu dynamically
function createMenu(buttonId, items) {
const button = document.getElementById(buttonId);
// Create menu element
const menu = document.createElement('ul');
menu.className = 'mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect';
menu.setAttribute('for', buttonId);
// Add menu items
items.forEach(item => {
const li = document.createElement('li');
li.className = 'mdl-menu__item';
li.textContent = item.text;
if (item.disabled) {
li.setAttribute('disabled', '');
}
if (item.icon) {
const icon = document.createElement('i');
icon.className = 'material-icons';
icon.textContent = item.icon;
li.insertBefore(icon, li.firstChild);
}
menu.appendChild(li);
});
// Insert menu after button
button.parentNode.insertBefore(menu, button.nextSibling);
// Upgrade the menu
componentHandler.upgradeElement(menu);
return menu.MaterialMenu;
}
// Usage
const dynamicMenu = createMenu('my-button', [
{ text: 'Edit', icon: 'edit' },
{ text: 'Delete', icon: 'delete', disabled: true },
{ text: 'Share', icon: 'share' }
]);Menus support full keyboard navigation:
// Keyboard navigation is automatic, but you can listen for events
document.addEventListener('keydown', (event) => {
const activeMenu = document.querySelector('.mdl-menu.is-visible');
if (activeMenu) {
const menu = activeMenu.MaterialMenu;
switch (event.key) {
case 'Escape':
menu.hide();
event.preventDefault();
break;
case 'ArrowUp':
navigateMenuUp(activeMenu);
event.preventDefault();
break;
case 'ArrowDown':
navigateMenuDown(activeMenu);
event.preventDefault();
break;
case 'Enter':
case ' ':
const focusedItem = activeMenu.querySelector('.mdl-menu__item:focus');
if (focusedItem && !focusedItem.hasAttribute('disabled')) {
focusedItem.click();
}
event.preventDefault();
break;
}
}
});
function navigateMenuUp(menu) {
const items = Array.from(menu.querySelectorAll('.mdl-menu__item:not([disabled])'));
const currentIndex = items.indexOf(document.activeElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
items[nextIndex].focus();
}
function navigateMenuDown(menu) {
const items = Array.from(menu.querySelectorAll('.mdl-menu__item:not([disabled])'));
const currentIndex = items.indexOf(document.activeElement);
const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
items[nextIndex].focus();
}/**
* Material Menu constants and configuration
*/
interface MenuConstants {
/** Total transition duration in seconds */
TRANSITION_DURATION_SECONDS: 0.3;
/** Fraction of transition used for main animation */
TRANSITION_DURATION_FRACTION: 0.8;
/** Timeout before closing menu after selection */
CLOSE_TIMEOUT: 150;
}
/**
* Keyboard codes used by menu navigation
*/
interface MenuKeyCodes {
ENTER: 13;
ESCAPE: 27;
SPACE: 32;
UP_ARROW: 38;
DOWN_ARROW: 40;
}
/**
* Menu positioning classes
*/
interface MenuPositions {
BOTTOM_LEFT: 'mdl-menu--bottom-left';
BOTTOM_RIGHT: 'mdl-menu--bottom-right';
TOP_LEFT: 'mdl-menu--top-left';
TOP_RIGHT: 'mdl-menu--top-right';
UNALIGNED: 'mdl-menu--unaligned';
}Create context menus that appear on right-click:
// Context menu implementation
function createContextMenu(targetSelector, menuItems) {
let contextMenu = null;
document.addEventListener('contextmenu', (event) => {
if (event.target.matches(targetSelector)) {
event.preventDefault();
// Remove existing context menu
if (contextMenu) {
contextMenu.remove();
}
// Create new context menu
contextMenu = document.createElement('ul');
contextMenu.className = 'mdl-menu mdl-js-menu mdl-menu--unaligned';
contextMenu.style.position = 'fixed';
contextMenu.style.left = event.clientX + 'px';
contextMenu.style.top = event.clientY + 'px';
// Add menu items
menuItems.forEach(item => {
const li = document.createElement('li');
li.className = 'mdl-menu__item';
li.textContent = item.text;
li.addEventListener('click', () => {
item.action(event.target);
contextMenu.MaterialMenu.hide();
});
contextMenu.appendChild(li);
});
document.body.appendChild(contextMenu);
componentHandler.upgradeElement(contextMenu);
// Show menu
contextMenu.MaterialMenu.show();
}
});
// Hide context menu on regular click
document.addEventListener('click', () => {
if (contextMenu) {
contextMenu.MaterialMenu.hide();
}
});
}
// Usage
createContextMenu('.data-item', [
{
text: 'Edit',
action: (target) => editDataItem(target)
},
{
text: 'Delete',
action: (target) => deleteDataItem(target)
},
{
text: 'Duplicate',
action: (target) => duplicateDataItem(target)
}
]);// Track menu states across the application
class MenuManager {
constructor() {
this.openMenus = new Set();
this.setupGlobalListeners();
}
setupGlobalListeners() {
// Track menu opening/closing
document.addEventListener('click', (event) => {
if (event.target.matches('.mdl-menu__item')) {
const menu = event.target.closest('.mdl-js-menu');
this.closeMenu(menu);
}
});
// Close all menus on escape
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
this.closeAllMenus();
}
});
// Close menus when clicking outside
document.addEventListener('click', (event) => {
if (!event.target.closest('.mdl-menu') &&
!event.target.matches('[class*="mdl-button"]')) {
this.closeAllMenus();
}
});
}
openMenu(menu) {
this.openMenus.add(menu);
menu.MaterialMenu.show();
}
closeMenu(menu) {
this.openMenus.delete(menu);
menu.MaterialMenu.hide();
}
closeAllMenus() {
this.openMenus.forEach(menu => {
menu.MaterialMenu.hide();
});
this.openMenus.clear();
}
toggleMenu(menu, event) {
if (this.openMenus.has(menu)) {
this.closeMenu(menu);
} else {
this.closeAllMenus(); // Close other menus first
this.openMenu(menu);
}
}
}
// Global menu manager instance
const menuManager = new MenuManager();
// Use with buttons
document.addEventListener('click', (event) => {
if (event.target.matches('[data-menu-trigger]')) {
const menuId = event.target.getAttribute('data-menu-trigger');
const menu = document.getElementById(menuId);
if (menu) {
menuManager.toggleMenu(menu, event);
}
}
});// Custom menu themes and styles
function applyMenuTheme(menu, theme) {
const themes = {
dark: {
backgroundColor: '#333',
color: '#fff',
itemHover: '#555'
},
light: {
backgroundColor: '#fff',
color: '#333',
itemHover: '#f5f5f5'
},
colored: {
backgroundColor: '#3f51b5',
color: '#fff',
itemHover: '#5c6bc0'
}
};
const themeConfig = themes[theme];
if (!themeConfig) return;
menu.style.backgroundColor = themeConfig.backgroundColor;
menu.style.color = themeConfig.color;
const items = menu.querySelectorAll('.mdl-menu__item');
items.forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.backgroundColor = themeConfig.itemHover;
});
item.addEventListener('mouseleave', () => {
item.style.backgroundColor = '';
});
});
}
// Animation customization
function customMenuAnimation(menu) {
// Override default animation
menu.addEventListener('mdl-componentupgraded', () => {
const menuInstance = menu.MaterialMenu;
// Custom show animation
const originalShow = menuInstance.show;
menuInstance.show = function(evt) {
originalShow.call(this, evt);
// Add custom animation class
menu.classList.add('custom-menu-animation');
setTimeout(() => {
menu.classList.add('custom-menu-visible');
}, 10);
};
// Custom hide animation
const originalHide = menuInstance.hide;
menuInstance.hide = function() {
menu.classList.remove('custom-menu-visible');
setTimeout(() => {
originalHide.call(this);
menu.classList.remove('custom-menu-animation');
}, 200);
};
});
}