'use client'; import React from 'react'; import { Animated, View, Platform } from 'react-native'; import TransitionProgressContext from '../TransitionProgressContext'; import DelayedFreeze from './helpers/DelayedFreeze'; import { ScreenProps } from '../types'; import { freezeEnabled, isNativePlatformSupported, screensEnabled, } from '../core'; // Native components import ScreenNativeComponent, { NativeProps as ScreenNativeComponentProps, } from '../fabric/ScreenNativeComponent'; import ModalScreenNativeComponent, { NativeProps as ModalScreenNativeComponentProps, } from '../fabric/ModalScreenNativeComponent'; import { usePrevious } from './helpers/usePrevious'; import { EDGE_TO_EDGE, transformEdgeToEdgeProps } from './helpers/edge-to-edge'; import { SHEET_DIMMED_ALWAYS, resolveSheetAllowedDetents, resolveSheetInitialDetentIndex, resolveSheetLargestUndimmedDetent, } from './helpers/sheet'; type NativeProps = ScreenNativeComponentProps | ModalScreenNativeComponentProps; const AnimatedNativeScreen = Animated.createAnimatedComponent( ScreenNativeComponent, ); const AnimatedNativeModalScreen = Animated.createAnimatedComponent( ModalScreenNativeComponent, ); // Incomplete type, all accessible properties available at: // react-native/Libraries/Components/View/ReactNativeViewViewConfig.js interface ViewConfig extends View { viewConfig: { validAttributes: { style: { display: boolean | null; }; }; }; _viewConfig: { validAttributes: { style: { display: boolean | null; }; }; }; } export const InnerScreen = React.forwardRef( function InnerScreen(props, ref) { const innerRef = React.useRef(null); React.useImperativeHandle(ref, () => innerRef.current!, []); const prevActivityState = usePrevious(props.activityState); const setRef = (ref: ViewConfig) => { innerRef.current = ref; props.onComponentRef?.(ref); }; const closing = React.useRef(new Animated.Value(0)).current; const progress = React.useRef(new Animated.Value(0)).current; const goingForward = React.useRef(new Animated.Value(0)).current; const { enabled = screensEnabled(), freezeOnBlur = freezeEnabled(), shouldFreeze, ...rest } = props; // To maintain default behavior of formSheet stack presentation style and to have reasonable // defaults for new medium-detent iOS API we need to set defaults here const { // formSheet presentation related props sheetAllowedDetents = [1.0], sheetLargestUndimmedDetentIndex = SHEET_DIMMED_ALWAYS, sheetGrabberVisible = false, sheetCornerRadius = -1.0, sheetExpandsWhenScrolledToEdge = true, sheetElevation = 24, sheetInitialDetentIndex = 0, // Other screenId, stackPresentation, // Events for override onAppear, onDisappear, onWillAppear, onWillDisappear, } = rest; if (enabled && isNativePlatformSupported) { const resolvedSheetAllowedDetents = resolveSheetAllowedDetents(sheetAllowedDetents); const resolvedSheetLargestUndimmedDetent = resolveSheetLargestUndimmedDetent( sheetLargestUndimmedDetentIndex, resolvedSheetAllowedDetents.length - 1, ); const resolvedSheetInitialDetentIndex = resolveSheetInitialDetentIndex( sheetInitialDetentIndex, resolvedSheetAllowedDetents.length - 1, ); // Due to how Yoga resolves layout, we need to have different components for modal nad non-modal screens (there is a need for different // shadow nodes). const shouldUseModalScreenComponent = Platform.select({ ios: !( stackPresentation === undefined || stackPresentation === 'push' || stackPresentation === 'containedModal' || stackPresentation === 'containedTransparentModal' ), android: false, default: false, }); const AnimatedScreen = shouldUseModalScreenComponent ? AnimatedNativeModalScreen : AnimatedNativeScreen; let { // Filter out active prop in this case because it is unused and // can cause problems depending on react-native version: // https://github.com/react-navigation/react-navigation/issues/4886 active, activityState, children, isNativeStack, gestureResponseDistance, onGestureCancel, style, ...props } = rest; if (active !== undefined && activityState === undefined) { console.warn( 'It appears that you are using old version of react-navigation library. Please update @react-navigation/bottom-tabs, @react-navigation/stack and @react-navigation/drawer to version 5.10.0 or above to take full advantage of new functionality added to react-native-screens', ); activityState = active !== 0 ? 2 : 0; // in the new version, we need one of the screens to have value of 2 after the transition } if ( isNativeStack && prevActivityState !== undefined && activityState !== undefined ) { if (prevActivityState > activityState) { throw new Error( '[RNScreens] activityState cannot be decreased in NativeStack', ); } } const handleRef = (ref: ViewConfig) => { // Workaround is necessary to prevent React Native from hiding frozen screens. // See this PR: https://github.com/grahammendick/navigation/pull/860 if (ref?.viewConfig?.validAttributes?.style) { ref.viewConfig.validAttributes.style = { ...ref.viewConfig.validAttributes.style, display: null, }; setRef(ref); } else if (ref?._viewConfig?.validAttributes?.style) { ref._viewConfig.validAttributes.style = { ...ref._viewConfig.validAttributes.style, display: null, }; setRef(ref); } }; const freeze = freezeOnBlur && (shouldFreeze !== undefined ? shouldFreeze : activityState === 0); return ( { // for internal use }) } // // Hierarchy of screens is handled on the native side and setting zIndex value causes this issue: // https://github.com/software-mansion/react-native-screens/issues/2345 // With below change of zIndex, we force RN diffing mechanism to NOT include detaching and attaching mutation in one transaction. // Detailed information can be found here https://github.com/software-mansion/react-native-screens/pull/2351 style={[style, { zIndex: undefined }]} activityState={activityState} screenId={screenId} sheetAllowedDetents={resolvedSheetAllowedDetents} sheetLargestUndimmedDetent={resolvedSheetLargestUndimmedDetent} sheetElevation={sheetElevation} sheetGrabberVisible={sheetGrabberVisible} sheetCornerRadius={sheetCornerRadius} sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge} sheetInitialDetent={resolvedSheetInitialDetentIndex} gestureResponseDistance={{ start: gestureResponseDistance?.start ?? -1, end: gestureResponseDistance?.end ?? -1, top: gestureResponseDistance?.top ?? -1, bottom: gestureResponseDistance?.bottom ?? -1, }} // This prevents showing blank screen when navigating between multiple screens with freezing // https://github.com/software-mansion/react-native-screens/pull/1208 ref={handleRef} onTransitionProgress={ !isNativeStack ? undefined : Animated.event( [ { nativeEvent: { progress, closing, goingForward, }, }, ], { useNativeDriver: true }, ) }> {!isNativeStack ? ( // see comment of this prop in types.tsx for information why it is needed children ) : ( {children} )} ); } else { // same reason as above let { active, activityState, style, // eslint-disable-next-line @typescript-eslint/no-unused-vars onComponentRef, ...props } = rest; if (active !== undefined && activityState === undefined) { activityState = active !== 0 ? 2 : 0; } return ( ); } }, ); // context to be used when the user wants to use enhanced implementation // e.g. to use `useReanimatedTransitionProgress` (see `reanimated` folder in repo) export const ScreenContext = React.createContext(InnerScreen); const Screen = React.forwardRef((props, ref) => { const ScreenWrapper = React.useContext(ScreenContext) || InnerScreen; return ( ); }); Screen.displayName = 'Screen'; export default Screen;