/** * VIBE BRUTALISM - JavaScript Component Library * Interactive functionality for neo-brutalist components * Version: 2.0.0 * WCAG 2.1 AA & Section 508 Compliant */ (function() { 'use strict'; // ============================================ // ACCESSIBILITY UTILITIES // ============================================ const a11y = { // Create ARIA live region for announcements createLiveRegion() { if (!document.getElementById('vb-live-region')) { const liveRegion = document.createElement('div'); liveRegion.id = 'vb-live-region'; liveRegion.className = 'vb-sr-only'; liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); document.body.appendChild(liveRegion); } return document.getElementById('vb-live-region'); }, // Announce message to screen readers announce(message, priority = 'polite') { const liveRegion = this.createLiveRegion(); liveRegion.setAttribute('aria-live', priority); liveRegion.textContent = message; // Clear after announcement setTimeout(() => { liveRegion.textContent = ''; }, 1000); }, // Trap focus within element trapFocus(element) { const focusableElements = element.querySelectorAll( 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return null; const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; const handler = (e) => { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } else if (!e.shiftKey && document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } }; element.addEventListener('keydown', handler); firstFocusable.focus(); // Return cleanup function return () => element.removeEventListener('keydown', handler); } }; // ============================================ // MODAL FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBModal { constructor(element) { this.modal = element; this.closeButtons = this.modal.querySelectorAll('.vb-modal-close, [data-vb-modal-close]'); this.previousActiveElement = null; this.focusTrapCleanup = null; this.escHandler = this.handleEscape.bind(this); this.clickHandler = this.handleBackdropClick.bind(this); this.init(); } init() { // Set ARIA attributes this.modal.setAttribute('role', 'dialog'); this.modal.setAttribute('aria-modal', 'true'); if (!this.modal.getAttribute('aria-labelledby')) { const title = this.modal.querySelector('.vb-modal-title'); if (title && !title.id) { title.id = `modal-title-${Math.random().toString(36).substr(2, 9)}`; } if (title) { this.modal.setAttribute('aria-labelledby', title.id); } } // Close button events this.closeButtons.forEach(btn => { btn.setAttribute('aria-label', 'Close dialog'); btn.addEventListener('click', () => this.close()); }); // Close on outside click this.modal.addEventListener('click', this.clickHandler); } handleBackdropClick(e) { if (e.target === this.modal) { this.close(); } } handleEscape(e) { if (e.key === 'Escape' && this.modal.classList.contains('active')) { this.close(); } } open() { this.previousActiveElement = document.activeElement; this.modal.classList.add('active'); document.body.style.overflow = 'hidden'; // Trap focus and store cleanup function this.focusTrapCleanup = a11y.trapFocus(this.modal); // Add ESC key handler document.addEventListener('keydown', this.escHandler); a11y.announce('Dialog opened'); } close() { this.modal.classList.remove('active'); document.body.style.overflow = ''; // Cleanup focus trap if (this.focusTrapCleanup) { this.focusTrapCleanup(); this.focusTrapCleanup = null; } // Remove ESC key handler document.removeEventListener('keydown', this.escHandler); // Return focus if (this.previousActiveElement) { this.previousActiveElement.focus(); } a11y.announce('Dialog closed'); } } // Initialize all modals const modals = {}; document.querySelectorAll('.vb-modal').forEach(modal => { const id = modal.id; if (id) { modals[id] = new VBModal(modal); } }); // Modal trigger buttons document.querySelectorAll('[data-vb-modal-target]').forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.dataset.vbModalTarget; if (modals[targetId]) { modals[targetId].open(); } }); }); // ============================================ // HAMBURGER MENU FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBHamburgerMenu { constructor(navbar) { this.navbar = navbar; this.toggle = navbar.querySelector('.vb-navbar-toggle'); this.menu = navbar.querySelector('.vb-navbar-menu'); this.escHandler = this.handleEscape.bind(this); if (this.toggle && this.menu) { this.init(); } } init() { // Set ARIA attributes this.toggle.setAttribute('aria-label', 'Toggle navigation menu'); this.toggle.setAttribute('aria-expanded', 'false'); this.toggle.setAttribute('aria-controls', this.menu.id || 'navbar-menu'); if (!this.menu.id) { this.menu.id = 'navbar-menu'; } // Toggle on click this.toggle.addEventListener('click', () => { this.toggleMenu(); }); // Close on ESC - using bound handler for proper cleanup document.addEventListener('keydown', this.escHandler); } handleEscape(e) { if (e.key === 'Escape' && this.menu.classList.contains('active')) { this.closeMenu(); this.toggle.focus(); } } toggleMenu() { const isOpen = this.menu.classList.toggle('active'); this.toggle.classList.toggle('active'); this.toggle.setAttribute('aria-expanded', isOpen); a11y.announce(isOpen ? 'Menu opened' : 'Menu closed'); } closeMenu() { this.menu.classList.remove('active'); this.toggle.classList.remove('active'); this.toggle.setAttribute('aria-expanded', 'false'); } } // Initialize hamburger menus document.querySelectorAll('.vb-navbar').forEach(navbar => { new VBHamburgerMenu(navbar); }); // ============================================ // DROPDOWN FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBDropdown { constructor(element) { this.dropdown = element; this.toggle = this.dropdown.querySelector('.vb-dropdown-toggle'); this.menu = this.dropdown.querySelector('.vb-dropdown-menu'); if (!this.toggle || !this.menu) { console.warn('Vibe Brutalism: Dropdown requires .vb-dropdown-toggle and .vb-dropdown-menu'); return; } this.items = this.menu.querySelectorAll('.vb-dropdown-item'); this.outsideClickHandler = this.handleOutsideClick.bind(this); this.init(); } init() { // Set ARIA attributes this.toggle.setAttribute('aria-haspopup', 'true'); this.toggle.setAttribute('aria-expanded', 'false'); this.menu.setAttribute('role', 'menu'); this.items.forEach(item => { item.setAttribute('role', 'menuitem'); item.setAttribute('tabindex', '-1'); }); // Toggle on click this.toggle.addEventListener('click', (e) => { e.stopPropagation(); this.toggleDropdown(); }); // Keyboard navigation this.toggle.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.open(); this.items[0]?.focus(); } }); this.items.forEach((item, index) => { item.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); this.items[(index + 1) % this.items.length]?.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.items[(index - 1 + this.items.length) % this.items.length]?.focus(); } else if (e.key === 'Escape') { e.preventDefault(); this.close(); this.toggle.focus(); } }); }); // Prevent closing when clicking inside menu this.menu.addEventListener('click', (e) => { e.stopPropagation(); }); } handleOutsideClick() { this.close(); } toggleDropdown() { const isOpen = this.dropdown.classList.toggle('active'); this.toggle.setAttribute('aria-expanded', isOpen); if (isOpen) { this.items.forEach(item => item.setAttribute('tabindex', '0')); // Add outside click handler when open setTimeout(() => { document.addEventListener('click', this.outsideClickHandler); }, 0); } else { this.items.forEach(item => item.setAttribute('tabindex', '-1')); // Remove outside click handler when closed document.removeEventListener('click', this.outsideClickHandler); } } open() { this.dropdown.classList.add('active'); this.toggle.setAttribute('aria-expanded', 'true'); this.items.forEach(item => item.setAttribute('tabindex', '0')); // Add outside click handler when open setTimeout(() => { document.addEventListener('click', this.outsideClickHandler); }, 0); } close() { this.dropdown.classList.remove('active'); this.toggle.setAttribute('aria-expanded', 'false'); this.items.forEach(item => item.setAttribute('tabindex', '-1')); // Remove outside click handler when closed document.removeEventListener('click', this.outsideClickHandler); } } // Initialize all dropdowns document.querySelectorAll('.vb-dropdown').forEach(dropdown => { new VBDropdown(dropdown); }); // ============================================ // TABS FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBTabs { constructor(element) { this.tabs = element; this.buttons = this.tabs.querySelectorAll('.vb-tab-button'); this.contents = this.tabs.querySelectorAll('.vb-tab-content'); this.init(); } init() { // Set ARIA attributes const tablist = this.tabs.querySelector('.vb-tab-list'); if (tablist) { tablist.setAttribute('role', 'tablist'); } this.buttons.forEach((button, index) => { const tabId = `tab-${Math.random().toString(36).substr(2, 9)}`; const panelId = `panel-${Math.random().toString(36).substr(2, 9)}`; button.setAttribute('role', 'tab'); button.setAttribute('id', tabId); button.setAttribute('aria-controls', panelId); button.setAttribute('tabindex', '-1'); this.contents[index].setAttribute('role', 'tabpanel'); this.contents[index].setAttribute('id', panelId); this.contents[index].setAttribute('aria-labelledby', tabId); this.contents[index].setAttribute('tabindex', '0'); // Click event button.addEventListener('click', () => { this.switchTab(index); }); // Keyboard navigation button.addEventListener('keydown', (e) => { let newIndex = index; if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); newIndex = (index + 1) % this.buttons.length; } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); newIndex = (index - 1 + this.buttons.length) % this.buttons.length; } else if (e.key === 'Home') { e.preventDefault(); newIndex = 0; } else if (e.key === 'End') { e.preventDefault(); newIndex = this.buttons.length - 1; } if (newIndex !== index) { this.switchTab(newIndex); this.buttons[newIndex].focus(); } }); }); // Activate first tab by default if (this.buttons.length > 0 && !this.tabs.querySelector('.vb-tab-button.active')) { this.switchTab(0); } } switchTab(index) { // Remove active class and update ARIA this.buttons.forEach((btn, i) => { btn.classList.remove('active'); btn.setAttribute('aria-selected', 'false'); btn.setAttribute('tabindex', '-1'); }); this.contents.forEach(content => { content.classList.remove('active'); }); // Add active class to selected button and content this.buttons[index].classList.add('active'); this.buttons[index].setAttribute('aria-selected', 'true'); this.buttons[index].setAttribute('tabindex', '0'); this.contents[index].classList.add('active'); a11y.announce(`Tab ${index + 1} selected`); } } // Initialize all tabs document.querySelectorAll('.vb-tabs').forEach(tabs => { new VBTabs(tabs); }); // ============================================ // ACCORDION FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBAccordion { constructor(element) { this.accordion = element; this.items = this.accordion.querySelectorAll('.vb-accordion-item'); this.init(); } init() { this.items.forEach((item, index) => { const header = item.querySelector('.vb-accordion-header'); const body = item.querySelector('.vb-accordion-body'); // Set ARIA attributes const headerId = `accordion-header-${Math.random().toString(36).substr(2, 9)}`; const bodyId = `accordion-body-${Math.random().toString(36).substr(2, 9)}`; header.setAttribute('id', headerId); header.setAttribute('aria-expanded', 'false'); header.setAttribute('aria-controls', bodyId); body.setAttribute('id', bodyId); body.setAttribute('role', 'region'); body.setAttribute('aria-labelledby', headerId); // Click event header.addEventListener('click', () => { this.toggle(header, body); }); // Keyboard support header.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); const nextHeader = this.items[index + 1]?.querySelector('.vb-accordion-header'); nextHeader?.focus(); } else if (e.key === 'ArrowUp') { e.preventDefault(); const prevHeader = this.items[index - 1]?.querySelector('.vb-accordion-header'); prevHeader?.focus(); } else if (e.key === 'Home') { e.preventDefault(); this.items[0]?.querySelector('.vb-accordion-header')?.focus(); } else if (e.key === 'End') { e.preventDefault(); this.items[this.items.length - 1]?.querySelector('.vb-accordion-header')?.focus(); } }); }); } toggle(header, body) { const isActive = header.classList.contains('active'); // Close all items this.accordion.querySelectorAll('.vb-accordion-header').forEach(h => { h.classList.remove('active'); h.setAttribute('aria-expanded', 'false'); }); this.accordion.querySelectorAll('.vb-accordion-body').forEach(b => { b.classList.remove('active'); }); // Toggle current item if (!isActive) { header.classList.add('active'); header.setAttribute('aria-expanded', 'true'); body.classList.add('active'); a11y.announce('Section expanded'); } else { a11y.announce('Section collapsed'); } } } // Initialize all accordions document.querySelectorAll('.vb-accordion').forEach(accordion => { new VBAccordion(accordion); }); // ============================================ // CAROUSEL FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBCarousel { constructor(element) { this.carousel = element; this.track = this.carousel.querySelector('.vb-carousel-track'); this.items = this.carousel.querySelectorAll('.vb-carousel-item'); this.prevBtn = this.carousel.querySelector('.vb-carousel-control-prev'); this.nextBtn = this.carousel.querySelector('.vb-carousel-control-next'); this.indicators = this.carousel.querySelectorAll('.vb-carousel-indicator'); this.currentIndex = 0; this.autoplayInterval = null; this.touchStartX = 0; this.touchEndX = 0; this.init(); } init() { if (this.items.length === 0 || !this.track) { console.warn('Vibe Brutalism: Carousel requires .vb-carousel-track and at least one .vb-carousel-item'); return; } // Set ARIA attributes this.carousel.setAttribute('role', 'region'); this.carousel.setAttribute('aria-label', 'Carousel'); this.carousel.setAttribute('aria-roledescription', 'carousel'); this.items.forEach((item, index) => { item.setAttribute('role', 'group'); item.setAttribute('aria-roledescription', 'slide'); item.setAttribute('aria-label', `Slide ${index + 1} of ${this.items.length}`); }); // Previous button if (this.prevBtn) { this.prevBtn.setAttribute('aria-label', 'Previous slide'); this.prevBtn.addEventListener('click', () => this.prev()); } // Next button if (this.nextBtn) { this.nextBtn.setAttribute('aria-label', 'Next slide'); this.nextBtn.addEventListener('click', () => this.next()); } // Indicators this.indicators.forEach((indicator, index) => { indicator.setAttribute('aria-label', `Go to slide ${index + 1}`); indicator.setAttribute('role', 'button'); indicator.addEventListener('click', () => this.goTo(index)); }); // Keyboard navigation this.carousel.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } }); // Pause autoplay on hover/focus this.carousel.addEventListener('mouseenter', () => this.pauseAutoplay()); this.carousel.addEventListener('mouseleave', () => this.startAutoplay()); this.carousel.addEventListener('focusin', () => this.pauseAutoplay()); this.carousel.addEventListener('focusout', () => this.startAutoplay()); // Touch/Swipe support this.carousel.addEventListener('touchstart', (e) => { this.touchStartX = e.changedTouches[0].screenX; }, { passive: true }); this.carousel.addEventListener('touchend', (e) => { this.touchEndX = e.changedTouches[0].screenX; this.handleSwipe(); }, { passive: true }); // Initialize this.goTo(0); // Start autoplay if enabled if (this.carousel.dataset.autoplay === 'true') { this.startAutoplay(); } } handleSwipe() { const swipeThreshold = 50; const diff = this.touchStartX - this.touchEndX; if (Math.abs(diff) > swipeThreshold) { if (diff > 0) { // Swipe left - go to next this.next(); } else { // Swipe right - go to previous this.prev(); } } } goTo(index) { this.currentIndex = index; const offset = -index * 100; this.track.style.transform = `translateX(${offset}%)`; // Update indicators this.indicators.forEach((indicator, i) => { indicator.classList.toggle('active', i === index); indicator.setAttribute('aria-current', i === index ? 'true' : 'false'); }); // Update items this.items.forEach((item, i) => { item.setAttribute('aria-hidden', i !== index); }); a11y.announce(`Slide ${index + 1} of ${this.items.length}`); } next() { const nextIndex = (this.currentIndex + 1) % this.items.length; this.goTo(nextIndex); } prev() { const prevIndex = (this.currentIndex - 1 + this.items.length) % this.items.length; this.goTo(prevIndex); } startAutoplay() { const interval = parseInt(this.carousel.dataset.interval) || 5000; this.autoplayInterval = setInterval(() => this.next(), interval); } pauseAutoplay() { if (this.autoplayInterval) { clearInterval(this.autoplayInterval); this.autoplayInterval = null; } } } // Initialize all carousels const carousels = {}; document.querySelectorAll('.vb-carousel').forEach(carousel => { const id = carousel.id || `carousel-${Math.random().toString(36).substr(2, 9)}`; carousel.id = id; carousels[id] = new VBCarousel(carousel); }); // ============================================ // TOAST NOTIFICATIONS (WCAG 2.1 AA) // ============================================ class VBToastManager { constructor() { this.container = null; this.toasts = []; } getContainer(position = 'top-right') { const containerId = `vb-toast-container-${position}`; let container = document.getElementById(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; container.className = `vb-toast-container ${position}`; container.setAttribute('aria-live', 'polite'); container.setAttribute('aria-atomic', 'false'); document.body.appendChild(container); } return container; } show(message, type = 'info', duration = 5000, position = 'top-right') { const container = this.getContainer(position); const toast = document.createElement('div'); toast.className = `vb-toast vb-toast-${type}`; toast.setAttribute('role', 'status'); toast.setAttribute('aria-live', 'polite'); // Icon const icons = { success: '✓', warning: '⚠', danger: '✕', info: 'ℹ' }; toast.innerHTML = `
${message}
`; // Close button const closeBtn = toast.querySelector('.vb-toast-close'); closeBtn.addEventListener('click', () => this.remove(toast)); container.appendChild(toast); this.toasts.push(toast); // Announce to screen readers a11y.announce(message, 'polite'); // Auto remove if (duration > 0) { setTimeout(() => this.remove(toast), duration); } return toast; } remove(toast) { toast.classList.add('removing'); setTimeout(() => { toast.remove(); const index = this.toasts.indexOf(toast); if (index > -1) { this.toasts.splice(index, 1); } }, 300); } } const toastManager = new VBToastManager(); // ============================================ // SNACKBAR FUNCTIONALITY (WCAG 2.1 AA) // ============================================ class VBSnackbar { constructor() { this.snackbar = null; this.timeout = null; } show(message, actionText = null, actionCallback = null, duration = 5000) { // Remove existing snackbar if (this.snackbar) { this.hide(); } // Create snackbar this.snackbar = document.createElement('div'); this.snackbar.className = 'vb-snackbar'; this.snackbar.setAttribute('role', 'status'); this.snackbar.setAttribute('aria-live', 'polite'); let html = `
${message}
`; if (actionText && actionCallback) { html += ``; } html += ``; this.snackbar.innerHTML = html; // Action button if (actionText && actionCallback) { const actionBtn = this.snackbar.querySelector('.vb-snackbar-action'); actionBtn.addEventListener('click', () => { actionCallback(); this.hide(); }); } // Close button const closeBtn = this.snackbar.querySelector('.vb-snackbar-close'); closeBtn.addEventListener('click', () => this.hide()); document.body.appendChild(this.snackbar); // Show with animation setTimeout(() => { this.snackbar.classList.add('show'); }, 10); // Announce to screen readers a11y.announce(message, 'polite'); // Auto hide if (duration > 0) { this.timeout = setTimeout(() => this.hide(), duration); } } hide() { if (this.snackbar) { this.snackbar.classList.remove('show'); setTimeout(() => { this.snackbar?.remove(); this.snackbar = null; }, 300); } if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } } } const snackbar = new VBSnackbar(); // ============================================ // ALERT CLOSE FUNCTIONALITY // ============================================ document.querySelectorAll('[data-vb-alert-close]').forEach(btn => { btn.addEventListener('click', () => { const alert = btn.closest('.vb-alert'); if (alert) { alert.style.opacity = '0'; setTimeout(() => { alert.remove(); }, 300); } }); }); // ============================================ // PROGRESS BAR // ============================================ window.VBSetProgress = function(elementId, percentage) { const progressBar = document.querySelector(`#${elementId} .vb-progress-bar`); if (progressBar) { progressBar.style.width = `${percentage}%`; progressBar.textContent = `${percentage}%`; progressBar.setAttribute('aria-valuenow', percentage); a11y.announce(`Progress: ${percentage}%`); } }; // ============================================ // FORM VALIDATION // ============================================ window.VBValidateForm = function(formElement) { const inputs = formElement.querySelectorAll('.vb-input[required], .vb-textarea[required]'); let isValid = true; inputs.forEach(input => { if (!input.value.trim()) { input.style.borderColor = 'var(--vb-danger)'; input.setAttribute('aria-invalid', 'true'); isValid = false; } else { input.style.borderColor = 'var(--vb-black)'; input.setAttribute('aria-invalid', 'false'); } }); return isValid; }; // ============================================ // GLOBAL API // ============================================ window.VB = { modals, carousels, Toast: (message, type, duration, position) => toastManager.show(message, type, duration, position), Snackbar: (message, actionText, actionCallback, duration) => snackbar.show(message, actionText, actionCallback, duration), SetProgress: window.VBSetProgress, ValidateForm: window.VBValidateForm, announce: (message, priority) => a11y.announce(message, priority) }; // Initialize accessibility features if (document.body) { a11y.createLiveRegion(); console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!'); } else { console.warn('Vibe Brutalism: document.body not available. Retrying on DOMContentLoaded...'); document.addEventListener('DOMContentLoaded', () => { a11y.createLiveRegion(); console.log('🎨 Vibe Brutalism v2.0.0 initialized with WCAG 2.1 AA compliance!'); }); } })();