diff --git a/example/src/Screens/StackTransparent.tsx b/example/src/Screens/StackTransparent.tsx index bc4b2eb0..5998c473 100644 --- a/example/src/Screens/StackTransparent.tsx +++ b/example/src/Screens/StackTransparent.tsx @@ -1,10 +1,18 @@ import * as React from 'react'; -import { View, StyleSheet, ScrollView, Platform } from 'react-native'; +import { + View, + StyleSheet, + ScrollView, + Platform, + Pressable, + Animated, +} from 'react-native'; import { Button, Paragraph } from 'react-native-paper'; import { ParamListBase, useTheme } from '@react-navigation/native'; import { createStackNavigator, StackScreenProps, + useCardAnimation, } from '@react-navigation/stack'; import Article from '../Shared/Article'; @@ -49,10 +57,28 @@ const DialogScreen = ({ navigation, }: StackScreenProps) => { const { colors } = useTheme(); + const { current } = useCardAnimation(); return ( - + navigation.goBack()} /> + Mise en place is a French term that literally means “put in place.” It also refers to a way cooks in professional kitchens and restaurants @@ -67,7 +93,7 @@ const DialogScreen = ({ - + ); }; @@ -84,7 +110,7 @@ export default function TransparentStackScreen({ navigation }: Props) { }, [navigation]); return ( - + ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - transform: [ - { - scale: progress.interpolate({ - inputRange: [0, 1], - outputRange: [0.9, 1], - extrapolate: 'clamp', - }), - }, - ], - }, - overlayStyle: { - opacity: progress.interpolate({ - inputRange: [0, 1], - outputRange: [0, 0.5], - extrapolate: 'clamp', - }), - }, - }), + presentation: 'transparentModal', }} /> @@ -146,6 +147,10 @@ const styles = StyleSheet.create({ maxWidth: 400, borderRadius: 3, }, + backdrop: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + }, close: { alignSelf: 'flex-end', }, diff --git a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx index 649531ec..f8907414 100644 --- a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx +++ b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx @@ -361,6 +361,29 @@ export function forBottomSheetAndroid({ }; } +/** + * Simple fade animation for dialogs + */ +export function forFadeFromCenter({ + current: { progress }, +}: StackCardInterpolationProps): StackCardInterpolatedStyle { + return { + cardStyle: { + opacity: progress.interpolate({ + inputRange: [0, 0.5, 0.9, 1], + outputRange: [0, 0.25, 0.7, 1], + }), + }, + overlayStyle: { + opacity: progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.5], + extrapolate: 'clamp', + }), + }, + }; +} + export function forNoAnimation(): StackCardInterpolatedStyle { return {}; } diff --git a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx index 38e14e85..c9548eef 100644 --- a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx +++ b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx @@ -7,6 +7,7 @@ import { forRevealFromBottomAndroid, forFadeFromBottomAndroid, forBottomSheetAndroid, + forFadeFromCenter as forFadeCard, } from './CardStyleInterpolators'; import { forFade } from './HeaderStyleInterpolators'; import { @@ -114,6 +115,19 @@ export const BottomSheetAndroid: TransitionPreset = { headerStyleInterpolator: forFade, }; +/** + * Fade transition for transparent modals. + */ +export const ModalFadeTransition: TransitionPreset = { + gestureDirection: 'vertical', + transitionSpec: { + open: BottomSheetSlideInSpec, + close: BottomSheetSlideOutSpec, + }, + cardStyleInterpolator: forFadeCard, + headerStyleInterpolator: forFade, +}; + /** * Default navigation transition for the current platform. */ diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index cd6710f8..ef585016 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -256,11 +256,16 @@ export type StackNavigationOptions = StackHeaderOptions & * - `modal`: Use Modal animations. This changes a few things: * - Sets `headerMode` to `screen` for the screen unless specified otherwise. * - Changes the screen animation to match the platform behavior for modals. + * - `transparentModal`: Similar to `modal`. This changes following things: + * - Sets `headerMode` to `screen` for the screen unless specified otherwise. + * - Sets background color of the screen to transparent, so previous screen is visible * - Adjusts the `detachPreviousScreen` option so that the previous screen stays rendered. + * - Prevents the previous screen from animating from its last position. + * - Changes the screen animation to a vertical slide animation. * * Defaults to 'card'. */ - presentation?: 'card' | 'modal'; + presentation?: 'card' | 'modal' | 'transparentModal'; /** * Whether transition animation should be enabled the screen. * If you set it to `false`, the screen won't animate when pushing or popping. diff --git a/packages/stack/src/views/Stack/Card.tsx b/packages/stack/src/views/Stack/Card.tsx index 27531e65..14d8a454 100755 --- a/packages/stack/src/views/Stack/Card.tsx +++ b/packages/stack/src/views/Stack/Card.tsx @@ -46,10 +46,10 @@ type Props = ViewProps & { gestureDirection: GestureDirection; onOpen: () => void; onClose: () => void; - onTransition?: (props: { closing: boolean; gesture: boolean }) => void; - onGestureBegin?: () => void; - onGestureCanceled?: () => void; - onGestureEnd?: () => void; + onTransition: (props: { closing: boolean; gesture: boolean }) => void; + onGestureBegin: () => void; + onGestureCanceled: () => void; + onGestureEnd: () => void; children: React.ReactNode; overlay: (props: { style: Animated.WithAnimatedValue>; diff --git a/packages/stack/src/views/Stack/CardContainer.tsx b/packages/stack/src/views/Stack/CardContainer.tsx index 4e9b64f2..156e2ff3 100644 --- a/packages/stack/src/views/Stack/CardContainer.tsx +++ b/packages/stack/src/views/Stack/CardContainer.tsx @@ -34,14 +34,14 @@ type Props = { renderScene: (props: { route: Route }) => React.ReactNode; onOpenRoute: (props: { route: Route }) => void; onCloseRoute: (props: { route: Route }) => void; - onTransitionStart?: ( + onTransitionStart: ( props: { route: Route }, closing: boolean ) => void; - onTransitionEnd?: (props: { route: Route }, closing: boolean) => void; - onGestureStart?: (props: { route: Route }) => void; - onGestureEnd?: (props: { route: Route }) => void; - onGestureCancel?: (props: { route: Route }) => void; + onTransitionEnd: (props: { route: Route }, closing: boolean) => void; + onGestureStart: (props: { route: Route }) => void; + onGestureEnd: (props: { route: Route }) => void; + onGestureCancel: (props: { route: Route }) => void; hasAbsoluteFloatHeader: boolean; headerHeight: number; onHeaderHeightChange: (props: { @@ -49,6 +49,8 @@ type Props = { height: number; }) => void; isParentHeaderShown: boolean; + isNextScreenTransparent: boolean; + detachCurrentScreen: boolean; }; const EPSILON = 0.1; @@ -66,6 +68,8 @@ function CardContainer({ headerHeight, onHeaderHeightChange, isParentHeaderShown, + isNextScreenTransparent, + detachCurrentScreen, interpolationIndex, layout, onCloseRoute, @@ -102,35 +106,35 @@ function CardContainer({ const handleOpen = () => { const { route } = scene.descriptor; - onTransitionEnd?.({ route }, false); + onTransitionEnd({ route }, false); onOpenRoute({ route }); }; const handleClose = () => { const { route } = scene.descriptor; - onTransitionEnd?.({ route }, true); + onTransitionEnd({ route }, true); onCloseRoute({ route }); }; const handleGestureBegin = () => { const { route } = scene.descriptor; - onPageChangeStart?.(); - onGestureStart?.({ route }); + onPageChangeStart(); + onGestureStart({ route }); }; const handleGestureCanceled = () => { const { route } = scene.descriptor; - onPageChangeCancel?.(); - onGestureCancel?.({ route }); + onPageChangeCancel(); + onGestureCancel({ route }); }; const handleGestureEnd = () => { const { route } = scene.descriptor; - onGestureEnd?.({ route }); + onGestureEnd({ route }); }; const handleTransition = ({ @@ -182,7 +186,6 @@ function CardContainer({ const { presentation, - detachPreviousScreen, animationEnabled, cardOverlay, cardOverlayEnabled, @@ -222,7 +225,7 @@ function CardContainer({ insets={insets} gesture={gesture} current={scene.progress.current} - next={scene.progress.next} + next={isNextScreenTransparent ? undefined : scene.progress.next} closing={closing} onOpen={handleOpen} onClose={handleClose} @@ -248,7 +251,15 @@ function CardContainer({ ? { marginTop: headerHeight } : null } - contentStyle={[{ backgroundColor: colors.background }, cardStyle]} + contentStyle={[ + { + backgroundColor: + presentation === 'transparentModal' + ? 'transparent' + : colors.background, + }, + cardStyle, + ]} style={[ { // This is necessary to avoid unfocused larger pages increasing scroll area @@ -258,8 +269,8 @@ function CardContainer({ // Hide unfocused screens when animation isn't enabled // This is also necessary for a11y on web animationEnabled === false && - detachPreviousScreen !== false && - presentation !== 'modal' && + isNextScreenTransparent === false && + detachCurrentScreen !== false && !focused ? 'none' : 'flex', diff --git a/packages/stack/src/views/Stack/CardStack.tsx b/packages/stack/src/views/Stack/CardStack.tsx index 26cb5f4d..80e58116 100755 --- a/packages/stack/src/views/Stack/CardStack.tsx +++ b/packages/stack/src/views/Stack/CardStack.tsx @@ -24,6 +24,7 @@ import CardContainer from './CardContainer'; import { DefaultTransition, ModalTransition, + ModalFadeTransition, } from '../../TransitionConfigs/TransitionPresets'; import { forModalPresentationIOS, @@ -63,9 +64,9 @@ type Props = { closing: boolean ) => void; onTransitionEnd: (props: { route: Route }, closing: boolean) => void; - onGestureStart?: (props: { route: Route }) => void; - onGestureEnd?: (props: { route: Route }) => void; - onGestureCancel?: (props: { route: Route }) => void; + onGestureStart: (props: { route: Route }) => void; + onGestureEnd: (props: { route: Route }) => void; + onGestureCancel: (props: { route: Route }) => void; detachInactiveScreens?: boolean; }; @@ -225,6 +226,8 @@ export default class CardStack extends React.Component { let defaultTransitionPreset = optionsForTransitionConfig.presentation === 'modal' ? ModalTransition + : optionsForTransitionConfig.presentation === 'transparentModal' + ? ModalFadeTransition : DefaultTransition; const { @@ -238,7 +241,8 @@ export default class CardStack extends React.Component { ? forNoAnimationCard : defaultTransitionPreset.cardStyleInterpolator, headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator, - cardOverlayEnabled = Platform.OS !== 'ios' || + cardOverlayEnabled = (Platform.OS !== 'ios' && + optionsForTransitionConfig.presentation !== 'transparentModal') || cardStyleInterpolator === forModalPresentationIOS, } = optionsForTransitionConfig; @@ -246,6 +250,7 @@ export default class CardStack extends React.Component { descriptor.options.headerMode ?? (!( optionsForTransitionConfig.presentation === 'modal' || + optionsForTransitionConfig.presentation === 'transparentModal' || cardStyleInterpolator === forModalPresentationIOS ) && Platform.OS === 'ios' && @@ -461,7 +466,7 @@ export default class CardStack extends React.Component { const { options } = scenes[i].descriptor; const { // By default, we don't want to detach the previous screen of the active one for modals - detachPreviousScreen = options.presentation === 'modal' || + detachPreviousScreen = options.presentation === 'transparentModal' || options.cardStyleInterpolator === forModalPresentationIOS ? i !== scenes.length - 1 : true, @@ -577,6 +582,14 @@ export default class CardStack extends React.Component { interpolationIndex++; } + const isNextScreenTransparent = + scenes[index + 1]?.descriptor.options.presentation === + 'transparentModal'; + + const detachCurrentScreen = + scenes[index + 1]?.descriptor.options.detachPreviousScreen !== + false; + return ( { onCloseRoute={onCloseRoute} onTransitionStart={onTransitionStart} onTransitionEnd={onTransitionEnd} + isNextScreenTransparent={isNextScreenTransparent} + detachCurrentScreen={detachCurrentScreen} /> );