templates/vitekit-claude/src/scripts/components/accordion.js
2026-04-12 21:03:18 +03:00

317 lines
7.6 KiB
JavaScript

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();
}
}