/** * Dropdown menu functionality * Provides click-based dropdown interactions with keyboard support */ export interface DropdownOptions { closeOnClick?: boolean; closeOnOutsideClick?: boolean; triggerEvent?: 'click' | 'hover'; } /** * Initialize dropdown menus * @param selector - CSS selector for dropdown containers * @param options - Configuration options */ export function initDropdowns( selector: string = '.dropdown', options: DropdownOptions = {} ): void { const defaults: DropdownOptions = { closeOnClick: true, closeOnOutsideClick: true, triggerEvent: 'click', }; const config = { ...defaults, ...options }; const dropdowns = document.querySelectorAll(selector); dropdowns.forEach((dropdown) => { if (dropdown.hasAttribute('data-dropdown-initialized')) return; dropdown.setAttribute('data-dropdown-initialized', 'true'); const trigger = dropdown.querySelector('.dropdown-trigger') as HTMLElement; const content = dropdown.querySelector('.dropdown-content') as HTMLElement; if (!trigger || !content) return; // Click-based toggle if (config.triggerEvent === 'click') { trigger.addEventListener('click', (e) => { e.preventDefault(); // Prevent default link behavior e.stopPropagation(); // Stop the event from bubbling up toggleDropdown(dropdown, trigger); }); // Close on item click if (config.closeOnClick) { const items = content.querySelectorAll('.dropdown-item'); items.forEach((item) => { item.addEventListener('click', () => { closeDropdown(dropdown, trigger); }); }); } } // Keyboard navigation trigger.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleDropdown(dropdown, trigger); } else if (e.key === 'Escape') { closeDropdown(dropdown, trigger); } }); // Navigate items with arrow keys content.addEventListener('keydown', (e) => { const items = Array.from( content.querySelectorAll('.dropdown-item:not([disabled]):not(.disabled)') ) as HTMLElement[]; const currentIndex = items.indexOf(document.activeElement as HTMLElement); if (e.key === 'ArrowDown') { e.preventDefault(); const nextIndex = (currentIndex + 1) % items.length; items[nextIndex]?.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); const prevIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1; items[prevIndex]?.focus(); } else if (e.key === 'Escape') { closeDropdown(dropdown, trigger); trigger.focus(); } else if (e.key === 'Tab') { closeDropdown(dropdown, trigger); } }); }); // Close on outside click if (config.closeOnOutsideClick) { document.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (!target.closest('.dropdown')) { closeAllDropdowns(); } }); } } /** * Toggle dropdown open/close state */ function toggleDropdown(dropdown: Element, trigger: HTMLElement): void { const isOpen = dropdown.classList.contains('open'); if (isOpen) { // If it's already open, just close it. closeDropdown(dropdown, trigger); } else { // If it's closed, close all others first, then open it. closeAllDropdowns(); openDropdown(dropdown, trigger); } } /** * Open dropdown */ function openDropdown(dropdown: Element, trigger: HTMLElement): void { dropdown.classList.add('open'); dropdown.setAttribute('data-state', 'open'); trigger.setAttribute('aria-expanded', 'true'); // Focus first item after animation setTimeout(() => { const firstItem = dropdown.querySelector( '.dropdown-item:not([disabled]):not(.disabled)' ) as HTMLElement; firstItem?.focus(); }, 150); } /** * Close dropdown */ function closeDropdown(dropdown: Element, trigger: HTMLElement): void { dropdown.classList.remove('open'); dropdown.setAttribute('data-state', 'closed'); trigger.setAttribute('aria-expanded', 'false'); trigger.blur(); // Remove focus to prevent :focus-within from keeping it open } /** * Close all dropdowns */ function closeAllDropdowns(): void { document.querySelectorAll('.dropdown.open').forEach((dropdown) => { const trigger = dropdown.querySelector('.dropdown-trigger') as HTMLElement; if (trigger) { closeDropdown(dropdown, trigger); } }); } /** * Create a simple dropdown API */ export function createDropdown() { return { /** * Initialize all dropdowns on the page */ init: (selector?: string, options?: DropdownOptions) => { // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { initDropdowns(selector, options); }); } else { initDropdowns(selector, options); } }, /** * Manually open a dropdown */ open: (selector: string) => { const dropdown = document.querySelector(selector); if (dropdown) { const trigger = dropdown.querySelector('.dropdown-trigger') as HTMLElement; if (trigger) openDropdown(dropdown, trigger); } }, /** * Manually close a dropdown */ close: (selector: string) => { const dropdown = document.querySelector(selector); if (dropdown) { const trigger = dropdown.querySelector('.dropdown-trigger') as HTMLElement; if (trigger) closeDropdown(dropdown, trigger); } }, /** * Close all dropdowns */ closeAll: () => { closeAllDropdowns(); }, }; }