import { ForwardedRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { VirtualizedListProps } from './VirtualizedList'; import { SpatialNavigationVirtualizedListWithVirtualNodes, SpatialNavigationVirtualizedListWithVirtualNodesProps, SpatialNavigationVirtualizedListWithVirtualNodesRef, } from './SpatialNavigationVirtualizedListWithVirtualNodes'; import { ScrollToNodeCallback, SpatialNavigatorParentScrollContext, useSpatialNavigatorParentScroll, } from '../../context/ParentScrollContext'; import { typedMemo } from '../../helpers/TypedMemo'; import { useSpatialNavigationDeviceType } from '../../context/DeviceContext'; import { View, Platform, ViewStyle } from 'react-native'; import { useSpatialNavigator } from '../../context/SpatialNavigatorContext'; import React from 'react'; import { typedForwardRef } from '../../helpers/TypedForwardRef'; import { SpatialNavigationVirtualizedListRef } from '../../types/SpatialNavigationVirtualizedListRef'; const ItemWrapperWithScrollContext = typedMemo( ({ setCurrentlyFocusedItemIndex, item, index, renderItem, }: { setCurrentlyFocusedItemIndex: (i: number) => void; item: T; index: number; renderItem: VirtualizedListProps['renderItem']; }) => { const { scrollToNodeIfNeeded: makeParentsScrollToNodeIfNeeded } = useSpatialNavigatorParentScroll(); const scrollToItem: ScrollToNodeCallback = useCallback( (newlyFocusedElementRef, additionalOffset) => { setCurrentlyFocusedItemIndex(index); // We need to propagate the scroll event for parents if we have nested ScrollViews/VirtualizedLists. makeParentsScrollToNodeIfNeeded(newlyFocusedElementRef, additionalOffset); }, [makeParentsScrollToNodeIfNeeded, setCurrentlyFocusedItemIndex, index], ); return ( {renderItem({ item, index })} ); }, ); ItemWrapperWithScrollContext.displayName = 'ItemWrapperWithScrollContext'; export type SpatialNavigationVirtualizedListWithScrollProps = Omit< SpatialNavigationVirtualizedListWithVirtualNodesProps, 'currentlyFocusedItemIndex' >; export type PointerScrollProps = { descendingArrow?: React.ReactElement; descendingArrowContainerStyle?: ViewStyle; ascendingArrow?: React.ReactElement; ascendingArrowContainerStyle?: ViewStyle; scrollInterval?: number; }; const useRemotePointerVirtualizedListScrollProps = ({ setCurrentlyFocusedItemIndex, scrollInterval, data, }: { setCurrentlyFocusedItemIndex: React.Dispatch>; scrollInterval: number; data: T[]; }) => { const { deviceType, deviceTypeRef, getScrollingIntervalId: getScrollingId, setScrollingIntervalId: setScrollingId, } = useSpatialNavigationDeviceType(); const navigator = useSpatialNavigator(); const idRef = useRef(null); const grabFocus = navigator.grabFocus; const onMouseEnterDescending = useCallback(() => { const callback = () => { setCurrentlyFocusedItemIndex((index) => { if (index > 0) { if (idRef.current) grabFocus(idRef.current.getNthVirtualNodeID(index - 1)); return index - 1; } else { return index; } }); }; const id = setInterval(() => { callback(); }, scrollInterval); setScrollingId(id); }, [grabFocus, scrollInterval, setCurrentlyFocusedItemIndex, setScrollingId]); const onMouseLeave = useCallback(() => { const intervalId = getScrollingId(); if (intervalId) { clearInterval(intervalId); setScrollingId(null); } }, [getScrollingId, setScrollingId]); const onMouseEnterAscending = useCallback(() => { const callback = () => { setCurrentlyFocusedItemIndex((index) => { if (index < data.length - 1) { if (idRef.current) { grabFocus(idRef.current.getNthVirtualNodeID(index + 1)); } return index + 1; } else { return index; } }); }; const id = setInterval(() => { callback(); }, scrollInterval); setScrollingId(id); }, [data.length, grabFocus, scrollInterval, setCurrentlyFocusedItemIndex, setScrollingId]); const descendingArrowProps = useMemo( () => Platform.select({ web: { onMouseEnter: onMouseEnterDescending, onMouseLeave: onMouseLeave, }, }), [onMouseEnterDescending, onMouseLeave], ); const ascendingArrowProps = useMemo( () => Platform.select({ web: { onMouseEnter: onMouseEnterAscending, onMouseLeave: onMouseLeave, }, }), [onMouseEnterAscending, onMouseLeave], ); return { descendingArrowProps, ascendingArrowProps, idRef, deviceType, deviceTypeRef, }; }; /** * This component wraps every item of a virtualizedList in a scroll handling context. */ export const SpatialNavigationVirtualizedListWithScroll = typedMemo( typedForwardRef( ( props: SpatialNavigationVirtualizedListWithScrollProps & PointerScrollProps, ref: ForwardedRef, ) => { const { data, renderItem, descendingArrow: descendingArrow, ascendingArrow: ascendingArrow, descendingArrowContainerStyle, ascendingArrowContainerStyle, scrollInterval = 100, } = props; const [currentlyFocusedItemIndex, setCurrentlyFocusedItemIndex] = useState(0); const spatialNavigator = useSpatialNavigator(); const { deviceType, deviceTypeRef, descendingArrowProps, ascendingArrowProps, idRef } = useRemotePointerVirtualizedListScrollProps({ setCurrentlyFocusedItemIndex, scrollInterval, data, }); const setCurrentlyFocusedItemIndexCallback = useCallback( (index: number) => { deviceTypeRef.current === 'remoteKeys' ? setCurrentlyFocusedItemIndex(index) : null; }, [deviceTypeRef], ); const scrollTo = useCallback( (index: number) => { if (idRef.current) { const newId = idRef.current.getNthVirtualNodeID(index); spatialNavigator.grabFocusDeferred(newId); } }, [idRef, spatialNavigator], ); useImperativeHandle( ref, () => ({ focus: (index: number) => { setCurrentlyFocusedItemIndex(index); scrollTo(index); }, scrollTo, currentlyFocusedItemIndex, }), [currentlyFocusedItemIndex, scrollTo], ); const renderWrappedItem: typeof props.renderItem = useCallback( ({ item, index }) => ( ), [setCurrentlyFocusedItemIndexCallback, renderItem], ); return ( <> {deviceType === 'remotePointer' ? ( ) : undefined} ); }, ), ); SpatialNavigationVirtualizedListWithScroll.displayName = 'SpatialNavigationVirtualizedListWithScroll'; const PointerScrollArrows = React.memo( ({ ascendingArrow, ascendingArrowProps, ascendingArrowContainerStyle, descendingArrow, descendingArrowProps, descendingArrowContainerStyle, }: PointerScrollProps & { descendingArrowProps?: { onMouseEnter: () => void; onMouseLeave: () => void }; ascendingArrowProps?: { onMouseEnter: () => void; onMouseLeave: () => void }; }) => { return ( <> {descendingArrow} {ascendingArrow} ); }, ); PointerScrollArrows.displayName = 'PointerScrollArrows';