aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
289 lines (286 loc) • 8.86 kB
JavaScript
'use client';
import { useState, useRef, useCallback, useEffect } from 'react';
const DEFAULT_SETTINGS = {
reducedMotion: false,
highContrast: false,
largeText: false,
colorScheme: 'auto',
forcedColors: false,
screenReader: false,
keyboardNavigation: false
};
/**
* Enhanced accessibility settings hook with comprehensive device detection
*/
function useAccessibilitySettings(options = {}) {
const {
enableStorage = true,
storageKey = 'aura-glass-a11y-settings',
detectScreenReader = true
} = options;
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [isLoading, setIsLoading] = useState(true);
const mediaQueriesRef = useRef([]);
const keyboardDetectionRef = useRef(false);
// Detect screen reader usage
const detectScreenReaderUsage = useCallback(() => {
if (!detectScreenReader || typeof window === 'undefined') return false;
// Check for common screen reader indicators
const indicators = [
// NVDA
() => 'speechSynthesis' in window && window.speechSynthesis.getVoices().length > 0,
// JAWS, NVDA, others
() => navigator.userAgent.includes('NVDA') || navigator.userAgent.includes('JAWS'),
// VoiceOver (macOS/iOS)
() => 'webkitSpeechRecognition' in window,
// General screen reader detection
() => window.navigator.userAgent.includes('Screenreader')];
return indicators.some(indicator => {
try {
return indicator();
} catch {
return false;
}
});
}, [detectScreenReader]);
// Detect keyboard navigation preference
const detectKeyboardNavigation = useCallback(() => {
let keyboardUsed = false;
const handleKeyDown = e => {
if (e.key === 'Tab' || e.key === 'Enter' || e.key === ' ' || e.key.startsWith('Arrow')) {
keyboardUsed = true;
keyboardDetectionRef.current = true;
setSettings(prev => ({
...prev,
keyboardNavigation: true
}));
}
};
const handleMouseDown = () => {
if (keyboardUsed) {
keyboardDetectionRef.current = false;
setSettings(prev => ({
...prev,
keyboardNavigation: false
}));
}
};
document.addEventListener('keydown', handleKeyDown, {
passive: true
});
document.addEventListener('mousedown', handleMouseDown, {
passive: true
});
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleMouseDown);
};
}, []);
// Load settings from storage
const loadStoredSettings = useCallback(() => {
if (!enableStorage || typeof window === 'undefined') return {};
try {
const stored = localStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : {};
} catch (error) {
console.warn('Failed to load accessibility settings from storage:', error);
return {};
}
}, [enableStorage, storageKey]);
// Save settings to storage
const saveSettings = useCallback(newSettings => {
if (!enableStorage || typeof window === 'undefined') return;
try {
localStorage.setItem(storageKey, JSON.stringify(newSettings));
} catch (error) {
console.warn('Failed to save accessibility settings to storage:', error);
}
}, [enableStorage, storageKey]);
// Update individual setting
const updateSetting = useCallback((key, value) => {
setSettings(prev => {
const newSettings = {
...prev,
[key]: value
};
saveSettings(newSettings);
return newSettings;
});
}, [saveSettings]);
// Reset to defaults
const resetSettings = useCallback(() => {
const detectedSettings = detectSystemSettings();
const newSettings = {
...DEFAULT_SETTINGS,
...detectedSettings
};
setSettings(newSettings);
saveSettings(newSettings);
}, [saveSettings]);
// Detect system accessibility preferences
const detectSystemSettings = useCallback(() => {
if (typeof window === 'undefined') return {};
const queries = [{
key: 'reducedMotion',
query: '(prefers-reduced-motion: reduce)'
}, {
key: 'highContrast',
query: '(prefers-contrast: high)'
}, {
key: 'forcedColors',
query: '(forced-colors: active)'
}];
const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
const detected = {
screenReader: detectScreenReaderUsage(),
colorScheme: colorSchemeQuery.matches ? 'dark' : 'light'
};
queries.forEach(({
key,
query
}) => {
try {
const mediaQuery = window.matchMedia(query);
detected[key] = mediaQuery.matches;
} catch (error) {
console.warn(`Failed to detect ${key} preference:`, error);
}
});
return detected;
}, [detectScreenReaderUsage]);
// Initialize settings
useEffect(() => {
if (typeof window === 'undefined') {
setIsLoading(false);
return;
}
const initializeSettings = async () => {
try {
// Detect system preferences
const systemSettings = detectSystemSettings();
// Load stored preferences
const storedSettings = loadStoredSettings();
// Merge with priority: stored > system > defaults
const initialSettings = {
...DEFAULT_SETTINGS,
...systemSettings,
...storedSettings
};
setSettings(initialSettings);
// Set up media query listeners
const queries = [{
key: 'reducedMotion',
query: '(prefers-reduced-motion: reduce)'
}, {
key: 'highContrast',
query: '(prefers-contrast: high)'
}, {
key: 'forcedColors',
query: '(forced-colors: active)'
}];
const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
const updateFromMediaQuery = () => {
const updates = {
colorScheme: colorSchemeQuery.matches ? 'dark' : 'light'
};
queries.forEach(({
key,
query
}) => {
try {
const mediaQuery = window.matchMedia(query);
updates[key] = mediaQuery.matches;
} catch (error) {
console.warn(`Failed to update ${key} from media query:`, error);
}
});
setSettings(prev => {
const newSettings = {
...prev,
...updates
};
saveSettings(newSettings);
return newSettings;
});
};
// Add listeners
const mediaQueries = [...queries.map(({
query
}) => window.matchMedia(query)), colorSchemeQuery];
mediaQueries.forEach(mq => {
mq.addEventListener('change', updateFromMediaQuery);
});
mediaQueriesRef.current = mediaQueries;
// Set up keyboard detection
const cleanupKeyboardDetection = detectKeyboardNavigation();
// Cleanup function
return () => {
mediaQueries.forEach(mq => {
mq.removeEventListener('change', updateFromMediaQuery);
});
cleanupKeyboardDetection();
};
} catch (error) {
console.error('Failed to initialize accessibility settings:', error);
} finally {
setIsLoading(false);
}
};
const cleanup = initializeSettings();
return () => {
cleanup?.then(fn => fn?.());
};
}, [detectSystemSettings, loadStoredSettings, saveSettings, detectKeyboardNavigation]);
// Cleanup on unmount
useEffect(() => {
return () => {
mediaQueriesRef.current = [];
};
}, []);
return {
settings,
updateSetting,
resetSettings,
isLoading
};
}
/**
* Hook for specific accessibility feature detection
*/
function useAccessibilityFeature(feature) {
const {
settings
} = useAccessibilitySettings();
return settings[feature];
}
/**
* Hook for accessibility-aware animations
*/
function useAccessibleAnimation(defaultEnabled = true) {
const {
settings
} = useAccessibilitySettings();
const shouldAnimate = defaultEnabled && !settings.reducedMotion;
const animationDuration = settings.reducedMotion ? 0 : 300;
const transitionDuration = settings.reducedMotion ? 0 : 200;
return {
shouldAnimate,
animationDuration,
transitionDuration
};
}
/**
* Hook for accessibility-aware colors
*/
function useAccessibleColors() {
const {
settings
} = useAccessibilitySettings();
return {
shouldUseHighContrast: settings.highContrast,
colorScheme: settings.colorScheme,
forcedColors: settings.forcedColors
};
}
export { useAccessibilityFeature, useAccessibilitySettings, useAccessibleAnimation, useAccessibleColors };
//# sourceMappingURL=useAccessibilitySettings.js.map