import * as R from "ramda"; import * as React from "react"; import { FlatList, FlatListProps, ListRenderItemInfo as IListRenderItemInfo, } from "react-native"; import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils"; import { ChildrenFocusDeactivatorView } from "../ChildrenFocusDeactivatorView"; import { useInitialFocus } from "@applicaster/zapp-react-native-utils/focusManager"; import { noop } from "@applicaster/zapp-react-native-utils/functionUtils"; import { useParentFocus, useCheckItemIdsForUnique } from "./hooks"; import { FocusableListItemWrapper } from "./FocusableListItemWrapper"; import { FocusableScrollView } from "../FocusableScrollView"; const mapIndexed = R.addIndex(R.map); export type IListRenderItem = ( info: { focused: boolean; onLoadFinished: () => void; onLoadFailed: () => void; } & IListRenderItemInfo ) => React.ReactElement | null; export const getFocusableId = (parentId, index) => `${parentId}___index:${index}`; type Item = ZappEntry | ZappUIComponent | any; export type Props = FlatListProps & { id?: number | string; horizontal?: boolean; loop?: boolean; numColumns?: number; data: ZappEntry[] | ZappUIComponent[] | any[]; onListElementFocus?: ( element: FocusManager.FocusableRef, renderItemProps: { item: Item; index: number }, options: FocusManager.Android.CallbackOptions, context: FocusManager.FocusContext ) => void; onListElementBlur?: (element, renderItemProps, direction) => void; onListElementPress?: (element, renderItemProps) => void; onListElementPressOut?: (element, renderItemProps) => void; onListElementLongPress?: (element, renderItemProps) => void; focusableItemProps?: any; focused?: boolean; initialScrollIndex?: number; withStateMemory?: boolean; extraChildrenData?: any; loadNextData?: () => void; initialFocusDirection?: FocusManager.Android.FocusNavigationDirections; onAllComponentsLoaded?: () => void; omitPropsPropagation?: Array>; renderItem: IListRenderItem | null | undefined; disableNextFocusDown?: boolean; disableNextFocusUp?: boolean; disableNextFocusLeft?: boolean; disableNextFocusRight?: boolean; useScrollView?: boolean; } & ParentFocus; function FocusableListComponent(props: Props, ref) { const { horizontal, onListElementFocus, onListElementBlur, onListElementPress, onListElementPressOut, onListElementLongPress, disableNextFocusDown, disableNextFocusRight, disableNextFocusLeft, disableNextFocusUp, nextFocusDown: parentNextFocusDown, nextFocusRight: parentNextFocusRight, nextFocusLeft: parentNextFocusLeft, nextFocusUp: parentNextFocusUp, numColumns, focused: parentFocused, focusableItemProps, loop = false, // only for horizontal layout initialScrollIndex = 0, withStateMemory = true, loadNextData = noop, initialFocusDirection = "down", data = [], // eslint-disable-next-line unused-imports/no-unused-vars omitPropsPropagation = [], useScrollView = false, onScrollToIndexFailed = noop, } = props; useCheckItemIdsForUnique({ componentId: props.id, items: data }); const { focused, nextFocusUp, nextFocusRight, nextFocusDown, nextFocusLeft } = useParentFocus({ parentFocused, disableNextFocusUp, disableNextFocusRight, disableNextFocusDown, disableNextFocusLeft, parentNextFocusUp, parentNextFocusRight, parentNextFocusDown, parentNextFocusLeft, }); const getFocusableEntryId = React.useCallback( (_entry: ZappEntry | null, index?: number) => { const entry = R.isNil(index) ? _entry : data[index as number]; return entry ? getFocusableId(props.id, index) : undefined; }, [props.id, data.length] ); const initialFocusOptions = React.useMemo( () => withStateMemory ? { withStateMemory, refsList: mapIndexed(getFocusableEntryId)(data), initialFocusDirection, initialScrollIndex, } : { initialFocusDirection }, [getFocusableEntryId, withStateMemory, initialFocusDirection, data.length] ); const updateFocusedIndex = useInitialFocus( focused, getFocusableEntryId(data[initialScrollIndex], initialScrollIndex), initialFocusOptions ); const onFocus = React.useCallback( (element, renderArgs, options, context) => { const { index } = renderArgs; updateFocusedIndex?.(index); onListElementFocus?.(element, renderArgs, options, context); }, [onListElementFocus, updateFocusedIndex] ); const getNextFocus = React.useCallback( (index) => { const itemCount = R.length(data); const getFocusableEntryIdForIndex = (entryIndex) => getFocusableEntryId(null, entryIndex); const getHorizontalFocus = () => ({ nextFocusLeft: getFocusableEntryIdForIndex(index - 1) || (loop ? getFocusableEntryIdForIndex(itemCount - 1) : nextFocusLeft), nextFocusRight: getFocusableEntryIdForIndex(index + 1) || (loop ? getFocusableEntryIdForIndex(0) : nextFocusRight), nextFocusUp, nextFocusDown, }); const getGridFocus = () => { const _numColumns = toFiniteNumberWithDefault(1, numColumns); const numberOfRows = Math.ceil(itemCount / _numColumns); const currentRowIndex = Math.floor(index / _numColumns); const isFirstRow = currentRowIndex === 0; const isLastRow = currentRowIndex + 1 === numberOfRows; const isLeftEdge = index % _numColumns === 0; const isRightEdge = (index + 1) % _numColumns === 0 || itemCount === index + 1; return { nextFocusUp: isFirstRow ? nextFocusUp : getFocusableEntryIdForIndex(index - _numColumns), nextFocusDown: isLastRow ? nextFocusDown : getFocusableEntryIdForIndex(index + _numColumns), nextFocusLeft: isLeftEdge ? nextFocusLeft : getFocusableEntryIdForIndex(index - 1), nextFocusRight: isRightEdge ? nextFocusRight : getFocusableEntryIdForIndex(index + 1), }; }; const getVerticalFocus = () => ({ nextFocusLeft, nextFocusRight, nextFocusUp: getFocusableEntryIdForIndex(index - 1) || nextFocusUp, nextFocusDown: getFocusableEntryIdForIndex(index + 1) || nextFocusDown, }); if (horizontal) { return getHorizontalFocus(); } else if (numColumns) { return getGridFocus(); } else { return getVerticalFocus(); } }, [ data.length, getFocusableEntryId, nextFocusDown, nextFocusUp, nextFocusLeft, nextFocusRight, loop, horizontal, ] ); const renderItem = React.useCallback( ({ item, index }) => { const id = getFocusableId(props.id, index); return ( ); }, [ getNextFocus, props.extraChildrenData, onFocus, props.renderItem, data.length, onListElementBlur, onListElementPress, onListElementLongPress, onListElementPressOut, ] ); const getFlatListProps = React.useCallback( R.omit( [ "onListElementFocus", "onListElementPress", "onListElementPressOut", "onListElementLongPress", "onListElementBlur", "nextFocusDown", "nextFocusRight", "nextFocusLeft", "nextFocusUp", "focusableItemProps", "renderItem", "extraChildrenData", "onAllComponentsLoaded", "focused", "id", "initialFocusDirection", "withStateMemory", "useSequentialLoading", "useScrollView", "onScrollToIndexFailed", ...omitPropsPropagation, ], R.__ ), [omitPropsPropagation] ); const onEndReached = React.useCallback( ({ distanceFromEnd }) => { if (distanceFromEnd > 1000) loadNextData(); }, [loadNextData] ); const initialNumToRender = toFiniteNumberWithDefault( 10, props.initialNumToRender ); return ( // @ts-ignore {useScrollView ? ( ) : ( )} ); } export const FocusableList = React.forwardRef(FocusableListComponent);