1/* 2 * ============================================================================ 3 * ACA MODAL SYSTEM v1.0 4 * ============================================================================ 5 * 6 * A modern, accessible modal system for the ACA website. 7 * 8 * Features: 9 * - Modern ES6+ syntax with classes 10 * - Inline content support with data attribute linking 11 * - Content from other elements via selector 12 * - Close on overlay click and ESC key 13 * - Full accessibility with ARIA attributes and focus management 14 * - Fade animations 15 * - Multiple modal support 16 * 17 * Usage: 18 * - Trigger: <button data-modal="modal-id">Open Modal</button> 19 * - Inline Content: <div id="modal-id" class="aca-modal-content">Content</div> 20 * - External Content: <button data-modal-content="#content-selector">Open</button> 21 * 22 * Last updated: October 28, 2025, by GitHub Copilot 23 * ============================================================================ 24 */ 25 26class ACAModal { 27 constructor() { 28 this.activeModal = null; 29 this.previousFocus = null; 30 this.modalContainer = null; 31 32 this.init(); 33 } 34 35 init() { 36 this.bindEvents(); 37 } 38 39 /** 40 * Creates the modal container and overlay structure 41 */ 42 createModalContainer() { 43 if (document.querySelector('.aca-modal-overlay')) return; 44 45 this.modalContainer = document.createElement('div'); 46 this.modalContainer.className = 'aca-modal-overlay'; 47 this.modalContainer.setAttribute('aria-hidden', 'true'); 48 this.modalContainer.innerHTML = ` 49 <div class="aca-modal-dialog" role="dialog" aria-modal="true"> 50 <div class="aca-modal-content-wrapper"> 51 <button class="aca-modal-close" aria-label="Close modal" type="button"> 52 <span aria-hidden="true">×</span> 53 </button> 54 <div class="aca-modal-body"></div> 55 </div> 56 </div> 57 `; 58 59 document.body.appendChild(this.modalContainer); 60 } 61 62 /** 63 * Binds all event listeners 64 */ 65 bindEvents() { 66 // Modal triggers 67 document.addEventListener('click', (e) => { 68 const trigger = e.target.closest('[data-modal], [data-modal-content]'); 69 if (trigger) { 70 e.preventDefault(); 71 this.openModal(trigger); 72 } 73 74 // Close button 75 if (e.target.closest('.aca-modal-close')) { 76 this.closeModal(); 77 } 78 79 // Overlay click 80 if (e.target.classList.contains('aca-modal-overlay')) { 81 this.closeModal(); 82 } 83 }); 84 85 // ESC key close 86 document.addEventListener('keydown', (e) => { 87 if (e.key === 'Escape' && this.activeModal) { 88 this.closeModal(); 89 } 90 91 // Trap focus within modal 92 if (e.key === 'Tab' && this.activeModal) { 93 this.trapFocus(e); 94 } 95 }); 96 } 97 98 /** 99 * Opens a modal with the specified content 100 * @param {HTMLElement} trigger - The element that triggered the modal 101 */ 102 openModal(trigger) { 103 // Lazy initialization - only create modal container when first needed 104 if (!this.modalContainer) { 105 this.createModalContainer(); 106 } 107 108 const modalId = trigger.getAttribute('data-modal'); 109 const contentSelector = trigger.getAttribute('data-modal-content'); 110 111 let content = ''; 112 113 if (modalId) { 114 // Inline content by ID 115 const contentElement = document.getElementById(modalId); 116 if (contentElement) { 117 content = contentElement.innerHTML; 118 } else { 119 console.warn(`Modal content with ID "${modalId}" not found`); 120 return; 121 } 122 } else if (contentSelector) { 123 // Content from selector 124 const contentElement = document.querySelector(contentSelector); 125 if (contentElement) { 126 content = contentElement.innerHTML; 127 } else { 128 console.warn(`Modal content with selector "${contentSelector}" not found`); 129 return; 130 } 131 } else { 132 console.warn('No modal content specified'); 133 return; 134 } 135
136 // Store previous focus 137 this.previousFocus = document.activeElement; 138 139 // Set content 140 const modalBody = this.modalContainer.querySelector('.aca-modal-body'); 141 modalBody.innerHTML = content; 142 143 // Show modal 144 this.modalContainer.classList.add('aca-modal-is-open'); 145 this.modalContainer.setAttribute('aria-hidden', 'false'); 146 document.body.classList.add('aca-modal-body-locked'); 147 148 // Set focus to modal 149 const focusableElement = this.modalContainer.querySelector('.aca-modal-dialog'); 150 focusableElement.focus(); 151 152 this.activeModal = this.modalContainer; 153 154 // Trigger custom event 155 const event = new CustomEvent('aca:modal:opened', { 156 detail: { trigger, modalId, contentSelector } 157 }); 158 document.dispatchEvent(event); 159 } 160 161 /** 162 * Closes the currently active modal 163 */ 164 closeModal() { 165 if (!this.activeModal) return; 166 167 this.modalContainer.classList.remove('aca-modal-is-open'); 168 this.modalContainer.setAttribute('aria-hidden', 'true'); 169 document.body.classList.remove('aca-modal-body-locked'); 170 171 // Restore focus 172 if (this.previousFocus) { 173 this.previousFocus.focus(); 174 this.previousFocus = null; 175 } 176 177 // Clear content after animation 178 setTimeout(() => { 179 if (!this.modalContainer.classList.contains('aca-modal-is-open')) { 180 const modalBody = this.modalContainer.querySelector('.aca-modal-body'); 181 modalBody.innerHTML = ''; 182 } 183 }, 300); // Match CSS animation duration 184 185 this.activeModal = null; 186 187 // Trigger custom event 188 const event = new CustomEvent('aca:modal:closed'); 189 document.dispatchEvent(event); 190 } 191 192 /** 193 * Traps focus within the modal 194 * @param {KeyboardEvent} e - The keyboard event 195 */ 196 trapFocus(e) { 197 const focusableElements = this.modalContainer.querySelectorAll( 198 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 199 ); 200 201 const firstElement = focusableElements[0]; 202 const lastElement = focusableElements[focusableElements.length - 1]; 203 204 if (e.shiftKey) { 205 if (document.activeElement === firstElement) { 206 lastElement.focus(); 207 e.preventDefault(); 208 } 209 } else { 210 if (document.activeElement === lastElement) { 211 firstElement.focus(); 212 e.preventDefault(); 213 } 214 } 215 } 216 217 /** 218 * Public method to programmatically open a modal 219 * @param {string} modalId - The ID of the modal content to open 220 */ 221 open(modalId) { 222 const trigger = document.createElement('div'); 223 trigger.setAttribute('data-modal', modalId); 224 this.openModal(trigger); 225 } 226 227 /** 228 * Public method to programmatically close the modal 229 */ 230 close() { 231 this.closeModal(); 232 } 233} 234 235// Initialize modal system when DOM is ready 236document.addEventListener('DOMContentLoaded', () => { 237 window.acaModal = new ACAModal(); 238}); 239 240// Also expose the class for manual instantiation 241window.ACAModal = ACAModal;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.