Skip to content

Commit

Permalink
feat: basic web implementation for native stack
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Aug 1, 2021
1 parent 73277d5 commit de84458
Show file tree
Hide file tree
Showing 4 changed files with 363 additions and 208 deletions.
2 changes: 1 addition & 1 deletion packages/elements/src/Header/getHeaderTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { HeaderOptions } from '../types';

export default function getHeaderTitle(
options: HeaderOptions & { title?: string },
options: { title?: string; headerTitle?: HeaderOptions['headerTitle'] },
fallback: string
): string {
return typeof options.headerTitle === 'string'
Expand Down
34 changes: 27 additions & 7 deletions packages/native-stack/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import type {
StackNavigationState,
StackRouterOptions,
} from '@react-navigation/native';
import type { ImageSourcePropType, StyleProp, ViewStyle } from 'react-native';
import type {
ImageSourcePropType,
StyleProp,
TextStyle,
ViewStyle,
} from 'react-native';
import type {
ScreenProps,
ScreenStackHeaderConfigProps,
Expand Down Expand Up @@ -65,6 +70,10 @@ export type NativeStackNavigationOptions = {
* You can use it to show a back button alongside `headerLeft` if you have specified it.
*
* This will have no effect on the first screen in the stack.
*
* Only supported on iOS.
*
* @platform ios
*/
headerBackVisible?: boolean;
/**
Expand Down Expand Up @@ -133,6 +142,10 @@ export type NativeStackNavigationOptions = {
headerLargeTitle?: boolean;
/**
* Whether drop shadow of header is visible when a large title is shown.
*
* Only supported on iOS.
*
* @platform ios
*/
headerLargeTitleShadowVisible?: boolean;
/**
Expand Down Expand Up @@ -223,12 +236,11 @@ export type NativeStackNavigationOptions = {
* - fontWeight
* - color
*/
headerTitleStyle?: StyleProp<{
fontFamily?: string;
fontSize?: number;
fontWeight?: string;
color?: string;
}>;
headerTitleStyle?: StyleProp<
Pick<TextStyle, 'fontFamily' | 'fontSize' | 'fontWeight'> & {
color?: string;
}
>;
/**
* Options to render a native search bar on iOS.
*
Expand Down Expand Up @@ -279,6 +291,8 @@ export type NativeStackNavigationOptions = {
* Supported values:
* - "push": the new screen will perform push animation.
* - "pop": the new screen will perform pop animation.
*
* Only supported on iOS and Android.
*/
animationTypeForReplace?: ScreenProps['replaceAnimation'];
/**
Expand All @@ -291,6 +305,8 @@ export type NativeStackNavigationOptions = {
* - "slide_from_right": slide in the new screen from right (Android only, uses default animation on iOS)
* - "slide_from_left": slide in the new screen from left (Android only, uses default animation on iOS)
* - "none": don't animate the screen
*
* Only supported on iOS and Android.
*/
animation?: ScreenProps['stackAnimation'];
/**
Expand All @@ -304,6 +320,8 @@ export type NativeStackNavigationOptions = {
* - "containedTransparentModal": will use "UIModalPresentationOverCurrentContext" modal style on iOS and will fallback to "transparentModal" on Android.
* - "fullScreenModal": will use "UIModalPresentationFullScreen" modal style on iOS and will fallback to "modal" on Android.
* - "formSheet": will use "UIModalPresentationFormSheet" modal style on iOS and will fallback to "modal" on Android.
*
* Only supported on iOS and Android.
*/
presentation?: Exclude<ScreenProps['stackPresentation'], 'push'> | 'card';
/**
Expand All @@ -318,6 +336,8 @@ export type NativeStackNavigationOptions = {
* - "landscape": landscape orientations are permitted.
* - "landscape_left": landscape-left orientation is permitted.
* - "landscape_right": landscape-right orientation is permitted.
*
* Only supported on iOS and Android.
*/
orientation?: ScreenProps['screenOrientation'];
};
Expand Down
229 changes: 229 additions & 0 deletions packages/native-stack/src/views/NativeStackView.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { SafeAreaProviderCompat } from '@react-navigation/elements';
import {
ParamListBase,
Route,
StackActions,
StackNavigationState,
useTheme,
} from '@react-navigation/native';
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import {
Screen,
ScreenStack,
StackPresentationTypes,
} from 'react-native-screens';
import warnOnce from 'warn-once';

import type {
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
NativeStackNavigationOptions,
} from '../types';
import DebugContainer from './DebugContainer';
import HeaderConfig from './HeaderConfig';

const isAndroid = Platform.OS === 'android';

const MaybeNestedStack = ({
options,
route,
presentation,
children,
}: {
options: NativeStackNavigationOptions;
route: Route<string>;
presentation: Exclude<StackPresentationTypes, 'push'> | 'card';
children: React.ReactNode;
}) => {
const { colors } = useTheme();
const { headerShown = true, contentStyle } = options;

const isHeaderInModal = isAndroid
? false
: presentation !== 'card' && headerShown === true;

const headerShownPreviousRef = React.useRef(headerShown);

React.useEffect(() => {
warnOnce(
!isAndroid &&
presentation !== 'card' &&
headerShownPreviousRef.current !== headerShown,
`Dynamically changing 'headerShown' in modals will result in remounting the screen and losing all local state. See options for the screen '${route.name}'.`
);

headerShownPreviousRef.current = headerShown;
}, [headerShown, presentation, route.name]);

const content = (
<DebugContainer
style={[
styles.container,
presentation !== 'transparentModal' &&
presentation !== 'containedTransparentModal' && {
backgroundColor: colors.background,
},
contentStyle,
]}
stackPresentation={presentation === 'card' ? 'push' : presentation}
>
{children}
</DebugContainer>
);

if (isHeaderInModal) {
return (
<ScreenStack style={styles.container}>
<Screen enabled style={StyleSheet.absoluteFill}>
<HeaderConfig {...options} route={route} />
{content}
</Screen>
</ScreenStack>
);
}

return content;
};

type Props = {
state: StackNavigationState<ParamListBase>;
navigation: NativeStackNavigationHelpers;
descriptors: NativeStackDescriptorMap;
};

function NativeStackViewInner({ state, navigation, descriptors }: Props) {
const [nextDismissedKey, setNextDismissedKey] =
React.useState<string | null>(null);

const dismissedRouteName = nextDismissedKey
? state.routes.find((route) => route.key === nextDismissedKey)?.name
: null;

React.useEffect(() => {
if (dismissedRouteName) {
const message =
`The screen '${dismissedRouteName}' was removed natively but didn't get removed from JS state. ` +
`This can happen if the action was prevented in a 'beforeRemove' listener, which is not fully supported in native-stack.\n\n` +
`Consider using 'gestureEnabled: false' to prevent back gesture and use a custom back button with 'headerLeft' option to override the native behavior.`;

console.error(message);
}
}, [dismissedRouteName]);

return (
<ScreenStack style={styles.container}>
{state.routes.map((route, index) => {
const { options, render: renderScene } = descriptors[route.key];
const {
gestureEnabled,
headerShown,
animationTypeForReplace = 'pop',
animation,
orientation,
statusBarAnimation,
statusBarHidden,
statusBarStyle,
} = options;

let { presentation = 'card' } = options;

if (index === 0) {
// first screen should always be treated as `card`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `card`
presentation = 'card';
}

const isHeaderInPush = isAndroid
? headerShown
: presentation === 'card' && headerShown !== false;

return (
<Screen
key={route.key}
enabled
style={StyleSheet.absoluteFill}
gestureEnabled={
isAndroid
? // This prop enables handling of system back gestures on Android
// Since we handle them in JS side, we disable this
false
: gestureEnabled
}
replaceAnimation={animationTypeForReplace}
stackPresentation={presentation === 'card' ? 'push' : presentation}
stackAnimation={animation}
screenOrientation={orientation}
statusBarAnimation={statusBarAnimation}
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
onWillAppear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: false },
target: route.key,
});
}}
onWillDisappear={() => {
navigation.emit({
type: 'transitionStart',
data: { closing: true },
target: route.key,
});
}}
onAppear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: false },
target: route.key,
});
}}
onDisappear={() => {
navigation.emit({
type: 'transitionEnd',
data: { closing: true },
target: route.key,
});
}}
onDismissed={() => {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
target: state.key,
});

setNextDismissedKey(route.key);
}}
>
<HeaderConfig
{...options}
route={route}
headerShown={isHeaderInPush}
/>
<MaybeNestedStack
options={options}
route={route}
presentation={presentation}
>
{renderScene()}
</MaybeNestedStack>
</Screen>
);
})}
</ScreenStack>
);
}

export default function NativeStackView(props: Props) {
return (
<SafeAreaProviderCompat>
<NativeStackViewInner {...props} />
</SafeAreaProviderCompat>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
});

0 comments on commit de84458

Please sign in to comment.