Really-amin's picture
Upload 317 files
eebf5c4 verified
/**
* ============================================
* ACCESSIBILITY ENHANCEMENTS
* Keyboard navigation, focus management, announcements
* ============================================
*/
class AccessibilityManager {
constructor() {
this.init();
}
init() {
this.detectInputMethod();
this.setupKeyboardNavigation();
this.setupAnnouncements();
this.setupFocusManagement();
console.log('[A11y] Accessibility manager initialized');
}
/**
* Detect if user is using keyboard or mouse
*/
detectInputMethod() {
// Track mouse usage
document.addEventListener('mousedown', () => {
document.body.classList.add('using-mouse');
});
// Track keyboard usage
document.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
document.body.classList.remove('using-mouse');
}
});
}
/**
* Setup keyboard navigation shortcuts
*/
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K: Focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('[role="searchbox"], input[type="search"]');
if (searchInput) searchInput.focus();
}
// Escape: Close modals/dropdowns
if (e.key === 'Escape') {
this.closeAllModals();
this.closeAllDropdowns();
}
// Arrow keys for tab navigation
if (e.target.getAttribute('role') === 'tab') {
this.handleTabNavigation(e);
}
});
}
/**
* Handle tab navigation with arrow keys
*/
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();
}
}
/**
* Setup screen reader announcements
*/
setupAnnouncements() {
// Create announcement regions if they don't exist
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 to screen readers
*/
announce(message, priority = 'polite') {
const region = document.getElementById(`aria-live-${priority}`);
if (!region) return;
// Clear and set new message
region.textContent = '';
setTimeout(() => {
region.textContent = message;
}, 100);
}
/**
* Setup focus management
*/
setupFocusManagement() {
// Trap focus in modals
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();
}
});
// Handle Tab key in modals
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();
}
}
});
}
/**
* Close all modals
*/
closeAllModals() {
document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.remove();
});
}
/**
* Close all dropdowns
*/
closeAllDropdowns() {
document.querySelectorAll('[aria-expanded="true"]').forEach(element => {
element.setAttribute('aria-expanded', 'false');
});
}
/**
* Set page title (announces to screen readers)
*/
setPageTitle(title) {
document.title = title;
this.announce(`Page: ${title}`);
}
/**
* Add skip link
*/
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);
// Add id to main content if it doesn't exist
const mainContent = document.querySelector('.main-content, main');
if (mainContent && !mainContent.id) {
mainContent.id = 'main-content';
}
}
/**
* Mark element as loading
*/
markAsLoading(element, label = 'Loading') {
element.setAttribute('aria-busy', 'true');
element.setAttribute('aria-label', label);
}
/**
* Unmark element as loading
*/
unmarkAsLoading(element) {
element.setAttribute('aria-busy', 'false');
element.removeAttribute('aria-label');
}
}
// Export singleton
window.a11y = new AccessibilityManager();
// Utility functions
window.announce = (message, priority) => window.a11y.announce(message, priority);