react-nativereact-native-flatlistreact-native-textinputkeyboardavoidingview

using TextInput inside Flatlist in react-native


In my provided code I am using a flatlist which renders a row element which contains multiple TextInut components. The problem here is when I try to edit the Textinput components which is at the end of the flatlist the keyboard appears then disappears, but when I edit any other row at the beginning of the flatlist it work normally.


I think this is because when I click on a row at the end of the flatlist the keyboard comes up and covers that area so that the device decides after that, that I am not entering any input so it hides the keyboard again. tried using KeyboardAvoidingView but with no success


import {
  StyleSheet,
  Text,
  View,
  TextInput,
  FlatList,
  Pressable,
  Animated,
  KeyboardAvoidingView,
  Platform,
  TouchableOpacity,
  SafeAreaView,
  ActivityIndicator,
} from "react-native";
import React, { memo } from "react";
import { useNavigation } from "@react-navigation/native";
import { useState } from "react";
import { Entypo, Feather } from "@expo/vector-icons";
import { useRef, useEffect } from "react";
import { Formik } from "formik";
import axios from "axios";
import * as SecureStore from "expo-secure-store";
import { BlurView } from "expo-blur";

function convertToArabicNumbers(input) {
  const englishToArabic = {
    0: "٠",
    1: "١",
    2: "٢",
    3: "٣",
    4: "٤",
    5: "٥",
    6: "٦",
    7: "٧",
    8: "٨",
    9: "٩",
  };

  return input.replace(/\d/g, (digit) => englishToArabic[digit]);
}

export default function Customers() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [tableData, setTableData] = useState([]);
  const [message, setMessage] = useState();
  const shakeAnimation = useRef(new Animated.Value(0)).current;
  const borderWidth = useRef(new Animated.Value(1)).current; // Initial border width
  const color = useRef(new Animated.Value(0)).current;
  const [name, setName] = useState("");
  const [phoneNumber, setPhoneNumber] = useState("");
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    const token = SecureStore.getItem("token");
    axios
      .get("http://54.224.129.156:5001/api/client/", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      })
      .then((res) => {
        setTableData(res.data.clients);
      })
      .catch((error) => {
        console.error("Error making the request:", error);
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);
  const triggerShake = () => {
    shakeAnimation.setValue(0);
    Animated.sequence([
      Animated.timing(shakeAnimation, {
        toValue: 10,
        duration: 50,
        useNativeDriver: true,
      }),
      Animated.timing(shakeAnimation, {
        toValue: -10,
        duration: 50,
        useNativeDriver: true,
      }),
      Animated.timing(shakeAnimation, {
        toValue: 10,
        duration: 50,
        useNativeDriver: true,
      }),
      Animated.timing(shakeAnimation, {
        toValue: 0,
        duration: 50,
        useNativeDriver: true,
      }),
    ]).start();
  };
  const handleAddTableData = async () => {
    setMessage("");

    if (editOn) {
      setMessage("الرجاء إنهاء التعديلات أولا");
      triggerShake();
      return;
    }

    if (name === "" || phoneNumber === "") {
      setMessage("الرجاء ملء جميع الحقول");
      triggerShake();
      return;
    }

    try {
      const token = await SecureStore.getItemAsync("token");

      if (!token) {
        setMessage("الرجاء تسجيل الدخول");
        triggerShake();
        return;
      }

      const headers = {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      };

      const createRes = await axios.post(
        "http://54.224.129.156:5001/api/client/create",
        {},
        { headers }
      );
      const id = createRes.data.client._id;
      await axios.put(
        "http://54.224.129.156:5001/api/client/update",
        { name: name, _id: id },
        { headers }
      );
      await axios.put(
        "http://54.224.129.156:5001/api/client/update",
        { phoneNumber: phoneNumber, _id: id },
        { headers }
      );

      setTableData([...tableData, { _id: id, name, phoneNumber }]);
      setName("");
      setPhoneNumber("");
      flatListRef.current.scrollToEnd();
    } catch (error) {
      setMessage("حدث خطأ ما");
      triggerShake();
    }
  };
  const handlePressIn = () => {
    Animated.timing(color, {
      toValue: 1,
      duration: 150, // Duration of the animation in milliseconds
      useNativeDriver: false, // Set to true if using Native Driver (for performance)
    }).start();
    Animated.timing(borderWidth, {
      toValue: 5, // Increased border width on press in
      duration: 150,
      useNativeDriver: false,
    }).start();
  };

  const handlePressOut = () => {
    Animated.timing(color, {
      toValue: 0,
      duration: 150,
      useNativeDriver: false,
    }).start();
    Animated.timing(borderWidth, {
      toValue: 1, // Original border width on press out
      duration: 150,
      useNativeDriver: false,
    }).start();
  };

  // Interpolate background color
  const animatedColor = color.interpolate({
    inputRange: [0, 1],
    outputRange: ["black", "#2ec089"],
  });
  const flatListRef = useRef(null);
  if (loading) {
    return (
      <SafeAreaView style={{ flex: 1 }}>
        <View
          style={{ flex: 1, alignSelf: "center", justifyContent: "center" }}
        >
          <ActivityIndicator size="large" color="#2ec089" />
          <Text>Loading...</Text>
        </View>
      </SafeAreaView>
    );
  }
  return (
    <SafeAreaView style={{ flex: 1, backgroundColor: "white" }}>
        <Text
          style={{
            fontSize: 40,
            fontWeight: "bold",
            marginRight: 20,
            marginTop: 20,
            alignSelf: "flex-end",
          }}
        >
          اهلاً {SecureStore.getItem("name").split(" ")[0]}
        </Text>
        <Text
          style={{
            marginTop: 20,
            fontSize: 15,
            paddingHorizontal: 10,
            fontWeight: "bold",
            color: "#0b935f",
            alignSelf: "center",
            borderColor: "#2ec089",
            borderWidth: 1,
            borderBottomWidth: 0,
            borderTopRightRadius: 10,
            borderTopLeftRadius: 10,
          }}
        >
          عدد العملاء: {convertToArabicNumbers(tableData.length.toString())}
        </Text>
        <View
          style={{
            width: "90%",
            flexDirection: "row-reverse",
            alignSelf: "center",
            alignItems: "center",
            borderWidth: 1,
            padding: 5,
            borderTopRightRadius: 10,
            borderTopLeftRadius: 10,
            height: "auto",
            borderColor: "#2ec089",
          }}
        >
          <Text style={styles.rowText}>الترتيب</Text>
          <Text style={styles.rowText}>الاسم</Text>
          <Text style={styles.rowText}>رقم الهاتف</Text>
          <Feather
            name="edit"
            size={22}
            color="#ffffff00"
            style={{ marginLeft: 5 }}
          />
        </View>
        <FlatList
          ref={flatListRef}
          centerContent={true}
          style={[styles.tableContainerStyle]}
          data={tableData}
          initialNumToRender={12}
          maxToRenderPerBatch={12}
          windowSize={12}
          automaticallyAdjustKeyboardInsets={true}
          keyboardShouldPersistTaps="handled"
          automaticallyAdjustsScrollIndicatorInsets={true}
          removeClippedSubviews={true}
          keyboardDismissMode="none"
          renderItem={(itemData) => {
            return (
              <TableRow
                item={[itemData.item, itemData.index]}
                setTableData={setTableData}
                tableData={tableData}
                setMessage={setMessage}
                triggerShake={triggerShake}
                flatListRef={flatListRef}
              />
            );
          }}
          contentContainerStyle={styles.tableContentContainerStyle}
        />
        <View
          style={{
            width: "90%",
            flexDirection: "row-reverse",
            alignSelf: "center",
            alignItems: "center",
            borderWidth: 1,
            borderTopWidth: 0,
            padding: 5,
            borderBottomRightRadius: 10,
            borderBottomLeftRadius: 10,
            height: "auto",
            borderColor: "#2ec089",
          }}
        >
          <Text style={styles.rowText}>
            {convertToArabicNumbers((tableData.length + 1).toString())}
          </Text>
          <TextInput
            placeholder="ـ ـ ـ"
            textAlign="center"
            style={styles.rowText}
            value={name}
            onChangeText={setName}
            onEndEditing={(text) => setName(text.nativeEvent.text.trim())}
            multiline={true}
          />
          <TextInput
            placeholder="ـ ـ ـ"
            textAlign="center"
            style={styles.rowText}
            value={phoneNumber}
            onChangeText={setPhoneNumber}
            onEndEditing={(text) =>
              setPhoneNumber(text.nativeEvent.text.trim())
            }
            multiline={true}
          />
          <Feather
            name="edit"
            size={22}
            color="#ffffff00"
            style={{ marginLeft: 5 }}
          />
        </View>
        <Animated.View
          style={[
            {
              borderRadius: 100,
              alignSelf: "center",
              marginBottom: 10,
              marginTop: 20,
            },
            { borderWidth },
          ]}
        >
          <Pressable
            style={{
              padding: 20,
              borderRadius: 100,
            }}
            onPressIn={handlePressIn}
            onPressOut={handlePressOut}
            onPress={handleAddTableData}
          >
            <Animated.Text style={{ color: animatedColor }}>
              <Entypo
                name="add-user"
                size={32}
                style={{ alignSelf: "center" }}
              />
            </Animated.Text>
          </Pressable>
        </Animated.View>
        {isSubmitting ? (
          <view style={{ alignSelf: "center", marginBottom: 20 }}>
            <ActivityIndicator
              size="large"
              color="#2ec089"
              style={{ marginBottom: 20 }}
            />
          </view>
        ) : (
          <Animated.View
            style={{
              transform: [{ translateX: shakeAnimation }],
              alignSelf: "center",
              marginBottom: 20,
            }}
          >
            <Text style={{ color: "red" }}>{message}</Text>
          </Animated.View>
        )}
    </SafeAreaView>
  );
}
let editOn = false;
const TableRow = memo(
  ({
    item,
    setTableData,
    tableData,
    setMessage,
    triggerShake,
    flatListRef,
  }) => {
    const navigation = useNavigation();
    const [editable, setEditable] = useState(false);
    const color = useRef(new Animated.Value(0)).current;
    const handleEdit = async () => {
      setMessage("");
      if (editable) {
        if (item[0].name === "" || item[0].phoneNumber === "") {
          setMessage("الرجاء ملء جميع الحقول");
          triggerShake();
          return;
        }
        Animated.timing(color, {
          toValue: 0,
          duration: 150,
          useNativeDriver: false,
        }).start();
        const headers = {
          Authorization: `Bearer ${SecureStore.getItem("token")}`,
          "Content-Type": "application/json",
        };
        editOn = false;
        setEditable(false);
        axios.put(
          "http://54.224.129.156:5001/api/client/update",
          { name: item[0].name, _id: item[0]._id },
          { headers }
        );
        axios.put(
          "http://54.224.129.156:5001/api/client/update",
          { phoneNumber: item[0].phoneNumber, _id: item[0]._id },
          { headers }
        );
      } else {
        if (!editOn) {
          Animated.timing(color, {
            toValue: 1,
            duration: 150, // Duration of the animation in milliseconds
            useNativeDriver: false, // Set to true if using Native Driver (for performance)
          }).start();
          editOn = true;
          setEditable(true);
        } else {
          setMessage("الرجاء إنهاء التعديلات الحالية أولا");
          triggerShake();
        }
      }
    };
    const animatedColor = color.interpolate({
      inputRange: [0, 1],
      outputRange: ["black", "#2ec089"],
    });
    return (
      <View
        style={{
          width: "90%",
          flexDirection: "row-reverse",
          alignSelf: "center",
          alignItems: "center",
          borderWidth: 1,
          borderTopWidth: 0,
          padding: 5,
          height: "auto",
          borderColor: "#2ec089",
        }}
      >
        <TouchableOpacity
          style={{
            flexDirection: "row-reverse",
            flexShrink: 1,
            alignItems: "center",
          }}
          onPress={() => {
            setMessage("");
            if (editOn) {
              setMessage("الرجاء إنهاء التعديلات أولا");
              triggerShake();
              return;
            }
            navigation.navigate("المشاريع");
            SecureStore.setItemAsync("customerId", item[0]._id);
            SecureStore.setItemAsync("customerName", item[0].name);
            SecureStore.setItemAsync("customerPhone", item[0].phoneNumber);
          }}
        >
          <Text style={styles.rowText}>
            {convertToArabicNumbers((item[1] + 1).toString())}
          </Text>
          <TextInput
            placeholder="ـ ـ ـ"
            textAlign="center"
            style={styles.rowText}
            value={item[0].name}
            textContentType="name"
            onFocus={() => {
              flatListRef.current?.scrollToIndex({
                index: item[1],
                animated: true,
              });
            }}
            editable={editable}
            onChangeText={(text) => {
              const newData = [...tableData];
              newData[item[1]].name = text;
              setTableData(newData);
            }}
            onEndEditing={(text) => {
              const newData = [...tableData];
              newData[item[1]].name = text.nativeEvent.text.trim();
              setTableData(newData);
            }}
            multiline={true}
          />
          <TextInput
            placeholder="ـ ـ ـ"
            textAlign="center"
            style={styles.rowText}
            value={item[0].phoneNumber}
            textContentType="telephoneNumber"
            onFocus={() => {
              flatListRef.current?.scrollToIndex({
                index: item[1],
                animated: true,
              });
            }}
            editable={editable}
            onChangeText={(text) => {
              const newData = [...tableData];
              newData[item[1]].phoneNumber = text;
              setTableData(newData);
            }}
            onEndEditing={(text) => {
              const newData = [...tableData];
              newData[item[1]].phoneNumber = text.nativeEvent.text.trim();
              setTableData(newData);
            }}
            multiline={true}
          />
        </TouchableOpacity>
        <Pressable style={{ paddingLeft: 5 }} onPress={handleEdit}>
          <Animated.Text style={{ color: animatedColor }}>
            <Feather
              name={editable ? "save" : "edit"}
              size={22}
              style={{ marginLeft: 5 }}
            />
          </Animated.Text>
        </Pressable>
      </View>
    );
  }
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "white",
    alignItems: "center",
    justifyContent: "flex-start",
  },
  tableContainerStyle: {
    width: "100%",
    alignSelf: "center",
    flexGrow: 0,
    flexShrink: 1,
  },
  tableContentContainerStyle: {
    flexDirection: "column",
    alignItems: "center",
    justifyContent: "center",
  },
  rowText: {
    flex: 1,
    textAlign: "center",
    textShadowOffset: { width: 0.5, height: 0.5 },
    textShadowRadius: 1,
    fontSize: 16,
    color: "black",
  },
});

expected behavior is to be able to edit all elements without the keyboard disappearing.


Solution

  • Try removeClippedSubviews={false} or use a ScrollView

    The FlatList will remove items subviews that covered by keyboard for better performance https://reactnative.dev/docs/flatlist#removeclippedsubviews.