4f34dc7dfb
- Fix useKeyboardNavigation handler type signature - Fix Footer.vue ref type annotations and filters removal - Add getAllScreens() export to registry/screens.ts - Vite build now succeeds: 737KB (204KB gzip) All tests verified. CI pipeline ready. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
113 lines
2.8 KiB
TypeScript
113 lines
2.8 KiB
TypeScript
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<string, (() => 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 }
|
|
}
|