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