/** * Utility class for accessibility-related checks and helpers. * Provides methods to detect user preferences for reduced motion, * screen reader usage, and other accessibility settings. */ export default class AccessibilityUtils { private static _shouldSkipAnimation: boolean | null = null; private static _hasScreenReader: boolean | null = null; private static _hasForcedColors: boolean | null = null; private static _prefersReducedMotion: boolean | null = null; /** * Checks if animations should be skipped for accessibility reasons. * Results are cached after first call for performance. * * Returns true if: * - User prefers reduced motion (OS setting) * - Screen reader is likely active (detected via various heuristics) * - High contrast / forced colors mode is active */ static shouldSkipAnimation(): boolean { // Return cached result if available if (this._shouldSkipAnimation !== null) { return this._shouldSkipAnimation; } // SSR safety check if (typeof window === 'undefined') { return false; } let shouldSkip = false; // Check for reduced motion preference if (this.prefersReducedMotion()) { shouldSkip = true; } // Check for screen reader if (!shouldSkip && this.hasScreenReader()) { shouldSkip = true; } // 3. Check for forced-colors mode (high contrast) - often used with screen readers if (!shouldSkip && this.hasForcedColors()) { shouldSkip = true; } // Cache the result this._shouldSkipAnimation = shouldSkip; return shouldSkip; } /** * Clears the cached accessibility check result. * Useful if you need to re-evaluate after user changes settings. */ static clearCache(): void { this._shouldSkipAnimation = null; this._hasScreenReader = null; this._hasForcedColors = null; this._prefersReducedMotion = null; } /** * Checks if the user prefers reduced motion. * This is a simpler check that only looks at the OS preference. */ static prefersReducedMotion(): boolean { if (this._prefersReducedMotion != null) { return this._prefersReducedMotion; } if (typeof window === 'undefined') { return false; } this._prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false; return this._prefersReducedMotion; } /** * Checks if high contrast / forced colors mode is active. */ static hasForcedColors(): boolean { if (this._hasForcedColors != null) { return this._hasForcedColors; } if (typeof window === 'undefined') { return false; } this._hasForcedColors = window.matchMedia?.('(forced-colors: active)').matches ?? false; return this._hasForcedColors; } /** * Checks if screen reader is active * Note: Direct screen reader detection is intentionally limited for privacy, * but these heuristics catch common cases */ static hasScreenReader(): boolean { if (this._hasScreenReader != null) { return this._hasScreenReader; } if (typeof window === 'undefined') { return false; } // 1. Check if accessibility object model is available and indicates assistive tech if ((navigator as any).accessibilityFeatures?.screenReader?.enabled) { this._hasScreenReader = true; return true; } const ua = navigator.userAgent.toLowerCase(); if (ua.includes('nvda') || ua.includes('jaws') || ua.includes('voiceover')) { this._hasScreenReader = true; return true; } this._hasScreenReader = false; return false; } }