import { onMounted, onUnmounted } from 'vue' interface KeyboardNavigationOptions { onArrowUp?: () => void onArrowDown?: () => void onArrowLeft?: () => void onArrowRight?: () => void onEnter?: () => void onEscape?: () => void onTab?: () => void } /** * Composable for keyboard navigation support * Handles common keyboard patterns for accessible UIs */ export function useKeyboardNavigation(options: KeyboardNavigationOptions) { const handleKeydown = (event: KeyboardEvent) => { const handlers: Record void) | undefined> = { 'ArrowUp': options.onArrowUp, 'ArrowDown': options.onArrowDown, 'ArrowLeft': options.onArrowLeft, 'ArrowRight': options.onArrowRight, 'Enter': options.onEnter, 'Escape': options.onEscape, 'Tab': options.onTab, } const handler = handlers[event.key] if (handler) { event.preventDefault() handler() } } onMounted(() => { document.addEventListener('keydown', handleKeydown) }) onUnmounted(() => { document.removeEventListener('keydown', handleKeydown) }) return { handleKeydown } } /** * Focus trap for modals and overlays */ export function useFocusTrap(elementRef: any) { const handleKeydown = (event: KeyboardEvent) => { if (event.key !== 'Tab') return const element = elementRef.value if (!element) return const focusableElements = element.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 (event.shiftKey) { // Shift+Tab if (document.activeElement === firstElement) { event.preventDefault() lastElement.focus() } } else { // Tab if (document.activeElement === lastElement) { event.preventDefault() firstElement.focus() } } } onMounted(() => { document.addEventListener('keydown', handleKeydown) }) onUnmounted(() => { document.removeEventListener('keydown', handleKeydown) }) return { handleKeydown } } /** * Announce content changes to screen readers */ export function useAnnounce() { const announce = (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) } return { announce } }