import type { Dispatch, ReactElement, SetStateAction } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import type { StyleProp, ViewProps, ViewStyle, ViewToken, LayoutChangeEvent, } from 'react-native'; import { Animated, FlatList, View } from 'react-native'; import { useTheme } from '../../theme'; import { useDeprecation } from '../../utils/hooks'; import { CardCarousel } from './CardCarousel'; import CarouselItem from './CarouselItem'; import { StyledBackDrop, StyledCarouselFooterWrapper, StyledCarouselView, StyledPageControl, StyledPageControlWrapper, } from './StyledCarousel'; import type { CarouselData } from './types'; interface CarouselProps extends ViewProps { /** * Additional style. */ style?: StyleProp; /** * Testing id of the component. */ testID?: string; /** * onItemIndexChange event handler receiving index of selected Item. */ onItemIndexChange?: Dispatch>; /** * Carousel data. */ items: CarouselData[]; /** * Render action elements function. */ renderActions?: (pageIndex: number) => ReactElement; /** * @deprecated will be removed in 9.0.0 * * Should show paginations */ shouldShowPagination?: (pageIndex: number) => boolean; /** * Current selected item index. */ selectedItemIndex?: number; /** * Position of the page control * * pageControlPosition['bottom'] - @deprecated */ pageControlPosition?: 'top' | 'bottom'; } function useStateFromProp( initialValue: T ): [T, Dispatch>] { const [value, setValue] = useState(initialValue); useEffect(() => setValue(initialValue), [initialValue]); return [value, setValue]; } const noop = (_: number): boolean => { return true; }; const Carousel = ({ items, onItemIndexChange, renderActions, selectedItemIndex = 0, style, shouldShowPagination = noop, testID, pageControlPosition = 'top', ...nativeProps }: CarouselProps) => { useDeprecation( `shouldShowPagination prop has been deprecated`, shouldShowPagination !== noop ); useDeprecation( `The use of 'pageControlPosition == bottom' has been deprecated`, pageControlPosition === 'bottom' ); const theme = useTheme(); const carouselRef = useRef(null); const [currentSlideIndex, setCurrentSlideIndex] = useStateFromProp(selectedItemIndex); const shouldRenderPagination = items.length > 1 && shouldShowPagination(currentSlideIndex); const internalOnItemIndexChange = useCallback( (index: number) => { setCurrentSlideIndex(index); if (onItemIndexChange) { onItemIndexChange(index); } }, [setCurrentSlideIndex, onItemIndexChange] ); const [flatListWidth, setFlatListWidth] = useState(0); const itemWidth = flatListWidth; const flatListOnLayout = useCallback( (e: LayoutChangeEvent) => { setFlatListWidth(e.nativeEvent.layout.width); }, [setFlatListWidth] ); const width = flatListWidth; const scrollX = useRef(new Animated.Value(0)).current; useEffect(() => { // must use setTimeout since when layout is mounted, the pagination dots are not set correct scrollX const handle = setTimeout(() => { scrollX.setValue(currentSlideIndex * width); carouselRef.current?.scrollToOffset({ offset: currentSlideIndex * width, animated: true, }); }, 100); return () => { clearTimeout(handle); }; }, [currentSlideIndex, carouselRef, scrollX, width]); const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current; const onViewCallBack = useRef( (info: { viewableItems: Array; changed: Array }) => { const firstVisibleItem = info.viewableItems.find( (view) => view.index != null && view.isViewable ); if (firstVisibleItem) { internalOnItemIndexChange(firstVisibleItem.index || 0); } } ); return ( {pageControlPosition === 'top' && shouldRenderPagination && ( )} onLayout={flatListOnLayout} testID={testID ? `${testID}_flatlist` : undefined} horizontal showsHorizontalScrollIndicator={false} pagingEnabled bounces={false} data={items} onViewableItemsChanged={onViewCallBack.current} viewabilityConfig={viewConfig} scrollEventThrottle={32} ref={carouselRef} onScroll={Animated.event( [{ nativeEvent: { contentOffset: { x: scrollX } } }], { useNativeDriver: false } )} renderItem={({ item }) => { if (!item) return null; const { image, heading, body, content } = item; return ( ); }} getItemLayout={(_, index) => ({ length: itemWidth, offset: itemWidth * index, index, })} /> {renderActions && renderActions(currentSlideIndex)} {pageControlPosition === 'bottom' && shouldRenderPagination && ( )} ); }; export default Object.assign(Carousel, { Card: CardCarousel, });