import type { Ref, ReactElement } from 'react'; import { useMemo, useCallback, forwardRef } from 'react'; import Shadowlist from './Shadowlist'; import type { ShadowlistCommands, SectionListProps, SectionListData, } from './types'; /* * Flattens `sections` into one tagged element stream (section header, items, section * footer) for Shadowlist. Section headers carry their flat indices to * `stickyHeaderIndices` for native pinning. */ type FlatRowType = 'sectionHeader' | 'item' | 'sectionFooter'; interface FlatRow { // Stable key for the flattened row, derived from section key and item key. id: string; type: FlatRowType; section: SectionListData; sectionIndex: number; // Item payload (item rows only) and its index within the section. item?: ItemT; itemIndex?: number; // Last item in its section (drives separators). isLastInSection?: boolean; // Last row of a non-final section (section separator). isSectionBoundary?: boolean; } const renderComponent = ( component: ReactElement | (() => ReactElement | null) | null | undefined ): ReactElement | null => { if (!component) return null; return typeof component === 'function' ? component() : component; }; function SectionListInner( { sections, renderItem, renderSectionHeader, renderSectionFooter, keyExtractor, stickySectionHeadersEnabled = true, ItemSeparatorComponent, SectionSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, style, elementStyle, inverted, initialElementsSize, containerOffsetIndex, keyboardAvoidingEnabled, keyboardAvoidingOffset, refreshing, onRefresh, refreshColor, onScroll, onStartReached, onEndReached, onStartReachedThreshold, onEndReachedThreshold, }: SectionListProps, ref: Ref ) { // Build the tagged row stream plus the flat indices of section-header rows. const { data, stickyHeaderIndices } = useMemo(() => { const rows: FlatRow[] = []; const stickyIndices: number[] = []; sections.forEach((section, sectionIndex) => { const sectionKey = section.key ?? `section-${sectionIndex}`; const sectionKeyExtractor = section.keyExtractor ?? keyExtractor; if (renderSectionHeader) { if (stickySectionHeadersEnabled) { stickyIndices.push(rows.length); } rows.push({ id: `sh:${sectionKey}`, type: 'sectionHeader', section, sectionIndex, }); } const lastItemIndex = section.data.length - 1; section.data.forEach((item, itemIndex) => { const itemKey = sectionKeyExtractor ? sectionKeyExtractor(item, itemIndex) : ((item as { id?: string })?.id ?? `${itemIndex}`); const isLastInSection = itemIndex === lastItemIndex; rows.push({ id: `si:${sectionKey}:${itemKey}`, type: 'item', section, sectionIndex, item, itemIndex, isLastInSection, isSectionBoundary: isLastInSection && !renderSectionFooter && sectionIndex < sections.length - 1, }); }); if (renderSectionFooter) { rows.push({ id: `sf:${sectionKey}`, type: 'sectionFooter', section, sectionIndex, isSectionBoundary: sectionIndex < sections.length - 1, }); } }); return { data: rows, stickyHeaderIndices: stickyIndices }; }, [ sections, keyExtractor, renderSectionHeader, renderSectionFooter, stickySectionHeadersEnabled, ]); // Render the active section's header for the sticky-header overlay. const renderStickyHeaderOverlay = useCallback( (activeIndex: number) => { const row = data[activeIndex]; if (!row || !renderSectionHeader) return null; return renderSectionHeader({ section: row.section }); }, [data, renderSectionHeader] ); const itemSeparator = useMemo( () => renderComponent(ItemSeparatorComponent), [ItemSeparatorComponent] ); const sectionSeparator = useMemo( () => renderComponent(SectionSeparatorComponent), [SectionSeparatorComponent] ); // Dispatch a flattened row to the right renderer. const renderElement = useCallback( ({ element }: { element: FlatRow; index: number }) => { if (element.type === 'sectionHeader') { return renderSectionHeader?.({ section: element.section }) ?? <>; } if (element.type === 'sectionFooter') { return ( <> {renderSectionFooter?.({ section: element.section })} {element.isSectionBoundary ? sectionSeparator : null} ); } const sectionRenderItem = element.section.renderItem ?? renderItem; const content = sectionRenderItem?.({ item: element.item as ItemT, index: element.itemIndex as number, section: element.section, }) ?? null; // Item separator between items; section separator at a section boundary. let separator: ReactElement | null = null; if (element.isSectionBoundary) { separator = sectionSeparator; } else if (!element.isLastInSection) { separator = itemSeparator; } return ( <> {content} {separator} ); }, [ renderItem, renderSectionHeader, renderSectionFooter, itemSeparator, sectionSeparator, ] ); return ( ); } // Cast preserves the generic item/section types for callers. const SectionList = forwardRef(SectionListInner) as ( props: SectionListProps & { ref?: Ref } ) => ReactElement; export default SectionList;