import { forwardRef, memo, PropsWithoutRef, ReactElement, ReactNode, RefAttributes, useCallback, useMemo, } from 'react'; import { type GestureResponderEvent, type ViewStyle, type SectionListProps, type SectionListRenderItemInfo, SectionList, View, } from 'react-native'; import { typedMemo } from '../../hocs'; import withKeyboardAccessibility, { useCurrentIndexStoreValue, } from '../../hocs/withKeyboardAccessibility'; import { useControlledValue, UseSearchableProps } from '../../hooks'; import { useSearchInputProps } from '../../hooks/useSearchable'; import { TextInput } from '../TextInput'; import { TouchableRipple } from '../TouchableRipple'; import { optionListStyles } from './utils'; type DefaultSectionT = { data: TItem[]; [key: string]: any; }; type DefaultItemT = { id: string | number; [key: string]: any; }; export type OptionListRenderItemInfo< TItem = DefaultItemT, TSection = DefaultSectionT, > = SectionListRenderItemInfo & { focused: boolean; onPress: (e?: GestureResponderEvent) => void; [key: string]: any; }; // To make a correct type inference export type IOptionList = >( props: PropsWithoutRef> & RefAttributes>, ) => ReactElement; export type Props> = UseSearchableProps & Omit, 'sections' | 'renderItem'> & { records: TSection[]; containerStyle?: ViewStyle; searchInputContainerStyle?: ViewStyle; /* * when set to true, the items will be selectable. Each item will be wrapped around by TouchableRipple component. Whenever they're pressed, onSelectedItem function will trigger * */ selectable?: boolean; /* * when set to true, multiple items can be selected and selectedItem will be an array. onSelectItem's argument will be an array. * */ multiple?: boolean; /* * Expects an array of TItem in multiple mode. If the item already exists in the array, it will be removed. * */ selection?: TItem | TItem[] | null; /* * passes the current selectedItem. Will be an array in multiple mode. Item is the specific item which is pressed * */ onSelectionChange?: ( selection: TItem | TItem[] | null, item: TItem, event?: GestureResponderEvent, ) => void; renderItem: ( info: SectionListRenderItemInfo & { onPress: (e?: GestureResponderEvent) => void; focused: boolean; }, ) => ReactNode; enableKeyboardNavigation?: boolean; onCancel?: () => void; }; const getIdToIndexMapFromRecords = < TItem extends DefaultItemT = DefaultItemT, TSection extends DefaultSectionT = DefaultSectionT, >( records: TSection[], ) => { const flattenRecords = records.reduce((acc, item, sectionIndex: Number) => { return acc.concat((item.data || []).map((t: any) => ({ ...t, sectionIndex }))); }, [] as TItem[]); return flattenRecords.reduce((acc, item, currentIndex) => { acc[item.id] = currentIndex; return acc; }, {} as Record); }; const OptionList = < TItem extends DefaultItemT = DefaultItemT, TSection extends DefaultSectionT = DefaultSectionT, >( { query, onQueryChange, searchInputProps: dirtySearchInputProps, searchable, containerStyle = {}, searchInputContainerStyle = {}, style: styleProp, records, multiple = false, selectable, selection: selectionProp, onSelectionChange: onSelectionChangeProp, renderItem: renderItemProp, testID, ...rest }: Props, ref: any, ) => { const [selection, onSelectionChange] = useControlledValue({ value: selectionProp, onChange: onSelectionChangeProp, }); const searchInputProps = useSearchInputProps(dirtySearchInputProps); const { containerStyles, searchInputContainerStyles, style } = useMemo(() => { const { container, searchInputContainer } = optionListStyles; return { containerStyles: [container, containerStyle], searchInputContainerStyles: [searchInputContainer, searchInputContainerStyle], style: styleProp, }; }, [containerStyle, searchInputContainerStyle, styleProp]); // To get the actual flatten indexes const idToIndexMap = useMemo(() => getIdToIndexMapFromRecords(records), [records]); const onPressItem = useCallback( (item: TItem, event?: GestureResponderEvent) => { const isSelected = Array.isArray(selection) ? selection.find(sItem => sItem?.id === item.id) : selection?.id === item.id; onSelectionChange( // if multiple we push the item into an array and if it's already exists we filter them multiple ? Array.isArray(selection) ? isSelected ? selection.filter(sItem => sItem.id !== item.id) : [...selection, item] : [item] : item, item, event, ); }, [multiple, onSelectionChange, selection], ); const renderItem = useCallback( (info: SectionListRenderItemInfo) => { return ( ); }, [idToIndexMap, testID, onPressItem, renderItemProp, selectable], ); const keyExtractor = useCallback((item: TItem) => `${item.id}`, []); return ( <> {searchable && ( )} ['renderItem']} style={style} /> ); }; type OptionListItemProps> = Pick< Props, 'renderItem' > & { info: SectionListRenderItemInfo; onPressItem?: (item: TItem, e?: GestureResponderEvent) => void; selectable?: boolean; index: number; testID?: string; }; const OptionListItem = typedMemo( < TItem extends DefaultItemT = DefaultItemT, TSection extends DefaultSectionT = DefaultSectionT, >({ info, renderItem, selectable, onPressItem, index, testID, }: OptionListItemProps) => { const focused = useCurrentIndexStoreValue(state => { return state.currentIndex === index; }); const onPress = useCallback( (event?: GestureResponderEvent) => { onPressItem?.(info.item, event); }, [info.item, onPressItem], ); const renderItemInfo = useMemo( () => ({ ...info, focused, onPress, }), [focused, info, onPress], ); if (selectable && info.item?.selectable !== false) { return ( {renderItem(renderItemInfo)} ); } return <>{renderItem(renderItemInfo)}; }, ); export default memo(withKeyboardAccessibility(forwardRef(OptionList))) as IOptionList;