UNPKG

@react-navigation/bottom-tabs

Version:

Bottom tab navigator following iOS design guidelines

169 lines (164 loc) 6.68 kB
"use strict"; import { getDefaultHeaderHeight, HeaderHeightContext, HeaderShownContext, useFrameSize } from '@react-navigation/elements'; import * as React from 'react'; import { Animated, Platform, StatusBar, StyleSheet, useAnimatedValue, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ScreenStack, ScreenStackItem } from 'react-native-screens'; import { debounce } from "./debounce.js"; import { AnimatedHeaderHeightContext } from "./useAnimatedHeaderHeight.js"; import { useHeaderConfig } from "./useHeaderConfig.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const ANDROID_DEFAULT_HEADER_HEIGHT = 56; export function NativeScreen({ route, navigation, options, children }) { const { header: renderCustomHeader, headerShown = renderCustomHeader != null, headerTransparent, headerBackground } = options; const isModal = false; const insets = useSafeAreaInsets(); // Modals are fullscreen in landscape only on iPhone const isIPhone = Platform.OS === 'ios' && !(Platform.isPad || Platform.isTV); const isParentHeaderShown = React.useContext(HeaderShownContext); const parentHeaderHeight = React.useContext(HeaderHeightContext); const isLandscape = useFrameSize(frame => frame.width > frame.height); const topInset = isParentHeaderShown || Platform.OS === 'ios' && isModal || isIPhone && isLandscape ? 0 : insets.top; const defaultHeaderHeight = useFrameSize(frame => Platform.select({ // FIXME: Currently screens isn't using Material 3 // So our `getDefaultHeaderHeight` doesn't return the correct value // So we hardcode the value here for now until screens is updated android: ANDROID_DEFAULT_HEADER_HEIGHT + topInset, default: getDefaultHeaderHeight(frame, isModal, topInset) })); const [headerHeight, setHeaderHeight] = React.useState(defaultHeaderHeight); // eslint-disable-next-line react-hooks/exhaustive-deps const setHeaderHeightDebounced = React.useCallback( // Debounce the header height updates to avoid excessive re-renders debounce(setHeaderHeight, 100), []); const hasCustomHeader = renderCustomHeader != null; let headerHeightCorrectionOffset = 0; if (Platform.OS === 'android' && !hasCustomHeader) { const statusBarHeight = StatusBar.currentHeight ?? 0; // FIXME: On Android, the native header height is not correctly calculated // It includes status bar height even if statusbar is not translucent // And the statusbar value itself doesn't match the actual status bar height // So we subtract the bogus status bar height and add the actual top inset headerHeightCorrectionOffset = -statusBarHeight + topInset; } const rawAnimatedHeaderHeight = useAnimatedValue(defaultHeaderHeight); const animatedHeaderHeight = React.useMemo(() => Animated.add(rawAnimatedHeaderHeight, headerHeightCorrectionOffset), [headerHeightCorrectionOffset, rawAnimatedHeaderHeight]); const headerTopInsetEnabled = topInset !== 0; const onHeaderHeightChange = Animated.event([{ nativeEvent: { headerHeight: rawAnimatedHeaderHeight } }], { useNativeDriver: true, listener: e => { if (hasCustomHeader) { // If we have a custom header, don't use native header height return; } if (Platform.OS === 'android' && (options.headerBackground != null || options.headerTransparent)) { // FIXME: On Android, we get 0 if the header is translucent // So we set a default height in that case setHeaderHeight(ANDROID_DEFAULT_HEADER_HEIGHT + topInset); return; } if (e.nativeEvent && typeof e.nativeEvent === 'object' && 'headerHeight' in e.nativeEvent && typeof e.nativeEvent.headerHeight === 'number') { const headerHeight = e.nativeEvent.headerHeight + headerHeightCorrectionOffset; // Only debounce if header has large title or search bar // As it's the only case where the header height can change frequently const doesHeaderAnimate = Platform.OS === 'ios' && (options.headerLargeTitleEnabled || options.headerSearchBarOptions); if (doesHeaderAnimate) { setHeaderHeightDebounced(headerHeight); } else { setHeaderHeight(headerHeight); } } } }); const headerConfig = useHeaderConfig({ ...options, route, headerHeight, headerShown: hasCustomHeader ? false : headerShown === true, headerTopInsetEnabled }); return /*#__PURE__*/_jsx(ScreenStack, { style: styles.container, children: /*#__PURE__*/_jsx(ScreenStackItem, { screenId: route.key // Needed to show search bar in tab bar with systemItem=search , stackPresentation: "push", headerConfig: headerConfig, onHeaderHeightChange: onHeaderHeightChange, children: /*#__PURE__*/_jsx(AnimatedHeaderHeightContext.Provider, { value: animatedHeaderHeight, children: /*#__PURE__*/_jsxs(HeaderHeightContext.Provider, { value: headerShown ? headerHeight : parentHeaderHeight ?? 0, children: [headerBackground != null ? /*#__PURE__*/ /** * To show a custom header background, we render it at the top of the screen below the header * The header also needs to be positioned absolutely (with `translucent` style) */ _jsx(View, { style: [styles.background, headerTransparent ? styles.translucent : null, { height: headerHeight }], children: headerBackground() }) : null, hasCustomHeader && headerShown ? /*#__PURE__*/_jsx(View, { onLayout: e => { const headerHeight = e.nativeEvent.layout.height; setHeaderHeight(headerHeight); rawAnimatedHeaderHeight.setValue(headerHeight); }, style: [styles.header, headerTransparent ? styles.absolute : null], children: renderCustomHeader?.({ route, navigation, options }) }) : null, /*#__PURE__*/_jsx(HeaderShownContext.Provider, { value: isParentHeaderShown || headerShown, children: children })] }) }) }) }); } const styles = StyleSheet.create({ container: { flex: 1 }, header: { zIndex: 1 }, absolute: { position: 'absolute', top: 0, start: 0, end: 0 }, translucent: { position: 'absolute', top: 0, start: 0, end: 0, zIndex: 1, elevation: 1 }, background: { overflow: 'hidden' } }); //# sourceMappingURL=NativeScreen.js.map