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