Skip to content

Commit

Permalink
fix: improve type of options in dynamic navigator
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Mar 11, 2024
1 parent 6e84eea commit 2b8dc20
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 132 deletions.
9 changes: 8 additions & 1 deletion example/src/Screens/NativeStackHeaderCustomization.tsx
Expand Up @@ -3,6 +3,7 @@ import { Button, HeaderButton } from '@react-navigation/elements';
import type { PathConfigMap } from '@react-navigation/native';
import {
createNativeStackNavigator,
type NativeStackScreenOptions,
type NativeStackScreenProps,
} from '@react-navigation/native-stack';
import * as React from 'react';
Expand Down Expand Up @@ -120,7 +121,13 @@ export function NativeStackHeaderCustomization() {
<Stack.Screen
name="Article"
component={ArticleScreen}
options={({ route, navigation }) => ({
options={({
route,
navigation,
}: NativeStackScreenOptions<
NativeHeaderCustomizationStackParams,
'Article'
>) => ({
title: `Article by ${route.params?.author ?? 'Unknown'}`,
headerTintColor: 'white',
headerTitle: ({ tintColor }) => (
Expand Down
18 changes: 10 additions & 8 deletions packages/bottom-tabs/src/__tests__/index.test.tsx
@@ -1,29 +1,36 @@
import {
createNavigationContainerRef,
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { act, fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Animated, Button, Text, View } from 'react-native';

import { type BottomTabScreenProps, createBottomTabNavigator } from '../index';

type BottomTabParamList = {
A: undefined;
B: undefined;
};

it('renders a bottom tab navigator with screens', async () => {
// @ts-expect-error: incomplete mock for testing
jest.spyOn(Animated, 'timing').mockImplementation(() => ({
start: (callback) => callback?.({ finished: true }),
}));

const Test = ({ route, navigation }: BottomTabScreenProps<ParamListBase>) => (
const Test = ({
route,
navigation,
}: BottomTabScreenProps<BottomTabParamList>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);

const Tab = createBottomTabNavigator();
const Tab = createBottomTabNavigator<BottomTabParamList>();

const { findByText, queryByText } = render(
<NavigationContainer>
Expand All @@ -42,11 +49,6 @@ it('renders a bottom tab navigator with screens', async () => {
expect(queryByText('Screen B')).not.toBeNull();
});

type BottomTabParamList = {
A: undefined;
B: undefined;
};

it('handles screens preloading', async () => {
const Tab = createBottomTabNavigator<BottomTabParamList>();

Expand Down
25 changes: 19 additions & 6 deletions packages/bottom-tabs/src/navigators/createBottomTabNavigator.tsx
Expand Up @@ -14,6 +14,7 @@ import type {
BottomTabNavigationConfig,
BottomTabNavigationEventMap,
BottomTabNavigationOptions,
BottomTabNavigationProp,
} from '../types';
import { BottomTabView } from '../views/BottomTabView';

Expand Down Expand Up @@ -67,9 +68,21 @@ function BottomTabNavigator({
);
}

export const createBottomTabNavigator = createNavigatorFactory<
TabNavigationState<ParamListBase>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap,
typeof BottomTabNavigator
>(BottomTabNavigator);
export const createBottomTabNavigator = <
ParamList extends {},
NavigatorID extends string | undefined = undefined,
>() =>
createNavigatorFactory<
ParamList,
TabNavigationState<ParamList>,
BottomTabNavigationOptions,
BottomTabNavigationEventMap,
{
[RouteName in keyof ParamList]: BottomTabNavigationProp<
ParamList,
RouteName,
NavigatorID
>;
},
typeof BottomTabNavigator
>(BottomTabNavigator)();
12 changes: 11 additions & 1 deletion packages/core/src/Screen.tsx
Expand Up @@ -11,7 +11,17 @@ export function Screen<
State extends NavigationState,
ScreenOptions extends {},
EventMap extends EventMapBase,
>(_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>) {
NavigationProp,
>(
_: RouteConfig<
ParamList,
RouteName,
State,
ScreenOptions,
EventMap,
NavigationProp
>
) {
/* istanbul ignore next */
return null;
}
7 changes: 5 additions & 2 deletions packages/core/src/createNavigatorFactory.tsx
Expand Up @@ -14,21 +14,23 @@ import type { EventMapBase, TypedNavigator } from './types';
* @returns Factory method to create a `Navigator` and `Screen` pair.
*/
export function createNavigatorFactory<
ParamList extends ParamListBase,
State extends NavigationState,
ScreenOptions extends {},
EventMap extends EventMapBase,
NavigationPropList extends { [RouteName in keyof ParamList]: any },
NavigatorComponent extends React.ComponentType<any>,
>(Navigator: NavigatorComponent) {
function createNavigator<ParamList extends ParamListBase>(): TypedNavigator<
function createNavigator(): TypedNavigator<
ParamList,
State,
ScreenOptions,
EventMap,
NavigationPropList,
typeof Navigator
>;

function createNavigator<
ParamList extends ParamListBase,
Config extends StaticConfig<
ParamList,
State,
Expand All @@ -43,6 +45,7 @@ export function createNavigatorFactory<
State,
ScreenOptions,
EventMap,
NavigationPropList,
typeof Navigator
> & { config: Config };

Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/types.tsx
Expand Up @@ -425,7 +425,7 @@ export type NavigationContainerProps = {
};

export type NavigationProp<
ParamList extends {},
ParamList extends ParamListBase,
RouteName extends keyof ParamList = Keyof<ParamList>,
NavigatorID extends string | undefined = undefined,
State extends NavigationState = NavigationState<ParamList>,
Expand Down Expand Up @@ -608,6 +608,7 @@ export type RouteConfig<
State extends NavigationState,
ScreenOptions extends {},
EventMap extends EventMapBase,
Navigation,
> = {
/**
* Optional key for this screen. This doesn't need to be unique.
Expand All @@ -628,7 +629,7 @@ export type RouteConfig<
| ScreenOptions
| ((props: {
route: RouteProp<ParamList, RouteName>;
navigation: any;
navigation: Navigation;
theme: ReactNavigation.Theme;
}) => ScreenOptions);

Expand Down Expand Up @@ -795,6 +796,7 @@ export type TypedNavigator<
State extends NavigationState,
ScreenOptions extends {},
EventMap extends EventMapBase,
NavigationPropList extends { [key in keyof ParamList]: unknown },
Navigator extends React.ComponentType<any>,
> = {
/**
Expand All @@ -815,7 +817,14 @@ export type TypedNavigator<
* Component used for specifying route configuration.
*/
Screen: <RouteName extends keyof ParamList>(
_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>
_: RouteConfig<
ParamList,
RouteName,
State,
ScreenOptions,
EventMap,
NavigationPropList[RouteName]
>
) => null;
};

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/useDescriptors.tsx
Expand Up @@ -36,7 +36,14 @@ export type ScreenConfigWithParent<
keys: (string | undefined)[];
options: (ScreenOptionsOrCallback<ScreenOptions> | undefined)[] | undefined;
layout: ScreenLayout | undefined;
props: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
props: RouteConfig<
ParamListBase,
string,
State,
ScreenOptions,
EventMap,
unknown
>;
};

type ScreenLayout = (props: {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/useNavigationBuilder.tsx
Expand Up @@ -103,7 +103,8 @@ const getRouteConfigsFromChildren = <
string,
State,
ScreenOptions,
EventMap
EventMap,
unknown
>,
});

Expand Down
15 changes: 7 additions & 8 deletions packages/drawer/src/__tests__/index.test.tsx
@@ -1,24 +1,28 @@
import {
createNavigationContainerRef,
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { act, fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Button, Text, View } from 'react-native';

import { createDrawerNavigator, type DrawerScreenProps } from '../index';

type DrawerParamList = {
A: undefined;
B: undefined;
};

it('renders a drawer navigator with screens', async () => {
const Test = ({ route, navigation }: DrawerScreenProps<ParamListBase>) => (
const Test = ({ route, navigation }: DrawerScreenProps<DrawerParamList>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);

const Drawer = createDrawerNavigator();
const Drawer = createDrawerNavigator<DrawerParamList>();

const { findByText, queryByText } = render(
<NavigationContainer>
Expand All @@ -37,11 +41,6 @@ it('renders a drawer navigator with screens', async () => {
expect(queryByText('Screen B')).not.toBeNull();
});

type DrawerParamList = {
A: undefined;
B: undefined;
};

it('handles screens preloading', async () => {
const Drawer = createDrawerNavigator<DrawerParamList>();

Expand Down
25 changes: 19 additions & 6 deletions packages/drawer/src/navigators/createDrawerNavigator.tsx
Expand Up @@ -14,6 +14,7 @@ import type {
DrawerNavigationConfig,
DrawerNavigationEventMap,
DrawerNavigationOptions,
DrawerNavigationProp,
} from '../types';
import { DrawerView } from '../views/DrawerView';

Expand Down Expand Up @@ -70,9 +71,21 @@ function DrawerNavigator({
);
}

export const createDrawerNavigator = createNavigatorFactory<
DrawerNavigationState<ParamListBase>,
DrawerNavigationOptions,
DrawerNavigationEventMap,
typeof DrawerNavigator
>(DrawerNavigator);
export const createDrawerNavigator = <
ParamList extends {},
NavigatorID extends string | undefined = undefined,
>() =>
createNavigatorFactory<
ParamList,
DrawerNavigationState<ParamList>,
DrawerNavigationOptions,
DrawerNavigationEventMap,
{
[RouteName in keyof ParamList]: DrawerNavigationProp<
ParamList,
RouteName,
NavigatorID
>;
},
typeof DrawerNavigator
>(DrawerNavigator)();
14 changes: 8 additions & 6 deletions packages/material-top-tabs/src/__tests__/index.test.tsx
@@ -1,7 +1,4 @@
import {
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { NavigationContainer } from '@react-navigation/native';
import { fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Button, Text, View } from 'react-native';
Expand All @@ -11,6 +8,11 @@ import {
type MaterialTopTabScreenProps,
} from '../index';

type TopTabParamList = {
A: undefined;
B: undefined;
};

jest.mock('react-native-pager-view', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const React = require('react');
Expand All @@ -30,15 +32,15 @@ it('renders a material top tab navigator with screens', async () => {
const Test = ({
route,
navigation,
}: MaterialTopTabScreenProps<ParamListBase>) => (
}: MaterialTopTabScreenProps<TopTabParamList>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);

const Tab = createMaterialTopTabNavigator();
const Tab = createMaterialTopTabNavigator<TopTabParamList>();

const { findByText, queryByText } = render(
<NavigationContainer>
Expand Down
Expand Up @@ -14,6 +14,7 @@ import type {
MaterialTopTabNavigationConfig,
MaterialTopTabNavigationEventMap,
MaterialTopTabNavigationOptions,
MaterialTopTabNavigationProp,
} from '../types';
import { MaterialTopTabView } from '../views/MaterialTopTabView';

Expand Down Expand Up @@ -67,9 +68,21 @@ function MaterialTopTabNavigator({
);
}

export const createMaterialTopTabNavigator = createNavigatorFactory<
TabNavigationState<ParamListBase>,
MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap,
typeof MaterialTopTabNavigator
>(MaterialTopTabNavigator);
export const createMaterialTopTabNavigator = <
ParamList extends {},
NavigatorID extends string | undefined = undefined,
>() =>
createNavigatorFactory<
ParamList,
TabNavigationState<ParamList>,
MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap,
{
[RouteName in keyof ParamList]: MaterialTopTabNavigationProp<
ParamList,
RouteName,
NavigatorID
>;
},
typeof MaterialTopTabNavigator
>(MaterialTopTabNavigator)();

0 comments on commit 2b8dc20

Please sign in to comment.