mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add a new component to group multiple screens with common options
This commit is contained in:
@@ -117,24 +117,29 @@ export default function SimpleStackScreen({
|
||||
return (
|
||||
<SimpleStack.Navigator
|
||||
screenOptions={{
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
headerMode: 'float',
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
}}
|
||||
>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<SimpleStack.Group
|
||||
screenOptions={{
|
||||
...TransitionPresets.SlideFromRightIOS,
|
||||
headerMode: 'float',
|
||||
}}
|
||||
>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
</SimpleStack.Group>
|
||||
<SimpleStack.Screen
|
||||
name="Albums"
|
||||
component={AlbumsScreen}
|
||||
|
||||
13
packages/core/src/Group.tsx
Normal file
13
packages/core/src/Group.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ParamListBase } from '@react-navigation/routers';
|
||||
import type { RouteGroupConfig } from './types';
|
||||
|
||||
/**
|
||||
* Empty component used for grouping screen configs.
|
||||
*/
|
||||
export default function Group<
|
||||
ParamList extends ParamListBase,
|
||||
ScreenOptions extends {}
|
||||
>(_: RouteGroupConfig<ParamList, ScreenOptions>) {
|
||||
/* istanbul ignore next */
|
||||
return null;
|
||||
}
|
||||
@@ -9,14 +9,10 @@ import NavigationStateContext from './NavigationStateContext';
|
||||
import StaticContainer from './StaticContainer';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import useOptionsGetters from './useOptionsGetters';
|
||||
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
|
||||
import type { NavigationProp, RouteConfigComponent } from './types';
|
||||
|
||||
type Props<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
> = {
|
||||
screen: RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>;
|
||||
type Props<State extends NavigationState, ScreenOptions extends {}> = {
|
||||
screen: RouteConfigComponent<ParamListBase, string> & { name: string };
|
||||
navigation: NavigationProp<ParamListBase, string, State, ScreenOptions>;
|
||||
route: Route<string>;
|
||||
routeState: NavigationState | PartialState<NavigationState> | undefined;
|
||||
@@ -31,8 +27,7 @@ type Props<
|
||||
*/
|
||||
export default function SceneView<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
ScreenOptions extends {}
|
||||
>({
|
||||
screen,
|
||||
route,
|
||||
@@ -41,7 +36,7 @@ export default function SceneView<
|
||||
getState,
|
||||
setState,
|
||||
options,
|
||||
}: Props<State, ScreenOptions, EventMap>) {
|
||||
}: Props<State, ScreenOptions>) {
|
||||
const navigatorKeyRef = React.useRef<string | undefined>();
|
||||
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from '@testing-library/react-native';
|
||||
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
|
||||
import Group from '../Group';
|
||||
import Screen from '../Screen';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
@@ -248,6 +249,53 @@ it('initializes state for nested screens in React.Fragment', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes state for nested screens in Group', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = (props: any) => {
|
||||
React.useEffect(() => {
|
||||
props.navigation.dispatch({ type: 'UPDATE' });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const onStateChange = jest.fn();
|
||||
|
||||
const element = (
|
||||
<BaseNavigationContainer onStateChange={onStateChange}>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={TestScreen} />
|
||||
<Group>
|
||||
<Screen name="bar" component={jest.fn()} />
|
||||
<Screen name="baz" component={jest.fn()} />
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(element).update(element);
|
||||
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
expect(onStateChange).toBeCalledWith({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
index: 0,
|
||||
key: '0',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes state for nested navigator on navigation', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
@@ -1450,7 +1498,7 @@ it('throws when Screen is not the direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'Bar')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'Bar')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1475,7 +1523,7 @@ it('throws when undefined component is a direct children', () => {
|
||||
spy.mockRestore();
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'undefined' for the screen 'foo')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'undefined' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1495,7 +1543,7 @@ it('throws when a tag is a direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'screen' for the screen 'foo')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'screen' for the screen 'foo')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1515,7 +1563,7 @@ it('throws when a React Element is not the direct children', () => {
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"A navigator can only contain 'Screen' components as its direct children (found 'Hello world')"
|
||||
"A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found 'Hello world')"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1838,19 +1886,16 @@ it("returns focused screen's options with getCurrentOptions when focused screen
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: 'data' })}
|
||||
>
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: 'data' }}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: 'data' }}
|
||||
options={{ sample2: '2' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
@@ -1863,15 +1908,122 @@ it("returns focused screen's options with getCurrentOptions when focused screen
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: 'data',
|
||||
sample2: 'data',
|
||||
sample: '1',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: 'data',
|
||||
sample3: 'data',
|
||||
sample2: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when focused screen is rendered when using screenOptions", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when focused screen is rendered when using Group", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return descriptors[state.routes[state.index].key].render();
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Group screenOptions={{ sample4: '4' }}>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
sample4: '4',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1891,19 +2043,16 @@ it("returns focused screen's options with getCurrentOptions when all screens are
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: 'data' })}
|
||||
>
|
||||
<TestNavigator initialRouteName="bar-a">
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: 'data' }}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: 'data' }}
|
||||
options={{ sample2: '2' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
@@ -1916,15 +2065,122 @@ it("returns focused screen's options with getCurrentOptions when all screens are
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: 'data',
|
||||
sample2: 'data',
|
||||
sample: '1',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: 'data',
|
||||
sample3: 'data',
|
||||
sample2: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when all screens are rendered with screenOptions", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return <>{state.routes.map((route) => descriptors[route.key].render())}</>;
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
});
|
||||
});
|
||||
|
||||
it("returns focused screen's options with getCurrentOptions when all screens are rendered with Group", () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return <>{state.routes.map((route) => descriptors[route.key].render())}</>;
|
||||
};
|
||||
|
||||
const TestScreen = () => null;
|
||||
|
||||
const navigation = createNavigationContainerRef<ParamListBase>();
|
||||
|
||||
const container = (
|
||||
<BaseNavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="bar" options={{ a: 'b' }}>
|
||||
{() => (
|
||||
<TestNavigator
|
||||
initialRouteName="bar-a"
|
||||
screenOptions={() => ({ sample2: '2' })}
|
||||
>
|
||||
<Screen
|
||||
name="bar-a"
|
||||
component={TestScreen}
|
||||
options={{ sample: '1' }}
|
||||
/>
|
||||
<Group screenOptions={{ sample4: '4' }}>
|
||||
<Screen
|
||||
name="bar-b"
|
||||
component={TestScreen}
|
||||
options={{ sample3: '3' }}
|
||||
/>
|
||||
</Group>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="xux" component={TestScreen} />
|
||||
</TestNavigator>
|
||||
</BaseNavigationContainer>
|
||||
);
|
||||
|
||||
render(container).update(container);
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample: '1',
|
||||
sample2: '2',
|
||||
});
|
||||
|
||||
act(() => navigation.navigate('bar-b'));
|
||||
|
||||
expect(navigation.getCurrentOptions()).toEqual({
|
||||
sample2: '2',
|
||||
sample3: '3',
|
||||
sample4: '4',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type * as React from 'react';
|
||||
import type { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
import Group from './Group';
|
||||
import Screen from './Screen';
|
||||
import type { TypedNavigator, EventMapBase } from './types';
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function createNavigatorFactory<
|
||||
|
||||
return {
|
||||
Navigator,
|
||||
Group,
|
||||
Screen,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ export type DefaultNavigatorOptions<
|
||||
> = DefaultRouterOptions<Keyof<ParamList>> & {
|
||||
/**
|
||||
* Children React Elements to extract the route configuration from.
|
||||
* Only `Screen` components are supported as children.
|
||||
* Only `Screen`, `Group` and `React.Fragment` are supported as children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
@@ -376,6 +376,38 @@ export type ScreenListeners<
|
||||
}
|
||||
>;
|
||||
|
||||
export type RouteConfigComponent<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList
|
||||
> =
|
||||
| {
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
getComponent?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Lazily get a React component to render for this screen.
|
||||
*/
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
component?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Render callback to render content of this screen.
|
||||
*/
|
||||
children: (props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}) => React.ReactNode;
|
||||
component?: never;
|
||||
getComponent?: never;
|
||||
};
|
||||
|
||||
export type RouteConfig<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList,
|
||||
@@ -420,35 +452,27 @@ export type RouteConfig<
|
||||
* Initial params object for the route.
|
||||
*/
|
||||
initialParams?: Partial<ParamList[RouteName]>;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
getComponent?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Lazily get a React component to render for this screen.
|
||||
*/
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
component?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Render callback to render content of this screen.
|
||||
*/
|
||||
children: (props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
} & RouteConfigComponent<ParamList, RouteName>;
|
||||
|
||||
export type RouteGroupConfig<
|
||||
ParamList extends ParamListBase,
|
||||
ScreenOptions extends {}
|
||||
> = {
|
||||
/**
|
||||
* Navigator options for this screen.
|
||||
*/
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, keyof ParamList>;
|
||||
navigation: any;
|
||||
}) => React.ReactNode;
|
||||
component?: never;
|
||||
getComponent?: never;
|
||||
}
|
||||
);
|
||||
}) => ScreenOptions);
|
||||
/**
|
||||
* Children React Elements to extract the route configuration from.
|
||||
* Only `Screen`, `Group` and `React.Fragment` are supported as children.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export type NavigationContainerEventMap = {
|
||||
/**
|
||||
@@ -536,6 +560,10 @@ export type TypedNavigator<
|
||||
> &
|
||||
DefaultNavigatorOptions<ScreenOptions, ParamList>
|
||||
>;
|
||||
/**
|
||||
* Component used for grouping multiple route configuration.
|
||||
*/
|
||||
Group: React.ComponentType<RouteGroupConfig<ParamList, ScreenOptions>>;
|
||||
/**
|
||||
* Component used for specifying route configuration.
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,22 @@ import type {
|
||||
NavigationProp,
|
||||
} from './types';
|
||||
|
||||
export type ScreenConfigWithParent<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
> = [
|
||||
(ScreenOptionsOrCallback<ScreenOptions> | undefined)[] | undefined,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
];
|
||||
|
||||
type ScreenOptionsOrCallback<ScreenOptions extends {}> =
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
|
||||
type Options<
|
||||
State extends NavigationState,
|
||||
ScreenOptions extends {},
|
||||
@@ -32,15 +48,10 @@ type Options<
|
||||
state: State;
|
||||
screens: Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
ScreenConfigWithParent<State, ScreenOptions, EventMap>
|
||||
>;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
screenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamListBase>;
|
||||
navigation: any;
|
||||
}) => ScreenOptions);
|
||||
screenOptions?: ScreenOptionsOrCallback<ScreenOptions>;
|
||||
defaultScreenOptions?:
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
@@ -137,29 +148,31 @@ export default function useDescriptors<
|
||||
>
|
||||
>
|
||||
>((acc, route, i) => {
|
||||
const screen = screens[route.name];
|
||||
const config = screens[route.name];
|
||||
const screen = config[1];
|
||||
const navigation = navigations[route.key];
|
||||
|
||||
const customOptions = {
|
||||
const optionsList = [
|
||||
// The default `screenOptions` passed to the navigator
|
||||
...(typeof screenOptions === 'object' || screenOptions == null
|
||||
? screenOptions
|
||||
: // @ts-expect-error: this is a function, but typescript doesn't think so
|
||||
screenOptions({
|
||||
route,
|
||||
navigation,
|
||||
})),
|
||||
// The `options` prop passed to `Screen` elements
|
||||
...(typeof screen.options === 'object' || screen.options == null
|
||||
? screen.options
|
||||
: // @ts-expect-error: this is a function, but typescript doesn't think so
|
||||
screen.options({
|
||||
route,
|
||||
navigation,
|
||||
})),
|
||||
screenOptions,
|
||||
// The `screenOptions` props passed to `Group` elements
|
||||
...((config[0]
|
||||
? config[0].filter(Boolean)
|
||||
: []) as ScreenOptionsOrCallback<ScreenOptions>[]),
|
||||
// The `options` prop passed to `Screen` elements,
|
||||
screen.options,
|
||||
// The options set via `navigation.setOptions`
|
||||
...options[route.key],
|
||||
};
|
||||
options[route.key],
|
||||
];
|
||||
|
||||
const customOptions = optionsList.reduce<ScreenOptions>(
|
||||
(acc, curr) =>
|
||||
Object.assign(
|
||||
acc,
|
||||
typeof curr !== 'function' ? curr : curr({ route, navigation })
|
||||
),
|
||||
{} as ScreenOptions
|
||||
);
|
||||
|
||||
const mergedOptions = {
|
||||
...(typeof defaultScreenOptions === 'function'
|
||||
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
} from '@react-navigation/routers';
|
||||
import NavigationStateContext from './NavigationStateContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import Group from './Group';
|
||||
import Screen from './Screen';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useRegisterNavigator from './useRegisterNavigator';
|
||||
import useDescriptors from './useDescriptors';
|
||||
import useDescriptors, { ScreenConfigWithParent } from './useDescriptors';
|
||||
import useNavigationHelpers from './useNavigationHelpers';
|
||||
import useOnAction from './useOnAction';
|
||||
import useFocusEvents from './useFocusEvents';
|
||||
@@ -57,33 +58,40 @@ const getRouteConfigsFromChildren = <
|
||||
ScreenOptions extends {},
|
||||
EventMap extends EventMapBase
|
||||
>(
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode,
|
||||
options?: ScreenConfigWithParent<State, ScreenOptions, EventMap>[0]
|
||||
) => {
|
||||
const configs = React.Children.toArray(children).reduce<
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>[]
|
||||
ScreenConfigWithParent<State, ScreenOptions, EventMap>[]
|
||||
>((acc, child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
if (child.type === Screen) {
|
||||
// We can only extract the config from `Screen` elements
|
||||
// If something else was rendered, it's probably a bug
|
||||
acc.push(
|
||||
acc.push([
|
||||
options,
|
||||
child.props as RouteConfig<
|
||||
ParamListBase,
|
||||
string,
|
||||
State,
|
||||
ScreenOptions,
|
||||
EventMap
|
||||
>
|
||||
);
|
||||
>,
|
||||
]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (child.type === React.Fragment) {
|
||||
// When we encounter a fragment, we need to dive into its children to extract the configs
|
||||
if (child.type === React.Fragment || child.type === Group) {
|
||||
// When we encounter a fragment or group, we need to dive into its children to extract the configs
|
||||
// This is handy to conditionally define a group of screens
|
||||
acc.push(
|
||||
...getRouteConfigsFromChildren<State, ScreenOptions, EventMap>(
|
||||
child.props.children
|
||||
child.props.children,
|
||||
child.type !== Group
|
||||
? options
|
||||
: options != null
|
||||
? [...options, child.props.screenOptions]
|
||||
: [child.props.screenOptions]
|
||||
)
|
||||
);
|
||||
return acc;
|
||||
@@ -91,7 +99,7 @@ const getRouteConfigsFromChildren = <
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`A navigator can only contain 'Screen' components as its direct children (found ${
|
||||
`A navigator can only contain 'Screen', 'Group' or 'React.Fragment' as its direct children (found ${
|
||||
React.isValidElement(child)
|
||||
? `'${
|
||||
typeof child.type === 'string' ? child.type : child.type?.name
|
||||
@@ -107,7 +115,7 @@ const getRouteConfigsFromChildren = <
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configs.forEach((config) => {
|
||||
const { name, children, component, getComponent } = config;
|
||||
const { name, children, component, getComponent } = config[1];
|
||||
|
||||
if (typeof name !== 'string' || !name) {
|
||||
throw new Error(
|
||||
@@ -220,25 +228,22 @@ export default function useNavigationBuilder<
|
||||
>(children);
|
||||
|
||||
const screens = routeConfigs.reduce<
|
||||
Record<
|
||||
string,
|
||||
RouteConfig<ParamListBase, string, State, ScreenOptions, EventMap>
|
||||
>
|
||||
Record<string, ScreenConfigWithParent<State, ScreenOptions, EventMap>>
|
||||
>((acc, config) => {
|
||||
if (config.name in acc) {
|
||||
if (config[1].name in acc) {
|
||||
throw new Error(
|
||||
`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config.name}')`
|
||||
`A navigator cannot contain multiple 'Screen' components with the same name (found duplicate screen named '${config[1].name}')`
|
||||
);
|
||||
}
|
||||
|
||||
acc[config.name] = config;
|
||||
acc[config[1].name] = config;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const routeNames = routeConfigs.map((config) => config.name);
|
||||
const routeNames = routeConfigs.map((config) => config[1].name);
|
||||
const routeParamList = routeNames.reduce<Record<string, object | undefined>>(
|
||||
(acc, curr) => {
|
||||
const { initialParams } = screens[curr];
|
||||
const { initialParams } = screens[curr][1];
|
||||
const initialParamsFromParams =
|
||||
route?.params?.state == null &&
|
||||
route?.params?.initial !== false &&
|
||||
@@ -263,7 +268,7 @@ export default function useNavigationBuilder<
|
||||
>(
|
||||
(acc, curr) =>
|
||||
Object.assign(acc, {
|
||||
[curr]: screens[curr].getId,
|
||||
[curr]: screens[curr][1].getId,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
@@ -481,7 +486,7 @@ export default function useNavigationBuilder<
|
||||
const listeners = ([] as (((e: any) => void) | undefined)[])
|
||||
.concat(
|
||||
...routeNames.map((name) => {
|
||||
const { listeners } = screens[name];
|
||||
const { listeners } = screens[name][1];
|
||||
const map =
|
||||
typeof listeners === 'function'
|
||||
? listeners({ route: route as any, navigation })
|
||||
|
||||
Reference in New Issue
Block a user