WellNuo/components/errors/FieldError.tsx
Sergei a238b7e35f Add comprehensive error handling system
- Add extended error types with severity levels, retry policies,
  and contextual information (types/errors.ts)
- Create centralized error handler service with user-friendly
  message translation and classification (services/errorHandler.ts)
- Add ErrorContext for global error state management with
  auto-dismiss and error queue support (contexts/ErrorContext.tsx)
- Create error UI components: ErrorToast, FieldError,
  FieldErrorSummary, FullScreenError, EmptyState, OfflineState
- Add useError hook with retry strategies and API response handling
- Add useAsync hook for async operations with comprehensive state
- Create error message utilities with validation helpers
- Add tests for errorHandler and errorMessages (88 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 17:43:07 -08:00

185 lines
4.1 KiB
TypeScript

/**
* FieldError - Inline field validation error display
*
* Features:
* - Animated appearance
* - Icon with error message
* - Compact inline design
* - Accessible labels
*/
import React, { useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
AccessibilityInfo,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
interface FieldErrorProps {
message: string;
visible?: boolean;
animated?: boolean;
accessibilityLabel?: string;
}
export function FieldError({
message,
visible = true,
animated = true,
accessibilityLabel,
}: FieldErrorProps) {
const opacityAnim = useRef(new Animated.Value(0)).current;
const translateYAnim = useRef(new Animated.Value(-10)).current;
useEffect(() => {
if (visible) {
if (animated) {
Animated.parallel([
Animated.timing(opacityAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
Animated.spring(translateYAnim, {
toValue: 0,
tension: 100,
friction: 10,
useNativeDriver: true,
}),
]).start();
} else {
opacityAnim.setValue(1);
translateYAnim.setValue(0);
}
// Announce error to screen readers
AccessibilityInfo.announceForAccessibility(
accessibilityLabel || `Error: ${message}`
);
} else {
Animated.timing(opacityAnim, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}).start();
}
}, [visible, message, animated, opacityAnim, translateYAnim, accessibilityLabel]);
if (!visible) {
return null;
}
return (
<Animated.View
style={[
styles.container,
{
opacity: opacityAnim,
transform: [{ translateY: translateYAnim }],
},
]}
accessibilityRole="alert"
accessibilityLabel={accessibilityLabel || message}
>
<Ionicons name="alert-circle" size={14} color="#DC2626" style={styles.icon} />
<Text style={styles.message}>{message}</Text>
</Animated.View>
);
}
/**
* FieldErrorSummary - Summary of multiple field errors
*/
interface FieldErrorSummaryProps {
errors: { field: string; message: string }[];
visible?: boolean;
}
export function FieldErrorSummary({ errors, visible = true }: FieldErrorSummaryProps) {
if (!visible || errors.length === 0) {
return null;
}
return (
<View style={styles.summaryContainer} accessibilityRole="alert">
<View style={styles.summaryHeader}>
<Ionicons name="alert-circle" size={18} color="#DC2626" />
<Text style={styles.summaryTitle}>
Please fix the following {errors.length === 1 ? 'error' : 'errors'}:
</Text>
</View>
<View style={styles.summaryList}>
{errors.map((error, index) => (
<View key={`${error.field}-${index}`} style={styles.summaryItem}>
<Text style={styles.summaryBullet}></Text>
<Text style={styles.summaryMessage}>{error.message}</Text>
</View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
paddingHorizontal: 2,
},
icon: {
marginRight: 4,
},
message: {
fontSize: 12,
color: '#DC2626',
flex: 1,
lineHeight: 16,
},
// Summary styles
summaryContainer: {
backgroundColor: '#FEE2E2',
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: '#FECACA',
marginVertical: 8,
},
summaryHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
summaryTitle: {
fontSize: 14,
fontWeight: '600',
color: '#991B1B',
marginLeft: 8,
},
summaryList: {
paddingLeft: 26,
},
summaryItem: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 4,
},
summaryBullet: {
fontSize: 12,
color: '#991B1B',
marginRight: 8,
lineHeight: 16,
},
summaryMessage: {
fontSize: 13,
color: '#991B1B',
flex: 1,
lineHeight: 16,
},
});
export default FieldError;