Spaces:
Running
Running
File size: 3,597 Bytes
227c43a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
import React, { createContext, useContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
const AccessibilityContext = createContext();
export const useAccessibility = () => {
const context = useContext(AccessibilityContext);
if (!context) {
throw new Error('useAccessibility must be used within AccessibilityProvider');
}
return context;
};
const AccessibilityProvider = ({ children }) => {
const [reducedMotion, setReducedMotion] = useState(false);
const [highContrast, setHighContrast] = useState(false);
const [fontSize, setFontSize] = useState('medium');
const [announcements, setAnnouncements] = useState([]);
// Detect user preferences
useEffect(() => {
// Check for reduced motion preference
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setReducedMotion(mediaQuery.matches);
const handleChange = (e) => setReducedMotion(e.matches);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
useEffect(() => {
// Check for high contrast preference
const mediaQuery = window.matchMedia('(prefers-contrast: high)');
setHighContrast(mediaQuery.matches);
const handleChange = (e) => setHighContrast(e.matches);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
// Announce to screen readers
const announce = (message, priority = 'polite') => {
const id = Date.now();
const announcement = { id, message, priority };
setAnnouncements(prev => [...prev, announcement]);
// Remove announcement after it's been read
setTimeout(() => {
setAnnouncements(prev => prev.filter(a => a.id !== id));
}, 1000);
};
// Skip to main content
const skipToMain = () => {
const main = document.querySelector('main');
if (main) {
main.focus();
main.scrollIntoView();
}
};
// Focus management
const focusElement = (selector) => {
const element = document.querySelector(selector);
if (element) {
element.focus();
return true;
}
return false;
};
const value = {
reducedMotion,
highContrast,
fontSize,
setFontSize,
announce,
skipToMain,
focusElement,
announcements,
};
return (
<AccessibilityContext.Provider value={value}>
{children}
{/* Screen reader announcements */}
<div className="sr-only" aria-live="polite" aria-atomic="true">
{announcements
.filter(a => a.priority === 'polite')
.map(a => (
<div key={a.id}>{a.message}</div>
))}
</div>
<div className="sr-only" aria-live="assertive" aria-atomic="true">
{announcements
.filter(a => a.priority === 'assertive')
.map(a => (
<div key={a.id}>{a.message}</div>
))}
</div>
{/* Skip to main content link */}
<button
onClick={skipToMain}
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-primary-500 text-white px-4 py-2 rounded-lg z-50 focus:z-[9999]"
>
Skip to main content
</button>
</AccessibilityContext.Provider>
);
};
AccessibilityProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AccessibilityProvider;
|