4NK_IA_front/src/hooks/useAccessibility.ts
4NK IA aad52027c1 ci: docker_tag=dev-test
- Alignement backend: seules 4 entités retournées (persons, companies, addresses, contractual)
- Version API mise à jour à 1.0.1 dans /api/health
- Interface onglets d entités: Personnes, Adresses, Entreprises, Contractuel
- Correction erreurs TypeScript pour build stricte
- Tests et documentation mis à jour
- CHANGELOG.md mis à jour avec version 1.1.1
2025-09-18 20:07:08 +00:00

215 lines
6.7 KiB
TypeScript

/**
* Hook pour améliorer l'accessibilité de l'application
*/
import { useCallback, useEffect, useRef, useState } from 'react'
interface AccessibilityState {
isKeyboardNavigation: boolean
isHighContrast: boolean
isReducedMotion: boolean
fontSize: number
focusVisible: boolean
}
export function useAccessibility() {
const [state, setState] = useState<AccessibilityState>({
isKeyboardNavigation: false,
isHighContrast: false,
isReducedMotion: false,
fontSize: 16,
focusVisible: false
})
const keyboardRef = useRef<boolean>(false)
const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// Détecter la navigation au clavier
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (event.key === 'Tab') {
keyboardRef.current = true
setState(prev => ({ ...prev, isKeyboardNavigation: true }))
}
}, [])
const handleMouseDown = useCallback(() => {
keyboardRef.current = false
setState(prev => ({ ...prev, isKeyboardNavigation: false }))
}, [])
// Gérer le focus visible
const handleFocusIn = useCallback((event: FocusEvent) => {
if (keyboardRef.current) {
setState(prev => ({ ...prev, focusVisible: true }))
// Ajouter une classe CSS pour le focus visible
const target = event.target as HTMLElement
target.classList.add('focus-visible')
// Nettoyer après un délai
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current)
}
focusTimeoutRef.current = setTimeout(() => {
target.classList.remove('focus-visible')
setState(prev => ({ ...prev, focusVisible: false }))
}, 100)
}
}, [])
// Détecter les préférences système
const detectSystemPreferences = useCallback(() => {
// Détecter le contraste élevé
if (window.matchMedia('(prefers-contrast: high)').matches) {
setState(prev => ({ ...prev, isHighContrast: true }))
}
// Détecter la réduction de mouvement
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setState(prev => ({ ...prev, isReducedMotion: true }))
}
// Détecter la taille de police
const computedStyle = window.getComputedStyle(document.documentElement)
const fontSize = parseFloat(computedStyle.fontSize)
setState(prev => ({ ...prev, fontSize }))
}, [])
// Appliquer les styles d'accessibilité
const applyAccessibilityStyles = useCallback(() => {
const root = document.documentElement
if (state.isHighContrast) {
root.classList.add('high-contrast')
} else {
root.classList.remove('high-contrast')
}
if (state.isReducedMotion) {
root.classList.add('reduced-motion')
} else {
root.classList.remove('reduced-motion')
}
if (state.isKeyboardNavigation) {
root.classList.add('keyboard-navigation')
} else {
root.classList.remove('keyboard-navigation')
}
}, [state])
// Annoncer les changements pour les lecteurs d'écran
const announceToScreenReader = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
const announcement = document.createElement('div')
announcement.setAttribute('aria-live', priority)
announcement.setAttribute('aria-atomic', 'true')
announcement.className = 'sr-only'
announcement.textContent = message
document.body.appendChild(announcement)
// Nettoyer après l'annonce
setTimeout(() => {
document.body.removeChild(announcement)
}, 1000)
}, [])
// Gérer la navigation au clavier
const handleKeyboardNavigation = useCallback((event: KeyboardEvent) => {
const { key, target } = event
const element = target as HTMLElement
// Navigation par tabulation
if (key === 'Tab') {
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const focusableArray = Array.from(focusableElements) as HTMLElement[]
const currentIndex = focusableArray.indexOf(element)
if (event.shiftKey) {
// Navigation vers l'arrière
if (currentIndex > 0) {
focusableArray[currentIndex - 1].focus()
} else {
focusableArray[focusableArray.length - 1].focus()
}
} else {
// Navigation vers l'avant
if (currentIndex < focusableArray.length - 1) {
focusableArray[currentIndex + 1].focus()
} else {
focusableArray[0].focus()
}
}
}
// Activation avec Entrée ou Espace
if ((key === 'Enter' || key === ' ') && element.tagName === 'BUTTON') {
event.preventDefault()
element.click()
}
// Échapper pour fermer les modales
if (key === 'Escape') {
const modal = document.querySelector('[role="dialog"]') as HTMLElement
if (modal) {
const closeButton = modal.querySelector('[aria-label*="fermer"], [aria-label*="close"]') as HTMLElement
if (closeButton) {
closeButton.focus()
}
}
}
}, [])
// Initialisation
useEffect(() => {
detectSystemPreferences()
// Écouter les changements de préférences
const contrastQuery = window.matchMedia('(prefers-contrast: high)')
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
const handleContrastChange = () => {
setState(prev => ({ ...prev, isHighContrast: contrastQuery.matches }))
}
const handleMotionChange = () => {
setState(prev => ({ ...prev, isReducedMotion: motionQuery.matches }))
}
contrastQuery.addEventListener('change', handleContrastChange)
motionQuery.addEventListener('change', handleMotionChange)
// Événements de navigation
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('mousedown', handleMouseDown)
document.addEventListener('focusin', handleFocusIn)
document.addEventListener('keydown', handleKeyboardNavigation)
return () => {
contrastQuery.removeEventListener('change', handleContrastChange)
motionQuery.removeEventListener('change', handleMotionChange)
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('mousedown', handleMouseDown)
document.removeEventListener('focusin', handleFocusIn)
document.removeEventListener('keydown', handleKeyboardNavigation)
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current)
}
}
}, [detectSystemPreferences, handleKeyDown, handleMouseDown, handleFocusIn, handleKeyboardNavigation])
// Appliquer les styles
useEffect(() => {
applyAccessibilityStyles()
}, [applyAccessibilityStyles])
return {
state,
announceToScreenReader,
applyAccessibilityStyles
}
}