React Native Paper List Accordion expanded prop does not seem to be working as expected

105 Views Asked by At

I am coding an accordion view with sub accordions and am searching through the accordions and their titles.

I want my accordions to open/close based on the below conditions:

  • Open/Close if I manually toggle the accordion

  • Open when the search condition is met

  • Closed if I am not searching ie searchQuery === ''

My bug is that if I click an accordion to open it, then start searching, the accordion which was clicked now becomes closed but all the other ones open.

import React, { useState, useEffect, useRef } from 'react';
import { SafeAreaView, Text, TouchableOpacity, View, ScrollView, Pressable } from 'react-native';
import { List, Searchbar } from 'react-native-paper';
import { useRouter } from 'expo-router'
import styles from './contents.styles.js'
import { COLORS, SIZES, icons, images, FONT } from '../../constants'

export default function Contents() {
  const DATA = [
  {
    title: 'Section 1',
    icon: 'folder',
    indentation: 1,
    color: COLORS.section1Blue.dark,
    items: [
      {
        title: 'Section 1.1',
        icon: 'folder',
        indentation: 2,
        color: COLORS.section1Blue.dark,
        items: [
          {
            title: 'Section 1.1.1',
            icon: 'file',
            indentation: 3,
            color: COLORS.section1Blue.light,
          },
          {
            title: 'Section 1.1.2',
            icon: 'file',
            indentation: 3,
            color: COLORS.section1Blue.light,
          },
        ],
      },
      {
        title: 'Section 1.2',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
      {
        title: 'Section 1.3',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
      {
        title: 'Section 1.4',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
    ],
  },
  {
    title: 'Section 2',
    icon: 'folder',
    indentation: 1,
    color: COLORS.section1Blue.dark,
    items: [
      {
        title: 'Section 2.1',
        icon: 'folder',
        indentation: 2,
        color: COLORS.section1Blue.dark,
        items: [
          {
            title: 'Section 2.1.1',
            icon: 'file',
            indentation: 3,
            color: COLORS.section1Blue.light,
          },
          {
            title: 'Section 2.1.2',
            icon: 'file',
            indentation: 3,
            color: COLORS.section1Blue.light,
          },
          {
            title: 'Section 2.1.3',
            icon: 'file',
            indentation: 3,
            color: COLORS.section1Blue.light,
          }
        ],
      },
      {
        title: 'Section 2.2',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
      {
        title: 'Section 2.3',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
      {
        title: 'Section 2.4',
        icon: 'file',
        indentation: 2,
        color: COLORS.section1Blue.light,
      },
    ],
  },
];

  let [searchQuery, setSearchQuery] = useState('');
  const router = useRouter()
  const addIndentation = 30
  let isOpen = useRef({});
  const [isManuallyToggled, setIsManuallyToggled] = useState({});
  const [isLoading, setIsLoading] = useState(false);

  const handleAccordionToggle = (item) => {
    setIsManuallyToggled({ ...isManuallyToggled, [item.title]: true });
    isOpen.current[item.title] = !isOpen.current[item.title];
  };

  const filterByCondition = (array) => {
    return array.reduce((accArray, obj) => {
      if (obj.title.toLowerCase().includes(searchQuery.toLowerCase())) {
        accArray.push({ ...obj }); // Include the object if the condition is met
      } else if (obj.items) {
        // Recursively filter items array if it exists and is not empty
        const items = filterByCondition(obj.items);
        if (items.length > 0) {
          // Include the object if it has non-empty items array after filtering
          accArray.push({ ...obj, items });
        }
      }
      return accArray; // Return the accumulated array
    }, []);
  };

  // Filter the array based on the condition
  const filteredArray = filterByCondition(DATA); // Create a copy of DATA using spread operator

  useEffect(() => {
    setIsLoading(true);
    setTimeout(() => {
      setIsLoading(false);
    }, 0);
  }, [searchQuery]);

  const anyItemMatchesQuery = (items, query) => {
    return items.some(item =>
      item.title.toLowerCase().includes(query.toLowerCase()) ||
      (item.items && anyItemMatchesQuery(item.items, query))
    );
  };

  const renderItems = (items) =>
    items.map((item, index) =>
      item.icon === 'file' ? (
        <TouchableOpacity
          style={{ paddingLeft: addIndentation * item.indentation }}
          key={`main-view-${item.title}`}
          onPress={() =>
            router.push({
              params: {
                id: item.id,
                title: item.title,
                imageTitle: item.images.map(image => image.title),
                imageDescription: item.images.map(image => image.description),
                imageCredit: item.images.map(image => image.credit),
                imageLink: item.images.map(image => image.link),
                commonDescription: item.commonDescription,
              },
              pathname: `/main-view/${item.title}`,
            })
          }
        >
          <List.Item
            titleStyle={{ fontFamily: FONT.regular }}
            left={() => <List.Icon icon={item.icon} color={item.color} />}
            key={index}
            title={item.title}
          />
        </TouchableOpacity>
      ) : (
        <List.Accordion
          titleStyle={{ fontFamily: FONT.regular }}
          theme={{ colors: { onSurfaceVariant: item.color, primary: item.color } }}
          onPress={() => {
            handleAccordionToggle(item);
            setIsManuallyToggled({ ...isManuallyToggled, [item.title]: true });
          }}
          expanded={(
            isManuallyToggled[item.title]) && searchQuery.length !== 0
            ? !isOpen.current[item.title]
            : isManuallyToggled[item.title] && searchQuery === ''
              ? isOpen.current[item.title]
              : (searchQuery !== '' && anyItemMatchesQuery(item.items, searchQuery))
          }
          key={item.title}
          title={item.title}
          left={() => <List.Icon icon={item.icon} color={item.color} />}
          style={{ paddingLeft: addIndentation * item.indentation }}
        >
          {renderItems(item.items)}
        </List.Accordion>
      ),
    );

  return (
    <View style={{ backgroundColor: 'white' }}>
      <Searchbar
        right={query => (
          <TouchableOpacity activeOpacity={0.5} onPress={() => setSearchQuery('')}>
            <List.Icon style={styles.clearIcon} icon="close-circle-outline" />
          </TouchableOpacity>)}
        placeholder="Search"
        onChangeText={query => setSearchQuery(query)}
        value={searchQuery}
        loading={isLoading}
        inputStyle={{ fontFamily: FONT.regular }}
      />
      <List.Section>
        {renderItems(filteredArray)}
      </List.Section>
    </View>
  );
};

I have tried many different solutions and each time I find a different solution which comes up with another bug. I think one of the problems may have been around setState and the value not updating when I want it to but I am not a react expert so I may be wrong

1

There are 1 best solutions below

1
talhamaqsood890 On

Focus on: setIsExpended({ id: -1 });

Expo Snack Link here

import { List, TextInput } from 'react-native-paper';

const App = () => {
  const [filteredData, setFilteredData] = useState(null);
  const [isExpended, setIsExpended] = useState({ id: -1 });

  const [searchQuery, setSearchQuery] = useState(null);
  const handleExpend = (e) => setIsExpended(e);

  const handleTextChange = (e) => {
    if (!e) {
      setIsExpended({ id: -1 });
      setFilteredData(DATA); // Reset filtered data if search query is empty
      return;
    }

    const filteredItems = DATA.map((section) => ({
      ...section,
      items: section.items.filter((subElement) =>
        subElement.title.toLowerCase().includes(e.toLowerCase())
      ),
    }));

    // Filter sections that have at least one matching item
    const temp = filteredItems.filter((section) => section.items.length > 0);

    if (temp.length === 1) {
      setIsExpended(temp[0]); // If there's only one section after filtering, expand it
    } else {
      setIsExpended({ id: -1 }); // Collapse all sections if more than one section or no sections match
    }

    setFilteredData(temp); // Update filtered data state
  };


  return (
    <SafeAreaView style={styles.container}>
      <TextInput
        placeholder="Search"
        value={searchQuery}
        onChangeText={handleTextChange}
        onFocus={() => console.log('==== focusing')}
        onBlur={() => setIsExpended({ id: -1 })}
      />
        <FlatList
        data={filteredData ? filteredData : DATA}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => {
          return (
            <List.Accordion
              onPress={() => handleExpend(item)}
              expanded={isExpended.id == item.id}
              key={item.title}
              title={item.title}
              left={() => (
                <List.Icon icon={'flag-outline'} color={item.color} />
              )}>
              <FlatList
                data={item.items}
                renderItem={({ item: subItem, index: subIndex }) => {
                  return (
                    <List.Item
                      left={() => (
                        <List.Icon icon={subItem.icon} color={subItem.color} />
                      )}
                      key={subIndex}
                      title={subItem.title}
                    />
                  );
                }}
              />
            </List.Accordion>
          );
        }}
      />
    </SafeAreaView>
  );
};