2025-06-12 18:44:17 +08:00
|
|
|
(() => {
|
|
|
|
|
const initDropdownMenu = (dropdownMenuComponent) => {
|
2025-06-16 17:58:01 +08:00
|
|
|
const trigger = dropdownMenuComponent.querySelector(':scope > button');
|
|
|
|
|
const popover = dropdownMenuComponent.querySelector(':scope > [data-popover]');
|
|
|
|
|
const menu = popover.querySelector('[role="menu"]');
|
|
|
|
|
|
|
|
|
|
if (!trigger || !menu || !popover) {
|
|
|
|
|
console.error('Dropdown menu component is missing a trigger, a menu, or a popover content element.', dropdownMenuComponent);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-01 20:53:03 +08:00
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
let menuItems = [];
|
|
|
|
|
let activeIndex = -1;
|
2025-05-01 20:53:03 +08:00
|
|
|
|
2025-06-16 17:58:01 +08:00
|
|
|
const closeMenu = () => {
|
|
|
|
|
if (trigger.getAttribute('aria-expanded') === 'false') return;
|
|
|
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|
|
|
|
trigger.removeAttribute('aria-activedescendant');
|
|
|
|
|
popover.setAttribute('aria-hidden', 'true');
|
|
|
|
|
trigger.focus();
|
|
|
|
|
activeIndex = -1;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openMenu = () => {
|
|
|
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|
|
|
|
popover.setAttribute('aria-hidden', 'false');
|
|
|
|
|
menuItems = Array.from(menu.querySelectorAll('[role^="menuitem"]:not([disabled])'));
|
|
|
|
|
if (menuItems.length > 0) {
|
|
|
|
|
setActiveItem(0);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
const setActiveItem = (index) => {
|
|
|
|
|
if (activeIndex > -1 && menuItems[activeIndex]) {
|
|
|
|
|
menuItems[activeIndex].classList.remove('active');
|
2025-05-01 20:53:03 +08:00
|
|
|
}
|
2025-06-12 18:44:17 +08:00
|
|
|
activeIndex = index;
|
|
|
|
|
if (activeIndex > -1 && menuItems[activeIndex]) {
|
|
|
|
|
const activeItem = menuItems[activeIndex];
|
|
|
|
|
activeItem.classList.add('active');
|
|
|
|
|
trigger.setAttribute('aria-activedescendant', activeItem.id);
|
2025-05-01 20:53:03 +08:00
|
|
|
} else {
|
2025-06-12 18:44:17 +08:00
|
|
|
trigger.removeAttribute('aria-activedescendant');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-16 17:58:01 +08:00
|
|
|
trigger.addEventListener('click', () => {
|
|
|
|
|
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
|
|
|
|
|
if (isExpanded) {
|
|
|
|
|
closeMenu();
|
|
|
|
|
} else {
|
|
|
|
|
openMenu();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropdownMenuComponent.addEventListener('keydown', (e) => {
|
|
|
|
|
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
|
|
|
|
|
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
if (isExpanded) closeMenu();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isExpanded) {
|
2025-06-12 18:44:17 +08:00
|
|
|
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {
|
|
|
|
|
e.preventDefault();
|
2025-06-16 17:58:01 +08:00
|
|
|
openMenu();
|
2025-06-12 18:44:17 +08:00
|
|
|
}
|
|
|
|
|
return;
|
2025-05-01 20:53:03 +08:00
|
|
|
}
|
|
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
if (menuItems.length === 0) return;
|
|
|
|
|
|
2025-06-16 17:58:01 +08:00
|
|
|
let nextIndex = activeIndex;
|
|
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
switch (e.key) {
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
e.preventDefault();
|
2025-06-16 17:58:01 +08:00
|
|
|
nextIndex = activeIndex < menuItems.length - 1 ? activeIndex + 1 : 0;
|
2025-06-12 18:44:17 +08:00
|
|
|
break;
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
e.preventDefault();
|
2025-06-16 17:58:01 +08:00
|
|
|
nextIndex = activeIndex > 0 ? activeIndex - 1 : menuItems.length - 1;
|
2025-06-12 18:44:17 +08:00
|
|
|
break;
|
|
|
|
|
case 'Home':
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
nextIndex = 0;
|
|
|
|
|
break;
|
|
|
|
|
case 'End':
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
nextIndex = menuItems.length - 1;
|
|
|
|
|
break;
|
|
|
|
|
case 'Enter':
|
|
|
|
|
case ' ':
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
menuItems[activeIndex]?.click();
|
2025-06-16 17:58:01 +08:00
|
|
|
closeMenu();
|
|
|
|
|
return;
|
2025-05-01 20:53:03 +08:00
|
|
|
}
|
2025-06-12 18:44:17 +08:00
|
|
|
|
|
|
|
|
if (nextIndex !== activeIndex) {
|
|
|
|
|
setActiveItem(nextIndex);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-05-01 20:53:03 +08:00
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
menu.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target.closest('[role^="menuitem"]')) {
|
2025-06-16 17:58:01 +08:00
|
|
|
closeMenu();
|
2025-06-12 18:44:17 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-16 17:58:01 +08:00
|
|
|
document.addEventListener('click', (e) => {
|
|
|
|
|
if (!dropdownMenuComponent.contains(e.target)) {
|
|
|
|
|
closeMenu();
|
2025-06-12 18:44:17 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropdownMenuComponent.dataset.dropdownMenuInitialized = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
|
|
|
|
|
|
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
|
|
|
mutations.forEach((mutation) => {
|
|
|
|
|
mutation.addedNodes.forEach((node) => {
|
2025-06-16 17:58:01 +08:00
|
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
|
|
|
if (node.matches('.dropdown-menu:not([data-dropdown-menu-initialized])')) {
|
|
|
|
|
initDropdownMenu(node);
|
2025-06-12 18:44:17 +08:00
|
|
|
}
|
2025-06-16 17:58:01 +08:00
|
|
|
node.querySelectorAll('.dropdown-menu:not([data-dropdown-menu-initialized])').forEach(initDropdownMenu);
|
2025-06-12 18:44:17 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-06-16 17:58:01 +08:00
|
|
|
|
2025-06-12 18:44:17 +08:00
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
|
})();
|