/** * Accessibility utilities */ /** * Get accessible label for an element */ export function getAccessibleLabel( label?: string, placeholder?: string, fallback = 'Interactive element' ): string { return label || placeholder || fallback } /** * Generate unique ID for form elements */ export function generateId(prefix = 'id'): string { return `${prefix}-${Math.random().toString(36).substring(2, 9)}` } /** * Check if element is focusable */ export function isFocusable(element: HTMLElement): boolean { const focusableSelectors = [ 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])', ].join(', ') return element.matches(focusableSelectors) } /** * Trap focus within a container */ export function trapFocus(container: HTMLElement): () => void { const focusableElements = container.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ) const firstElement = focusableElements[0] const lastElement = focusableElements[focusableElements.length - 1] const handleTab = (e: KeyboardEvent) => { if (e.key !== 'Tab') { return } if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault() lastElement?.focus() } } else { if (document.activeElement === lastElement) { e.preventDefault() firstElement?.focus() } } } container.addEventListener('keydown', handleTab) // Return cleanup function return () => { container.removeEventListener('keydown', handleTab) } } /** * Announce message to screen readers */ export function announceToScreenReader(message: string, priority: 'polite' | 'assertive' = 'polite') { const announcement = document.createElement('div') announcement.setAttribute('role', 'status') announcement.setAttribute('aria-live', priority) announcement.setAttribute('aria-atomic', 'true') announcement.className = 'sr-only' announcement.textContent = message document.body.appendChild(announcement) setTimeout(() => { document.body.removeChild(announcement) }, 1000) }