UNPKG

@react-navigation/bottom-tabs

Version:

Bottom tab navigator following iOS design guidelines

255 lines (252 loc) 9.45 kB
"use strict"; import { getHeaderTitle, Header, SafeAreaProviderCompat, Screen } from '@react-navigation/elements'; import { StackActions } from '@react-navigation/native'; import * as React from 'react'; import { Animated, Platform, StyleSheet } from 'react-native'; import { SafeAreaInsetsContext } from 'react-native-safe-area-context'; import { FadeTransition, ShiftTransition } from "../TransitionConfigs/TransitionPresets.js"; import { BottomTabBarHeightCallbackContext } from "../utils/BottomTabBarHeightCallbackContext.js"; import { BottomTabBarHeightContext } from "../utils/BottomTabBarHeightContext.js"; import { useAnimatedHashMap } from "../utils/useAnimatedHashMap.js"; import { BottomTabBar, getTabBarHeight } from "./BottomTabBar.js"; import { MaybeScreen, MaybeScreenContainer } from "./ScreenFallback.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const EPSILON = 1e-5; const STATE_INACTIVE = 0; const STATE_TRANSITIONING_OR_BELOW_TOP = 1; const STATE_ON_TOP = 2; const NAMED_TRANSITIONS_PRESETS = { fade: FadeTransition, shift: ShiftTransition, none: { sceneStyleInterpolator: undefined, transitionSpec: { animation: 'timing', config: { duration: 0 } } } }; const useNativeDriver = Platform.OS !== 'web'; const hasAnimation = options => { const { animation, transitionSpec } = options; if (animation) { return animation !== 'none'; } return Boolean(transitionSpec); }; const renderTabBarDefault = props => /*#__PURE__*/_jsx(BottomTabBar, { ...props }); export function BottomTabView(props) { const { tabBar = renderTabBarDefault, state, navigation, descriptors, safeAreaInsets, detachInactiveScreens = Platform.OS === 'web' || Platform.OS === 'android' || Platform.OS === 'ios' } = props; const focusedRouteKey = state.routes[state.index].key; /** * List of loaded tabs, tabs will be loaded when navigated to. */ const [loaded, setLoaded] = React.useState([focusedRouteKey]); if (!loaded.includes(focusedRouteKey)) { // Set the current tab to be loaded if it was not loaded before setLoaded([...loaded, focusedRouteKey]); } const previousRouteKeyRef = React.useRef(focusedRouteKey); const tabAnims = useAnimatedHashMap(state); React.useEffect(() => { const previousRouteKey = previousRouteKeyRef.current; let popToTopAction; if (previousRouteKey !== focusedRouteKey && descriptors[previousRouteKey]?.options.popToTopOnBlur) { const prevRoute = state.routes.find(route => route.key === previousRouteKey); if (prevRoute?.state?.type === 'stack' && prevRoute.state.key) { popToTopAction = { ...StackActions.popToTop(), target: prevRoute.state.key }; } } const animateToIndex = () => { if (previousRouteKey !== focusedRouteKey) { navigation.emit({ type: 'transitionStart', target: focusedRouteKey }); } Animated.parallel(state.routes.map((route, index) => { const { options } = descriptors[route.key]; const { animation = 'none', transitionSpec = NAMED_TRANSITIONS_PRESETS[animation].transitionSpec } = options; let spec = transitionSpec; if (route.key !== previousRouteKey && route.key !== focusedRouteKey) { // Don't animate if the screen is not previous one or new one // This will avoid flicker for screens not involved in the transition spec = NAMED_TRANSITIONS_PRESETS.none.transitionSpec; } spec = spec ?? NAMED_TRANSITIONS_PRESETS.none.transitionSpec; const toValue = index === state.index ? 0 : index >= state.index ? 1 : -1; return Animated[spec.animation](tabAnims[route.key], { ...spec.config, toValue, useNativeDriver }); }).filter(Boolean)).start(({ finished }) => { if (finished && popToTopAction) { navigation.dispatch(popToTopAction); } if (previousRouteKey !== focusedRouteKey) { navigation.emit({ type: 'transitionEnd', target: focusedRouteKey }); } }); }; animateToIndex(); previousRouteKeyRef.current = focusedRouteKey; }, [descriptors, focusedRouteKey, navigation, state.index, state.routes, tabAnims]); const dimensions = SafeAreaProviderCompat.initialMetrics.frame; const [tabBarHeight, setTabBarHeight] = React.useState(() => getTabBarHeight({ state, descriptors, dimensions, insets: { ...SafeAreaProviderCompat.initialMetrics.insets, ...props.safeAreaInsets }, style: descriptors[state.routes[state.index].key].options.tabBarStyle })); const renderTabBar = () => { return /*#__PURE__*/_jsx(SafeAreaInsetsContext.Consumer, { children: insets => tabBar({ state: state, descriptors: descriptors, navigation: navigation, insets: { top: safeAreaInsets?.top ?? insets?.top ?? 0, right: safeAreaInsets?.right ?? insets?.right ?? 0, bottom: safeAreaInsets?.bottom ?? insets?.bottom ?? 0, left: safeAreaInsets?.left ?? insets?.left ?? 0 } }) }); }; const { routes } = state; // If there is no animation, we only have 2 states: visible and invisible const hasTwoStates = !routes.some(route => hasAnimation(descriptors[route.key].options)); const { tabBarPosition = 'bottom' } = descriptors[focusedRouteKey].options; const tabBarElement = /*#__PURE__*/_jsx(BottomTabBarHeightCallbackContext.Provider, { value: setTabBarHeight, children: renderTabBar() }, "tabbar"); return /*#__PURE__*/_jsxs(SafeAreaProviderCompat, { style: { flexDirection: tabBarPosition === 'left' || tabBarPosition === 'right' ? 'row' : 'column' }, children: [tabBarPosition === 'top' || tabBarPosition === 'left' ? tabBarElement : null, /*#__PURE__*/_jsx(MaybeScreenContainer, { enabled: detachInactiveScreens, hasTwoStates: hasTwoStates, style: styles.screens, children: routes.map((route, index) => { const descriptor = descriptors[route.key]; const { lazy = true, animation = 'none', sceneStyleInterpolator = NAMED_TRANSITIONS_PRESETS[animation].sceneStyleInterpolator } = descriptor.options; const isFocused = state.index === index; const isPreloaded = state.preloadedRouteKeys.includes(route.key); if (lazy && !loaded.includes(route.key) && !isFocused && !isPreloaded) { // Don't render a lazy screen if we've never navigated to it or it wasn't preloaded return null; } const { freezeOnBlur, header = ({ layout, options }) => /*#__PURE__*/_jsx(Header, { ...options, layout: layout, title: getHeaderTitle(options, route.name) }), headerShown, headerStatusBarHeight, headerTransparent, sceneStyle: customSceneStyle } = descriptor.options; const { sceneStyle } = sceneStyleInterpolator?.({ current: { progress: tabAnims[route.key] } }) ?? {}; const animationEnabled = hasAnimation(descriptor.options); const activityState = isFocused ? STATE_ON_TOP // the screen is on top after the transition : animationEnabled // is animation is not enabled, immediately move to inactive state ? tabAnims[route.key].interpolate({ inputRange: [0, 1 - EPSILON, 1], outputRange: [STATE_TRANSITIONING_OR_BELOW_TOP, // screen visible during transition STATE_TRANSITIONING_OR_BELOW_TOP, STATE_INACTIVE // the screen is detached after transition ], extrapolate: 'extend' }) : STATE_INACTIVE; return /*#__PURE__*/_jsx(MaybeScreen, { style: [StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }], active: activityState, enabled: detachInactiveScreens, freezeOnBlur: freezeOnBlur, shouldFreeze: activityState === STATE_INACTIVE && !isPreloaded, children: /*#__PURE__*/_jsx(BottomTabBarHeightContext.Provider, { value: tabBarPosition === 'bottom' ? tabBarHeight : 0, children: /*#__PURE__*/_jsx(Screen, { focused: isFocused, route: descriptor.route, navigation: descriptor.navigation, headerShown: headerShown, headerStatusBarHeight: headerStatusBarHeight, headerTransparent: headerTransparent, header: header({ layout: dimensions, route: descriptor.route, navigation: descriptor.navigation, options: descriptor.options }), style: [customSceneStyle, animationEnabled && sceneStyle], children: descriptor.render() }) }) }, route.key); }) }, "screens"), tabBarPosition === 'bottom' || tabBarPosition === 'right' ? tabBarElement : null] }); } const styles = StyleSheet.create({ screens: { flex: 1, overflow: 'hidden' } }); //# sourceMappingURL=BottomTabView.js.map