import React, { forwardRef, useCallback, useEffect, useRef, useState, } from 'react'; import type { LayoutChangeEvent, ListRenderItem, StyleProp, ViewStyle, ViewToken, } from 'react-native'; import { FlatList, Platform, View } from 'react-native'; import { useTheme } from '../../theme'; import { StyledCard, StyledItemWrapper, StyledPageControl, StyledWrapper, } from './StyledCardCarousel'; import { ITEM_WIDTH_RATE, VIEW_POSITION_CENTER } from './contants'; export type CardCarouselHandles = { snapToIndex: (index: number) => void; }; export interface CardCarouselProps { /** * Whether to scroll automatically. */ autoPlay?: boolean; /** * Set interval of each slide. */ autoPlayInterval?: number; /** * onItemIndexChange event handler receiving index of selected Item. */ onItemIndexChange?: (index: number) => void; /** * Carousel items. */ items: React.ReactNode[]; /** * Indicates hide or show page control. */ hidePageControl?: boolean; /** * Additional styles */ style?: StyleProp; /** * Testing id of the component. */ testID?: string; /** * Component ref. */ ref?: React.Ref; /* * onLayout event handler. */ onLayout?: (event: LayoutChangeEvent) => void; /** * Gap between items. */ gap?: 'xsmall' | 'small' | 'medium'; } export const getCardCarouselValidIndex = (index: number, length: number) => { return Math.min(Math.max(index, 0), length - 1); }; export const CardCarousel = forwardRef( ( { onItemIndexChange, items, hidePageControl = false, style, testID, autoPlay = false, autoPlayInterval = 3000, onLayout, gap = 'medium', }: CardCarouselProps, ref?: React.Ref ) => { const [currentIndex, setCurrentIndex] = useState(0); const theme = useTheme(); const [flatListWidth, setFlatListWidth] = useState(0); const itemWidth = flatListWidth * ITEM_WIDTH_RATE; const carouselRef = useRef(null); const viewPosition = Platform.OS === 'ios' ? VIEW_POSITION_CENTER : undefined; const snapToIndex = useCallback( (index: number) => { const validIndex = getCardCarouselValidIndex(index, items.length); carouselRef.current?.scrollToIndex({ index: validIndex, animated: true, viewPosition, }); }, [items.length, viewPosition] ); /* * snap to the next index. If the curent index is the last one, snap to the first one. */ const snapToNext = useCallback(() => { let nextIndex = currentIndex + 1; if (nextIndex >= items.length) { nextIndex = 0; } carouselRef.current?.scrollToIndex({ index: nextIndex, animated: true, viewPosition, }); }, [currentIndex, items.length, viewPosition]); React.useImperativeHandle( ref, () => ({ snapToIndex: (index: number) => { snapToIndex(index); }, // we don't expose this method, it's for testing https://medium.com/developer-rants/how-to-test-useref-without-mocking-useref-699165f4994e getFlatListRef: () => carouselRef.current, }), [snapToIndex] ); useEffect(() => { let timer: ReturnType; if (autoPlay) { timer = setInterval(() => { snapToNext(); }, autoPlayInterval); } return () => { clearInterval(timer); }; }, [autoPlay, snapToNext, currentIndex, autoPlayInterval]); const visibleSlideChanged = useCallback( ({ viewableItems }: { viewableItems: ViewToken[] }) => { if (!viewableItems || (viewableItems && !viewableItems.length)) return; const { index } = viewableItems[0]; setCurrentIndex(index || 0); if (onItemIndexChange) { onItemIndexChange(index || 0); } }, [onItemIndexChange] ); const getItemLayout = useCallback( (_: ArrayLike | null | undefined, index: number) => ({ length: itemWidth, offset: itemWidth * index, index, }), [itemWidth] ); const flatListOnLayout = useCallback( (e: LayoutChangeEvent) => { setFlatListWidth(e.nativeEvent.layout.width); setTimeout(() => { onLayout?.(e); }); }, [setFlatListWidth, onLayout] ); const renderItem: ListRenderItem = useCallback( ({ item }) => { return ( {item} ); }, [itemWidth, gap] ); const { contentContainerPaddingHorizontal } = theme.__hd__.cardCarousel.space; return ( contentInset={{ top: 0, left: contentContainerPaddingHorizontal, bottom: 0, right: contentContainerPaddingHorizontal, }} ListHeaderComponent={Platform.select({ android: ( ), })} ListFooterComponent={Platform.select({ android: ( ), })} onLayout={flatListOnLayout} data={items} horizontal showsHorizontalScrollIndicator={false} pagingEnabled bounces={false} scrollEventThrottle={32} snapToAlignment="center" getItemLayout={getItemLayout} ref={carouselRef} renderItem={renderItem} keyExtractor={(_, index) => `${index}`} decelerationRate="fast" renderToHardwareTextureAndroid snapToInterval={Platform.select({ ios: itemWidth, })} onViewableItemsChanged={visibleSlideChanged} viewabilityConfig={{ itemVisiblePercentThreshold: 80, }} /> {!hidePageControl && ( )} ); } ); CardCarousel.displayName = 'CardCarousel';