import { getHeaderTitle, HeaderBackContext, HeaderHeightContext, HeaderShownContext, } from '@react-navigation/elements'; import { Route, useTheme } from '@react-navigation/native'; import * as React from 'react'; import { Animated, StyleSheet, View } from 'react-native'; import type { Layout, Scene } from '../../types'; import ModalPresentationContext from '../../utils/ModalPresentationContext'; import useKeyboardManager from '../../utils/useKeyboardManager'; import type { Props as HeaderContainerProps } from '../Header/HeaderContainer'; import Card from './Card'; type Props = { interpolationIndex: number; index: number; active: boolean; focused: boolean; closing: boolean; modal: boolean; layout: Layout; gesture: Animated.Value; scene: Scene; headerDarkContent: boolean | undefined; safeAreaInsetTop: number; safeAreaInsetRight: number; safeAreaInsetBottom: number; safeAreaInsetLeft: number; getPreviousScene: (props: { route: Route<string> }) => Scene | undefined; getFocusedRoute: () => Route<string>; renderHeader: (props: HeaderContainerProps) => React.ReactNode; renderScene: (props: { route: Route<string> }) => React.ReactNode; onOpenRoute: (props: { route: Route<string> }) => void; onCloseRoute: (props: { route: Route<string> }) => void; onTransitionStart: ( props: { route: Route<string> }, closing: boolean ) => void; onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void; onGestureStart: (props: { route: Route<string> }) => void; onGestureEnd: (props: { route: Route<string> }) => void; onGestureCancel: (props: { route: Route<string> }) => void; hasAbsoluteFloatHeader: boolean; headerHeight: number; onHeaderHeightChange: (props: { route: Route<string>; height: number; }) => void; isParentHeaderShown: boolean; isNextScreenTransparent: boolean; detachCurrentScreen: boolean; }; const EPSILON = 0.1; function CardContainer({ interpolationIndex, index, active, closing, gesture, focused, modal, getPreviousScene, getFocusedRoute, headerDarkContent, hasAbsoluteFloatHeader, headerHeight, onHeaderHeightChange, isParentHeaderShown, isNextScreenTransparent, detachCurrentScreen, layout, onCloseRoute, onOpenRoute, onGestureCancel, onGestureEnd, onGestureStart, onTransitionEnd, onTransitionStart, renderHeader, renderScene, safeAreaInsetBottom, safeAreaInsetLeft, safeAreaInsetRight, safeAreaInsetTop, scene, }: Props) { const parentHeaderHeight = React.useContext(HeaderHeightContext); const { onPageChangeStart, onPageChangeCancel, onPageChangeConfirm } = useKeyboardManager( React.useCallback(() => { const { options, navigation } = scene.descriptor; return ( navigation.isFocused() && options.keyboardHandlingEnabled !== false ); }, [scene.descriptor]) ); const handleOpen = () => { const { route } = scene.descriptor; onTransitionEnd({ route }, false); onOpenRoute({ route }); }; const handleClose = () => { const { route } = scene.descriptor; onTransitionEnd({ route }, true); onCloseRoute({ route }); }; const handleGestureBegin = () => { const { route } = scene.descriptor; onPageChangeStart(); onGestureStart({ route }); }; const handleGestureCanceled = () => { const { route } = scene.descriptor; onPageChangeCancel(); onGestureCancel({ route }); }; const handleGestureEnd = () => { const { route } = scene.descriptor; onGestureEnd({ route }); }; const handleTransition = ({ closing, gesture, }: { closing: boolean; gesture: boolean; }) => { const { route } = scene.descriptor; if (!gesture) { onPageChangeConfirm?.(true); } else if (active && closing) { onPageChangeConfirm?.(false); } else { onPageChangeCancel?.(); } onTransitionStart?.({ route }, closing); }; const insets = { top: safeAreaInsetTop, right: safeAreaInsetRight, bottom: safeAreaInsetBottom, left: safeAreaInsetLeft, }; const { colors } = useTheme(); const [pointerEvents, setPointerEvents] = React.useState<'box-none' | 'none'>( 'box-none' ); React.useEffect(() => { const listener = scene.progress.next?.addListener?.( ({ value }: { value: number }) => { setPointerEvents(value <= EPSILON ? 'box-none' : 'none'); } ); return () => { if (listener) { scene.progress.next?.removeListener?.(listener); } }; }, [pointerEvents, scene.progress.next]); const { presentation, animationEnabled, cardOverlay, cardOverlayEnabled, cardShadowEnabled, cardStyle, cardStyleInterpolator, gestureDirection, gestureEnabled, gestureResponseDistance, gestureVelocityImpact, headerMode, headerShown, transitionSpec, } = scene.descriptor.options; const previousScene = getPreviousScene({ route: scene.descriptor.route }); let backTitle: string | undefined; if (previousScene) { const { options, route } = previousScene.descriptor; backTitle = getHeaderTitle(options, route.name); } const headerBack = React.useMemo( () => (backTitle !== undefined ? { title: backTitle } : undefined), [backTitle] ); return ( <Card interpolationIndex={interpolationIndex} gestureDirection={gestureDirection} layout={layout} insets={insets} gesture={gesture} current={scene.progress.current} next={scene.progress.next} closing={closing} onOpen={handleOpen} onClose={handleClose} overlay={cardOverlay} overlayEnabled={cardOverlayEnabled} shadowEnabled={cardShadowEnabled} onTransition={handleTransition} onGestureBegin={handleGestureBegin} onGestureCanceled={handleGestureCanceled} onGestureEnd={handleGestureEnd} gestureEnabled={index === 0 ? false : gestureEnabled} gestureResponseDistance={gestureResponseDistance} gestureVelocityImpact={gestureVelocityImpact} transitionSpec={transitionSpec} styleInterpolator={cardStyleInterpolator} accessibilityElementsHidden={!focused} importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'} pointerEvents={active ? 'box-none' : pointerEvents} pageOverflowEnabled={headerMode !== 'float' && presentation !== 'modal'} headerDarkContent={headerDarkContent} containerStyle={ hasAbsoluteFloatHeader && headerMode !== 'screen' ? { marginTop: headerHeight } : null } contentStyle={[ { backgroundColor: presentation === 'transparentModal' ? 'transparent' : colors.background, }, cardStyle, ]} style={[ { // This is necessary to avoid unfocused larger pages increasing scroll area // The issue can be seen on the web when a smaller screen is pushed over a larger one overflow: active ? undefined : 'hidden', display: // Hide unfocused screens when animation isn't enabled // This is also necessary for a11y on web animationEnabled === false && isNextScreenTransparent === false && detachCurrentScreen !== false && !focused ? 'none' : 'flex', }, StyleSheet.absoluteFill, ]} > <View style={styles.container}> <ModalPresentationContext.Provider value={modal}> <View style={styles.scene}> <HeaderBackContext.Provider value={headerBack}> <HeaderShownContext.Provider value={isParentHeaderShown || headerShown !== false} > <HeaderHeightContext.Provider value={headerShown ? headerHeight : parentHeaderHeight ?? 0} > {renderScene({ route: scene.descriptor.route })} </HeaderHeightContext.Provider> </HeaderShownContext.Provider> </HeaderBackContext.Provider> </View> {headerMode !== 'float' ? renderHeader({ mode: 'screen', layout, scenes: [previousScene, scene], getPreviousScene, getFocusedRoute, onContentHeightChange: onHeaderHeightChange, }) : null} </ModalPresentationContext.Provider> </View> </Card> ); } export default React.memo(CardContainer); const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column-reverse', }, scene: { flex: 1, }, });