diff --git a/packages/elements/src/Header/Header.tsx b/packages/elements/src/Header/Header.tsx index 7bc12eb0..501b6806 100644 --- a/packages/elements/src/Header/Header.tsx +++ b/packages/elements/src/Header/Header.tsx @@ -12,6 +12,10 @@ import HeaderShownContext from './HeaderShownContext'; import HeaderTitle from './HeaderTitle'; type Props = HeaderOptions & { + /** + * Whether the header is in a modal + */ + modal?: boolean; /** * Layout of the screen. */ @@ -46,6 +50,7 @@ export default function Header(props: Props) { const { layout = frame, + modal = false, title, headerTitle: customTitle, headerTitleAlign = Platform.select({ @@ -69,7 +74,11 @@ export default function Header(props: Props) { headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top, } = props; - const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight); + const defaultHeight = getDefaultHeaderHeight( + layout, + modal, + headerStatusBarHeight + ); const { height = defaultHeight, diff --git a/packages/elements/src/Header/getDefaultHeaderHeight.tsx b/packages/elements/src/Header/getDefaultHeaderHeight.tsx index a22da0cd..518e7b64 100644 --- a/packages/elements/src/Header/getDefaultHeaderHeight.tsx +++ b/packages/elements/src/Header/getDefaultHeaderHeight.tsx @@ -4,17 +4,30 @@ import type { Layout } from '../types'; export default function getDefaultHeaderHeight( layout: Layout, + modal: boolean, statusBarHeight: number ): number { - const isLandscape = layout.width > layout.height; - let headerHeight; + const isLandscape = layout.width > layout.height; + if (Platform.OS === 'ios') { - if (isLandscape && !Platform.isPad) { - headerHeight = 32; + if (Platform.isPad) { + if (modal) { + headerHeight = 56; + } else { + headerHeight = 50; + } } else { - headerHeight = 44; + if (isLandscape) { + headerHeight = 32; + } else { + if (modal) { + headerHeight = 56; + } else { + headerHeight = 44; + } + } } } else if (Platform.OS === 'android') { headerHeight = 56; diff --git a/packages/elements/src/Screen.tsx b/packages/elements/src/Screen.tsx index af7cb123..782e37c2 100644 --- a/packages/elements/src/Screen.tsx +++ b/packages/elements/src/Screen.tsx @@ -19,6 +19,7 @@ import HeaderShownContext from './Header/HeaderShownContext'; type Props = { focused: boolean; + modal?: boolean; navigation: NavigationProp; route: RouteProp; header: React.ReactNode; @@ -37,6 +38,7 @@ export default function Screen(props: Props) { const { focused, + modal = false, header, headerShown = true, headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top, @@ -47,7 +49,7 @@ export default function Screen(props: Props) { } = props; const [headerHeight, setHeaderHeight] = React.useState(() => - getDefaultHeaderHeight(dimensions, headerStatusBarHeight) + getDefaultHeaderHeight(dimensions, modal, headerStatusBarHeight) ); return ( diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index d452f39b..50f4c893 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -80,6 +80,10 @@ type SceneOptionsDefaults = TransitionPreset & { }; export type Scene = { + /** + * Route object for the current screen. + */ + route: Route; /** * Descriptor object for the screen. */ diff --git a/packages/stack/src/views/Header/Header.tsx b/packages/stack/src/views/Header/Header.tsx index 99123c97..eb82e01d 100644 --- a/packages/stack/src/views/Header/Header.tsx +++ b/packages/stack/src/views/Header/Header.tsx @@ -62,6 +62,7 @@ export default React.memo(function Header({ progress={progress} insets={insets} layout={layout} + modal={isModal} headerBackTitle={ options.headerBackTitle !== undefined ? options.headerBackTitle diff --git a/packages/stack/src/views/Header/HeaderSegment.tsx b/packages/stack/src/views/Header/HeaderSegment.tsx index eff23080..d8f560de 100644 --- a/packages/stack/src/views/Header/HeaderSegment.tsx +++ b/packages/stack/src/views/Header/HeaderSegment.tsx @@ -26,6 +26,7 @@ type Props = StackHeaderOptions & { layout: Layout; title: string; insets: EdgeInsets; + modal: boolean; onGoBack?: () => void; progress: SceneProgress; styleInterpolator: StackHeaderStyleInterpolator; @@ -99,6 +100,7 @@ export default function HeaderSegment(props: Props) { progress, insets, layout, + modal, onGoBack, headerTitle: title, headerLeft: left, @@ -120,7 +122,11 @@ export default function HeaderSegment(props: Props) { ...rest } = props; - const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight); + const defaultHeight = getDefaultHeaderHeight( + layout, + modal, + headerStatusBarHeight + ); const { height = defaultHeight } = StyleSheet.flatten( customHeaderStyle || {} @@ -172,6 +178,7 @@ export default function HeaderSegment(props: Props) { return (
layout.width; const scale = 1 - 20 / layout.width; const offset = (insets.top - 34) * scale; @@ -31,10 +30,6 @@ export default function ModalStatusBarManager({ )?.translateY; React.useEffect(() => { - if (!enabled) { - return; - } - const listener = ({ value }: { value: number }) => { setOverlapping(value < offset); }; @@ -42,11 +37,7 @@ export default function ModalStatusBarManager({ const sub = translateY?.addListener(listener); return () => translateY?.removeListener(sub); - }, [enabled, offset, translateY]); - - if (!enabled) { - return null; - } + }, [offset, translateY]); const darkContent = dark ?? !darkTheme; diff --git a/packages/stack/src/views/Stack/CardStack.tsx b/packages/stack/src/views/Stack/CardStack.tsx index 31aa0654..e1ab0f44 100755 --- a/packages/stack/src/views/Stack/CardStack.tsx +++ b/packages/stack/src/views/Stack/CardStack.tsx @@ -87,28 +87,56 @@ const STATE_ON_TOP = 2; const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} }); +const getInterpolationIndex = (scenes: Scene[], index: number) => { + const { cardStyleInterpolator } = scenes[index].descriptor.options; + + // Start from current card and count backwards the number of cards with same interpolation + let interpolationIndex = 0; + + for (let i = index - 1; i >= 0; i--) { + const cardStyleInterpolatorCurrent = + scenes[i]?.descriptor.options.cardStyleInterpolator; + + if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) { + break; + } + + interpolationIndex++; + } + + return interpolationIndex; +}; + const getHeaderHeights = ( - routes: Route[], + scenes: Scene[], insets: EdgeInsets, isParentHeaderShown: boolean, - descriptors: StackDescriptorMap, layout: Layout, previous: Record ) => { - return routes.reduce>((acc, curr) => { - const { options = {} } = descriptors[curr.key] || {}; - const style: any = StyleSheet.flatten(options.headerStyle || {}); + return scenes.reduce>((acc, curr, index) => { + const { + headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top, + cardStyleInterpolator, + headerStyle, + } = curr.descriptor.options; + + const style = StyleSheet.flatten(headerStyle || {}); const height = - typeof style.height === 'number' ? style.height : previous[curr.key]; + typeof style.height === 'number' + ? style.height + : previous[curr.route.key]; - const { headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top } = - options; + const interpolationIndex = getInterpolationIndex(scenes, index); + const isModalPresentation = + cardStyleInterpolator === forModalPresentationIOS; + const isModal = isModalPresentation && interpolationIndex !== 0; - acc[curr.key] = + acc[curr.route.key] = typeof height === 'number' ? height - : getDefaultHeaderHeight(layout, headerStatusBarHeight); + : getDefaultHeaderHeight(layout, isModal, headerStatusBarHeight); return acc; }, {}); @@ -184,152 +212,152 @@ export default class CardStack extends React.Component { return acc; }, {}); - return { - routes: props.routes, - scenes: props.routes.map((route, index, self) => { - const previousRoute = self[index - 1]; - const nextRoute = self[index + 1]; + const scenes = props.routes.map((route, index, self) => { + const previousRoute = self[index - 1]; + const nextRoute = self[index + 1]; - const oldScene = state.scenes[index]; + const oldScene = state.scenes[index]; - const currentGesture = gestures[route.key]; - const previousGesture = previousRoute - ? gestures[previousRoute.key] - : undefined; - const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined; + const currentGesture = gestures[route.key]; + const previousGesture = previousRoute + ? gestures[previousRoute.key] + : undefined; + const nextGesture = nextRoute ? gestures[nextRoute.key] : undefined; - const descriptor = - props.descriptors[route.key] || - state.descriptors[route.key] || - (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR); + const descriptor = + props.descriptors[route.key] || + state.descriptors[route.key] || + (oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR); - const nextDescriptor = - props.descriptors[nextRoute?.key] || - state.descriptors[nextRoute?.key]; + const nextDescriptor = + props.descriptors[nextRoute?.key] || state.descriptors[nextRoute?.key]; - const previousDescriptor = - props.descriptors[previousRoute?.key] || - state.descriptors[previousRoute?.key]; + const previousDescriptor = + props.descriptors[previousRoute?.key] || + state.descriptors[previousRoute?.key]; - // When a screen is not the last, it should use next screen's transition config - // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right - // For example combining a slide and a modal transition would look wrong otherwise - // With this approach, combining different transition styles in the same navigator mostly looks right - // This will still be broken when 2 transitions have different idle state (e.g. modal presentation), - // but majority of the transitions look alright - const optionsForTransitionConfig = - index !== self.length - 1 && - nextDescriptor && - nextDescriptor.options.presentation !== 'transparentModal' - ? nextDescriptor.options - : descriptor.options; + // When a screen is not the last, it should use next screen's transition config + // Many transitions also animate the previous screen, so using 2 different transitions doesn't look right + // For example combining a slide and a modal transition would look wrong otherwise + // With this approach, combining different transition styles in the same navigator mostly looks right + // This will still be broken when 2 transitions have different idle state (e.g. modal presentation), + // but majority of the transitions look alright + const optionsForTransitionConfig = + index !== self.length - 1 && + nextDescriptor && + nextDescriptor.options.presentation !== 'transparentModal' + ? nextDescriptor.options + : descriptor.options; - let defaultTransitionPreset = - optionsForTransitionConfig.presentation === 'modal' - ? ModalTransition - : optionsForTransitionConfig.presentation === 'transparentModal' - ? ModalFadeTransition - : DefaultTransition; + let defaultTransitionPreset = + optionsForTransitionConfig.presentation === 'modal' + ? ModalTransition + : optionsForTransitionConfig.presentation === 'transparentModal' + ? ModalFadeTransition + : DefaultTransition; - const { - animationEnabled = Platform.OS !== 'web' && - Platform.OS !== 'windows' && - Platform.OS !== 'macos', - gestureEnabled = Platform.OS === 'ios' && animationEnabled, - gestureDirection = defaultTransitionPreset.gestureDirection, - transitionSpec = defaultTransitionPreset.transitionSpec, - cardStyleInterpolator = animationEnabled === false - ? forNoAnimationCard - : defaultTransitionPreset.cardStyleInterpolator, - headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator, - cardOverlayEnabled = (Platform.OS !== 'ios' && - optionsForTransitionConfig.presentation !== 'transparentModal') || - cardStyleInterpolator === forModalPresentationIOS, - } = optionsForTransitionConfig; + const { + animationEnabled = Platform.OS !== 'web' && + Platform.OS !== 'windows' && + Platform.OS !== 'macos', + gestureEnabled = Platform.OS === 'ios' && animationEnabled, + gestureDirection = defaultTransitionPreset.gestureDirection, + transitionSpec = defaultTransitionPreset.transitionSpec, + cardStyleInterpolator = animationEnabled === false + ? forNoAnimationCard + : defaultTransitionPreset.cardStyleInterpolator, + headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator, + cardOverlayEnabled = (Platform.OS !== 'ios' && + optionsForTransitionConfig.presentation !== 'transparentModal') || + cardStyleInterpolator === forModalPresentationIOS, + } = optionsForTransitionConfig; - const headerMode: StackHeaderMode = - descriptor.options.headerMode ?? - (!( - optionsForTransitionConfig.presentation === 'modal' || - optionsForTransitionConfig.presentation === 'transparentModal' || - nextDescriptor?.options.presentation === 'modal' || - nextDescriptor?.options.presentation === 'transparentModal' || - cardStyleInterpolator === forModalPresentationIOS - ) && - Platform.OS === 'ios' && - descriptor.options.header === undefined - ? 'float' - : 'screen'); + const headerMode: StackHeaderMode = + descriptor.options.headerMode ?? + (!( + optionsForTransitionConfig.presentation === 'modal' || + optionsForTransitionConfig.presentation === 'transparentModal' || + nextDescriptor?.options.presentation === 'modal' || + nextDescriptor?.options.presentation === 'transparentModal' || + cardStyleInterpolator === forModalPresentationIOS + ) && + Platform.OS === 'ios' && + descriptor.options.header === undefined + ? 'float' + : 'screen'); - const scene = { - route, - descriptor: { - ...descriptor, - options: { - ...descriptor.options, - animationEnabled, - cardOverlayEnabled, - cardStyleInterpolator, - gestureDirection, - gestureEnabled, - headerStyleInterpolator, - transitionSpec, - headerMode, - }, + const scene = { + route, + descriptor: { + ...descriptor, + options: { + ...descriptor.options, + animationEnabled, + cardOverlayEnabled, + cardStyleInterpolator, + gestureDirection, + gestureEnabled, + headerStyleInterpolator, + transitionSpec, + headerMode, }, - progress: { - current: getProgressFromGesture( - currentGesture, - state.layout, - descriptor - ), - next: - nextGesture && - nextDescriptor.options.presentation !== 'transparentModal' - ? getProgressFromGesture( - nextGesture, - state.layout, - nextDescriptor - ) - : undefined, - previous: previousGesture + }, + progress: { + current: getProgressFromGesture( + currentGesture, + state.layout, + descriptor + ), + next: + nextGesture && + nextDescriptor.options.presentation !== 'transparentModal' ? getProgressFromGesture( - previousGesture, + nextGesture, state.layout, - previousDescriptor + nextDescriptor ) : undefined, - }, - __memo: [ - state.layout, - descriptor, - nextDescriptor, - previousDescriptor, - currentGesture, - nextGesture, - previousGesture, - ], - }; + previous: previousGesture + ? getProgressFromGesture( + previousGesture, + state.layout, + previousDescriptor + ) + : undefined, + }, + __memo: [ + state.layout, + descriptor, + nextDescriptor, + previousDescriptor, + currentGesture, + nextGesture, + previousGesture, + ], + }; - if ( - oldScene && - scene.__memo.every((it, i) => { - // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere - return oldScene.__memo[i] === it; - }) - ) { - return oldScene; - } + if ( + oldScene && + scene.__memo.every((it, i) => { + // @ts-expect-error: we haven't added __memo to the annotation to prevent usage elsewhere + return oldScene.__memo[i] === it; + }) + ) { + return oldScene; + } - return scene; - }), + return scene; + }); + + return { + routes: props.routes, + scenes, gestures, descriptors: props.descriptors, headerHeights: getHeaderHeights( - props.routes, + scenes, props.insets, props.isParentHeaderShown, - state.descriptors, state.layout, state.headerHeights ), @@ -367,10 +395,9 @@ export default class CardStack extends React.Component { return { layout, headerHeights: getHeaderHeights( - props.routes, + state.scenes, props.insets, props.isParentHeaderShown, - state.descriptors, layout, state.headerHeights ), @@ -549,7 +576,6 @@ export default class CardStack extends React.Component { } const { - cardStyleInterpolator, headerShown = true, headerTransparent, headerStyle, @@ -578,18 +604,7 @@ export default class CardStack extends React.Component { } // Start from current card and count backwards the number of cards with same interpolation - let interpolationIndex = 0; - - for (let i = index - 1; i >= 0; i--) { - const cardStyleInterpolatorCurrent = - scenes[i]?.descriptor.options.cardStyleInterpolator; - - if (cardStyleInterpolatorCurrent !== cardStyleInterpolator) { - break; - } - - interpolationIndex++; - } + const interpolationIndex = getInterpolationIndex(scenes, index); const isNextScreenTransparent = scenes[index + 1]?.descriptor.options.presentation ===