import { useTheme } from '@emotion/react'; import type { ReactElement, ReactNode } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; import { Animated, TouchableWithoutFeedback, View } from 'react-native'; import type { PagerViewOnPageScrollEventData } from 'react-native-pager-view'; import PagerView from 'react-native-pager-view'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { IconName } from '../Icon'; import Typography from '../Typography'; import ActiveTabIndicator from './ActiveTabIndicator'; import SceneView from './SceneView'; import ScrollableTabs from './ScrollableTabs'; import ScrollableTabHeader from './ScrollableTabsHeader/ScrollableTabsHeader'; import { HeaderTab, HeaderTabItem, HeaderTabWrapper, TabContainer, } from './StyledTabs'; import type { BadgeConfigType } from './TabWithBadge'; import TabWithBadge from './TabWithBadge'; import useHandlePageScroll from './useHandlePageScroll'; import { ScreenContext, TabContext, useIsFocused } from './useIsFocused'; export type ItemType = | string | IconName | ((props: { color: string }) => ReactNode); export type TabType = { key: string; activeItem: ItemType; inactiveItem?: ItemType; component: ReactNode; testID?: string; badge?: BadgeConfigType; }; export interface TabsHeaderProps { tabs: TabType[]; selectedTabKey: string; onTabPress: (key: string) => void; barStyle?: StyleProp; insets: { top: number; right: number; bottom: number; left: number }; componentTestID?: string; tabsWidth: number; setTabsWidth: (width: number) => void; positionAnimatedValue: Animated.Value; scrollOffsetAnimatedValue: Animated.Value; } export interface TabsProps extends ViewProps { /** * Callback which is called on tab press, receiving key of upcoming active Tab. */ onTabPress: (key: string) => void; /** * Current selected tab key. */ selectedTabKey: string; /** * List of Tabs to be rendered. Each Tab must have an unique key. */ tabs: TabType[]; /** * Style for the container of Tab. */ containerStyle?: StyleProp; /** * Style for the tab navigation bar. */ barStyle?: StyleProp; /** * Whether inactive screen should be removed and unmounted in * Defaults value is `false`. */ lazy?: boolean; /** * Only work when lazy is `true`. You can specify how many adjacent screens should be preloaded. * Defaults value is `1`. */ lazyPreloadDistance?: number; /** * Boolean indicating whether to enable swipe gestures. Passing `false` will disable swipe gestures, but the user can still switch tabs by pressing the tab bar. * Defaults value is `true`. */ swipeEnabled?: boolean; /** * Testing id of the component. */ testID?: string; /** * Custom header component. */ header?: (props: TabsHeaderProps) => ReactElement; } const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); const getTabItem = ({ item, color, active, }: { item: ItemType; color: string; active: boolean; }) => { if (typeof item === 'string') { return ( {item} ); } return item({ color }); }; const TabsHeader = ({ tabs, selectedTabKey, onTabPress, barStyle, insets, componentTestID, tabsWidth, setTabsWidth, positionAnimatedValue, scrollOffsetAnimatedValue, }: TabsHeaderProps) => { const theme = useTheme(); return ( { const { width } = e.nativeEvent.layout; if (tabsWidth !== width) { setTabsWidth(width); } }} > {tabs.map((tab) => { const { key, testID, activeItem, inactiveItem: originalInactiveItem, badge, } = tab; const active = selectedTabKey === key; const inactiveItem = originalInactiveItem ?? activeItem; const tabItem = getTabItem({ item: active ? activeItem : inactiveItem, color: theme.__hd__.tabs.colors.text, active, }); return ( { onTabPress(key); }} testID={testID} > ); })} ); }; const Tabs = ({ onTabPress, selectedTabKey, tabs, containerStyle, barStyle, lazy = false, lazyPreloadDistance = 1, swipeEnabled = true, testID: componentTestID, header, }: TabsProps): ReactElement => { const insets = useSafeAreaInsets(); const pagerViewRef = useRef(null); const selectedTabIndex = tabs.findIndex( (item) => item.key === selectedTabKey ); const scrollOffsetAnimatedValue = useRef(new Animated.Value(0)).current; const positionAnimatedValue = useRef(new Animated.Value(0)).current; const [tabsWidth, setTabsWidth] = useState(0); const { onPageScrollStateChanged, hasScrolled } = useHandlePageScroll(); useEffect(() => { if (selectedTabIndex !== -1) { pagerViewRef.current?.setPage(selectedTabIndex); } }, [selectedTabIndex]); const tabContextProviderValue = useMemo( () => ({ selectedTabKey, }), [selectedTabKey] ); const headerProps = useMemo( () => ({ tabs, selectedTabKey, onTabPress, barStyle, insets, componentTestID, tabsWidth, setTabsWidth, positionAnimatedValue, scrollOffsetAnimatedValue, }), [ tabs, selectedTabKey, onTabPress, barStyle, insets, componentTestID, tabsWidth, setTabsWidth, positionAnimatedValue, scrollOffsetAnimatedValue, ] ); return ( {header ? header(headerProps) : } { const index = e.nativeEvent.position; const selectedItem = tabs[index]; if (hasScrolled.current && selectedItem) { onTabPress(selectedItem.key); } }} onPageScrollStateChanged={onPageScrollStateChanged} onPageScroll={Animated.event( [ { nativeEvent: { offset: scrollOffsetAnimatedValue, position: positionAnimatedValue, }, }, ], { useNativeDriver: true, } )} scrollEnabled={swipeEnabled} style={{ flex: 1 }} > {tabs.map((tab, index) => { const { key, component, testID } = tab; return ( {component} ); })} ); }; export default Object.assign(Tabs, { Header: TabsHeader, Scroll: ScrollableTabs, ScrollHeader: ScrollableTabHeader, useIsFocused, });