166 lines
4.4 KiB
TypeScript
166 lines
4.4 KiB
TypeScript
/**
|
|
* User Menu Handler
|
|
* Manages the responsive user menu that works for both desktop and mobile layouts
|
|
*/
|
|
|
|
interface UserMenuElements {
|
|
button: HTMLButtonElement;
|
|
menu: HTMLDivElement;
|
|
menuIcon: SVGElement | null;
|
|
closeIcon: SVGElement | null;
|
|
chevronIcon: HTMLElement | null;
|
|
}
|
|
|
|
class UserMenu {
|
|
private elements: UserMenuElements | null = null;
|
|
|
|
constructor() {
|
|
this.initialize();
|
|
}
|
|
|
|
private initialize(): void {
|
|
const userMenuButton = document.querySelector(
|
|
'#user-menu-button',
|
|
) as HTMLButtonElement;
|
|
const userMenu = document.querySelector('#user-menu') as HTMLDivElement;
|
|
const menuIcon = document.querySelector('.menu-icon') as SVGElement | null;
|
|
const closeIcon = document.querySelector(
|
|
'.close-icon',
|
|
) as SVGElement | null;
|
|
const chevronIcon = document.querySelector(
|
|
'.chevron-icon',
|
|
) as HTMLElement | null;
|
|
|
|
if (!userMenuButton || !userMenu) {
|
|
return; // User menu not present (e.g., guest user)
|
|
}
|
|
|
|
this.elements = {
|
|
button: userMenuButton,
|
|
menu: userMenu,
|
|
menuIcon,
|
|
closeIcon,
|
|
chevronIcon,
|
|
};
|
|
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
private attachEventListeners(): void {
|
|
if (!this.elements) return;
|
|
|
|
// Toggle menu on button click
|
|
this.elements.button.addEventListener(
|
|
'click',
|
|
this.handleMenuToggle.bind(this),
|
|
);
|
|
|
|
// Close menu when clicking outside
|
|
document.addEventListener('click', this.handleClickOutside.bind(this));
|
|
|
|
// Close menu on escape key
|
|
document.addEventListener('keydown', this.handleEscapeKey.bind(this));
|
|
}
|
|
|
|
private handleMenuToggle(): void {
|
|
if (!this.elements) return;
|
|
|
|
const isOpen = !this.elements.menu.classList.contains('hidden');
|
|
|
|
if (isOpen) {
|
|
this.closeMenu();
|
|
} else {
|
|
this.openMenu();
|
|
}
|
|
}
|
|
|
|
private handleClickOutside(event: MouseEvent): void {
|
|
if (!this.elements) return;
|
|
|
|
const target = event.target as Node;
|
|
|
|
if (
|
|
!this.elements.button.contains(target) &&
|
|
!this.elements.menu.contains(target)
|
|
) {
|
|
this.closeMenu();
|
|
}
|
|
}
|
|
|
|
private handleEscapeKey(event: KeyboardEvent): void {
|
|
if (!this.elements) return;
|
|
|
|
if (
|
|
event.key === 'Escape' &&
|
|
!this.elements.menu.classList.contains('hidden')
|
|
) {
|
|
this.closeMenu();
|
|
}
|
|
}
|
|
|
|
private openMenu(): void {
|
|
if (!this.elements) return;
|
|
|
|
// Show menu but keep it in initial animation state
|
|
this.elements.menu.classList.remove('hidden');
|
|
this.elements.button.setAttribute('aria-expanded', 'true');
|
|
|
|
// Ensure initial state is set
|
|
this.elements.menu.classList.add('opacity-0', '-translate-y-2');
|
|
this.elements.menu.classList.remove('opacity-100', 'translate-y-0');
|
|
|
|
// Force a reflow to ensure the initial state is applied
|
|
this.elements.menu.offsetHeight;
|
|
|
|
// Trigger animation by transitioning to visible state
|
|
requestAnimationFrame(() => {
|
|
this.elements!.menu.classList.remove('opacity-0', '-translate-y-2');
|
|
this.elements!.menu.classList.add('opacity-100', 'translate-y-0');
|
|
});
|
|
|
|
// Update mobile icons (hamburger -> close)
|
|
if (this.elements.menuIcon && this.elements.closeIcon) {
|
|
this.elements.menuIcon.classList.add('hidden');
|
|
this.elements.closeIcon.classList.remove('hidden');
|
|
}
|
|
|
|
// Update desktop chevron (rotate down)
|
|
if (this.elements.chevronIcon) {
|
|
this.elements.chevronIcon.classList.add('rotate-180');
|
|
}
|
|
}
|
|
|
|
private closeMenu(): void {
|
|
if (!this.elements) return;
|
|
|
|
// Animate out: fade and slide up
|
|
this.elements.menu.classList.remove('opacity-100', 'translate-y-0');
|
|
this.elements.menu.classList.add('opacity-0', '-translate-y-2');
|
|
|
|
// Hide menu after CSS animation completes
|
|
setTimeout(() => {
|
|
if (this.elements) {
|
|
this.elements.menu.classList.add('hidden');
|
|
}
|
|
}, 150); // Match the CSS transition duration
|
|
|
|
this.elements.button.setAttribute('aria-expanded', 'false');
|
|
|
|
// Update mobile icons (close -> hamburger)
|
|
if (this.elements.menuIcon && this.elements.closeIcon) {
|
|
this.elements.menuIcon.classList.remove('hidden');
|
|
this.elements.closeIcon.classList.add('hidden');
|
|
}
|
|
|
|
// Update desktop chevron (rotate back up)
|
|
if (this.elements.chevronIcon) {
|
|
this.elements.chevronIcon.classList.remove('rotate-180');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize user menu when DOM is loaded
|
|
export function initUserMenu(): void {
|
|
new UserMenu();
|
|
}
|