/ components / ExpandableText.tsx
ExpandableText.tsx
1 import React, { useState, useCallback } from 'react'; 2 import { 3 Text, 4 TouchableOpacity, 5 StyleSheet, 6 TextStyle, 7 LayoutAnimation, 8 } from 'react-native'; 9 10 interface ExpandableTextProps { 11 text: string; 12 initialLines?: number; 13 style?: TextStyle; 14 expandedStyle?: TextStyle; 15 stateKey?: string; 16 } 17 18 const ExpandableText: React.FC<ExpandableTextProps> = ({ 19 text, 20 initialLines = 3, 21 style, 22 expandedStyle, 23 }) => { 24 const [isExpanded, setIsExpanded] = useState(false); 25 const [isTruncated, setIsTruncated] = useState(false); 26 27 const onFullTextLayout = useCallback( 28 (e: any) => { 29 const lineCount = e.nativeEvent.lines.length; 30 console.log(`Full text layout: ${lineCount} lines`); 31 if (lineCount > initialLines) { 32 setIsTruncated(true); 33 console.log('Text needs truncation'); 34 } 35 }, 36 [initialLines] 37 ); 38 39 const onTruncatedTextLayout = useCallback((e: any) => { 40 const lineCount = e.nativeEvent.lines.length; 41 console.log(`Truncated text layout: ${lineCount} lines`); 42 }, []); 43 44 const toggleExpand = useCallback(() => { 45 console.log('Toggle expand pressed, isTruncated:', isTruncated); 46 if (!isTruncated) return; 47 48 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); 49 setIsExpanded((prev) => { 50 console.log('Toggling from', prev, 'to', !prev); 51 return !prev; 52 }); 53 }, [isTruncated]); 54 55 console.log( 56 'Rendering with isExpanded:', 57 isExpanded, 58 'isTruncated:', 59 isTruncated 60 ); 61 62 return ( 63 <TouchableOpacity 64 onPress={toggleExpand} 65 testID="expandable-text" 66 activeOpacity={0.7} 67 style={styles.container} 68 > 69 {/* Hidden text to measure full height */} 70 <Text 71 style={[styles.text, style, styles.hiddenText]} 72 onTextLayout={onFullTextLayout} 73 > 74 {text} 75 </Text> 76 77 {/* Visible text */} 78 <Text 79 numberOfLines={isExpanded ? undefined : initialLines} 80 onTextLayout={onTruncatedTextLayout} 81 style={[styles.text, style, isExpanded && expandedStyle]} 82 > 83 {text} 84 </Text> 85 {isTruncated && ( 86 <Text 87 style={[styles.expandIndicator, { color: style?.color || '#666' }]} 88 > 89 {isExpanded ? ' ▲ Tap to collapse' : ' ▼ Tap to expand'} 90 </Text> 91 )} 92 </TouchableOpacity> 93 ); 94 }; 95 96 const styles = StyleSheet.create({ 97 container: { 98 overflow: 'hidden', 99 }, 100 expandableContainer: { 101 borderRadius: 4, 102 }, 103 text: { 104 fontSize: 16, 105 lineHeight: 24, 106 }, 107 hiddenText: { 108 position: 'absolute', 109 opacity: 0, 110 zIndex: -1, 111 }, 112 truncatedText: { 113 marginBottom: 2, 114 }, 115 expandIndicator: { 116 fontSize: 12, 117 fontWeight: '500', 118 marginTop: 4, 119 opacity: 0.7, 120 }, 121 }); 122 123 export default React.memo(ExpandableText);