export class AccordionComponent { constructor(element, options = {}) { this.element = element; this.options = { allowMultiple: false, animationDuration: 300, autoClose: true, keyboardNavigation: true, ...options }; this.items = []; this.activeItems = new Set(); this.init(); } init() { this.bindElements(); this.bindEvents(); this.setInitialState(); this.setupAccessibility(); } bindElements() { const itemElements = this.element.querySelectorAll('.accordion__item'); this.items = Array.from(itemElements).map(item => { const trigger = item.querySelector('.accordion__header'); const content = item.querySelector('.accordion__content'); const id = trigger?.dataset.accordionTrigger || this.generateId(); return { element: item, trigger, content, id, isOpen: false }; }); if (this.items.length === 0) { console.warn('AccordionComponent: No accordion items found'); return; } } bindEvents() { this.items.forEach((item, index) => { if (!item.trigger || !item.content) return; // Click events item.trigger.addEventListener('click', (e) => { e.preventDefault(); this.toggle(index); }); // Keyboard events if (this.options.keyboardNavigation) { item.trigger.addEventListener('keydown', (e) => { this.handleKeyboard(e, index); }); } }); } setInitialState() { this.items.forEach((item, index) => { const isInitiallyOpen = item.trigger?.getAttribute('aria-expanded') === 'true'; if (isInitiallyOpen) { this.open(index, false); // Open without animation initially } else { this.close(index, false); } }); } setupAccessibility() { this.items.forEach((item, index) => { if (!item.trigger || !item.content) return; const triggerId = `accordion-trigger-${item.id}`; const contentId = `accordion-content-${item.id}`; // Set up ARIA attributes item.trigger.setAttribute('id', triggerId); item.trigger.setAttribute('aria-controls', contentId); item.trigger.setAttribute('tabindex', '0'); item.content.setAttribute('id', contentId); item.content.setAttribute('aria-labelledby', triggerId); item.content.setAttribute('role', 'region'); }); } toggle(index) { const item = this.items[index]; if (!item) return; if (item.isOpen) { this.close(index); } else { this.open(index); } } open(index, animate = true) { const item = this.items[index]; if (!item || item.isOpen) return; // Close other items if multiple is not allowed if (!this.options.allowMultiple && this.options.autoClose) { this.closeAll(index); } item.isOpen = true; this.activeItems.add(index); // Update ARIA attributes item.trigger.setAttribute('aria-expanded', 'true'); item.content.setAttribute('aria-hidden', 'false'); // Add active class item.element.classList.add('accordion__item--active'); if (animate) { this.animateOpen(item); } else { item.content.style.maxHeight = 'none'; } // Emit custom event this.element.dispatchEvent(new CustomEvent('accordionopen', { detail: { index, item } })); } close(index, animate = true) { const item = this.items[index]; if (!item || !item.isOpen) return; item.isOpen = false; this.activeItems.delete(index); // Update ARIA attributes item.trigger.setAttribute('aria-expanded', 'false'); item.content.setAttribute('aria-hidden', 'true'); // Remove active class item.element.classList.remove('accordion__item--active'); if (animate) { this.animateClose(item); } else { item.content.style.maxHeight = '0'; } // Emit custom event this.element.dispatchEvent(new CustomEvent('accordionclose', { detail: { index, item } })); } closeAll(except = -1) { this.items.forEach((item, index) => { if (index !== except && item.isOpen) { this.close(index); } }); } openAll() { if (!this.options.allowMultiple) return; this.items.forEach((item, index) => { if (!item.isOpen) { this.open(index); } }); } animateOpen(item) { const content = item.content; const scrollHeight = content.scrollHeight; // Set initial state content.style.maxHeight = '0'; content.style.overflow = 'hidden'; // Force reflow content.offsetHeight; // Add transition content.style.transition = `max-height ${this.options.animationDuration}ms ease-out`; // Animate to full height content.style.maxHeight = `${scrollHeight}px`; // Clean up after animation setTimeout(() => { content.style.maxHeight = 'none'; content.style.overflow = ''; content.style.transition = ''; }, this.options.animationDuration); } animateClose(item) { const content = item.content; const scrollHeight = content.scrollHeight; // Set initial state content.style.maxHeight = `${scrollHeight}px`; content.style.overflow = 'hidden'; // Force reflow content.offsetHeight; // Add transition content.style.transition = `max-height ${this.options.animationDuration}ms ease-in`; // Animate to zero height content.style.maxHeight = '0'; // Clean up after animation setTimeout(() => { content.style.overflow = ''; content.style.transition = ''; }, this.options.animationDuration); } handleKeyboard(e, index) { const currentItem = this.items[index]; if (!currentItem) return; switch (e.key) { case 'Enter': case ' ': e.preventDefault(); this.toggle(index); break; case 'ArrowDown': e.preventDefault(); this.focusNext(index); break; case 'ArrowUp': e.preventDefault(); this.focusPrev(index); break; case 'Home': e.preventDefault(); this.focusFirst(); break; case 'End': e.preventDefault(); this.focusLast(); break; } } focusNext(currentIndex) { const nextIndex = currentIndex < this.items.length - 1 ? currentIndex + 1 : 0; this.items[nextIndex]?.trigger?.focus(); } focusPrev(currentIndex) { const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.items.length - 1; this.items[prevIndex]?.trigger?.focus(); } focusFirst() { this.items[0]?.trigger?.focus(); } focusLast() { this.items[this.items.length - 1]?.trigger?.focus(); } generateId() { return `accordion-${Math.random().toString(36).substr(2, 9)}`; } // Public API getActiveItems() { return Array.from(this.activeItems); } isOpen(index) { return this.items[index]?.isOpen || false; } destroy() { // Remove event listeners and reset state this.items.forEach(item => { if (item.trigger) { item.trigger.removeAttribute('aria-expanded'); item.trigger.removeAttribute('aria-controls'); item.trigger.removeAttribute('tabindex'); } if (item.content) { item.content.removeAttribute('aria-hidden'); item.content.removeAttribute('aria-labelledby'); item.content.removeAttribute('role'); item.content.style.maxHeight = ''; item.content.style.overflow = ''; item.content.style.transition = ''; } item.element.classList.remove('accordion__item--active'); }); this.activeItems.clear(); } }