import React from "react"; import { FlashListProps, FlashList } from "@shopify/flash-list"; import { FlatListProps } from "react-native"; import { FlatList as FlatListComponent } from "react-native-gesture-handler"; import SectionHeader, { DefaultSectionHeader } from "./SectionHeader"; import { flattenReactFragments } from "../../utilities"; import FlatList from "../FlatList"; type ListComponentType = "FlatList" | "FlashList"; interface AdditionalSectionListProps { sectionKey: string; stickyHeader?: boolean; renderItem: (itemInfo: { item?: T; index: number; section: string; }) => React.JSX.Element; keyExtractor?: (item: T, index: number) => string; listComponent?: ListComponentType; sectionHeaderBackgroundColor?: string; sectionHeaderTextColor?: string; } export type FlatListSectionListProps = Omit, "renderItem"> & AdditionalSectionListProps; export type FlashListSectionListProps = Omit< FlashListProps, "renderItem" > & AdditionalSectionListProps; interface SectionListDataItem { type: "DATA_ITEM"; data: T; } interface SectionListSectionItem { type: "SECTION_ITEM"; title: string; } type SectionListItem = SectionListDataItem | SectionListSectionItem; export const DEFAULT_SECTION = "Uncategorized"; const SectionList = React.forwardRef( ( { sectionKey, stickyHeader = false, listComponent = "FlatList", data: dataProp, renderItem: renderItemProp, keyExtractor: keyExtractorProp, sectionHeaderBackgroundColor, sectionHeaderTextColor, ...rest }: FlatListSectionListProps | FlashListSectionListProps, ref: React.Ref> ) => { const data = React.useMemo( () => (Array.isArray(dataProp) ? dataProp : []) as T[], [dataProp] ); const dataWithSections = React.useMemo(() => { const result: SectionListItem[] = []; const sectionDataItems: { [key: string]: T[] } = {}; for (const item of data) { const section = item[sectionKey]?.toString() || DEFAULT_SECTION; if (sectionDataItems[section]) { sectionDataItems[section].push(item); } else { sectionDataItems[section] = [item]; } } for (const section in sectionDataItems) { result.push({ type: "SECTION_ITEM", title: section }); const sectionItems: SectionListDataItem[] = sectionDataItems[ section ].map((item) => ({ type: "DATA_ITEM", data: item })); result.push(...sectionItems); } return result; }, [data, sectionKey]); const sectionHeaderIndicies = React.useMemo( () => stickyHeader ? dataWithSections .filter((item) => item.type === "SECTION_ITEM") .map((item) => dataWithSections.indexOf(item)) : undefined, [dataWithSections, stickyHeader] ); const extractSectionHeader = ( element: React.JSX.Element | null ): React.JSX.Element | null => { if (!element) { return null; } const props = element.props || {}; const children = flattenReactFragments( React.Children.toArray(props.children) as React.ReactElement[] ); if (element.type === SectionHeader) { return element; } else { for (const child of children) { if (child.type === SectionHeader) { return child; } } } return null; }; const extractRemainingNonSectionHeader = ( element: React.JSX.Element | null ): React.JSX.Element | null => { if (!element) { return null; } const props = element.props || {}; const children = flattenReactFragments( React.Children.toArray(props.children) as React.ReactElement[] ); if (element.type === SectionHeader) { return null; } else { const newChildren = []; for (const child of children) { if (child.type !== SectionHeader) { newChildren.push(child); } } return React.cloneElement(element, { ...props, children: newChildren, }); } }; const renderItem = ({ item, index, }: { item: SectionListItem; index: number; }) => { switch (item.type) { case "SECTION_ITEM": { const renderedItem = renderItemProp({ index, section: item.title, }); return ( extractSectionHeader(renderedItem) || ( ) ); } case "DATA_ITEM": { const renderedItem = renderItemProp({ item: item.data, index, section: item.data[sectionKey] || DEFAULT_SECTION, }); return extractRemainingNonSectionHeader(renderedItem); } } }; const keyExtractor = (item: SectionListItem, index: number) => { switch (item.type) { case "SECTION_ITEM": { return `section_${index.toString()}`; } case "DATA_ITEM": { return keyExtractorProp?.(item.data, index) || index.toString(); } } }; switch (listComponent) { case "FlatList": return ( } stickyHeaderIndices={sectionHeaderIndicies} {...(rest as FlatListProps>)} data={dataWithSections} renderItem={renderItem} keyExtractor={keyExtractor} /> ); case "FlashList": return ( >)} data={dataWithSections} renderItem={renderItem} keyExtractor={keyExtractor} /> ); } } ); export default SectionList;