/ 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);