- 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
215 lines
6.7 KiB
TypeScript
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
|
|
}
|
|
}
|