| |
| |
| |
| |
| |
| |
|
|
| class AccessibilityManager { |
| constructor() { |
| this.init(); |
| } |
|
|
| init() { |
| this.detectInputMethod(); |
| this.setupKeyboardNavigation(); |
| this.setupAnnouncements(); |
| this.setupFocusManagement(); |
| console.log('[A11y] Accessibility manager initialized'); |
| } |
|
|
| |
| |
| |
| detectInputMethod() { |
| |
| document.addEventListener('mousedown', () => { |
| document.body.classList.add('using-mouse'); |
| }); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'Tab') { |
| document.body.classList.remove('using-mouse'); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| setupKeyboardNavigation() { |
| document.addEventListener('keydown', (e) => { |
| |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { |
| e.preventDefault(); |
| const searchInput = document.querySelector('[role="searchbox"], input[type="search"]'); |
| if (searchInput) searchInput.focus(); |
| } |
|
|
| |
| if (e.key === 'Escape') { |
| this.closeAllModals(); |
| this.closeAllDropdowns(); |
| } |
|
|
| |
| if (e.target.getAttribute('role') === 'tab') { |
| this.handleTabNavigation(e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| handleTabNavigation(e) { |
| const tabs = Array.from(document.querySelectorAll('[role="tab"]')); |
| const currentIndex = tabs.indexOf(e.target); |
|
|
| let nextIndex; |
| if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { |
| nextIndex = (currentIndex + 1) % tabs.length; |
| } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { |
| nextIndex = (currentIndex - 1 + tabs.length) % tabs.length; |
| } |
|
|
| if (nextIndex !== undefined) { |
| e.preventDefault(); |
| tabs[nextIndex].focus(); |
| tabs[nextIndex].click(); |
| } |
| } |
|
|
| |
| |
| |
| setupAnnouncements() { |
| |
| if (!document.getElementById('aria-live-polite')) { |
| const polite = document.createElement('div'); |
| polite.id = 'aria-live-polite'; |
| polite.setAttribute('aria-live', 'polite'); |
| polite.setAttribute('aria-atomic', 'true'); |
| polite.className = 'sr-only'; |
| document.body.appendChild(polite); |
| } |
|
|
| if (!document.getElementById('aria-live-assertive')) { |
| const assertive = document.createElement('div'); |
| assertive.id = 'aria-live-assertive'; |
| assertive.setAttribute('aria-live', 'assertive'); |
| assertive.setAttribute('aria-atomic', 'true'); |
| assertive.className = 'sr-only'; |
| document.body.appendChild(assertive); |
| } |
| } |
|
|
| |
| |
| |
| announce(message, priority = 'polite') { |
| const region = document.getElementById(`aria-live-${priority}`); |
| if (!region) return; |
|
|
| |
| region.textContent = ''; |
| setTimeout(() => { |
| region.textContent = message; |
| }, 100); |
| } |
|
|
| |
| |
| |
| setupFocusManagement() { |
| |
| document.addEventListener('focusin', (e) => { |
| const modal = document.querySelector('.modal-backdrop'); |
| if (!modal) return; |
|
|
| const focusableElements = modal.querySelectorAll( |
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' |
| ); |
|
|
| if (focusableElements.length === 0) return; |
|
|
| const firstElement = focusableElements[0]; |
| const lastElement = focusableElements[focusableElements.length - 1]; |
|
|
| if (!modal.contains(e.target)) { |
| firstElement.focus(); |
| } |
| }); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key !== 'Tab') return; |
|
|
| const modal = document.querySelector('.modal-backdrop'); |
| if (!modal) return; |
|
|
| const focusableElements = modal.querySelectorAll( |
| 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' |
| ); |
|
|
| if (focusableElements.length === 0) return; |
|
|
| const firstElement = focusableElements[0]; |
| const lastElement = focusableElements[focusableElements.length - 1]; |
|
|
| if (e.shiftKey) { |
| if (document.activeElement === firstElement) { |
| e.preventDefault(); |
| lastElement.focus(); |
| } |
| } else { |
| if (document.activeElement === lastElement) { |
| e.preventDefault(); |
| firstElement.focus(); |
| } |
| } |
| }); |
| } |
|
|
| |
| |
| |
| closeAllModals() { |
| document.querySelectorAll('.modal-backdrop').forEach(modal => { |
| modal.remove(); |
| }); |
| } |
|
|
| |
| |
| |
| closeAllDropdowns() { |
| document.querySelectorAll('[aria-expanded="true"]').forEach(element => { |
| element.setAttribute('aria-expanded', 'false'); |
| }); |
| } |
|
|
| |
| |
| |
| setPageTitle(title) { |
| document.title = title; |
| this.announce(`Page: ${title}`); |
| } |
|
|
| |
| |
| |
| addSkipLink() { |
| const skipLink = document.createElement('a'); |
| skipLink.href = '#main-content'; |
| skipLink.className = 'skip-link'; |
| skipLink.textContent = 'Skip to main content'; |
| document.body.insertBefore(skipLink, document.body.firstChild); |
|
|
| |
| const mainContent = document.querySelector('.main-content, main'); |
| if (mainContent && !mainContent.id) { |
| mainContent.id = 'main-content'; |
| } |
| } |
|
|
| |
| |
| |
| markAsLoading(element, label = 'Loading') { |
| element.setAttribute('aria-busy', 'true'); |
| element.setAttribute('aria-label', label); |
| } |
|
|
| |
| |
| |
| unmarkAsLoading(element) { |
| element.setAttribute('aria-busy', 'false'); |
| element.removeAttribute('aria-label'); |
| } |
| } |
|
|
| |
| window.a11y = new AccessibilityManager(); |
|
|
| |
| window.announce = (message, priority) => window.a11y.announce(message, priority); |
|
|