UNPKG

@react-navigation/bottom-tabs

Version:

Bottom tab navigator following iOS design guidelines

266 lines (263 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BottomTabView = BottomTabView; var _elements = require("@react-navigation/elements"); var _native = require("@react-navigation/native"); var React = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeSafeAreaContext = require("react-native-safe-area-context"); var _TransitionPresets = require("../TransitionConfigs/TransitionPresets.js"); var _BottomTabBarHeightCallbackContext = require("../utils/BottomTabBarHeightCallbackContext.js"); var _BottomTabBarHeightContext = require("../utils/BottomTabBarHeightContext.js"); var _useAnimatedHashMap = require("../utils/useAnimatedHashMap.js"); var _BottomTabBar = require("./BottomTabBar.js"); var _ScreenFallback = require("./ScreenFallback.js"); var _jsxRuntime = require("react/jsx-runtime"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } 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: _TransitionPresets.FadeTransition, shift: _TransitionPresets.ShiftTransition, none: { sceneStyleInterpolator: undefined, transitionSpec: { animation: 'timing', config: { duration: 0 } } } }; const useNativeDriver = _reactNative.Platform.OS !== 'web'; const hasAnimation = options => { const { animation, transitionSpec } = options; if (animation) { return animation !== 'none'; } return Boolean(transitionSpec); }; const renderTabBarDefault = props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_BottomTabBar.BottomTabBar, { ...props }); function BottomTabView(props) { const { tabBar = renderTabBarDefault, state, navigation, descriptors, safeAreaInsets, detachInactiveScreens = _reactNative.Platform.OS === 'web' || _reactNative.Platform.OS === 'android' || _reactNative.Platform.OS === 'ios' } = props; const focusedRouteKey = state.routes[state.index].key; const { direction } = (0, _native.useLocale)(); /** * 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 = (0, _useAnimatedHashMap.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 = { ..._native.StackActions.popToTop(), target: prevRoute.state.key }; } } const animateToIndex = () => { if (previousRouteKey !== focusedRouteKey) { navigation.emit({ type: 'transitionStart', target: focusedRouteKey }); } _reactNative.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 _reactNative.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 = _elements.SafeAreaProviderCompat.initialMetrics.frame; const [tabBarHeight, setTabBarHeight] = React.useState(() => (0, _BottomTabBar.getTabBarHeight)({ state, descriptors, dimensions, insets: { ..._elements.SafeAreaProviderCompat.initialMetrics.insets, ...props.safeAreaInsets }, style: descriptors[state.routes[state.index].key].options.tabBarStyle })); const renderTabBar = () => { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeSafeAreaContext.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; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_elements.SafeAreaProviderCompat, { style: { flexDirection: tabBarPosition === 'left' || tabBarPosition === 'right' ? tabBarPosition === 'left' && direction === 'ltr' || tabBarPosition === 'right' && direction === 'rtl' ? 'row-reverse' : 'row' : 'column' }, children: [tabBarPosition === 'top' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_BottomTabBarHeightCallbackContext.BottomTabBarHeightCallbackContext.Provider, { value: setTabBarHeight, children: renderTabBar() }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_ScreenFallback.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__*/(0, _jsxRuntime.jsx)(_elements.Header, { ...options, layout: layout, title: (0, _elements.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__*/(0, _jsxRuntime.jsx)(_ScreenFallback.MaybeScreen, { style: [_reactNative.StyleSheet.absoluteFill, { zIndex: isFocused ? 0 : -1 }], active: activityState, enabled: detachInactiveScreens, freezeOnBlur: freezeOnBlur, shouldFreeze: activityState === STATE_INACTIVE && !isPreloaded, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_BottomTabBarHeightContext.BottomTabBarHeightContext.Provider, { value: tabBarPosition === 'bottom' ? tabBarHeight : 0, children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_elements.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); }) }), tabBarPosition !== 'top' ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_BottomTabBarHeightCallbackContext.BottomTabBarHeightCallbackContext.Provider, { value: setTabBarHeight, children: renderTabBar() }) : null] }); } const styles = _reactNative.StyleSheet.create({ screens: { flex: 1, overflow: 'hidden' } }); //# sourceMappingURL=BottomTabView.js.map