feat(a11y): accessibility audit, high contrast, i18n foundation, skip nav (M17.1-M17.8)
- Keyboard navigation: useFocusTrap() for modals, useRovingTabindex() for grids - ARIA audit: composables for focus trap and roving tabindex patterns - High contrast mode: @media (prefers-contrast: more) + .high-contrast class - i18n foundation: en/es/fr locale files, useI18n() composable with auto-detect - RTL layout support: dir attribute toggling based on locale - Dyslexia-friendly font: .font-dyslexia CSS class with OpenDyslexic support - Skip navigation link: .skip-nav CSS with focus-visible positioning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
85936d3005
commit
725f629f69
@@ -0,0 +1,53 @@
|
||||
import { onMounted, onUnmounted, type Ref } from 'vue'
|
||||
|
||||
export function useFocusTrap(containerRef: Ref<HTMLElement | null>) {
|
||||
function getFocusableElements(): HTMLElement[] {
|
||||
const container = containerRef.value
|
||||
if (!container) return []
|
||||
return Array.from(
|
||||
container.querySelectorAll<HTMLElement>(
|
||||
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
// Bubble up for close handlers
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key !== 'Tab') return
|
||||
|
||||
const focusable = getFocusableElements()
|
||||
if (focusable.length === 0) return
|
||||
|
||||
const first = focusable[0]
|
||||
const last = focusable[focusable.length - 1]
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault()
|
||||
last.focus()
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault()
|
||||
first.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
// Auto-focus first element
|
||||
const focusable = getFocusableElements()
|
||||
if (focusable.length > 0) {
|
||||
focusable[0].focus()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ref, onMounted, onUnmounted, type Ref } from 'vue'
|
||||
|
||||
export function useRovingTabindex(containerRef: Ref<HTMLElement | null>, selector = '[role="listitem"], [data-roving]') {
|
||||
const currentIndex = ref(0)
|
||||
|
||||
function getItems(): HTMLElement[] {
|
||||
const container = containerRef.value
|
||||
if (!container) return []
|
||||
return Array.from(container.querySelectorAll<HTMLElement>(selector))
|
||||
}
|
||||
|
||||
function updateTabindex() {
|
||||
const items = getItems()
|
||||
items.forEach((item, i) => {
|
||||
item.setAttribute('tabindex', i === currentIndex.value ? '0' : '-1')
|
||||
})
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const items = getItems()
|
||||
if (items.length === 0) return
|
||||
|
||||
let handled = false
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
currentIndex.value = (currentIndex.value + 1) % items.length
|
||||
handled = true
|
||||
break
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
currentIndex.value = (currentIndex.value - 1 + items.length) % items.length
|
||||
handled = true
|
||||
break
|
||||
case 'Home':
|
||||
currentIndex.value = 0
|
||||
handled = true
|
||||
break
|
||||
case 'End':
|
||||
currentIndex.value = items.length - 1
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault()
|
||||
updateTabindex()
|
||||
items[currentIndex.value]?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const container = containerRef.value
|
||||
if (container) {
|
||||
container.addEventListener('keydown', onKeyDown)
|
||||
updateTabindex()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const container = containerRef.value
|
||||
if (container) {
|
||||
container.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
})
|
||||
|
||||
return { currentIndex, updateTabindex }
|
||||
}
|
||||
Reference in New Issue
Block a user