"use client"; import React, { useEffect, useCallback, useState } from "react"; interface AccessibilityPreferences { prefersReducedMotion: boolean; prefersHighContrast: boolean; prefersLargeText: boolean; supportsTouchOnly: boolean; screenReaderActive: boolean; } interface TouchTarget { minSize: number; spacing: number; feedbackDuration: number; } interface MobileAccessibilityEnhancerProps { children: React.ReactNode; enableHapticFeedback?: boolean; enableVoiceAnnouncements?: boolean; touchTargetSize?: TouchTarget; className?: string; } // Hook for detecting accessibility preferences export function useAccessibilityPreferences(): AccessibilityPreferences { const [preferences, setPreferences] = useState({ prefersReducedMotion: false, prefersHighContrast: false, prefersLargeText: false, supportsTouchOnly: false, screenReaderActive: false, }); useEffect(() => { if (typeof window === "undefined") return; const checkPreferences = () => { // Reduced motion preference const reducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; // High contrast preference const highContrast = window.matchMedia( "(prefers-contrast: high)" ).matches; // Large text preference (approximate detection) const largeText = window.devicePixelRatio > 1.5 && window.innerWidth < 768; // Touch-only device detection const touchOnly = "ontouchstart" in window && !window.matchMedia("(hover: hover)").matches; // Screen reader detection (heuristic) const screenReader = !window.speechSynthesis || window.navigator.userAgent.includes("NVDA") || window.navigator.userAgent.includes("JAWS") || window.navigator.userAgent.includes("VoiceOver"); setPreferences({ prefersReducedMotion: reducedMotion, prefersHighContrast: highContrast, prefersLargeText: largeText, supportsTouchOnly: touchOnly, screenReaderActive: screenReader, }); }; checkPreferences(); // Listen for preference changes const mediaQueries = [ window.matchMedia("(prefers-reduced-motion: reduce)"), window.matchMedia("(prefers-contrast: high)"), window.matchMedia("(hover: hover)"), ]; const handleChange = () => checkPreferences(); mediaQueries.forEach((mq) => { if (mq.addEventListener) { mq.addEventListener("change", handleChange); } else { mq.addListener(handleChange); } }); return () => { mediaQueries.forEach((mq) => { if (mq.removeEventListener) { mq.removeEventListener("change", handleChange); } else { mq.removeListener(handleChange); } }); }; }, []); return preferences; } // Hook for haptic feedback export function useHapticFeedback(enabled: boolean = true) { const triggerHaptic = useCallback( (type: "light" | "medium" | "heavy" | "error" = "light") => { if (!enabled || typeof navigator === "undefined") return; const vibrationPatterns = { light: [10], medium: [20], heavy: [50], error: [100, 50, 100], }; if ("vibrate" in navigator) { navigator.vibrate(vibrationPatterns[type]); } }, [enabled] ); return triggerHaptic; } // Hook for voice announcements export function useVoiceAnnouncements(enabled: boolean = true) { const announce = useCallback( (message: string, priority: "polite" | "assertive" = "polite") => { if (!enabled || typeof window === "undefined") return; // Create or get existing live region let liveRegion = document.getElementById("mobile-chat-announcements"); if (!liveRegion) { liveRegion = document.createElement("div"); liveRegion.id = "mobile-chat-announcements"; liveRegion.setAttribute("aria-live", priority); liveRegion.setAttribute("aria-atomic", "true"); liveRegion.style.position = "absolute"; liveRegion.style.left = "-10000px"; liveRegion.style.width = "1px"; liveRegion.style.height = "1px"; liveRegion.style.overflow = "hidden"; document.body.appendChild(liveRegion); } // Clear previous content and set new message liveRegion.textContent = ""; setTimeout(() => { liveRegion!.textContent = message; }, 100); // Clear message after a delay setTimeout(() => { if (liveRegion) { liveRegion.textContent = ""; } }, 3000); }, [enabled] ); return announce; } // Touch-friendly button component interface TouchButtonProps { children: React.ReactNode; onClick: () => void; disabled?: boolean; variant?: "primary" | "secondary" | "ghost"; size?: "sm" | "md" | "lg"; className?: string; ariaLabel?: string; hapticFeedback?: boolean; } export function TouchButton({ children, onClick, disabled = false, variant = "primary", size = "md", className = "", ariaLabel, hapticFeedback = true, }: TouchButtonProps) { const preferences = useAccessibilityPreferences(); const triggerHaptic = useHapticFeedback(hapticFeedback); const announce = useVoiceAnnouncements(); const handleClick = useCallback(() => { if (disabled) return; triggerHaptic("light"); onClick(); if (ariaLabel) { announce(`${ariaLabel} activated`, "polite"); } }, [disabled, triggerHaptic, onClick, ariaLabel, announce]); const getSizeStyles = () => { const sizes = { sm: { minHeight: 44, minWidth: 44, padding: "8px 12px", fontSize: "14px", }, md: { minHeight: 48, minWidth: 48, padding: "12px 16px", fontSize: "16px", }, lg: { minHeight: 56, minWidth: 56, padding: "16px 20px", fontSize: "18px", }, }; const baseSize = sizes[size]; // Increase size for large text preference if (preferences.prefersLargeText) { return { ...baseSize, minHeight: baseSize.minHeight + 8, minWidth: baseSize.minWidth + 8, fontSize: `calc(${baseSize.fontSize} * 1.2)`, }; } return baseSize; }; const getVariantStyles = () => { const variants = { primary: { background: preferences.prefersHighContrast ? "#000" : "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", color: "#fff", border: preferences.prefersHighContrast ? "2px solid #fff" : "none", }, secondary: { background: preferences.prefersHighContrast ? "#fff" : "#f8f9fa", color: preferences.prefersHighContrast ? "#000" : "#333", border: preferences.prefersHighContrast ? "2px solid #000" : "1px solid #e1e5e9", }, ghost: { background: "transparent", color: preferences.prefersHighContrast ? "#000" : "#667eea", border: preferences.prefersHighContrast ? "2px solid #000" : "1px solid transparent", }, }; return variants[variant]; }; const sizeStyles = getSizeStyles(); const variantStyles = getVariantStyles(); return ( ); } // Touch-friendly input component interface TouchInputProps { value: string; onChange: (value: string) => void; onSubmit?: () => void; placeholder?: string; disabled?: boolean; className?: string; ariaLabel?: string; autoFocus?: boolean; } export function TouchInput({ value, onChange, onSubmit, placeholder, disabled = false, className = "", ariaLabel, autoFocus = false, }: TouchInputProps) { const preferences = useAccessibilityPreferences(); const handleKeyPress = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && onSubmit) { e.preventDefault(); onSubmit(); } }, [onSubmit] ); const getInputStyles = () => { const baseStyles = { minHeight: preferences.prefersLargeText ? 56 : 48, padding: preferences.prefersLargeText ? "16px 20px" : "12px 16px", fontSize: preferences.prefersLargeText ? "18px" : "16px", borderRadius: "24px", border: preferences.prefersHighContrast ? "2px solid #000" : "2px solid #e1e5e9", background: preferences.prefersHighContrast ? "#fff" : "#f8f9fa", color: preferences.prefersHighContrast ? "#000" : "#333", outline: "none", width: "100%", touchAction: "manipulation", }; return baseStyles; }; return ( onChange(e.target.value)} onKeyPress={handleKeyPress} placeholder={placeholder} disabled={disabled} className={className} aria-label={ariaLabel} autoFocus={autoFocus} style={getInputStyles()} /> ); } // Main accessibility enhancer component export function MobileAccessibilityEnhancer({ children, enableHapticFeedback = true, enableVoiceAnnouncements = true, touchTargetSize = { minSize: 48, spacing: 8, feedbackDuration: 200 }, className = "", }: MobileAccessibilityEnhancerProps) { const preferences = useAccessibilityPreferences(); useEffect(() => { // Apply global accessibility styles const style = document.createElement("style"); style.textContent = ` .mobile-accessibility-enhanced { ${ preferences.prefersReducedMotion ? "animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important;" : "" } ${preferences.prefersHighContrast ? "filter: contrast(150%);" : ""} } .mobile-accessibility-enhanced * { -webkit-tap-highlight-color: transparent; touch-action: manipulation; } .mobile-accessibility-enhanced button, .mobile-accessibility-enhanced input, .mobile-accessibility-enhanced [role="button"] { min-height: ${touchTargetSize.minSize}px; min-width: ${touchTargetSize.minSize}px; margin: ${touchTargetSize.spacing}px; } .mobile-accessibility-enhanced input, .mobile-accessibility-enhanced textarea { font-size: 16px !important; /* Prevent zoom on iOS */ } ${ preferences.prefersLargeText ? ` .mobile-accessibility-enhanced { font-size: 1.2em; } .mobile-accessibility-enhanced button, .mobile-accessibility-enhanced input { padding: 16px; } ` : "" } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, [preferences, touchTargetSize]); return (
{children}
); }