mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-14 09:32:32 +08:00
Compare commits
42 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e813dfb5b | ||
|
|
ce4eb7e927 | ||
|
|
baea77e332 | ||
|
|
15f9b9573e | ||
|
|
b70e3fe618 | ||
|
|
1aa8219021 | ||
|
|
486c3defd2 | ||
|
|
0d6a43f663 | ||
|
|
5e358b3aad | ||
|
|
7c2b28ae1e | ||
|
|
af8b27414c | ||
|
|
b2a99c2a88 | ||
|
|
2f74541811 | ||
|
|
cf09f00472 | ||
|
|
513482425a | ||
|
|
f4180295bf | ||
|
|
c665c027a6 | ||
|
|
849e04ab6a | ||
|
|
374b081b1c | ||
|
|
96c7b688ce | ||
|
|
e63580edbe | ||
|
|
eea9860323 | ||
|
|
13c9d1e281 | ||
|
|
8f5286ef50 | ||
|
|
a255e350f9 | ||
|
|
7a74bdb24e | ||
|
|
7c3a0a0f23 | ||
|
|
bddb1f0046 | ||
|
|
c1521e81e8 | ||
|
|
bce6c4fc3b | ||
|
|
6925e92dc3 | ||
|
|
1801a13323 | ||
|
|
9671c76c51 | ||
|
|
ec840692ec | ||
|
|
1cae93331d | ||
|
|
4edc2a64e2 | ||
|
|
75c99b5a12 | ||
|
|
9ba2f84d18 | ||
|
|
2477db47a0 | ||
|
|
d1210a861b | ||
|
|
c4d2a8a828 | ||
|
|
fc95d7a256 |
@@ -52,10 +52,10 @@ jobs:
|
|||||||
- attach_project
|
- attach_project
|
||||||
- run:
|
- run:
|
||||||
name: Run unit tests
|
name: Run unit tests
|
||||||
command: yarn test --coverage
|
command: yarn test --maxWorkers=2 --coverage
|
||||||
- run:
|
- run:
|
||||||
name: Upload test coverage
|
name: Upload test coverage
|
||||||
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
command: yarn codecov
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: coverage
|
path: coverage
|
||||||
destination: coverage
|
destination: coverage
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "satya164",
|
"extends": "satya164",
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": { "version": "16" },
|
"react": {
|
||||||
|
"version": "16"
|
||||||
|
},
|
||||||
"import/core-modules": [
|
"import/core-modules": [
|
||||||
"@react-navigation/core",
|
"@react-navigation/core",
|
||||||
"@react-navigation/native",
|
"@react-navigation/native",
|
||||||
@@ -15,5 +17,11 @@
|
|||||||
"@react-navigation/devtools"
|
"@react-navigation/devtools"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"env": { "browser": true, "node": true }
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/no-unused-prop-types": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
.github/workflows/expo-preview.yml
vendored
14
.github/workflows/expo-preview.yml
vendored
@@ -47,9 +47,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
|
const body = 'The Expo app for the example from this branch is ready!\n\n[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})\n\n<a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>';
|
||||||
|
|
||||||
|
const comments = await github.issues.listComments({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comments.some(comment => comment.body === body)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
github.issues.createComment({
|
github.issues.createComment({
|
||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body: 'The Expo app for the example from this branch is ready!\n\n[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})\n\n<a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>'
|
body
|
||||||
})
|
})
|
||||||
|
|||||||
2
.github/workflows/versions.yml
vendored
2
.github/workflows/versions.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Check versions
|
name: Check versions
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened, edited]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-versions:
|
check-versions:
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ['module:metro-react-native-babel-preset'],
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
useBuiltIns: 'usage',
|
|
||||||
corejs: 3,
|
|
||||||
targets: {
|
|
||||||
node: 'current',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@babel/preset-react',
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-proposal-class-properties',
|
|
||||||
'@babel/plugin-proposal-optional-chaining',
|
|
||||||
'@babel/transform-flow-strip-types',
|
|
||||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
|
"babel-loader": "^8.1.0",
|
||||||
"babel-plugin-module-resolver": "^4.0.0",
|
"babel-plugin-module-resolver": "^4.0.0",
|
||||||
"babel-preset-expo": "^8.2.1",
|
"babel-preset-expo": "^8.2.1",
|
||||||
"cheerio": "^1.0.0-rc.3",
|
"cheerio": "^1.0.0-rc.3",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTheme, ParamListBase } from '@react-navigation/native';
|
|||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
HeaderBackButton,
|
HeaderBackButton,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
|
|
||||||
type AuthStackParams = {
|
type AuthStackParams = {
|
||||||
@@ -81,10 +81,6 @@ const HomeScreen = () => {
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<AuthStackParams>();
|
const SimpleStack = createStackNavigator<AuthStackParams>();
|
||||||
|
|
||||||
type Props = {
|
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isSignout: boolean;
|
isSignout: boolean;
|
||||||
@@ -96,7 +92,9 @@ type Action =
|
|||||||
| { type: 'SIGN_IN'; token: string }
|
| { type: 'SIGN_IN'; token: string }
|
||||||
| { type: 'SIGN_OUT' };
|
| { type: 'SIGN_OUT' };
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation }: Props) {
|
export default function SimpleStackScreen({
|
||||||
|
navigation,
|
||||||
|
}: StackScreenProps<ParamListBase>) {
|
||||||
const [state, dispatch] = React.useReducer<React.Reducer<State, Action>>(
|
const [state, dispatch] = React.useReducer<React.Reducer<State, Action>>(
|
||||||
(prevState, action) => {
|
(prevState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -135,9 +133,11 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
});
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
const authContext = React.useMemo(
|
const authContext = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -147,6 +147,10 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
return <SplashScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={authContext}>
|
<AuthContext.Provider value={authContext}>
|
||||||
<SimpleStack.Navigator
|
<SimpleStack.Navigator
|
||||||
@@ -156,13 +160,7 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{state.isLoading ? (
|
{state.userToken === undefined ? (
|
||||||
<SimpleStack.Screen
|
|
||||||
name="Splash"
|
|
||||||
component={SplashScreen}
|
|
||||||
options={{ title: 'Auth Flow' }}
|
|
||||||
/>
|
|
||||||
) : state.userToken === undefined ? (
|
|
||||||
<SimpleStack.Screen
|
<SimpleStack.Screen
|
||||||
name="SignIn"
|
name="SignIn"
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import type { StackScreenProps } from '@react-navigation/stack';
|
import type { StackScreenProps } from '@react-navigation/stack';
|
||||||
import {
|
import {
|
||||||
createBottomTabNavigator,
|
createBottomTabNavigator,
|
||||||
BottomTabNavigationProp,
|
BottomTabScreenProps,
|
||||||
} from '@react-navigation/bottom-tabs';
|
} from '@react-navigation/bottom-tabs';
|
||||||
import TouchableBounce from '../Shared/TouchableBounce';
|
import TouchableBounce from '../Shared/TouchableBounce';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
@@ -36,9 +36,7 @@ const scrollEnabled = Platform.select({ web: true, default: false });
|
|||||||
|
|
||||||
const AlbumsScreen = ({
|
const AlbumsScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
}: {
|
}: BottomTabScreenProps<BottomTabParams>) => {
|
||||||
navigation: BottomTabNavigationProp<BottomTabParams>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -99,6 +97,7 @@ export default function BottomTabsScreen({
|
|||||||
options={{
|
options={{
|
||||||
tabBarLabel: 'Chat',
|
tabBarLabel: 'Chat',
|
||||||
tabBarIcon: getTabBarIcon('message-reply'),
|
tabBarIcon: getTabBarIcon('message-reply'),
|
||||||
|
tabBarBadge: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackNavigationProp,
|
||||||
|
StackScreenProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
@@ -81,7 +82,6 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
|
|||||||
NestedStackParams,
|
NestedStackParams,
|
||||||
'Article'
|
'Article'
|
||||||
>> = ({ navigation }) => {
|
>> = ({ navigation }) => {
|
||||||
navigation.dangerouslyGetParent();
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -126,8 +126,8 @@ const CompatStack = createCompatStackNavigator<
|
|||||||
StackNavigationProp<NestedStackParams>
|
StackNavigationProp<NestedStackParams>
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
Feed: FeedScreen,
|
Feed: { getScreen: () => FeedScreen },
|
||||||
Article: ArticleScreen,
|
Article: { getScreen: () => ArticleScreen },
|
||||||
},
|
},
|
||||||
{ navigationOptions: { headerShown: false } }
|
{ navigationOptions: { headerShown: false } }
|
||||||
),
|
),
|
||||||
@@ -143,12 +143,12 @@ const CompatStack = createCompatStackNavigator<
|
|||||||
|
|
||||||
export default function CompatStackScreen({
|
export default function CompatStackScreen({
|
||||||
navigation,
|
navigation,
|
||||||
}: {
|
}: StackScreenProps<{}>) {
|
||||||
navigation: StackNavigationProp<{}>;
|
React.useLayoutEffect(() => {
|
||||||
}) {
|
navigation.setOptions({
|
||||||
navigation.setOptions({
|
headerShown: false,
|
||||||
headerShown: false,
|
});
|
||||||
});
|
}, [navigation]);
|
||||||
|
|
||||||
return <CompatStack />;
|
return <CompatStack />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { Button } from 'react-native-paper';
|
|||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
StackActions,
|
StackActions,
|
||||||
RouteProp,
|
|
||||||
ParamListBase,
|
ParamListBase,
|
||||||
useLinkProps,
|
useLinkProps,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
@@ -20,8 +19,6 @@ type SimpleStackParams = {
|
|||||||
Albums: undefined;
|
Albums: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
|
||||||
|
|
||||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const LinkButton = ({
|
const LinkButton = ({
|
||||||
@@ -45,10 +42,7 @@ const LinkButton = ({
|
|||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -88,11 +82,7 @@ const ArticleScreen = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumsScreen = ({
|
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -124,14 +114,15 @@ const AlbumsScreen = ({
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
StackScreenProps<ParamListBase>;
|
||||||
};
|
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
});
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleStack.Navigator {...rest}>
|
<SimpleStack.Navigator {...rest}>
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import {
|
|||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createDrawerNavigator,
|
createDrawerNavigator,
|
||||||
DrawerNavigationProp,
|
DrawerScreenProps,
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerContentComponentProps,
|
DrawerContentComponentProps,
|
||||||
DrawerContentOptions,
|
DrawerContentOptions,
|
||||||
} from '@react-navigation/drawer';
|
} from '@react-navigation/drawer';
|
||||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
import type { StackScreenProps } from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
import NewsFeed from '../Shared/NewsFeed';
|
import NewsFeed from '../Shared/NewsFeed';
|
||||||
@@ -24,8 +24,6 @@ type DrawerParams = {
|
|||||||
Albums: undefined;
|
Albums: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
|
|
||||||
|
|
||||||
const useIsLargeScreen = () => {
|
const useIsLargeScreen = () => {
|
||||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||||
|
|
||||||
@@ -60,7 +58,9 @@ const Header = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
const ArticleScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: DrawerScreenProps<DrawerParams, 'Article'>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
|
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
@@ -69,7 +69,9 @@ const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
const NewsFeedScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: DrawerScreenProps<DrawerParams, 'NewsFeed'>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
|
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
@@ -78,7 +80,9 @@ const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
|
const AlbumsScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: DrawerScreenProps<DrawerParams, 'Albums'>) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
|
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
|
||||||
@@ -106,15 +110,16 @@ const CustomDrawerContent = (
|
|||||||
|
|
||||||
const Drawer = createDrawerNavigator<DrawerParams>();
|
const Drawer = createDrawerNavigator<DrawerParams>();
|
||||||
|
|
||||||
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
|
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> &
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
StackScreenProps<ParamListBase>;
|
||||||
};
|
|
||||||
|
|
||||||
export default function DrawerScreen({ navigation, ...rest }: Props) {
|
export default function DrawerScreen({ navigation, ...rest }: Props) {
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
gestureEnabled: false,
|
headerShown: false,
|
||||||
});
|
gestureEnabled: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
const isLargeScreen = useIsLargeScreen();
|
const isLargeScreen = useIsLargeScreen();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { ParamListBase } from '@react-navigation/native';
|
import type { ParamListBase } from '@react-navigation/native';
|
||||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
import type { StackScreenProps } from '@react-navigation/stack';
|
||||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
import Contacts from '../Shared/Contacts';
|
import Contacts from '../Shared/Contacts';
|
||||||
@@ -14,14 +14,14 @@ type MaterialTopTabParams = {
|
|||||||
|
|
||||||
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
|
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
|
||||||
|
|
||||||
type Props = {
|
export default function MaterialTopTabsScreen({
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
navigation,
|
||||||
};
|
}: StackScreenProps<ParamListBase>) {
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
export default function MaterialTopTabsScreen({ navigation }: Props) {
|
navigation.setOptions({
|
||||||
navigation.setOptions({
|
cardStyle: { flex: 1 },
|
||||||
cardStyle: { flex: 1 },
|
});
|
||||||
});
|
}, [navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MaterialTopTabs.Navigator>
|
<MaterialTopTabs.Navigator>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import type { RouteProp, ParamListBase } from '@react-navigation/native';
|
import type { ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
|
StackNavigationOptions,
|
||||||
TransitionPresets,
|
TransitionPresets,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
@@ -15,17 +16,12 @@ type ModalStackParams = {
|
|||||||
Albums: undefined;
|
Albums: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
|
||||||
|
|
||||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
}: {
|
}: StackScreenProps<ModalStackParams, 'Article'>) => {
|
||||||
navigation: ModalStackNavigation;
|
|
||||||
route: RouteProp<ModalStackParams, 'Article'>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -52,7 +48,7 @@ const ArticleScreen = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
const AlbumsScreen = ({ navigation }: StackScreenProps<ModalStackParams>) => {
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -78,15 +74,16 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
|||||||
|
|
||||||
const ModalPresentationStack = createStackNavigator<ModalStackParams>();
|
const ModalPresentationStack = createStackNavigator<ModalStackParams>();
|
||||||
|
|
||||||
type Props = {
|
type Props = StackScreenProps<ParamListBase> & {
|
||||||
options?: React.ComponentProps<typeof ModalPresentationStack.Navigator>;
|
options?: StackNavigationOptions;
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, options }: Props) {
|
export default function SimpleStackScreen({ navigation, options }: Props) {
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
});
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalPresentationStack.Navigator
|
<ModalPresentationStack.Navigator
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { StyleSheet, Text, View } from 'react-native';
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
import type { StackScreenProps } from '@react-navigation/stack';
|
||||||
|
|
||||||
const NotFoundScreen = ({
|
const NotFoundScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
}: {
|
}: StackScreenProps<{ Home: undefined }>) => {
|
||||||
navigation: StackNavigationProp<{ Home: undefined }>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.title}>404 Not Found</Text>
|
<Text style={styles.title}>404 Not Found</Text>
|
||||||
|
|||||||
170
example/src/Screens/PreventRemove.tsx
Normal file
170
example/src/Screens/PreventRemove.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
View,
|
||||||
|
TextInput,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { Button } from 'react-native-paper';
|
||||||
|
import {
|
||||||
|
useTheme,
|
||||||
|
CommonActions,
|
||||||
|
ParamListBase,
|
||||||
|
NavigationAction,
|
||||||
|
} from '@react-navigation/native';
|
||||||
|
import {
|
||||||
|
createStackNavigator,
|
||||||
|
StackScreenProps,
|
||||||
|
} from '@react-navigation/stack';
|
||||||
|
import Article from '../Shared/Article';
|
||||||
|
|
||||||
|
type PreventRemoveParams = {
|
||||||
|
Article: { author: string };
|
||||||
|
Input: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
|
const ArticleScreen = ({
|
||||||
|
navigation,
|
||||||
|
route,
|
||||||
|
}: StackScreenProps<PreventRemoveParams, 'Article'>) => {
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<View style={styles.buttons}>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
onPress={() => navigation.push('Input')}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Push Input
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => navigation.popToTop()}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Pop to top
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<Article
|
||||||
|
author={{ name: route.params?.author ?? 'Unknown' }}
|
||||||
|
scrollEnabled={scrollEnabled}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputScreen = ({
|
||||||
|
navigation,
|
||||||
|
}: StackScreenProps<PreventRemoveParams, 'Input'>) => {
|
||||||
|
const [text, setText] = React.useState('');
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const hasUnsavedChanges = Boolean(text);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
navigation.addListener('beforeRemove', (e) => {
|
||||||
|
const action: NavigationAction & { payload?: { confirmed?: boolean } } =
|
||||||
|
e.data.action;
|
||||||
|
|
||||||
|
if (!hasUnsavedChanges || action.payload?.confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
'Discard changes?',
|
||||||
|
'You have unsaved changes. Are you sure to discard them and leave the screen?',
|
||||||
|
[
|
||||||
|
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
|
||||||
|
{
|
||||||
|
text: 'Discard',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => navigation.dispatch(action),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[hasUnsavedChanges, navigation]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.content}>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
style={[
|
||||||
|
styles.input,
|
||||||
|
{ backgroundColor: colors.card, color: colors.text },
|
||||||
|
]}
|
||||||
|
value={text}
|
||||||
|
placeholder="Type something…"
|
||||||
|
onChangeText={setText}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
color="tomato"
|
||||||
|
onPress={() =>
|
||||||
|
navigation.dispatch({
|
||||||
|
...CommonActions.goBack(),
|
||||||
|
payload: { confirmed: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Discard and go back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => navigation.push('Article', { author: text })}
|
||||||
|
style={styles.button}
|
||||||
|
>
|
||||||
|
Push Article
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleStack = createStackNavigator<PreventRemoveParams>();
|
||||||
|
|
||||||
|
type Props = StackScreenProps<ParamListBase>;
|
||||||
|
|
||||||
|
export default function SimpleStackScreen({ navigation }: Props) {
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleStack.Navigator>
|
||||||
|
<SimpleStack.Screen name="Input" component={InputScreen} />
|
||||||
|
<SimpleStack.Screen name="Article" component={ArticleScreen} />
|
||||||
|
</SimpleStack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
margin: 8,
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 3,
|
||||||
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderColor: 'rgba(0, 0, 0, 0.08)',
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
margin: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,38 +1,33 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||||
import { Button } from 'react-native-paper';
|
import { Button } from 'react-native-paper';
|
||||||
import type { RouteProp, ParamListBase } from '@react-navigation/native';
|
import type { ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
import NewsFeed from '../Shared/NewsFeed';
|
import NewsFeed from '../Shared/NewsFeed';
|
||||||
|
|
||||||
type SimpleStackParams = {
|
type SimpleStackParams = {
|
||||||
Article: { author: string };
|
Article: { author: string } | undefined;
|
||||||
NewsFeed: undefined;
|
NewsFeed: { date: number };
|
||||||
Albums: undefined;
|
Albums: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
|
||||||
|
|
||||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => navigation.replace('NewsFeed')}
|
onPress={() => navigation.replace('NewsFeed', { date: Date.now() })}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
Replace with feed
|
Replace with feed
|
||||||
@@ -46,7 +41,7 @@ const ArticleScreen = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article
|
<Article
|
||||||
author={{ name: route.params.author }}
|
author={{ name: route.params?.author ?? 'Unknown' }}
|
||||||
scrollEnabled={scrollEnabled}
|
scrollEnabled={scrollEnabled}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -54,10 +49,9 @@ const ArticleScreen = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NewsFeedScreen = ({
|
const NewsFeedScreen = ({
|
||||||
|
route,
|
||||||
navigation,
|
navigation,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -76,16 +70,14 @@ const NewsFeedScreen = ({
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumsScreen = ({
|
const AlbumsScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -111,14 +103,14 @@ const AlbumsScreen = ({
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
type Props = {
|
export default function SimpleStackScreen({
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
navigation,
|
||||||
};
|
}: StackScreenProps<ParamListBase>) {
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
export default function SimpleStackScreen({ navigation }: Props) {
|
navigation.setOptions({
|
||||||
navigation.setOptions({
|
headerShown: false,
|
||||||
headerShown: false,
|
});
|
||||||
});
|
}, [navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleStack.Navigator>
|
<SimpleStack.Navigator>
|
||||||
@@ -126,7 +118,7 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
|||||||
name="Article"
|
name="Article"
|
||||||
component={ArticleScreen}
|
component={ArticleScreen}
|
||||||
options={({ route }) => ({
|
options={({ route }) => ({
|
||||||
title: `Article by ${route.params.author}`,
|
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||||
})}
|
})}
|
||||||
initialParams={{ author: 'Gandalf' }}
|
initialParams={{ author: 'Gandalf' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Button, Appbar } from 'react-native-paper';
|
import { Button, Appbar } from 'react-native-paper';
|
||||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import { useTheme, RouteProp, ParamListBase } from '@react-navigation/native';
|
import { useTheme, ParamListBase } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
HeaderBackground,
|
HeaderBackground,
|
||||||
useHeaderHeight,
|
useHeaderHeight,
|
||||||
Header,
|
Header,
|
||||||
@@ -27,17 +27,12 @@ type SimpleStackParams = {
|
|||||||
Albums: undefined;
|
Albums: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
|
||||||
|
|
||||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -64,11 +59,7 @@ const ArticleScreen = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AlbumsScreen = ({
|
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
}) => {
|
|
||||||
const headerHeight = useHeaderHeight();
|
const headerHeight = useHeaderHeight();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,9 +87,8 @@ const AlbumsScreen = ({
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
StackScreenProps<ParamListBase>;
|
||||||
};
|
|
||||||
|
|
||||||
function CustomHeader(props: StackHeaderProps) {
|
function CustomHeader(props: StackHeaderProps) {
|
||||||
const { current, next } = props.scene.progress;
|
const { current, next } = props.scene.progress;
|
||||||
@@ -120,9 +110,11 @@ function CustomHeader(props: StackHeaderProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
});
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
const { colors, dark } = useTheme();
|
const { colors, dark } = useTheme();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||||
import { Button, Paragraph } from 'react-native-paper';
|
import { Button, Paragraph } from 'react-native-paper';
|
||||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
import { ParamListBase, useTheme } from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
|
|
||||||
@@ -13,17 +13,12 @@ type SimpleStackParams = {
|
|||||||
Dialog: undefined;
|
Dialog: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
|
||||||
|
|
||||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||||
|
|
||||||
const ArticleScreen = ({
|
const ArticleScreen = ({
|
||||||
navigation,
|
navigation,
|
||||||
route,
|
route,
|
||||||
}: {
|
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
@@ -50,11 +45,7 @@ const ArticleScreen = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DialogScreen = ({
|
const DialogScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: SimpleStackNavigation;
|
|
||||||
}) => {
|
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,14 +72,15 @@ const DialogScreen = ({
|
|||||||
|
|
||||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||||
|
|
||||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||||
navigation: StackNavigationProp<ParamListBase>;
|
StackScreenProps<ParamListBase>;
|
||||||
};
|
|
||||||
|
|
||||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||||
navigation.setOptions({
|
React.useLayoutEffect(() => {
|
||||||
headerShown: false,
|
navigation.setOptions({
|
||||||
});
|
headerShown: false,
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleStack.Navigator mode="modal" {...rest}>
|
<SimpleStack.Navigator mode="modal" {...rest}>
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import {
|
|||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
type Props = Partial<ScrollViewProps>;
|
type Props = Partial<ScrollViewProps> & {
|
||||||
|
date?: number;
|
||||||
|
};
|
||||||
|
|
||||||
const Author = () => {
|
const Author = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ import {
|
|||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
createDrawerNavigator,
|
createDrawerNavigator,
|
||||||
DrawerNavigationProp,
|
DrawerScreenProps,
|
||||||
} from '@react-navigation/drawer';
|
} from '@react-navigation/drawer';
|
||||||
import {
|
import {
|
||||||
createStackNavigator,
|
createStackNavigator,
|
||||||
StackNavigationProp,
|
StackScreenProps,
|
||||||
HeaderStyleInterpolators,
|
HeaderStyleInterpolators,
|
||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
|
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
|
||||||
@@ -53,9 +53,10 @@ import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
|||||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||||
import NotFound from './Screens/NotFound';
|
import NotFound from './Screens/NotFound';
|
||||||
import DynamicTabs from './Screens/DynamicTabs';
|
import DynamicTabs from './Screens/DynamicTabs';
|
||||||
import AuthFlow from './Screens/AuthFlow';
|
|
||||||
import CompatAPI from './Screens/CompatAPI';
|
|
||||||
import MasterDetail from './Screens/MasterDetail';
|
import MasterDetail from './Screens/MasterDetail';
|
||||||
|
import AuthFlow from './Screens/AuthFlow';
|
||||||
|
import PreventRemove from './Screens/PreventRemove';
|
||||||
|
import CompatAPI from './Screens/CompatAPI';
|
||||||
import LinkComponent from './Screens/LinkComponent';
|
import LinkComponent from './Screens/LinkComponent';
|
||||||
|
|
||||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||||
@@ -109,6 +110,10 @@ const SCREENS = {
|
|||||||
title: 'Auth Flow',
|
title: 'Auth Flow',
|
||||||
component: AuthFlow,
|
component: AuthFlow,
|
||||||
},
|
},
|
||||||
|
PreventRemove: {
|
||||||
|
title: 'Prevent removing screen',
|
||||||
|
component: PreventRemove,
|
||||||
|
},
|
||||||
CompatAPI: {
|
CompatAPI: {
|
||||||
title: 'Compat Layer',
|
title: 'Compat Layer',
|
||||||
component: CompatAPI,
|
component: CompatAPI,
|
||||||
@@ -272,6 +277,10 @@ export default function App() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fallback={<Text>Loading…</Text>}
|
fallback={<Text>Loading…</Text>}
|
||||||
|
documentTitle={{
|
||||||
|
formatter: (options, route) =>
|
||||||
|
`${options?.title ?? route?.name} - React Navigation Example`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
@@ -283,11 +292,7 @@ export default function App() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({
|
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => (
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: DrawerNavigationProp<RootDrawerParamList>;
|
|
||||||
}) => (
|
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||||
@@ -308,11 +313,7 @@ export default function App() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({
|
{({ navigation }: StackScreenProps<RootStackParamList>) => (
|
||||||
navigation,
|
|
||||||
}: {
|
|
||||||
navigation: StackNavigationProp<RootStackParamList>;
|
|
||||||
}) => (
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ backgroundColor: theme.colors.background }}
|
style={{ backgroundColor: theme.colors.background }}
|
||||||
>
|
>
|
||||||
@@ -361,7 +362,7 @@ export default function App() {
|
|||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
component={SCREENS[name].component}
|
getComponent={() => SCREENS[name].component}
|
||||||
options={{ title: SCREENS[name].title }}
|
options={{ title: SCREENS[name].title }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
/* eslint-env jest */
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
|
||||||
|
import 'react-native-gesture-handler/jestSetup';
|
||||||
|
|
||||||
|
jest.mock('react-native-reanimated', () => {
|
||||||
|
const Reanimated = require('react-native-reanimated/mock');
|
||||||
|
|
||||||
|
// The mock for `call` immediately calls the callback which is incorrect
|
||||||
|
// So we override it with a no-op
|
||||||
|
Reanimated.default.call = () => {};
|
||||||
|
|
||||||
|
return Reanimated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
|
||||||
|
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
|
||||||
|
|
||||||
const error = console.error;
|
const error = console.error;
|
||||||
|
|
||||||
console.error = (...args) =>
|
console.error = (...args) =>
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
|
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint --ext '.js,.ts,.tsx' .",
|
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
||||||
"typescript": "tsc --noEmit --composite false",
|
"typescript": "tsc --noEmit --composite false",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prerelease": "lerna run clean",
|
"prerelease": "lerna run clean",
|
||||||
@@ -25,24 +25,17 @@
|
|||||||
"example": "yarn --cwd example"
|
"example": "yarn --cwd example"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.10.1",
|
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.10.1",
|
|
||||||
"@babel/preset-env": "^7.10.2",
|
|
||||||
"@babel/preset-flow": "^7.10.1",
|
|
||||||
"@babel/preset-react": "^7.10.1",
|
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
|
||||||
"@babel/runtime": "^7.10.2",
|
|
||||||
"@commitlint/config-conventional": "^8.3.4",
|
"@commitlint/config-conventional": "^8.3.4",
|
||||||
"@types/jest": "^26.0.0",
|
"@types/jest": "^26.0.0",
|
||||||
"babel-jest": "^26.0.1",
|
"babel-jest": "^26.0.1",
|
||||||
"codecov": "^3.7.0",
|
"codecov": "^3.7.0",
|
||||||
"commitlint": "^8.3.5",
|
"commitlint": "^8.3.5",
|
||||||
"core-js": "^3.6.5",
|
|
||||||
"eslint": "^7.2.0",
|
"eslint": "^7.2.0",
|
||||||
"eslint-config-satya164": "^3.1.7",
|
"eslint-config-satya164": "^3.1.7",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"jest": "^26.0.1",
|
"jest": "^26.0.1",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
|
"metro-react-native-babel-preset": "^0.59.0",
|
||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
@@ -59,9 +52,6 @@
|
|||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testRegex": "/__tests__/.*\\.(test|spec)\\.(js|tsx?)$",
|
"testRegex": "/__tests__/.*\\.(test|spec)\\.(js|tsx?)$",
|
||||||
"transform": {
|
|
||||||
"^.+\\.(js|ts|tsx)$": "babel-jest"
|
|
||||||
},
|
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"<rootDir>/jest/setup.js"
|
"<rootDir>/jest/setup.js"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,6 +3,61 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.7.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.2...@react-navigation/bottom-tabs@5.7.3) (2020-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add accessibilityState property ([#8548](https://github.com/react-navigation/react-navigation/issues/8548)) ([ce4eb7e](https://github.com/react-navigation/react-navigation/commit/ce4eb7e9273a25e4433eb82e255a58ba3bf4d632))
|
||||||
|
* pass label position flag to label rendering in BottomTabBar ([#8557](https://github.com/react-navigation/react-navigation/issues/8557)) ([baea77e](https://github.com/react-navigation/react-navigation/commit/baea77e3325f0d7e5ce331ad61979a9362dd01fa))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.7.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.1...@react-navigation/bottom-tabs@5.7.2) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.0...@react-navigation/bottom-tabs@5.7.1) (2020-07-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't render badge on bottom tabs if not visible. closes [#8577](https://github.com/react-navigation/react-navigation/issues/8577) ([2f74541](https://github.com/react-navigation/react-navigation/commit/2f74541811bac4d36e89c159cd1f4b267063e7f9))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.6.1...@react-navigation/bottom-tabs@5.7.0) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix bottom tab bar to match iOS defaults ([849e04a](https://github.com/react-navigation/react-navigation/commit/849e04ab6a541fffb490ffdfa9819608b88494f4))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for badges to bottom tab bar ([96c7b68](https://github.com/react-navigation/react-navigation/commit/96c7b688ce773b3dd1f1cf7775367cd7080c94a2))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.6.0...@react-navigation/bottom-tabs@5.6.1) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.5.2...@react-navigation/bottom-tabs@5.6.0) (2020-06-24)
|
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.5.2...@react-navigation/bottom-tabs@5.6.0) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/bottom-tabs",
|
"name": "@react-navigation/bottom-tabs",
|
||||||
"description": "Bottom tab navigator following iOS design guidelines",
|
"description": "Bottom tab navigator following iOS design guidelines",
|
||||||
"version": "5.6.0",
|
"version": "5.7.3",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"react-native": "~0.61.5",
|
"react-native": "~0.61.5",
|
||||||
"react-native-safe-area-context": "^1.0.0",
|
"react-native-safe-area-context": "^1.0.0",
|
||||||
"react-native-screens": "^2.7.0",
|
"react-native-screens": "^2.7.0",
|
||||||
|
"react-native-testing-library": "^2.1.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
33
packages/bottom-tabs/src/__tests__/index.test.tsx
Normal file
33
packages/bottom-tabs/src/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, Text, Button } from 'react-native';
|
||||||
|
import { render, fireEvent } from 'react-native-testing-library';
|
||||||
|
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
|
||||||
|
import { createBottomTabNavigator, BottomTabScreenProps } from '../index';
|
||||||
|
|
||||||
|
it('renders a bottom tab navigator with screens', async () => {
|
||||||
|
const Test = ({ route, navigation }: BottomTabScreenProps<ParamListBase>) => (
|
||||||
|
<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 { findByText, queryByText } = render(
|
||||||
|
<NavigationContainer>
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name="A" component={Test} />
|
||||||
|
<Tab.Screen name="B" component={Test} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Screen A')).not.toBeNull();
|
||||||
|
expect(queryByText('Screen B')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.press(await findByText('Go to B'));
|
||||||
|
|
||||||
|
expect(queryByText('Screen B')).not.toBeNull();
|
||||||
|
});
|
||||||
@@ -63,12 +63,16 @@ export type BottomTabNavigationOptions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Title string of a tab displayed in the tab bar
|
* Title string of a tab displayed in the tab bar
|
||||||
* or a function that given { focused: boolean, color: string } returns a React.Node to display in tab bar.
|
* or a function that given { focused: boolean, color: string, position: 'below-icon' | 'beside-icon' } returns a React.Node to display in tab bar.
|
||||||
* When undefined, scene title is used. To hide, see tabBarOptions.showLabel in the previous section.
|
* When undefined, scene title is used. To hide, see tabBarOptions.showLabel in the previous section.
|
||||||
*/
|
*/
|
||||||
tabBarLabel?:
|
tabBarLabel?:
|
||||||
| string
|
| string
|
||||||
| ((props: { focused: boolean; color: string }) => React.ReactNode);
|
| ((props: {
|
||||||
|
focused: boolean;
|
||||||
|
color: string;
|
||||||
|
position: LabelPosition;
|
||||||
|
}) => React.ReactNode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that given { focused: boolean, color: string } returns a React.Node to display in the tab bar.
|
* A function that given { focused: boolean, color: string } returns a React.Node to display in the tab bar.
|
||||||
@@ -79,6 +83,11 @@ export type BottomTabNavigationOptions = {
|
|||||||
size: number;
|
size: number;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text to show in a badge on the tab icon.
|
||||||
|
*/
|
||||||
|
tabBarBadge?: number | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||||
* It's recommended to set this if you don't have a label for the tab.
|
* It's recommended to set this if you don't have a label for the tab.
|
||||||
@@ -178,7 +187,8 @@ export type BottomTabBarOptions = {
|
|||||||
tabStyle?: StyleProp<ViewStyle>;
|
tabStyle?: StyleProp<ViewStyle>;
|
||||||
/**
|
/**
|
||||||
* Whether the label is rendered below the icon or beside the icon.
|
* Whether the label is rendered below the icon or beside the icon.
|
||||||
* By default, in `vertical` orientation, label is rendered below and in `horizontal` orientation, it's rendered beside.
|
* By default, the position is chosen automatically based on device width.
|
||||||
|
* In `below-icon` orientation (typical for iPhones), the label is rendered below and in `beside-icon` orientation, it's rendered beside (typical for iPad).
|
||||||
*/
|
*/
|
||||||
labelPosition?: LabelPosition;
|
labelPosition?: LabelPosition;
|
||||||
/**
|
/**
|
||||||
|
|||||||
108
packages/bottom-tabs/src/views/Badge.tsx
Normal file
108
packages/bottom-tabs/src/views/Badge.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Animated, StyleSheet, StyleProp, TextStyle } from 'react-native';
|
||||||
|
import color from 'color';
|
||||||
|
import { useTheme } from '@react-navigation/native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/**
|
||||||
|
* Whether the badge is visible
|
||||||
|
*/
|
||||||
|
visible: boolean;
|
||||||
|
/**
|
||||||
|
* Content of the `Badge`.
|
||||||
|
*/
|
||||||
|
children?: string | number;
|
||||||
|
/**
|
||||||
|
* Size of the `Badge`.
|
||||||
|
*/
|
||||||
|
size?: number;
|
||||||
|
/**
|
||||||
|
* Style object for the tab bar container.
|
||||||
|
*/
|
||||||
|
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Badge({
|
||||||
|
visible = true,
|
||||||
|
size = 18,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}: Props) {
|
||||||
|
const [opacity] = React.useState(() => new Animated.Value(visible ? 1 : 0));
|
||||||
|
const [rendered, setRendered] = React.useState(visible ? true : false);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!rendered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animated.timing(opacity, {
|
||||||
|
toValue: visible ? 1 : 0,
|
||||||
|
duration: 150,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start(({ finished }) => {
|
||||||
|
if (finished && !visible) {
|
||||||
|
setRendered(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [opacity, rendered, visible]);
|
||||||
|
|
||||||
|
if (visible && !rendered) {
|
||||||
|
setRendered(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visible && !rendered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error: backgroundColor definitely exists
|
||||||
|
const { backgroundColor = theme.colors.notification, ...restStyle } =
|
||||||
|
StyleSheet.flatten(style) || {};
|
||||||
|
const textColor = color(backgroundColor).isLight() ? 'black' : 'white';
|
||||||
|
|
||||||
|
const borderRadius = size / 2;
|
||||||
|
const fontSize = Math.floor((size * 3) / 4);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.Text
|
||||||
|
numberOfLines={1}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
opacity,
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scale: opacity.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0.5, 1],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
backgroundColor,
|
||||||
|
color: textColor,
|
||||||
|
fontSize,
|
||||||
|
lineHeight: size - 1,
|
||||||
|
height: size,
|
||||||
|
minWidth: size,
|
||||||
|
borderRadius,
|
||||||
|
},
|
||||||
|
styles.container,
|
||||||
|
restStyle,
|
||||||
|
]}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Animated.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
textAlign: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -25,7 +25,7 @@ type Props = BottomTabBarProps & {
|
|||||||
inactiveTintColor?: string;
|
inactiveTintColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_TABBAR_HEIGHT = 50;
|
const DEFAULT_TABBAR_HEIGHT = 49;
|
||||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||||
|
|
||||||
const useNativeDriver = Platform.OS !== 'web';
|
const useNativeDriver = Platform.OS !== 'web';
|
||||||
@@ -152,6 +152,8 @@ export default function BottomTabBar({
|
|||||||
left: safeAreaInsets?.left ?? defaultInsets.left,
|
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paddingBottom = Math.max(insets.bottom - 4, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
@@ -165,7 +167,7 @@ export default function BottomTabBar({
|
|||||||
{
|
{
|
||||||
translateY: visible.interpolate({
|
translateY: visible.interpolate({
|
||||||
inputRange: [0, 1],
|
inputRange: [0, 1],
|
||||||
outputRange: [layout.height + insets.bottom, 0],
|
outputRange: [layout.height + paddingBottom, 0],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -174,8 +176,8 @@ export default function BottomTabBar({
|
|||||||
position: isTabBarHidden ? 'absolute' : null,
|
position: isTabBarHidden ? 'absolute' : null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
|
height: DEFAULT_TABBAR_HEIGHT + paddingBottom,
|
||||||
paddingBottom: insets.bottom,
|
paddingBottom,
|
||||||
paddingHorizontal: Math.max(insets.left, insets.right),
|
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||||
},
|
},
|
||||||
style,
|
style,
|
||||||
@@ -245,6 +247,7 @@ export default function BottomTabBar({
|
|||||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||||
button={options.tabBarButton}
|
button={options.tabBarButton}
|
||||||
icon={options.tabBarIcon}
|
icon={options.tabBarIcon}
|
||||||
|
badge={options.tabBarBadge}
|
||||||
label={label}
|
label={label}
|
||||||
showLabel={showLabel}
|
showLabel={showLabel}
|
||||||
labelStyle={labelStyle}
|
labelStyle={labelStyle}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Link, Route, useTheme } from '@react-navigation/native';
|
|||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
import TabBarIcon from './TabBarIcon';
|
import TabBarIcon from './TabBarIcon';
|
||||||
import type { BottomTabBarButtonProps } from '../types';
|
import type { BottomTabBarButtonProps, LabelPosition } from '../types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
@@ -30,7 +30,11 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
label:
|
label:
|
||||||
| string
|
| string
|
||||||
| ((props: { focused: boolean; color: string }) => React.ReactNode);
|
| ((props: {
|
||||||
|
focused: boolean;
|
||||||
|
color: string;
|
||||||
|
position: LabelPosition;
|
||||||
|
}) => React.ReactNode);
|
||||||
/**
|
/**
|
||||||
* Icon to display for the tab.
|
* Icon to display for the tab.
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +43,10 @@ type Props = {
|
|||||||
size: number;
|
size: number;
|
||||||
color: string;
|
color: string;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* Text to show in a badge on the tab icon.
|
||||||
|
*/
|
||||||
|
badge?: number | string;
|
||||||
/**
|
/**
|
||||||
* URL to use for the link to the tab.
|
* URL to use for the link to the tab.
|
||||||
*/
|
*/
|
||||||
@@ -113,6 +121,7 @@ export default function BottomTabBarItem({
|
|||||||
route,
|
route,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
|
badge,
|
||||||
to,
|
to,
|
||||||
button = ({
|
button = ({
|
||||||
children,
|
children,
|
||||||
@@ -206,7 +215,11 @@ export default function BottomTabBarItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return label({ focused, color });
|
return label({
|
||||||
|
focused,
|
||||||
|
color,
|
||||||
|
position: horizontal ? 'beside-icon' : 'below-icon',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIcon = ({ focused }: { focused: boolean }) => {
|
const renderIcon = ({ focused }: { focused: boolean }) => {
|
||||||
@@ -220,16 +233,14 @@ export default function BottomTabBarItem({
|
|||||||
return (
|
return (
|
||||||
<TabBarIcon
|
<TabBarIcon
|
||||||
route={route}
|
route={route}
|
||||||
size={horizontal ? 17 : 24}
|
horizontal={horizontal}
|
||||||
|
badge={badge}
|
||||||
activeOpacity={activeOpacity}
|
activeOpacity={activeOpacity}
|
||||||
inactiveOpacity={inactiveOpacity}
|
inactiveOpacity={inactiveOpacity}
|
||||||
activeTintColor={activeTintColor}
|
activeTintColor={activeTintColor}
|
||||||
inactiveTintColor={inactiveTintColor}
|
inactiveTintColor={inactiveTintColor}
|
||||||
renderIcon={icon}
|
renderIcon={icon}
|
||||||
style={[
|
style={iconStyle}
|
||||||
horizontal ? styles.iconHorizontal : styles.iconVertical,
|
|
||||||
iconStyle,
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -247,6 +258,7 @@ export default function BottomTabBarItem({
|
|||||||
testID,
|
testID,
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
accessibilityRole: 'button',
|
accessibilityRole: 'button',
|
||||||
|
accessibilityState: { selected: focused },
|
||||||
accessibilityStates: focused ? ['selected'] : [],
|
accessibilityStates: focused ? ['selected'] : [],
|
||||||
style: [
|
style: [
|
||||||
styles.tab,
|
styles.tab,
|
||||||
@@ -276,23 +288,17 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
iconVertical: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
iconHorizontal: {
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
label: {
|
label: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
labelBeneath: {
|
labelBeneath: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
marginBottom: 1.5,
|
|
||||||
},
|
},
|
||||||
labelBeside: {
|
labelBeside: {
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
marginLeft: 20,
|
marginLeft: 20,
|
||||||
|
marginTop: 3,
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||||
import type { Route } from '@react-navigation/native';
|
import type { Route } from '@react-navigation/native';
|
||||||
|
import Badge from './Badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
route: Route<string>;
|
route: Route<string>;
|
||||||
size: number;
|
horizontal: boolean;
|
||||||
|
badge?: string | number;
|
||||||
activeOpacity: number;
|
activeOpacity: number;
|
||||||
inactiveOpacity: number;
|
inactiveOpacity: number;
|
||||||
activeTintColor: string;
|
activeTintColor: string;
|
||||||
@@ -18,18 +20,23 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TabBarIcon({
|
export default function TabBarIcon({
|
||||||
|
horizontal,
|
||||||
|
badge,
|
||||||
activeOpacity,
|
activeOpacity,
|
||||||
inactiveOpacity,
|
inactiveOpacity,
|
||||||
activeTintColor,
|
activeTintColor,
|
||||||
inactiveTintColor,
|
inactiveTintColor,
|
||||||
renderIcon,
|
renderIcon,
|
||||||
size,
|
|
||||||
style,
|
style,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const size = 25;
|
||||||
|
|
||||||
// We render the icon twice at the same position on top of each other:
|
// We render the icon twice at the same position on top of each other:
|
||||||
// active and inactive one, so we can fade between them.
|
// active and inactive one, so we can fade between them.
|
||||||
return (
|
return (
|
||||||
<View style={style}>
|
<View
|
||||||
|
style={[horizontal ? styles.iconHorizontal : styles.iconVertical, style]}
|
||||||
|
>
|
||||||
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
||||||
{renderIcon({
|
{renderIcon({
|
||||||
focused: true,
|
focused: true,
|
||||||
@@ -44,6 +51,16 @@ export default function TabBarIcon({
|
|||||||
color: inactiveTintColor,
|
color: inactiveTintColor,
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
<Badge
|
||||||
|
visible={badge != null}
|
||||||
|
style={[
|
||||||
|
styles.badge,
|
||||||
|
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
|
||||||
|
]}
|
||||||
|
size={(size * 3) / 4}
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,4 +79,21 @@ const styles = StyleSheet.create({
|
|||||||
// Workaround for react-native >= 0.54 layout bug
|
// Workaround for react-native >= 0.54 layout bug
|
||||||
minWidth: 25,
|
minWidth: 25,
|
||||||
},
|
},
|
||||||
|
iconVertical: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
iconHorizontal: {
|
||||||
|
height: '100%',
|
||||||
|
marginTop: 3,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 3,
|
||||||
|
},
|
||||||
|
badgeVertical: {
|
||||||
|
top: 3,
|
||||||
|
},
|
||||||
|
badgeHorizontal: {
|
||||||
|
top: 7,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,63 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.2.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.3...@react-navigation/compat@5.2.4) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.1...@react-navigation/compat@5.2.3) (2020-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix false warning due to change in Object.assign in metro preset ([5e358b3](https://github.com/react-navigation/react-navigation/commit/5e358b3aadac7bb186521872d515fff2e571a940)), closes [#8584](https://github.com/react-navigation/react-navigation/issues/8584)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.1...@react-navigation/compat@5.2.2) (2020-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix false warning due to change in Object.assign in metro preset ([240a706](https://github.com/react-navigation/react-navigation/commit/240a706a56220b63d603a52407a738c2872349dd)), closes [#8584](https://github.com/react-navigation/react-navigation/issues/8584)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.0...@react-navigation/compat@5.2.1) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.2.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.28...@react-navigation/compat@5.2.0) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a getComponent prop to lazily specify components ([f418029](https://github.com/react-navigation/react-navigation/commit/f4180295bf22e32c65f6a7ab7089523cb2de58fb))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.28](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.27...@react-navigation/compat@5.1.28) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.1.27](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.26...@react-navigation/compat@5.1.27) (2020-06-24)
|
## [5.1.27](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.26...@react-navigation/compat@5.1.27) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/compat",
|
"name": "@react-navigation/compat",
|
||||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||||
"version": "5.1.27",
|
"version": "5.2.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type {
|
|
||||||
NavigationProp,
|
|
||||||
ParamListBase,
|
|
||||||
RouteProp,
|
|
||||||
} from '@react-navigation/native';
|
|
||||||
import ScreenPropsContext from './ScreenPropsContext';
|
import ScreenPropsContext from './ScreenPropsContext';
|
||||||
import useCompatNavigation from './useCompatNavigation';
|
import useCompatNavigation from './useCompatNavigation';
|
||||||
|
|
||||||
type Props<ParamList extends ParamListBase> = {
|
type Props = {
|
||||||
navigation: NavigationProp<ParamList>;
|
getComponent: () => React.ComponentType<any>;
|
||||||
route: RouteProp<ParamList, string>;
|
|
||||||
component: React.ComponentType<any>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ScreenComponent<ParamList extends ParamListBase>(
|
function CompatScreen({ getComponent }: Props) {
|
||||||
props: Props<ParamList>
|
|
||||||
) {
|
|
||||||
const navigation = useCompatNavigation();
|
const navigation = useCompatNavigation();
|
||||||
const screenProps = React.useContext(ScreenPropsContext);
|
const screenProps = React.useContext(ScreenPropsContext);
|
||||||
|
const ScreenComponent = getComponent();
|
||||||
|
|
||||||
return <props.component navigation={navigation} screenProps={screenProps} />;
|
return <ScreenComponent navigation={navigation} screenProps={screenProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(ScreenComponent);
|
export default React.memo(CompatScreen);
|
||||||
|
|||||||
@@ -143,9 +143,12 @@ export default function createCompatNavigationProp<
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
...state,
|
// @ts-expect-error: these properties may actually exist
|
||||||
|
key: state.key,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
routeName: state.name,
|
routeName: state.name,
|
||||||
|
// @ts-expect-error
|
||||||
|
params: state.params ?? {},
|
||||||
get index() {
|
get index() {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (state.index !== undefined) {
|
if (state.index !== undefined) {
|
||||||
@@ -154,11 +157,11 @@ export default function createCompatNavigationProp<
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"Accessing child navigation state for a route is not safe and won't work correctly."
|
"Looks like you are using 'navigation.state.index' in your code. Accessing child navigation state for a route is not safe and won't work correctly. You should refactor it not to access the 'index' property in the child navigation state."
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
return state.state ? state.state.index : undefined;
|
return state.state?.index;
|
||||||
},
|
},
|
||||||
get routes() {
|
get routes() {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -168,11 +171,11 @@ export default function createCompatNavigationProp<
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"Accessing child navigation state for a route is not safe and won't work correctly."
|
"Looks like you are using 'navigation.state.routes' in your code. Accessing child navigation state for a route is not safe and won't work correctly. You should refactor it not to access the 'routes' property in the child navigation state."
|
||||||
);
|
);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
return state.state ? state.state.routes : undefined;
|
return state.state?.routes;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getParam<T extends keyof ParamList>(
|
getParam<T extends keyof ParamList>(
|
||||||
|
|||||||
@@ -142,13 +142,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
initialParams={{ ...parentRouteParams, ...initialParams }}
|
initialParams={{ ...parentRouteParams, ...initialParams }}
|
||||||
options={screenOptions}
|
options={screenOptions}
|
||||||
>
|
>
|
||||||
{({ navigation, route }) => (
|
{() => <CompatScreen getComponent={getScreenComponent} />}
|
||||||
<CompatScreen
|
|
||||||
navigation={navigation}
|
|
||||||
route={route}
|
|
||||||
component={getScreenComponent()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pair.Screen>
|
</Pair.Screen>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,6 +3,57 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.12.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.1...@react-navigation/core@5.12.2) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.12.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.0...@react-navigation/core@5.12.1) (2020-07-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make sure new state events are emitted when new navigators mount ([af8b274](https://github.com/react-navigation/react-navigation/commit/af8b27414c8628570d946003f4fdff3341cb8954))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.12.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.11.1...@react-navigation/core@5.12.0) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* avoid error setting warning for devtools migration. closes [#8534](https://github.com/react-navigation/react-navigation/issues/8534) ([1801a13](https://github.com/react-navigation/react-navigation/commit/1801a13323eff149fb6bc4e3c3f12422b401f178))
|
||||||
|
* fix bubbling actions to correct target when specified ([9671c76](https://github.com/react-navigation/react-navigation/commit/9671c76c5121aaa64a956e2ca696b2f1712cd6f4))
|
||||||
|
* fix options event being emitted incorrectly ([#8559](https://github.com/react-navigation/react-navigation/issues/8559)) ([a255e35](https://github.com/react-navigation/react-navigation/commit/a255e350f9a54c6d8e410167c9c8661e70b23779))
|
||||||
|
* improve the warning message for non-serializable values ([e63580e](https://github.com/react-navigation/react-navigation/commit/e63580edbef8e77239f3dbefc919d1a41723eff1))
|
||||||
|
* mark some types as read-only ([7c3a0a0](https://github.com/react-navigation/react-navigation/commit/7c3a0a0f23629da0beb956ba5a9689ab965061ce))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a `beforeRemove` event ([6925e92](https://github.com/react-navigation/react-navigation/commit/6925e92dc3e9885e3f552ca5e5eb51ae1521e54e))
|
||||||
|
* add a getComponent prop to lazily specify components ([f418029](https://github.com/react-navigation/react-navigation/commit/f4180295bf22e32c65f6a7ab7089523cb2de58fb))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.11.0...@react-navigation/core@5.11.1) (2020-06-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix error with type definitions. closes [#8511](https://github.com/react-navigation/react-navigation/issues/8511) ([d1210a8](https://github.com/react-navigation/react-navigation/commit/d1210a861b37201827c333a5c012c4f0ebd9bb6a))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.10.0...@react-navigation/core@5.11.0) (2020-06-24)
|
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.10.0...@react-navigation/core@5.11.0) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/core",
|
"name": "@react-navigation/core",
|
||||||
"description": "Core utilities for building navigators",
|
"description": "Core utilities for building navigators",
|
||||||
"version": "5.11.0",
|
"version": "5.12.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/routers": "^5.4.8",
|
"@react-navigation/routers": "^5.4.10",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"nanoid": "^3.1.9",
|
"nanoid": "^3.1.9",
|
||||||
"query-string": "^6.13.1",
|
"query-string": "^6.13.1",
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import NavigationBuilderContext from './NavigationBuilderContext';
|
|||||||
import NavigationStateContext from './NavigationStateContext';
|
import NavigationStateContext from './NavigationStateContext';
|
||||||
import UnhandledActionContext from './UnhandledActionContext';
|
import UnhandledActionContext from './UnhandledActionContext';
|
||||||
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||||
import useFocusedListeners from './useFocusedListeners';
|
import useChildListeners from './useChildListeners';
|
||||||
import useStateGetters from './useStateGetters';
|
import useKeyedChildListeners from './useKeyedChildListeners';
|
||||||
import useOptionsGetters from './useOptionsGetters';
|
import useOptionsGetters from './useOptionsGetters';
|
||||||
import useEventEmitter from './useEventEmitter';
|
import useEventEmitter from './useEventEmitter';
|
||||||
import useSyncState from './useSyncState';
|
import useSyncState from './useSyncState';
|
||||||
import isSerializable from './isSerializable';
|
import checkSerializable from './checkSerializable';
|
||||||
import type {
|
import type {
|
||||||
NavigationContainerEventMap,
|
NavigationContainerEventMap,
|
||||||
NavigationContainerRef,
|
NavigationContainerRef,
|
||||||
@@ -29,22 +29,26 @@ type State = NavigationState | PartialState<NavigationState> | undefined;
|
|||||||
const NOT_INITIALIZED_ERROR =
|
const NOT_INITIALIZED_ERROR =
|
||||||
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
|
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
|
||||||
|
|
||||||
let hasWarnedForSerialization = false;
|
const serializableWarnings: string[] = [];
|
||||||
|
|
||||||
/**
|
try {
|
||||||
* Migration instructions for removal of devtools from core
|
/**
|
||||||
*/
|
* Migration instructions for removal of devtools from core
|
||||||
Object.defineProperty(
|
*/
|
||||||
global,
|
Object.defineProperty(
|
||||||
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
|
global,
|
||||||
{
|
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
|
||||||
set(_) {
|
{
|
||||||
console.warn(
|
set(_) {
|
||||||
"Redux devtools extension integration can be enabled with the '@react-navigation/devtools' package. For more details, see https://reactnavigation.org/docs/devtools"
|
console.warn(
|
||||||
);
|
"Redux devtools extension integration can be enabled with the '@react-navigation/devtools' package. For more details, see https://reactnavigation.org/docs/devtools"
|
||||||
},
|
);
|
||||||
}
|
},
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
|
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
|
||||||
@@ -123,29 +127,26 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
navigatorKeyRef.current = key;
|
navigatorKeyRef.current = key;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const {
|
const { listeners, addListener } = useChildListeners();
|
||||||
listeners,
|
|
||||||
addListener: addFocusedListener,
|
|
||||||
} = useFocusedListeners();
|
|
||||||
|
|
||||||
const { getStateForRoute, addStateGetter } = useStateGetters();
|
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
||||||
|
|
||||||
const dispatch = (
|
const dispatch = (
|
||||||
action: NavigationAction | ((state: NavigationState) => NavigationAction)
|
action: NavigationAction | ((state: NavigationState) => NavigationAction)
|
||||||
) => {
|
) => {
|
||||||
if (listeners[0] == null) {
|
if (listeners.focus[0] == null) {
|
||||||
throw new Error(NOT_INITIALIZED_ERROR);
|
throw new Error(NOT_INITIALIZED_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners[0]((navigation) => navigation.dispatch(action));
|
listeners.focus[0]((navigation) => navigation.dispatch(action));
|
||||||
};
|
};
|
||||||
|
|
||||||
const canGoBack = () => {
|
const canGoBack = () => {
|
||||||
if (listeners[0] == null) {
|
if (listeners.focus[0] == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result, handled } = listeners[0]((navigation) =>
|
const { result, handled } = listeners.focus[0]((navigation) =>
|
||||||
navigation.canGoBack()
|
navigation.canGoBack()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -164,8 +165,8 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getRootState = React.useCallback(() => {
|
const getRootState = React.useCallback(() => {
|
||||||
return getStateForRoute('root');
|
return keyedListeners.getState.root?.();
|
||||||
}, [getStateForRoute]);
|
}, [keyedListeners.getState]);
|
||||||
|
|
||||||
const getCurrentRoute = React.useCallback(() => {
|
const getCurrentRoute = React.useCallback(() => {
|
||||||
let state = getRootState();
|
let state = getRootState();
|
||||||
@@ -213,8 +214,16 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
[emitter]
|
[emitter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const lastEmittedOptionsRef = React.useRef<object | undefined>();
|
||||||
|
|
||||||
const onOptionsChange = React.useCallback(
|
const onOptionsChange = React.useCallback(
|
||||||
(options) => {
|
(options) => {
|
||||||
|
if (lastEmittedOptionsRef.current === options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEmittedOptionsRef.current = options;
|
||||||
|
|
||||||
emitter.emit({
|
emitter.emit({
|
||||||
type: 'options',
|
type: 'options',
|
||||||
data: { options },
|
data: { options },
|
||||||
@@ -225,12 +234,12 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
|
|
||||||
const builderContext = React.useMemo(
|
const builderContext = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
addFocusedListener,
|
addListener,
|
||||||
addStateGetter,
|
addKeyedListener,
|
||||||
onDispatchAction,
|
onDispatchAction,
|
||||||
onOptionsChange,
|
onOptionsChange,
|
||||||
}),
|
}),
|
||||||
[addFocusedListener, addStateGetter, onDispatchAction, onOptionsChange]
|
[addListener, addKeyedListener, onDispatchAction, onOptionsChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const scheduleContext = React.useMemo(
|
const scheduleContext = React.useMemo(
|
||||||
@@ -238,6 +247,10 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
[scheduleUpdate, flushUpdates]
|
[scheduleUpdate, flushUpdates]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isInitialRef = React.useRef(true);
|
||||||
|
|
||||||
|
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
state,
|
state,
|
||||||
@@ -245,29 +258,78 @@ const BaseNavigationContainer = React.forwardRef(
|
|||||||
setState,
|
setState,
|
||||||
getKey,
|
getKey,
|
||||||
setKey,
|
setKey,
|
||||||
|
getIsInitial,
|
||||||
addOptionsGetter,
|
addOptionsGetter,
|
||||||
}),
|
}),
|
||||||
[getKey, getState, setKey, setState, state, addOptionsGetter]
|
[
|
||||||
|
state,
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
getKey,
|
||||||
|
setKey,
|
||||||
|
getIsInitial,
|
||||||
|
addOptionsGetter,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStateChangeRef = React.useRef(onStateChange);
|
const onStateChangeRef = React.useRef(onStateChange);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
isInitialRef.current = false;
|
||||||
onStateChangeRef.current = onStateChange;
|
onStateChangeRef.current = onStateChange;
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
if (
|
if (state !== undefined) {
|
||||||
state !== undefined &&
|
const result = checkSerializable(state);
|
||||||
!isSerializable(state) &&
|
|
||||||
!hasWarnedForSerialization
|
|
||||||
) {
|
|
||||||
hasWarnedForSerialization = true;
|
|
||||||
|
|
||||||
console.warn(
|
if (!result.serializable) {
|
||||||
"Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
|
const { location, reason } = result;
|
||||||
);
|
|
||||||
|
let path = '';
|
||||||
|
let pointer: Record<any, any> = state;
|
||||||
|
let params = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < location.length; i++) {
|
||||||
|
const curr = location[i];
|
||||||
|
const prev = location[i - 1];
|
||||||
|
|
||||||
|
pointer = pointer[curr];
|
||||||
|
|
||||||
|
if (!params && curr === 'state') {
|
||||||
|
continue;
|
||||||
|
} else if (!params && curr === 'routes') {
|
||||||
|
if (path) {
|
||||||
|
path += ' > ';
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
!params &&
|
||||||
|
typeof curr === 'number' &&
|
||||||
|
prev === 'routes'
|
||||||
|
) {
|
||||||
|
path += pointer?.name;
|
||||||
|
} else if (!params) {
|
||||||
|
path += ` > ${curr}`;
|
||||||
|
params = true;
|
||||||
|
} else {
|
||||||
|
if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) {
|
||||||
|
path += `[${curr}]`;
|
||||||
|
} else if (/^[a-z$_]+$/i.test(curr)) {
|
||||||
|
path += `.${curr}`;
|
||||||
|
} else {
|
||||||
|
path += `[${JSON.stringify(curr)}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `Non-serializable values were found in the navigation state. Check:\n\n${path} (${reason})\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`;
|
||||||
|
|
||||||
|
if (!serializableWarnings.includes(message)) {
|
||||||
|
serializableWarnings.push(message);
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ import type {
|
|||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import type { NavigationHelpers } from './types';
|
import type { NavigationHelpers } from './types';
|
||||||
|
|
||||||
|
export type ListenerMap = {
|
||||||
|
action: ChildActionListener;
|
||||||
|
focus: FocusedNavigationListener;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeyedListenerMap = {
|
||||||
|
getState: GetStateListener;
|
||||||
|
beforeRemove: ChildBeforeRemoveListener;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddListener = <T extends keyof ListenerMap>(
|
||||||
|
type: T,
|
||||||
|
listener: ListenerMap[T]
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type AddKeyedListener = <T extends keyof KeyedListenerMap>(
|
||||||
|
type: T,
|
||||||
|
key: string,
|
||||||
|
listener: KeyedListenerMap[T]
|
||||||
|
) => void;
|
||||||
|
|
||||||
export type ChildActionListener = (
|
export type ChildActionListener = (
|
||||||
action: NavigationAction,
|
action: NavigationAction,
|
||||||
visitedNavigators?: Set<string>
|
visitedNavigators?: Set<string>
|
||||||
@@ -19,7 +40,9 @@ export type FocusedNavigationListener = <T>(
|
|||||||
callback: FocusedNavigationCallback<T>
|
callback: FocusedNavigationCallback<T>
|
||||||
) => { handled: boolean; result: T };
|
) => { handled: boolean; result: T };
|
||||||
|
|
||||||
export type NavigatorStateGetter = () => NavigationState;
|
export type GetStateListener = () => NavigationState;
|
||||||
|
|
||||||
|
export type ChildBeforeRemoveListener = (action: NavigationAction) => boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context which holds the required helpers needed to build nested navigators.
|
* Context which holds the required helpers needed to build nested navigators.
|
||||||
@@ -29,11 +52,10 @@ const NavigationBuilderContext = React.createContext<{
|
|||||||
action: NavigationAction,
|
action: NavigationAction,
|
||||||
visitedNavigators?: Set<string>
|
visitedNavigators?: Set<string>
|
||||||
) => boolean;
|
) => boolean;
|
||||||
addActionListener?: (listener: ChildActionListener) => void;
|
addListener?: AddListener;
|
||||||
addFocusedListener?: (listener: FocusedNavigationListener) => void;
|
addKeyedListener?: AddKeyedListener;
|
||||||
onRouteFocus?: (key: string) => void;
|
onRouteFocus?: (key: string) => void;
|
||||||
onDispatchAction: (action: NavigationAction, noop: boolean) => void;
|
onDispatchAction: (action: NavigationAction, noop: boolean) => void;
|
||||||
addStateGetter?: (key: string, getter: NavigatorStateGetter) => void;
|
|
||||||
onOptionsChange: (options: object) => void;
|
onOptionsChange: (options: object) => void;
|
||||||
}>({
|
}>({
|
||||||
onDispatchAction: () => undefined,
|
onDispatchAction: () => undefined,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default React.createContext<{
|
|||||||
setState: (
|
setState: (
|
||||||
state: NavigationState | PartialState<NavigationState> | undefined
|
state: NavigationState | PartialState<NavigationState> | undefined
|
||||||
) => void;
|
) => void;
|
||||||
|
getIsInitial: () => boolean;
|
||||||
addOptionsGetter?: (
|
addOptionsGetter?: (
|
||||||
key: string,
|
key: string,
|
||||||
getter: () => object | undefined | null
|
getter: () => object | undefined | null
|
||||||
@@ -32,4 +33,7 @@ export default React.createContext<{
|
|||||||
get setState(): any {
|
get setState(): any {
|
||||||
throw new Error(MISSING_CONTEXT_ERROR);
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
},
|
},
|
||||||
|
get getIsInitial(): any {
|
||||||
|
throw new Error(MISSING_CONTEXT_ERROR);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import NavigationStateContext from './NavigationStateContext';
|
|||||||
import StaticContainer from './StaticContainer';
|
import StaticContainer from './StaticContainer';
|
||||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||||
import useOptionsGetters from './useOptionsGetters';
|
import useOptionsGetters from './useOptionsGetters';
|
||||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
|
||||||
import useFocusEffect from './useFocusEffect';
|
|
||||||
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
|
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
|
||||||
|
|
||||||
type Props<
|
type Props<
|
||||||
@@ -45,26 +43,14 @@ export default function SceneView<
|
|||||||
options,
|
options,
|
||||||
}: Props<State, ScreenOptions, EventMap>) {
|
}: Props<State, ScreenOptions, EventMap>) {
|
||||||
const navigatorKeyRef = React.useRef<string | undefined>();
|
const navigatorKeyRef = React.useRef<string | undefined>();
|
||||||
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
|
|
||||||
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
|
||||||
const optionsRef = React.useRef<object | undefined>(options);
|
|
||||||
const getOptions = React.useCallback(() => optionsRef.current, []);
|
|
||||||
|
|
||||||
const { addOptionsGetter, hasAnyChildListener } = useOptionsGetters({
|
const { addOptionsGetter } = useOptionsGetters({
|
||||||
key: route.key,
|
key: route.key,
|
||||||
getState,
|
options,
|
||||||
getOptions,
|
navigation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const optionsChange = React.useCallback(() => {
|
|
||||||
optionsRef.current = options;
|
|
||||||
if (!hasAnyChildListener) {
|
|
||||||
onOptionsChange(options);
|
|
||||||
}
|
|
||||||
}, [onOptionsChange, options, hasAnyChildListener]);
|
|
||||||
|
|
||||||
useFocusEffect(optionsChange);
|
|
||||||
|
|
||||||
const setKey = React.useCallback((key: string) => {
|
const setKey = React.useCallback((key: string) => {
|
||||||
navigatorKeyRef.current = key;
|
navigatorKeyRef.current = key;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -90,6 +76,14 @@ export default function SceneView<
|
|||||||
[getState, route.key, setState]
|
[getState, route.key, setState]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isInitialRef = React.useRef(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
isInitialRef.current = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
state: route.state,
|
state: route.state,
|
||||||
@@ -97,31 +91,36 @@ export default function SceneView<
|
|||||||
setState: setCurrentState,
|
setState: setCurrentState,
|
||||||
getKey,
|
getKey,
|
||||||
setKey,
|
setKey,
|
||||||
|
getIsInitial,
|
||||||
addOptionsGetter,
|
addOptionsGetter,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
getCurrentState,
|
|
||||||
getKey,
|
|
||||||
route.state,
|
route.state,
|
||||||
|
getCurrentState,
|
||||||
setCurrentState,
|
setCurrentState,
|
||||||
|
getKey,
|
||||||
setKey,
|
setKey,
|
||||||
|
getIsInitial,
|
||||||
addOptionsGetter,
|
addOptionsGetter,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ScreenComponent = screen.getComponent
|
||||||
|
? screen.getComponent()
|
||||||
|
: screen.component;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationStateContext.Provider value={context}>
|
<NavigationStateContext.Provider value={context}>
|
||||||
<EnsureSingleNavigator>
|
<EnsureSingleNavigator>
|
||||||
<StaticContainer
|
<StaticContainer
|
||||||
name={screen.name}
|
name={screen.name}
|
||||||
// @ts-expect-error: these properties exist on screen, but TS is confused
|
render={ScreenComponent || screen.children}
|
||||||
render={screen.component || screen.children}
|
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
route={route}
|
route={route}
|
||||||
>
|
>
|
||||||
{'component' in screen && screen.component !== undefined ? (
|
{ScreenComponent !== undefined ? (
|
||||||
<screen.component navigation={navigation} route={route} />
|
<ScreenComponent navigation={navigation} route={route} />
|
||||||
) : 'children' in screen && screen.children !== undefined ? (
|
) : screen.children !== undefined ? (
|
||||||
screen.children({ navigation, route })
|
screen.children({ navigation, route })
|
||||||
) : null}
|
) : null}
|
||||||
</StaticContainer>
|
</StaticContainer>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { act, render } from 'react-native-testing-library';
|
import { act, render } from 'react-native-testing-library';
|
||||||
import type {
|
import {
|
||||||
DefaultRouterOptions,
|
DefaultRouterOptions,
|
||||||
NavigationState,
|
NavigationState,
|
||||||
Router,
|
Router,
|
||||||
|
StackRouter,
|
||||||
|
TabRouter,
|
||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||||
import NavigationStateContext from '../NavigationStateContext';
|
import NavigationStateContext from '../NavigationStateContext';
|
||||||
@@ -364,6 +366,7 @@ it('handles getRootState', () => {
|
|||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits state events when the state changes', () => {
|
it('emits state events when the state changes', () => {
|
||||||
const TestNavigator = (props: any) => {
|
const TestNavigator = (props: any) => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
@@ -399,6 +402,7 @@ it('emits state events when the state changes', () => {
|
|||||||
ref.current?.navigate('bar');
|
ref.current?.navigate('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
expect(listener.mock.calls[0][0].data.state).toEqual({
|
expect(listener.mock.calls[0][0].data.state).toEqual({
|
||||||
type: 'test',
|
type: 'test',
|
||||||
stale: false,
|
stale: false,
|
||||||
@@ -416,6 +420,7 @@ it('emits state events when the state changes', () => {
|
|||||||
ref.current?.navigate('baz', { answer: 42 });
|
ref.current?.navigate('baz', { answer: 42 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(2);
|
||||||
expect(listener.mock.calls[1][0].data.state).toEqual({
|
expect(listener.mock.calls[1][0].data.state).toEqual({
|
||||||
type: 'test',
|
type: 'test',
|
||||||
stale: false,
|
stale: false,
|
||||||
@@ -430,7 +435,9 @@ it('emits state events when the state changes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits state events when options change', () => {
|
it('emits state events when new navigator mounts', () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
const TestNavigator = (props: any) => {
|
const TestNavigator = (props: any) => {
|
||||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||||
|
|
||||||
@@ -443,6 +450,95 @@ it('emits state events when options change', () => {
|
|||||||
|
|
||||||
const ref = React.createRef<NavigationContainerRef>();
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const NestedNavigator = () => {
|
||||||
|
const [isRendered, setIsRendered] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setTimeout(() => setIsRendered(true), 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isRendered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="baz">{() => null}</Screen>
|
||||||
|
<Screen name="bax">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar" component={NestedNavigator} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element).update(element);
|
||||||
|
|
||||||
|
const listener = jest.fn();
|
||||||
|
|
||||||
|
ref.current?.addListener('state', listener);
|
||||||
|
|
||||||
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
expect(onStateChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultState = {
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 0,
|
||||||
|
key: '10',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo', name: 'foo' },
|
||||||
|
{
|
||||||
|
key: 'bar',
|
||||||
|
name: 'bar',
|
||||||
|
state: {
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 0,
|
||||||
|
key: '11',
|
||||||
|
routeNames: ['baz', 'bax'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bax', name: 'bax' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
expect(listener.mock.calls[0][0].data.state).toEqual(resultState);
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).lastCalledWith(resultState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits option events when options change with tab router', () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(TabRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
<BaseNavigationContainer ref={ref}>
|
<BaseNavigationContainer ref={ref}>
|
||||||
<TestNavigator>
|
<TestNavigator>
|
||||||
@@ -455,7 +551,10 @@ it('emits state events when options change', () => {
|
|||||||
<Screen name="baz" options={{ v: 3 }}>
|
<Screen name="baz" options={{ v: 3 }}>
|
||||||
{() => (
|
{() => (
|
||||||
<TestNavigator>
|
<TestNavigator>
|
||||||
<Screen name="foo" options={{ g: 5 }}>
|
<Screen name="qux" options={{ g: 5 }}>
|
||||||
|
{() => null}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="quxx" options={{ h: 9 }}>
|
||||||
{() => null}
|
{() => null}
|
||||||
</Screen>
|
</Screen>
|
||||||
</TestNavigator>
|
</TestNavigator>
|
||||||
@@ -474,19 +573,105 @@ it('emits state events when options change', () => {
|
|||||||
ref.current?.navigate('bar');
|
ref.current?.navigate('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(listener.mock.calls[0][0].data.options).toEqual({
|
expect(listener).toBeCalledTimes(1);
|
||||||
y: 2,
|
expect(listener.mock.calls[0][0].data.options).toEqual({ y: 2 });
|
||||||
});
|
expect(ref.current?.getCurrentOptions()).toEqual({ y: 2 });
|
||||||
|
|
||||||
ref.current?.removeListener('options', listener);
|
ref.current?.removeListener('options', listener);
|
||||||
|
|
||||||
const listener2 = jest.fn();
|
const listener2 = jest.fn();
|
||||||
|
|
||||||
ref.current?.addListener('options', listener2);
|
ref.current?.addListener('options', listener2);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
ref.current?.navigate('baz');
|
ref.current?.navigate('baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(listener2).toBeCalledTimes(1);
|
||||||
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
|
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
|
||||||
|
expect(ref.current?.getCurrentOptions()).toEqual({ g: 5 });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current?.navigate('quxx');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener2).toBeCalledTimes(2);
|
||||||
|
expect(listener2.mock.calls[1][0].data.options).toEqual({ h: 9 });
|
||||||
|
expect(ref.current?.getCurrentOptions()).toEqual({ h: 9 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits option events when options change with stack router', () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo" options={{ x: 1 }}>
|
||||||
|
{() => null}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="bar" options={{ y: 2 }}>
|
||||||
|
{() => null}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="baz" options={{ v: 3 }}>
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="qux" options={{ g: 5 }}>
|
||||||
|
{() => null}
|
||||||
|
</Screen>
|
||||||
|
<Screen name="quxx" options={{ h: 9 }}>
|
||||||
|
{() => null}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const listener = jest.fn();
|
||||||
|
|
||||||
|
render(element).update(element);
|
||||||
|
ref.current?.addListener('options', listener);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current?.navigate('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toBeCalledTimes(1);
|
||||||
|
expect(listener.mock.calls[0][0].data.options).toEqual({ y: 2 });
|
||||||
|
expect(ref.current?.getCurrentOptions()).toEqual({ y: 2 });
|
||||||
|
|
||||||
|
ref.current?.removeListener('options', listener);
|
||||||
|
|
||||||
|
const listener2 = jest.fn();
|
||||||
|
|
||||||
|
ref.current?.addListener('options', listener2);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current?.navigate('baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener2).toBeCalledTimes(1);
|
||||||
|
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
|
||||||
|
expect(ref.current?.getCurrentOptions()).toEqual({ g: 5 });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current?.navigate('quxx');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener2).toBeCalledTimes(2);
|
||||||
|
expect(listener2.mock.calls[1][0].data.options).toEqual({ h: 9 });
|
||||||
|
expect(ref.current?.getCurrentOptions()).toEqual({ h: 9 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if there is no navigator rendered', () => {
|
it('throws if there is no navigator rendered', () => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import isSerializable from '../isSerializable';
|
import checkSerializable from '../checkSerializable';
|
||||||
|
|
||||||
it('returns true for serializable object', () => {
|
it('returns true for serializable object', () => {
|
||||||
expect(
|
expect(
|
||||||
isSerializable({
|
checkSerializable({
|
||||||
index: 0,
|
index: 0,
|
||||||
key: '7',
|
key: '7',
|
||||||
routeNames: ['foo', 'bar'],
|
routeNames: ['foo', 'bar'],
|
||||||
@@ -22,12 +22,12 @@ it('returns true for serializable object', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
).toBe(true);
|
).toEqual({ serializable: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false for non-serializable object', () => {
|
it('returns false for non-serializable object', () => {
|
||||||
expect(
|
expect(
|
||||||
isSerializable({
|
checkSerializable({
|
||||||
index: 0,
|
index: 0,
|
||||||
key: '7',
|
key: '7',
|
||||||
routeNames: ['foo', 'bar'],
|
routeNames: ['foo', 'bar'],
|
||||||
@@ -47,7 +47,38 @@ it('returns false for non-serializable object', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
).toBe(false);
|
).toEqual({
|
||||||
|
serializable: false,
|
||||||
|
location: ['routes', 0, 'state', 'routes', 0, 'params'],
|
||||||
|
reason: 'Function',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
checkSerializable({
|
||||||
|
index: 0,
|
||||||
|
key: '7',
|
||||||
|
routeNames: ['foo', 'bar'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'foo',
|
||||||
|
name: 'foo',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: '8',
|
||||||
|
routeNames: ['qux', 'lex'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'qux', name: 'qux', params: { foo: Symbol('test') } },
|
||||||
|
{ key: 'lex', name: 'lex' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
serializable: false,
|
||||||
|
location: ['routes', 0, 'state', 'routes', 0, 'params', 'foo'],
|
||||||
|
reason: 'Symbol(test)',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false for circular references', () => {
|
it('returns false for circular references', () => {
|
||||||
@@ -59,7 +90,11 @@ it('returns false for circular references', () => {
|
|||||||
x.b.b2 = x;
|
x.b.b2 = x;
|
||||||
x.c = x.b;
|
x.c = x.b;
|
||||||
|
|
||||||
expect(isSerializable(x)).toBe(false);
|
expect(checkSerializable(x)).toEqual({
|
||||||
|
serializable: false,
|
||||||
|
location: ['b', 'b2'],
|
||||||
|
reason: 'Circular reference',
|
||||||
|
});
|
||||||
|
|
||||||
const y: any = [
|
const y: any = [
|
||||||
{
|
{
|
||||||
@@ -72,7 +107,11 @@ it('returns false for circular references', () => {
|
|||||||
y[0].children[0].parent = y[0];
|
y[0].children[0].parent = y[0];
|
||||||
y[1].extend.home = y[0].children[0];
|
y[1].extend.home = y[0].children[0];
|
||||||
|
|
||||||
expect(isSerializable(y)).toBe(false);
|
expect(checkSerializable(y)).toEqual({
|
||||||
|
serializable: false,
|
||||||
|
location: [0, 'children', 0, 'parent'],
|
||||||
|
reason: 'Circular reference',
|
||||||
|
});
|
||||||
|
|
||||||
const z: any = {
|
const z: any = {
|
||||||
name: 'sun',
|
name: 'sun',
|
||||||
@@ -81,14 +120,18 @@ it('returns false for circular references', () => {
|
|||||||
|
|
||||||
z.child[0].parent = z;
|
z.child[0].parent = z;
|
||||||
|
|
||||||
expect(isSerializable(z)).toBe(false);
|
expect(checkSerializable(z)).toEqual({
|
||||||
|
serializable: false,
|
||||||
|
location: ['child', 0, 'parent'],
|
||||||
|
reason: 'Circular reference',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't fail if same object used multiple times", () => {
|
it("doesn't fail if same object used multiple times", () => {
|
||||||
const o = { foo: 'bar' };
|
const o = { foo: 'bar' };
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
isSerializable({
|
checkSerializable({
|
||||||
baz: 'bax',
|
baz: 'bax',
|
||||||
first: o,
|
first: o,
|
||||||
second: o,
|
second: o,
|
||||||
@@ -96,5 +139,5 @@ it("doesn't fail if same object used multiple times", () => {
|
|||||||
b: o,
|
b: o,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).toBe(true);
|
).toEqual({ serializable: true });
|
||||||
});
|
});
|
||||||
@@ -1403,6 +1403,7 @@ it('throws if both children and component are passed', () => {
|
|||||||
const element = (
|
const element = (
|
||||||
<BaseNavigationContainer>
|
<BaseNavigationContainer>
|
||||||
<TestNavigator>
|
<TestNavigator>
|
||||||
|
{/* @ts-ignore */}
|
||||||
<Screen name="foo" component={jest.fn()}>
|
<Screen name="foo" component={jest.fn()}>
|
||||||
{jest.fn()}
|
{jest.fn()}
|
||||||
</Screen>
|
</Screen>
|
||||||
@@ -1415,6 +1416,48 @@ it('throws if both children and component are passed', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws if both children and getComponent are passed', () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
useNavigationBuilder(MockRouter, props);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<Screen name="foo" getComponent={jest.fn()}>
|
||||||
|
{jest.fn()}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
|
"Got both 'getComponent' and 'children' props for the screen 'foo'. You must pass only one of them."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if both component and getComponent are passed', () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
useNavigationBuilder(MockRouter, props);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<Screen name="foo" component={jest.fn()} getComponent={jest.fn()} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
|
"Got both 'component' and 'getComponent' props for the screen 'foo'. You must pass only one of them."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('throws descriptive error for undefined screen component', () => {
|
it('throws descriptive error for undefined screen component', () => {
|
||||||
const TestNavigator = (props: any) => {
|
const TestNavigator = (props: any) => {
|
||||||
useNavigationBuilder(MockRouter, props);
|
useNavigationBuilder(MockRouter, props);
|
||||||
@@ -1430,7 +1473,7 @@ it('throws descriptive error for undefined screen component', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(() => render(element).update(element)).toThrowError(
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
"Couldn't find a 'component' or 'children' prop for the screen 'foo'"
|
"Couldn't find a 'component', 'getComponent' or 'children' prop for the screen 'foo'"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1453,6 +1496,25 @@ it('throws descriptive error for invalid screen component', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws descriptive error for invalid getComponent prop', () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
useNavigationBuilder(MockRouter, props);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo" getComponent={{} as any} />
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => render(element).update(element)).toThrowError(
|
||||||
|
"Got an invalid value for 'getComponent' prop for the screen 'foo'. It must be a function returning a React Component."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('throws descriptive error for invalid children', () => {
|
it('throws descriptive error for invalid children', () => {
|
||||||
const TestNavigator = (props: any) => {
|
const TestNavigator = (props: any) => {
|
||||||
useNavigationBuilder(MockRouter, props);
|
useNavigationBuilder(MockRouter, props);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { render } from 'react-native-testing-library';
|
import { act, render } from 'react-native-testing-library';
|
||||||
import type {
|
import {
|
||||||
Router,
|
Router,
|
||||||
DefaultRouterOptions,
|
DefaultRouterOptions,
|
||||||
NavigationState,
|
NavigationState,
|
||||||
|
StackRouter,
|
||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import useNavigationBuilder from '../useNavigationBuilder';
|
import useNavigationBuilder from '../useNavigationBuilder';
|
||||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||||
@@ -12,8 +13,19 @@ import MockRouter, {
|
|||||||
MockActions,
|
MockActions,
|
||||||
MockRouterKey,
|
MockRouterKey,
|
||||||
} from './__fixtures__/MockRouter';
|
} from './__fixtures__/MockRouter';
|
||||||
|
import type { NavigationContainerRef } from '../types';
|
||||||
|
|
||||||
beforeEach(() => (MockRouterKey.current = 0));
|
jest.mock('nanoid/non-secure', () => {
|
||||||
|
const m = { nanoid: () => String(++m.__key), __key: 0 };
|
||||||
|
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
MockRouterKey.current = 0;
|
||||||
|
|
||||||
|
require('nanoid/non-secure').__key = 0;
|
||||||
|
});
|
||||||
|
|
||||||
it("lets parent handle the action if child didn't", () => {
|
it("lets parent handle the action if child didn't", () => {
|
||||||
function CurrentRouter(options: DefaultRouterOptions) {
|
function CurrentRouter(options: DefaultRouterOptions) {
|
||||||
@@ -224,6 +236,147 @@ it("lets children handle the action if parent didn't", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('action goes to correct navigator if target is specified', () => {
|
||||||
|
function CurrentTestRouter(options: DefaultRouterOptions) {
|
||||||
|
const CurrentMockRouter = MockRouter(options);
|
||||||
|
const TestRouter: Router<
|
||||||
|
NavigationState,
|
||||||
|
MockActions | { type: 'REVERSE' }
|
||||||
|
> = {
|
||||||
|
...CurrentMockRouter,
|
||||||
|
|
||||||
|
shouldActionChangeFocus() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStateForAction(state, action, options) {
|
||||||
|
if (action.type === 'REVERSE') {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
routes: state.routes.slice().reverse(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return CurrentMockRouter.getStateForAction(state, action, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return TestRouter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChildNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(
|
||||||
|
CurrentTestRouter,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParentNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(
|
||||||
|
CurrentTestRouter,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestScreen = (props: any) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
props.navigation.dispatch({ type: 'REVERSE', target: '0' });
|
||||||
|
}, [props.navigation]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 1,
|
||||||
|
key: '0',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'baz',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 0,
|
||||||
|
key: '1',
|
||||||
|
routeNames: ['qux', 'lex'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'lex', name: 'lex' },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
{ key: 'foo', name: 'foo' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer
|
||||||
|
initialState={initialState}
|
||||||
|
onStateChange={onStateChange}
|
||||||
|
>
|
||||||
|
<ParentNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar">{() => null}</Screen>
|
||||||
|
<Screen name="baz">
|
||||||
|
{() => (
|
||||||
|
<ChildNavigator>
|
||||||
|
<Screen name="qux">{() => null}</Screen>
|
||||||
|
<Screen name="lex" component={TestScreen} />
|
||||||
|
</ChildNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</ParentNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element).update(element);
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 1,
|
||||||
|
key: '0',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo', name: 'foo' },
|
||||||
|
{ key: 'bar', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
stale: false,
|
||||||
|
type: 'test',
|
||||||
|
index: 0,
|
||||||
|
key: '1',
|
||||||
|
routeNames: ['qux', 'lex'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'lex', name: 'lex' },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("action doesn't bubble if target is specified", () => {
|
it("action doesn't bubble if target is specified", () => {
|
||||||
const CurrentParentRouter = MockRouter;
|
const CurrentParentRouter = MockRouter;
|
||||||
|
|
||||||
@@ -379,3 +532,649 @@ it('logs error if no navigator handled the action', () => {
|
|||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prevents removing a screen with 'beforeRemove' event", () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBeforeRemove = jest.fn();
|
||||||
|
|
||||||
|
let shouldPrevent = true;
|
||||||
|
let shouldContinue = false;
|
||||||
|
|
||||||
|
const TestScreen = (props: any) => {
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
props.navigation.addListener('beforeRemove', (e: any) => {
|
||||||
|
onBeforeRemove();
|
||||||
|
|
||||||
|
if (shouldPrevent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (shouldContinue) {
|
||||||
|
props.navigation.dispatch(e.data.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[props.navigation]
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar" component={TestScreen} />
|
||||||
|
<Screen name="baz">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 1,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('baz'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz-5',
|
||||||
|
name: 'baz',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onBeforeRemove).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{ key: 'baz-5', name: 'baz' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(3);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = true;
|
||||||
|
shouldContinue = true;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(5);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prevents removing a child screen with 'beforeRemove' event", () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBeforeRemove = jest.fn();
|
||||||
|
|
||||||
|
let shouldPrevent = true;
|
||||||
|
let shouldContinue = false;
|
||||||
|
|
||||||
|
const TestScreen = (props: any) => {
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
props.navigation.addListener('beforeRemove', (e: any) => {
|
||||||
|
onBeforeRemove();
|
||||||
|
|
||||||
|
if (shouldPrevent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (shouldContinue) {
|
||||||
|
props.navigation.dispatch(e.data.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[props.navigation]
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar">{() => null}</Screen>
|
||||||
|
<Screen name="baz">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="qux" component={TestScreen} />
|
||||||
|
<Screen name="lex">{() => null}</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 1,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('baz'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz-5',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-7',
|
||||||
|
routeNames: ['qux', 'lex'],
|
||||||
|
routes: [{ key: 'qux-8', name: 'qux' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onBeforeRemove).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz-5',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-7',
|
||||||
|
routeNames: ['qux', 'lex'],
|
||||||
|
routes: [{ key: 'qux-8', name: 'qux' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(3);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = true;
|
||||||
|
shouldContinue = true;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(5);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prevents removing a grand child screen with 'beforeRemove' event", () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBeforeRemove = jest.fn();
|
||||||
|
|
||||||
|
let shouldPrevent = true;
|
||||||
|
let shouldContinue = false;
|
||||||
|
|
||||||
|
const TestScreen = (props: any) => {
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
props.navigation.addListener('beforeRemove', (e: any) => {
|
||||||
|
onBeforeRemove();
|
||||||
|
|
||||||
|
if (shouldPrevent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (shouldContinue) {
|
||||||
|
props.navigation.dispatch(e.data.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[props.navigation]
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar">{() => null}</Screen>
|
||||||
|
<Screen name="baz">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="qux">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="lex" component={TestScreen} />
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 1,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('baz'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz-5',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-7',
|
||||||
|
routeNames: ['qux'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'qux-8',
|
||||||
|
name: 'qux',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-10',
|
||||||
|
routeNames: ['lex'],
|
||||||
|
routes: [{ key: 'lex-11', name: 'lex' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onBeforeRemove).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual({
|
||||||
|
index: 2,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{
|
||||||
|
key: 'baz-5',
|
||||||
|
name: 'baz',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-7',
|
||||||
|
routeNames: ['qux'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'qux-8',
|
||||||
|
name: 'qux',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-10',
|
||||||
|
routeNames: ['lex'],
|
||||||
|
routes: [{ key: 'lex-11', name: 'lex' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(3);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldPrevent = true;
|
||||||
|
shouldContinue = true;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('bar'));
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(5);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prevents removing by multiple screens with 'beforeRemove' event", () => {
|
||||||
|
const TestNavigator = (props: any) => {
|
||||||
|
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{state.routes.map((route) => descriptors[route.key].render())}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBeforeRemove = {
|
||||||
|
bar: jest.fn(),
|
||||||
|
baz: jest.fn(),
|
||||||
|
lex: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldPrevent = {
|
||||||
|
bar: true,
|
||||||
|
baz: true,
|
||||||
|
lex: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TestScreen = (props: any) => {
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
props.navigation.addListener('beforeRemove', (e: any) => {
|
||||||
|
// @ts-expect-error: we should have the required mocks
|
||||||
|
onBeforeRemove[props.route.name]();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// @ts-expect-error: we should have the required properties
|
||||||
|
if (!shouldPrevent[props.route.name]) {
|
||||||
|
props.navigation.dispatch(e.data.action);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[props.navigation, props.route.name]
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStateChange = jest.fn();
|
||||||
|
|
||||||
|
const ref = React.createRef<NavigationContainerRef>();
|
||||||
|
|
||||||
|
const element = (
|
||||||
|
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="foo">{() => null}</Screen>
|
||||||
|
<Screen name="bar" component={TestScreen} />
|
||||||
|
<Screen name="baz" component={TestScreen} />
|
||||||
|
<Screen name="bax">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="qux">
|
||||||
|
{() => (
|
||||||
|
<TestNavigator>
|
||||||
|
<Screen name="lex" component={TestScreen} />
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
)}
|
||||||
|
</Screen>
|
||||||
|
</TestNavigator>
|
||||||
|
</BaseNavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(element);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ref.current?.navigate('bar');
|
||||||
|
ref.current?.navigate('baz');
|
||||||
|
ref.current?.navigate('bax');
|
||||||
|
});
|
||||||
|
|
||||||
|
const preventedState = {
|
||||||
|
index: 3,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz', 'bax'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'foo-3', name: 'foo' },
|
||||||
|
{ key: 'bar-4', name: 'bar' },
|
||||||
|
{ key: 'baz-5', name: 'baz' },
|
||||||
|
{
|
||||||
|
key: 'bax-6',
|
||||||
|
name: 'bax',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-8',
|
||||||
|
routeNames: ['qux'],
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
key: 'qux-9',
|
||||||
|
name: 'qux',
|
||||||
|
state: {
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-11',
|
||||||
|
routeNames: ['lex'],
|
||||||
|
routes: [{ key: 'lex-12', name: 'lex' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onStateChange).toBeCalledWith(preventedState);
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onBeforeRemove.lex).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual(preventedState);
|
||||||
|
|
||||||
|
shouldPrevent.lex = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onBeforeRemove.baz).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual(preventedState);
|
||||||
|
|
||||||
|
shouldPrevent.baz = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(1);
|
||||||
|
expect(onBeforeRemove.bar).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ref.current?.getRootState()).toEqual(preventedState);
|
||||||
|
|
||||||
|
shouldPrevent.bar = false;
|
||||||
|
|
||||||
|
act(() => ref.current?.navigate('foo'));
|
||||||
|
|
||||||
|
expect(onStateChange).toBeCalledTimes(2);
|
||||||
|
expect(onStateChange).toBeCalledWith({
|
||||||
|
index: 0,
|
||||||
|
key: 'stack-2',
|
||||||
|
routeNames: ['foo', 'bar', 'baz', 'bax'],
|
||||||
|
routes: [{ key: 'foo-3', name: 'foo' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
74
packages/core/src/checkSerializable.tsx
Normal file
74
packages/core/src/checkSerializable.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const checkSerializableWithoutCircularReference = (
|
||||||
|
o: { [key: string]: any },
|
||||||
|
seen: Set<any>,
|
||||||
|
location: (string | number)[]
|
||||||
|
):
|
||||||
|
| { serializable: true }
|
||||||
|
| {
|
||||||
|
serializable: false;
|
||||||
|
location: (string | number)[];
|
||||||
|
reason: string;
|
||||||
|
} => {
|
||||||
|
if (
|
||||||
|
o === undefined ||
|
||||||
|
o === null ||
|
||||||
|
typeof o === 'boolean' ||
|
||||||
|
typeof o === 'number' ||
|
||||||
|
typeof o === 'string'
|
||||||
|
) {
|
||||||
|
return { serializable: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.prototype.toString.call(o) !== '[object Object]' &&
|
||||||
|
!Array.isArray(o)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
serializable: false,
|
||||||
|
location,
|
||||||
|
reason: typeof o === 'function' ? 'Function' : String(o),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(o)) {
|
||||||
|
return {
|
||||||
|
serializable: false,
|
||||||
|
reason: 'Circular reference',
|
||||||
|
location,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(o);
|
||||||
|
|
||||||
|
if (Array.isArray(o)) {
|
||||||
|
for (let i = 0; i < o.length; i++) {
|
||||||
|
const childResult = checkSerializableWithoutCircularReference(
|
||||||
|
o[i],
|
||||||
|
new Set<any>(seen),
|
||||||
|
[...location, i]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!childResult.serializable) {
|
||||||
|
return childResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key in o) {
|
||||||
|
const childResult = checkSerializableWithoutCircularReference(
|
||||||
|
o[key],
|
||||||
|
new Set<any>(seen),
|
||||||
|
[...location, key]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!childResult.serializable) {
|
||||||
|
return childResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { serializable: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function checkSerializable(o: { [key: string]: any }) {
|
||||||
|
return checkSerializableWithoutCircularReference(o, new Set<any>(), []);
|
||||||
|
}
|
||||||
@@ -260,6 +260,7 @@ export default function getStateFromPath(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
// @ts-expect-error: params should be treated as read-only, but we're creating the state here so it doesn't matter
|
||||||
route.params = { ...route.params, ...params };
|
route.params = { ...route.params, ...params };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
const isSerializableWithoutCircularReference = (
|
|
||||||
o: { [key: string]: any },
|
|
||||||
seen: Set<any>
|
|
||||||
): boolean => {
|
|
||||||
if (
|
|
||||||
o === undefined ||
|
|
||||||
o === null ||
|
|
||||||
typeof o === 'boolean' ||
|
|
||||||
typeof o === 'number' ||
|
|
||||||
typeof o === 'string'
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Object.prototype.toString.call(o) !== '[object Object]' &&
|
|
||||||
!Array.isArray(o)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seen.has(o)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.add(o);
|
|
||||||
|
|
||||||
if (Array.isArray(o)) {
|
|
||||||
for (const it of o) {
|
|
||||||
if (!isSerializableWithoutCircularReference(it, new Set<any>(seen))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const key in o) {
|
|
||||||
if (!isSerializableWithoutCircularReference(o[key], new Set<any>(seen))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function isSerializable(o: { [key: string]: any }) {
|
|
||||||
return isSerializableWithoutCircularReference(o, new Set<any>());
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,7 @@ export type EventMapCore<State extends NavigationState> = {
|
|||||||
focus: { data: undefined };
|
focus: { data: undefined };
|
||||||
blur: { data: undefined };
|
blur: { data: undefined };
|
||||||
state: { data: { state: State } };
|
state: { data: { state: State } };
|
||||||
|
beforeRemove: { data: { action: NavigationAction }; canPreventDefault: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventArg<
|
export type EventArg<
|
||||||
@@ -61,7 +62,9 @@ export type EventArg<
|
|||||||
preventDefault(): void;
|
preventDefault(): void;
|
||||||
}
|
}
|
||||||
: {}) &
|
: {}) &
|
||||||
(undefined extends Data ? {} : { readonly data: Data });
|
(undefined extends Data
|
||||||
|
? { readonly data?: Readonly<Data> }
|
||||||
|
: { readonly data: Readonly<Data> });
|
||||||
|
|
||||||
export type EventListenerCallback<
|
export type EventListenerCallback<
|
||||||
EventMap extends EventMapBase,
|
EventMap extends EventMapBase,
|
||||||
@@ -108,7 +111,7 @@ export type EventEmitter<EventMap extends EventMapBase> = {
|
|||||||
? { canPreventDefault: true }
|
? { canPreventDefault: true }
|
||||||
: {}) &
|
: {}) &
|
||||||
(undefined extends EventMap[EventName]['data']
|
(undefined extends EventMap[EventName]['data']
|
||||||
? {}
|
? { data?: EventMap[EventName]['data'] }
|
||||||
: { data: EventMap[EventName]['data'] })
|
: { data: EventMap[EventName]['data'] })
|
||||||
): EventArg<
|
): EventArg<
|
||||||
EventName,
|
EventName,
|
||||||
@@ -276,13 +279,18 @@ export type RouteProp<
|
|||||||
RouteName extends keyof ParamList
|
RouteName extends keyof ParamList
|
||||||
> = Omit<Route<Extract<RouteName, string>>, 'params'> &
|
> = Omit<Route<Extract<RouteName, string>>, 'params'> &
|
||||||
(undefined extends ParamList[RouteName]
|
(undefined extends ParamList[RouteName]
|
||||||
? {}
|
? Readonly<{
|
||||||
: {
|
|
||||||
/**
|
/**
|
||||||
* Params for this route
|
* Params for this route
|
||||||
*/
|
*/
|
||||||
params: ParamList[RouteName];
|
params?: Readonly<ParamList[RouteName]>;
|
||||||
});
|
}>
|
||||||
|
: Readonly<{
|
||||||
|
/**
|
||||||
|
* Params for this route
|
||||||
|
*/
|
||||||
|
params: Readonly<ParamList[RouteName]>;
|
||||||
|
}>);
|
||||||
|
|
||||||
export type CompositeNavigationProp<
|
export type CompositeNavigationProp<
|
||||||
A extends NavigationProp<ParamListBase, string, any, any>,
|
A extends NavigationProp<ParamListBase, string, any, any>,
|
||||||
@@ -398,6 +406,16 @@ export type RouteConfig<
|
|||||||
* React component to render for this screen.
|
* React component to render for this screen.
|
||||||
*/
|
*/
|
||||||
component: React.ComponentType<any>;
|
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;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
@@ -407,6 +425,8 @@ export type RouteConfig<
|
|||||||
route: RouteProp<ParamList, RouteName>;
|
route: RouteProp<ParamList, RouteName>;
|
||||||
navigation: any;
|
navigation: any;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
component?: never;
|
||||||
|
getComponent?: never;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import type { ChildActionListener } from './NavigationBuilderContext';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook which lets child navigators add action listeners.
|
|
||||||
*/
|
|
||||||
export default function useChildActionListeners() {
|
|
||||||
const { current: listeners } = React.useRef<ChildActionListener[]>([]);
|
|
||||||
|
|
||||||
const addListener = React.useCallback(
|
|
||||||
(listener: ChildActionListener) => {
|
|
||||||
listeners.push(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const index = listeners.indexOf(listener);
|
|
||||||
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[listeners]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
listeners,
|
|
||||||
addListener,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
36
packages/core/src/useChildListeners.tsx
Normal file
36
packages/core/src/useChildListeners.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import type { ListenerMap } from './NavigationBuilderContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook which lets child navigators add action listeners.
|
||||||
|
*/
|
||||||
|
export default function useChildListeners() {
|
||||||
|
const { current: listeners } = React.useRef<
|
||||||
|
{
|
||||||
|
[K in keyof ListenerMap]: ListenerMap[K][];
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
action: [],
|
||||||
|
focus: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const addListener = React.useCallback(
|
||||||
|
<T extends keyof ListenerMap>(type: T, listener: ListenerMap[T]) => {
|
||||||
|
// @ts-expect-error: listener should be correct type according to `type`
|
||||||
|
listeners[type].push(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// @ts-expect-error: listener should be correct type according to `type`
|
||||||
|
const index = listeners[type].indexOf(listener);
|
||||||
|
|
||||||
|
listeners[type].splice(index, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[listeners]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
listeners,
|
||||||
|
addListener,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,12 +7,13 @@ import type {
|
|||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import SceneView from './SceneView';
|
import SceneView from './SceneView';
|
||||||
import NavigationBuilderContext, {
|
import NavigationBuilderContext, {
|
||||||
ChildActionListener,
|
AddListener,
|
||||||
FocusedNavigationListener,
|
AddKeyedListener,
|
||||||
NavigatorStateGetter,
|
|
||||||
} from './NavigationBuilderContext';
|
} from './NavigationBuilderContext';
|
||||||
import type { NavigationEventEmitter } from './useEventEmitter';
|
import type { NavigationEventEmitter } from './useEventEmitter';
|
||||||
import useNavigationCache from './useNavigationCache';
|
import useNavigationCache from './useNavigationCache';
|
||||||
|
import NavigationContext from './NavigationContext';
|
||||||
|
import NavigationRouteContext from './NavigationRouteContext';
|
||||||
import type {
|
import type {
|
||||||
Descriptor,
|
Descriptor,
|
||||||
NavigationHelpers,
|
NavigationHelpers,
|
||||||
@@ -20,8 +21,6 @@ import type {
|
|||||||
RouteProp,
|
RouteProp,
|
||||||
EventMapBase,
|
EventMapBase,
|
||||||
} from './types';
|
} from './types';
|
||||||
import NavigationContext from './NavigationContext';
|
|
||||||
import NavigationRouteContext from './NavigationRouteContext';
|
|
||||||
|
|
||||||
type Options<
|
type Options<
|
||||||
State extends NavigationState,
|
State extends NavigationState,
|
||||||
@@ -46,9 +45,8 @@ type Options<
|
|||||||
) => boolean;
|
) => boolean;
|
||||||
getState: () => State;
|
getState: () => State;
|
||||||
setState: (state: State) => void;
|
setState: (state: State) => void;
|
||||||
addActionListener: (listener: ChildActionListener) => void;
|
addListener: AddListener;
|
||||||
addFocusedListener: (listener: FocusedNavigationListener) => void;
|
addKeyedListener: AddKeyedListener;
|
||||||
addStateGetter: (key: string, getter: NavigatorStateGetter) => void;
|
|
||||||
onRouteFocus: (key: string) => void;
|
onRouteFocus: (key: string) => void;
|
||||||
router: Router<State, NavigationAction>;
|
router: Router<State, NavigationAction>;
|
||||||
emitter: NavigationEventEmitter<any>;
|
emitter: NavigationEventEmitter<any>;
|
||||||
@@ -74,9 +72,8 @@ export default function useDescriptors<
|
|||||||
onAction,
|
onAction,
|
||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
addActionListener,
|
addListener,
|
||||||
addFocusedListener,
|
addKeyedListener,
|
||||||
addStateGetter,
|
|
||||||
onRouteFocus,
|
onRouteFocus,
|
||||||
router,
|
router,
|
||||||
emitter,
|
emitter,
|
||||||
@@ -90,21 +87,19 @@ export default function useDescriptors<
|
|||||||
() => ({
|
() => ({
|
||||||
navigation,
|
navigation,
|
||||||
onAction,
|
onAction,
|
||||||
addActionListener,
|
addListener,
|
||||||
addFocusedListener,
|
addKeyedListener,
|
||||||
addStateGetter,
|
|
||||||
onRouteFocus,
|
onRouteFocus,
|
||||||
onDispatchAction,
|
onDispatchAction,
|
||||||
onOptionsChange,
|
onOptionsChange,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addActionListener,
|
|
||||||
addFocusedListener,
|
|
||||||
addStateGetter,
|
|
||||||
navigation,
|
navigation,
|
||||||
onAction,
|
onAction,
|
||||||
onDispatchAction,
|
addListener,
|
||||||
|
addKeyedListener,
|
||||||
onRouteFocus,
|
onRouteFocus,
|
||||||
|
onDispatchAction,
|
||||||
onOptionsChange,
|
onOptionsChange,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import type { FocusedNavigationListener } from './NavigationBuilderContext';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook which lets child navigators add listeners to be called for focused navigators.
|
|
||||||
*/
|
|
||||||
export default function useFocusedListeners() {
|
|
||||||
const { current: listeners } = React.useRef<FocusedNavigationListener[]>([]);
|
|
||||||
|
|
||||||
const addListener = React.useCallback(
|
|
||||||
(listener: FocusedNavigationListener) => {
|
|
||||||
listeners.push(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const index = listeners.indexOf(listener);
|
|
||||||
|
|
||||||
listeners.splice(index, 1);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[listeners]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
listeners,
|
|
||||||
addListener,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ export default function useFocusedListenersChildrenAdapter({
|
|||||||
navigation,
|
navigation,
|
||||||
focusedListeners,
|
focusedListeners,
|
||||||
}: Options) {
|
}: Options) {
|
||||||
const { addFocusedListener } = React.useContext(NavigationBuilderContext);
|
const { addListener } = React.useContext(NavigationBuilderContext);
|
||||||
|
|
||||||
const listener = React.useCallback(
|
const listener = React.useCallback(
|
||||||
(callback: FocusedNavigationCallback<any>) => {
|
(callback: FocusedNavigationCallback<any>) => {
|
||||||
@@ -39,8 +39,8 @@ export default function useFocusedListenersChildrenAdapter({
|
|||||||
[focusedListeners, navigation]
|
[focusedListeners, navigation]
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => addFocusedListener?.(listener), [
|
React.useEffect(() => addListener?.('focus', listener), [
|
||||||
addFocusedListener,
|
addListener,
|
||||||
listener,
|
listener,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
42
packages/core/src/useKeyedChildListeners.tsx
Normal file
42
packages/core/src/useKeyedChildListeners.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import type { KeyedListenerMap } from './NavigationBuilderContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook which lets child navigators add getters to be called for obtaining rehydrated state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useKeyedChildListeners() {
|
||||||
|
const { current: keyedListeners } = React.useRef<
|
||||||
|
{
|
||||||
|
[K in keyof KeyedListenerMap]: Record<
|
||||||
|
string,
|
||||||
|
KeyedListenerMap[K] | undefined
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
getState: {},
|
||||||
|
beforeRemove: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const addKeyedListener = React.useCallback(
|
||||||
|
<T extends keyof KeyedListenerMap>(
|
||||||
|
type: T,
|
||||||
|
key: string,
|
||||||
|
listener: KeyedListenerMap[T]
|
||||||
|
) => {
|
||||||
|
// @ts-expect-error: listener should be correct type according to `type`
|
||||||
|
keyedListeners[type][key] = listener;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// @ts-expect-error: listener should be correct type according to `type`
|
||||||
|
keyedListeners[type][key] = undefined;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[keyedListeners]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyedListeners,
|
||||||
|
addKeyedListener,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,8 +21,7 @@ import useNavigationHelpers from './useNavigationHelpers';
|
|||||||
import useOnAction from './useOnAction';
|
import useOnAction from './useOnAction';
|
||||||
import useFocusEvents from './useFocusEvents';
|
import useFocusEvents from './useFocusEvents';
|
||||||
import useOnRouteFocus from './useOnRouteFocus';
|
import useOnRouteFocus from './useOnRouteFocus';
|
||||||
import useChildActionListeners from './useChildActionListeners';
|
import useChildListeners from './useChildListeners';
|
||||||
import useFocusedListeners from './useFocusedListeners';
|
|
||||||
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
|
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
|
||||||
import {
|
import {
|
||||||
DefaultNavigatorOptions,
|
DefaultNavigatorOptions,
|
||||||
@@ -31,7 +30,7 @@ import {
|
|||||||
EventMapBase,
|
EventMapBase,
|
||||||
EventMapCore,
|
EventMapCore,
|
||||||
} from './types';
|
} from './types';
|
||||||
import useStateGetters from './useStateGetters';
|
import useKeyedChildListeners from './useKeyedChildListeners';
|
||||||
import useOnGetState from './useOnGetState';
|
import useOnGetState from './useOnGetState';
|
||||||
import useScheduleUpdate from './useScheduleUpdate';
|
import useScheduleUpdate from './useScheduleUpdate';
|
||||||
import useCurrentRender from './useCurrentRender';
|
import useCurrentRender from './useCurrentRender';
|
||||||
@@ -103,7 +102,7 @@ const getRouteConfigsFromChildren = <
|
|||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
configs.forEach((config) => {
|
configs.forEach((config) => {
|
||||||
const { name, children, component } = config as any;
|
const { name, children, component, getComponent } = config;
|
||||||
|
|
||||||
if (typeof name !== 'string' || !name) {
|
if (typeof name !== 'string' || !name) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -113,13 +112,29 @@ const getRouteConfigsFromChildren = <
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children != null || component !== undefined) {
|
if (
|
||||||
|
children != null ||
|
||||||
|
component !== undefined ||
|
||||||
|
getComponent !== undefined
|
||||||
|
) {
|
||||||
if (children != null && component !== undefined) {
|
if (children != null && component !== undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.`
|
`Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (children != null && getComponent !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Got both 'getComponent' and 'children' props for the screen '${name}'. You must pass only one of them.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component !== undefined && getComponent !== undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Got both 'component' and 'getComponent' props for the screen '${name}'. You must pass only one of them.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (children != null && typeof children !== 'function') {
|
if (children != null && typeof children !== 'function') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.`
|
`Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.`
|
||||||
@@ -132,6 +147,12 @@ const getRouteConfigsFromChildren = <
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getComponent !== undefined && typeof getComponent !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
`Got an invalid value for 'getComponent' prop for the screen '${name}'. It must be a function returning a React Component.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof component === 'function' && component.name === 'component') {
|
if (typeof component === 'function' && component.name === 'component') {
|
||||||
// Inline anonymous functions passed in the `component` prop will have the name of the prop
|
// Inline anonymous functions passed in the `component` prop will have the name of the prop
|
||||||
// It's relatively safe to assume that it's not a component since it should also have PascalCase name
|
// It's relatively safe to assume that it's not a component since it should also have PascalCase name
|
||||||
@@ -142,7 +163,7 @@ const getRouteConfigsFromChildren = <
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Couldn't find a 'component' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`
|
`Couldn't find a 'component', 'getComponent' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -258,6 +279,7 @@ export default function useNavigationBuilder<
|
|||||||
setState,
|
setState,
|
||||||
setKey,
|
setKey,
|
||||||
getKey,
|
getKey,
|
||||||
|
getIsInitial,
|
||||||
} = React.useContext(NavigationStateContext);
|
} = React.useContext(NavigationStateContext);
|
||||||
|
|
||||||
const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
|
const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
|
||||||
@@ -351,6 +373,13 @@ export default function useNavigationBuilder<
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setKey(navigatorKey);
|
setKey(navigatorKey);
|
||||||
|
|
||||||
|
if (!getIsInitial()) {
|
||||||
|
// If it's not initial render, we need to update the state
|
||||||
|
// This will make sure that our container gets notifier of state changes due to new mounts
|
||||||
|
// This is necessary for proper screen tracking, URL updates etc.
|
||||||
|
setState(nextState);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// We need to clean up state for this navigator on unmount
|
// We need to clean up state for this navigator on unmount
|
||||||
// We do it in a timeout because we need to detect if another navigator mounted in the meantime
|
// We do it in a timeout because we need to detect if another navigator mounted in the meantime
|
||||||
@@ -430,28 +459,22 @@ export default function useNavigationBuilder<
|
|||||||
emitter.emit({ type: 'state', data: { state } });
|
emitter.emit({ type: 'state', data: { state } });
|
||||||
}, [emitter, state]);
|
}, [emitter, state]);
|
||||||
|
|
||||||
const {
|
const { listeners: childListeners, addListener } = useChildListeners();
|
||||||
listeners: actionListeners,
|
|
||||||
addListener: addActionListener,
|
|
||||||
} = useChildActionListeners();
|
|
||||||
|
|
||||||
const {
|
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
||||||
listeners: focusedListeners,
|
|
||||||
addListener: addFocusedListener,
|
|
||||||
} = useFocusedListeners();
|
|
||||||
|
|
||||||
const { getStateForRoute, addStateGetter } = useStateGetters();
|
|
||||||
|
|
||||||
const onAction = useOnAction({
|
const onAction = useOnAction({
|
||||||
router,
|
router,
|
||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
key: route?.key,
|
key: route?.key,
|
||||||
listeners: actionListeners,
|
actionListeners: childListeners.action,
|
||||||
|
beforeRemoveListeners: keyedListeners.beforeRemove,
|
||||||
routerConfigOptions: {
|
routerConfigOptions: {
|
||||||
routeNames,
|
routeNames,
|
||||||
routeParamList,
|
routeParamList,
|
||||||
},
|
},
|
||||||
|
emitter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onRouteFocus = useOnRouteFocus({
|
const onRouteFocus = useOnRouteFocus({
|
||||||
@@ -470,12 +493,12 @@ export default function useNavigationBuilder<
|
|||||||
|
|
||||||
useFocusedListenersChildrenAdapter({
|
useFocusedListenersChildrenAdapter({
|
||||||
navigation,
|
navigation,
|
||||||
focusedListeners,
|
focusedListeners: childListeners.focus,
|
||||||
});
|
});
|
||||||
|
|
||||||
useOnGetState({
|
useOnGetState({
|
||||||
getState,
|
getState,
|
||||||
getStateForRoute,
|
getStateListeners: keyedListeners.getState,
|
||||||
});
|
});
|
||||||
|
|
||||||
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
|
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
|
||||||
@@ -487,9 +510,8 @@ export default function useNavigationBuilder<
|
|||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
onRouteFocus,
|
onRouteFocus,
|
||||||
addActionListener,
|
addListener,
|
||||||
addFocusedListener,
|
addKeyedListener,
|
||||||
addStateGetter,
|
|
||||||
router,
|
router,
|
||||||
emitter,
|
emitter,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import type {
|
|||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import NavigationBuilderContext, {
|
import NavigationBuilderContext, {
|
||||||
ChildActionListener,
|
ChildActionListener,
|
||||||
|
ChildBeforeRemoveListener,
|
||||||
} from './NavigationBuilderContext';
|
} from './NavigationBuilderContext';
|
||||||
|
import useOnPreventRemove, { shouldPreventRemove } from './useOnPreventRemove';
|
||||||
|
import type { NavigationEventEmitter } from './useEventEmitter';
|
||||||
|
import type { EventMapCore } from './types';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
router: Router<NavigationState, NavigationAction>;
|
router: Router<NavigationState, NavigationAction>;
|
||||||
key?: string;
|
key?: string;
|
||||||
getState: () => NavigationState;
|
getState: () => NavigationState;
|
||||||
setState: (state: NavigationState | PartialState<NavigationState>) => void;
|
setState: (state: NavigationState | PartialState<NavigationState>) => void;
|
||||||
listeners: ChildActionListener[];
|
actionListeners: ChildActionListener[];
|
||||||
|
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
|
||||||
routerConfigOptions: RouterConfigOptions;
|
routerConfigOptions: RouterConfigOptions;
|
||||||
|
emitter: NavigationEventEmitter<EventMapCore<any>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,13 +39,15 @@ export default function useOnAction({
|
|||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
key,
|
key,
|
||||||
listeners,
|
actionListeners,
|
||||||
|
beforeRemoveListeners,
|
||||||
routerConfigOptions,
|
routerConfigOptions,
|
||||||
|
emitter,
|
||||||
}: Options) {
|
}: Options) {
|
||||||
const {
|
const {
|
||||||
onAction: onActionParent,
|
onAction: onActionParent,
|
||||||
onRouteFocus: onRouteFocusParent,
|
onRouteFocus: onRouteFocusParent,
|
||||||
addActionListener: addActionListenerParent,
|
addListener: addListenerParent,
|
||||||
onDispatchAction,
|
onDispatchAction,
|
||||||
} = React.useContext(NavigationBuilderContext);
|
} = React.useContext(NavigationBuilderContext);
|
||||||
|
|
||||||
@@ -66,38 +74,56 @@ export default function useOnAction({
|
|||||||
|
|
||||||
visitedNavigators.add(state.key);
|
visitedNavigators.add(state.key);
|
||||||
|
|
||||||
if (typeof action.target === 'string' && action.target !== state.key) {
|
if (typeof action.target !== 'string' || action.target === state.key) {
|
||||||
return false;
|
let result = router.getStateForAction(
|
||||||
}
|
state,
|
||||||
|
action,
|
||||||
|
routerConfigOptionsRef.current
|
||||||
|
);
|
||||||
|
|
||||||
let result = router.getStateForAction(
|
// If a target is specified and set to current navigator, the action shouldn't bubble
|
||||||
state,
|
// So instead of `null`, we use the state object for such cases to signal that action was handled
|
||||||
action,
|
result =
|
||||||
routerConfigOptionsRef.current
|
result === null && action.target === state.key ? state : result;
|
||||||
);
|
|
||||||
|
|
||||||
// If a target is specified and set to current navigator, the action shouldn't bubble
|
if (result !== null) {
|
||||||
// So instead of `null`, we use the state object for such cases to signal that action was handled
|
onDispatchAction(action, state === result);
|
||||||
result = result === null && action.target === state.key ? state : result;
|
|
||||||
|
|
||||||
if (result !== null) {
|
if (state !== result) {
|
||||||
onDispatchAction(action, state === result);
|
const nextRouteKeys = (result.routes as any[]).map(
|
||||||
|
(route: { key?: string }) => route.key
|
||||||
|
);
|
||||||
|
|
||||||
if (state !== result) {
|
const removedRoutes = state.routes.filter(
|
||||||
setState(result);
|
(route) => !nextRouteKeys.includes(route.key)
|
||||||
}
|
);
|
||||||
|
|
||||||
if (onRouteFocusParent !== undefined) {
|
const isPrevented = shouldPreventRemove(
|
||||||
// Some actions such as `NAVIGATE` also want to bring the navigated route to focus in the whole tree
|
emitter,
|
||||||
// This means we need to focus all of the parent navigators of this navigator as well
|
beforeRemoveListeners,
|
||||||
const shouldFocus = router.shouldActionChangeFocus(action);
|
removedRoutes,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
|
||||||
if (shouldFocus && key !== undefined) {
|
if (isPrevented) {
|
||||||
onRouteFocusParent(key);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(result);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
if (onRouteFocusParent !== undefined) {
|
||||||
|
// Some actions such as `NAVIGATE` also want to bring the navigated route to focus in the whole tree
|
||||||
|
// This means we need to focus all of the parent navigators of this navigator as well
|
||||||
|
const shouldFocus = router.shouldActionChangeFocus(action);
|
||||||
|
|
||||||
|
if (shouldFocus && key !== undefined) {
|
||||||
|
onRouteFocusParent(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onActionParent !== undefined) {
|
if (onActionParent !== undefined) {
|
||||||
@@ -108,8 +134,8 @@ export default function useOnAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the action wasn't handled by current navigator or a parent navigator, let children handle it
|
// If the action wasn't handled by current navigator or a parent navigator, let children handle it
|
||||||
for (let i = listeners.length - 1; i >= 0; i--) {
|
for (let i = actionListeners.length - 1; i >= 0; i--) {
|
||||||
const listener = listeners[i];
|
const listener = actionListeners[i];
|
||||||
|
|
||||||
if (listener(action, visitedNavigators)) {
|
if (listener(action, visitedNavigators)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -119,19 +145,27 @@ export default function useOnAction({
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
actionListeners,
|
||||||
|
beforeRemoveListeners,
|
||||||
|
emitter,
|
||||||
getState,
|
getState,
|
||||||
router,
|
key,
|
||||||
onActionParent,
|
onActionParent,
|
||||||
onDispatchAction,
|
onDispatchAction,
|
||||||
onRouteFocusParent,
|
onRouteFocusParent,
|
||||||
|
router,
|
||||||
setState,
|
setState,
|
||||||
key,
|
|
||||||
listeners,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => addActionListenerParent?.(onAction), [
|
useOnPreventRemove({
|
||||||
addActionListenerParent,
|
getState,
|
||||||
|
emitter,
|
||||||
|
beforeRemoveListeners,
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => addListenerParent?.('action', onAction), [
|
||||||
|
addListenerParent,
|
||||||
onAction,
|
onAction,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { NavigationState } from '@react-navigation/routers';
|
import type { NavigationState } from '@react-navigation/routers';
|
||||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
import NavigationBuilderContext, {
|
||||||
|
GetStateListener,
|
||||||
|
} from './NavigationBuilderContext';
|
||||||
import NavigationRouteContext from './NavigationRouteContext';
|
import NavigationRouteContext from './NavigationRouteContext';
|
||||||
import isArrayEqual from './isArrayEqual';
|
import isArrayEqual from './isArrayEqual';
|
||||||
|
|
||||||
export default function useOnGetState({
|
type Options = {
|
||||||
getStateForRoute,
|
|
||||||
getState,
|
|
||||||
}: {
|
|
||||||
getStateForRoute: (routeName: string) => NavigationState | undefined;
|
|
||||||
getState: () => NavigationState;
|
getState: () => NavigationState;
|
||||||
}) {
|
getStateListeners: Record<string, GetStateListener | undefined>;
|
||||||
const { addStateGetter } = React.useContext(NavigationBuilderContext);
|
};
|
||||||
|
|
||||||
|
export default function useOnGetState({
|
||||||
|
getState,
|
||||||
|
getStateListeners,
|
||||||
|
}: Options) {
|
||||||
|
const { addKeyedListener } = React.useContext(NavigationBuilderContext);
|
||||||
const route = React.useContext(NavigationRouteContext);
|
const route = React.useContext(NavigationRouteContext);
|
||||||
const key = route ? route.key : 'root';
|
const key = route ? route.key : 'root';
|
||||||
|
|
||||||
@@ -20,7 +24,7 @@ export default function useOnGetState({
|
|||||||
|
|
||||||
// Avoid returning new route objects if we don't need to
|
// Avoid returning new route objects if we don't need to
|
||||||
const routes = state.routes.map((route) => {
|
const routes = state.routes.map((route) => {
|
||||||
const childState = getStateForRoute(route.key);
|
const childState = getStateListeners[route.key]?.();
|
||||||
|
|
||||||
if (route.state === childState) {
|
if (route.state === childState) {
|
||||||
return route;
|
return route;
|
||||||
@@ -34,9 +38,9 @@ export default function useOnGetState({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { ...state, routes };
|
return { ...state, routes };
|
||||||
}, [getState, getStateForRoute]);
|
}, [getState, getStateListeners]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return addStateGetter?.(key, getRehydratedState);
|
return addKeyedListener?.('getState', key, getRehydratedState);
|
||||||
}, [addStateGetter, getRehydratedState, key]);
|
}, [addKeyedListener, getRehydratedState, key]);
|
||||||
}
|
}
|
||||||
|
|||||||
93
packages/core/src/useOnPreventRemove.tsx
Normal file
93
packages/core/src/useOnPreventRemove.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import type {
|
||||||
|
NavigationState,
|
||||||
|
Route,
|
||||||
|
NavigationAction,
|
||||||
|
} from '@react-navigation/routers';
|
||||||
|
import NavigationBuilderContext, {
|
||||||
|
ChildBeforeRemoveListener,
|
||||||
|
} from './NavigationBuilderContext';
|
||||||
|
import NavigationRouteContext from './NavigationRouteContext';
|
||||||
|
import type { NavigationEventEmitter } from './useEventEmitter';
|
||||||
|
import type { EventMapCore } from './types';
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
getState: () => NavigationState;
|
||||||
|
emitter: NavigationEventEmitter<EventMapCore<any>>;
|
||||||
|
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VISITED_ROUTE_KEYS = Symbol('VISITED_ROUTE_KEYS');
|
||||||
|
|
||||||
|
export const shouldPreventRemove = (
|
||||||
|
emitter: NavigationEventEmitter<EventMapCore<any>>,
|
||||||
|
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>,
|
||||||
|
routes: Route<string>[],
|
||||||
|
action: NavigationAction
|
||||||
|
) => {
|
||||||
|
// Call these in reverse order so last screens handle the event first
|
||||||
|
const reversedRoutes = [...routes].reverse();
|
||||||
|
|
||||||
|
const visitedRouteKeys: Set<string> =
|
||||||
|
// @ts-expect-error: add this property to mark that we've already emitted this action
|
||||||
|
action[VISITED_ROUTE_KEYS] ?? new Set<string>();
|
||||||
|
|
||||||
|
const beforeRemoveAction = {
|
||||||
|
...action,
|
||||||
|
[VISITED_ROUTE_KEYS]: visitedRouteKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const route of reversedRoutes) {
|
||||||
|
if (visitedRouteKeys.has(route.key)) {
|
||||||
|
// Skip if we've already emitted this action for this screen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, we need to check if any child screens want to prevent it
|
||||||
|
const isPrevented = beforeRemoveListeners[route.key]?.(beforeRemoveAction);
|
||||||
|
|
||||||
|
if (isPrevented) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitedRouteKeys.add(route.key);
|
||||||
|
|
||||||
|
const event = emitter.emit({
|
||||||
|
type: 'beforeRemove',
|
||||||
|
target: route.key,
|
||||||
|
data: { action: beforeRemoveAction },
|
||||||
|
canPreventDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useOnPreventRemove({
|
||||||
|
getState,
|
||||||
|
emitter,
|
||||||
|
beforeRemoveListeners,
|
||||||
|
}: Options) {
|
||||||
|
const { addKeyedListener } = React.useContext(NavigationBuilderContext);
|
||||||
|
const route = React.useContext(NavigationRouteContext);
|
||||||
|
const routeKey = route?.key;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (routeKey) {
|
||||||
|
return addKeyedListener?.('beforeRemove', routeKey, (action) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
return shouldPreventRemove(
|
||||||
|
emitter,
|
||||||
|
beforeRemoveListeners,
|
||||||
|
state.routes,
|
||||||
|
action
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [addKeyedListener, beforeRemoveListeners, emitter, getState, routeKey]);
|
||||||
|
}
|
||||||
@@ -1,56 +1,76 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import type { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||||
import NavigationStateContext from './NavigationStateContext';
|
import NavigationStateContext from './NavigationStateContext';
|
||||||
import type { NavigationState } from '@react-navigation/routers';
|
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||||
|
import type { NavigationProp } from './types';
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
key?: string;
|
||||||
|
navigation?: NavigationProp<ParamListBase, string, NavigationState, object>;
|
||||||
|
options?: object | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export default function useOptionsGetters({
|
export default function useOptionsGetters({
|
||||||
key,
|
key,
|
||||||
getOptions,
|
options,
|
||||||
getState,
|
navigation,
|
||||||
}: {
|
}: Options) {
|
||||||
key?: string;
|
const optionsRef = React.useRef<object | undefined>(options);
|
||||||
getOptions?: () => object | undefined;
|
const optionsGettersFromChildRef = React.useRef<
|
||||||
getState?: () => NavigationState;
|
|
||||||
}) {
|
|
||||||
let [
|
|
||||||
numberOfChildrenListeners,
|
|
||||||
setNumberOfChildrenListeners,
|
|
||||||
] = React.useState(0);
|
|
||||||
const optionsGettersFromChild = React.useRef<
|
|
||||||
Record<string, () => object | undefined | null>
|
Record<string, () => object | undefined | null>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
|
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
|
||||||
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
|
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
|
||||||
NavigationStateContext
|
NavigationStateContext
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const optionsChangeListener = React.useCallback(() => {
|
||||||
|
const isFocused = navigation?.isFocused() ?? true;
|
||||||
|
const hasChildren = Object.keys(optionsGettersFromChildRef.current).length;
|
||||||
|
|
||||||
|
if (isFocused && !hasChildren) {
|
||||||
|
onOptionsChange(optionsRef.current ?? {});
|
||||||
|
}
|
||||||
|
}, [navigation, onOptionsChange]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
optionsRef.current = options;
|
||||||
|
optionsChangeListener();
|
||||||
|
|
||||||
|
return navigation?.addListener('focus', optionsChangeListener);
|
||||||
|
}, [navigation, options, optionsChangeListener]);
|
||||||
|
|
||||||
const getOptionsFromListener = React.useCallback(() => {
|
const getOptionsFromListener = React.useCallback(() => {
|
||||||
for (let key in optionsGettersFromChild.current) {
|
for (let key in optionsGettersFromChildRef.current) {
|
||||||
if (optionsGettersFromChild.current.hasOwnProperty(key)) {
|
if (optionsGettersFromChildRef.current.hasOwnProperty(key)) {
|
||||||
const result = optionsGettersFromChild.current[key]?.();
|
const result = optionsGettersFromChildRef.current[key]?.();
|
||||||
|
|
||||||
// null means unfocused route
|
// null means unfocused route
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getCurrentOptions = React.useCallback(() => {
|
const getCurrentOptions = React.useCallback(() => {
|
||||||
if (getState) {
|
const isFocused = navigation?.isFocused() ?? true;
|
||||||
const state = getState();
|
|
||||||
if (state.routes[state.index].key !== key) {
|
if (!isFocused) {
|
||||||
// null means unfocused route
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsFromListener = getOptionsFromListener();
|
const optionsFromListener = getOptionsFromListener();
|
||||||
|
|
||||||
if (optionsFromListener !== null) {
|
if (optionsFromListener !== null) {
|
||||||
return optionsFromListener;
|
return optionsFromListener;
|
||||||
}
|
}
|
||||||
return getOptions?.() ?? undefined;
|
|
||||||
}, [getState, getOptionsFromListener, getOptions, key]);
|
return optionsRef.current;
|
||||||
|
}, [navigation, getOptionsFromListener]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return parentAddOptionsGetter?.(key!, getCurrentOptions);
|
return parentAddOptionsGetter?.(key!, getCurrentOptions);
|
||||||
@@ -58,26 +78,20 @@ export default function useOptionsGetters({
|
|||||||
|
|
||||||
const addOptionsGetter = React.useCallback(
|
const addOptionsGetter = React.useCallback(
|
||||||
(key: string, getter: () => object | undefined | null) => {
|
(key: string, getter: () => object | undefined | null) => {
|
||||||
optionsGettersFromChild.current[key] = getter;
|
optionsGettersFromChildRef.current[key] = getter;
|
||||||
setNumberOfChildrenListeners((prev) => prev + 1);
|
optionsChangeListener();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setNumberOfChildrenListeners((prev) => prev - 1);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete optionsGettersFromChild.current[key];
|
delete optionsGettersFromChildRef.current[key];
|
||||||
|
optionsChangeListener();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[]
|
[optionsChangeListener]
|
||||||
);
|
|
||||||
|
|
||||||
const hasAnyChildListener = React.useMemo(
|
|
||||||
() => numberOfChildrenListeners > 0,
|
|
||||||
[numberOfChildrenListeners]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addOptionsGetter,
|
addOptionsGetter,
|
||||||
getCurrentOptions,
|
getCurrentOptions,
|
||||||
hasAnyChildListener,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import type { NavigatorStateGetter } from './NavigationBuilderContext';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook which lets child navigators add getters to be called for obtaining rehydrated state.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function useStateGetters() {
|
|
||||||
const stateGetters = React.useRef<
|
|
||||||
Record<string, NavigatorStateGetter | undefined>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const getStateForRoute = React.useCallback(
|
|
||||||
(routeKey: string) => {
|
|
||||||
const getter = stateGetters.current[routeKey];
|
|
||||||
return getter === undefined ? undefined : getter();
|
|
||||||
},
|
|
||||||
[stateGetters]
|
|
||||||
);
|
|
||||||
|
|
||||||
const addStateGetter = React.useCallback(
|
|
||||||
(key: string, getter: NavigatorStateGetter) => {
|
|
||||||
stateGetters.current[key] = getter;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stateGetters.current[key] = undefined;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getStateForRoute,
|
|
||||||
addStateGetter,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,38 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.3...@react-navigation/devtools@5.1.4) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/devtools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.2...@react-navigation/devtools@5.1.3) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/devtools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.1...@react-navigation/devtools@5.1.2) (2020-07-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/devtools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.1.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.0...@react-navigation/devtools@5.1.1) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/devtools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 5.1.0 (2020-06-24)
|
# 5.1.0 (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/devtools",
|
"name": "@react-navigation/devtools",
|
||||||
"description": "Developer tools for React Navigation",
|
"description": "Developer tools for React Navigation",
|
||||||
"version": "5.1.0",
|
"version": "5.1.4",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^5.11.0",
|
"@react-navigation/core": "^5.12.2",
|
||||||
"deep-equal": "^2.0.3"
|
"deep-equal": "^2.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,6 +3,41 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.8.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.6...@react-navigation/drawer@5.8.7) (2020-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add accessibilityState property ([#8548](https://github.com/react-navigation/react-navigation/issues/8548)) ([ce4eb7e](https://github.com/react-navigation/react-navigation/commit/ce4eb7e9273a25e4433eb82e255a58ba3bf4d632))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.8.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.5...@react-navigation/drawer@5.8.6) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.8.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.4...@react-navigation/drawer@5.8.5) (2020-07-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.8.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.3...@react-navigation/drawer@5.8.4) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/drawer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.8.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.2...@react-navigation/drawer@5.8.3) (2020-06-24)
|
## [5.8.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.2...@react-navigation/drawer@5.8.3) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/drawer",
|
"name": "@react-navigation/drawer",
|
||||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||||
"version": "5.8.3",
|
"version": "5.8.7",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.1",
|
"del-cli": "^3.0.1",
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"react-native-reanimated": "^1.8.0",
|
"react-native-reanimated": "^1.8.0",
|
||||||
"react-native-safe-area-context": "^1.0.0",
|
"react-native-safe-area-context": "^1.0.0",
|
||||||
"react-native-screens": "^2.7.0",
|
"react-native-screens": "^2.7.0",
|
||||||
|
"react-native-testing-library": "^2.1.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
33
packages/drawer/src/__tests__/index.test.tsx
Normal file
33
packages/drawer/src/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, Text, Button } from 'react-native';
|
||||||
|
import { render, fireEvent } from 'react-native-testing-library';
|
||||||
|
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
|
||||||
|
import { createDrawerNavigator, DrawerScreenProps } from '../index';
|
||||||
|
|
||||||
|
it('renders a drawer navigator with screens', async () => {
|
||||||
|
const Test = ({ route, navigation }: DrawerScreenProps<ParamListBase>) => (
|
||||||
|
<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 { findByText, queryByText } = render(
|
||||||
|
<NavigationContainer>
|
||||||
|
<Drawer.Navigator>
|
||||||
|
<Drawer.Screen name="A" component={Test} />
|
||||||
|
<Drawer.Screen name="B" component={Test} />
|
||||||
|
</Drawer.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Screen A')).not.toBeNull();
|
||||||
|
expect(queryByText('Screen B')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent(await findByText('Go to B'), 'press');
|
||||||
|
|
||||||
|
expect(queryByText('Screen B')).not.toBeNull();
|
||||||
|
});
|
||||||
@@ -156,6 +156,7 @@ export default function DrawerItem(props: Props) {
|
|||||||
accessibilityTraits={focused ? ['button', 'selected'] : 'button'}
|
accessibilityTraits={focused ? ['button', 'selected'] : 'button'}
|
||||||
accessibilityComponentType="button"
|
accessibilityComponentType="button"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ selected: focused }}
|
||||||
accessibilityStates={focused ? ['selected'] : []}
|
accessibilityStates={focused ? ['selected'] : []}
|
||||||
to={to}
|
to={to}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,38 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.2.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.14...@react-navigation/material-bottom-tabs@5.2.15) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.13...@react-navigation/material-bottom-tabs@5.2.14) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.12...@react-navigation/material-bottom-tabs@5.2.13) (2020-07-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.11...@react-navigation/material-bottom-tabs@5.2.12) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.10...@react-navigation/material-bottom-tabs@5.2.11) (2020-06-24)
|
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.10...@react-navigation/material-bottom-tabs@5.2.11) (2020-06-24)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-bottom-tabs",
|
"name": "@react-navigation/material-bottom-tabs",
|
||||||
"description": "Integration for bottom navigation component from react-native-paper",
|
"description": "Integration for bottom navigation component from react-native-paper",
|
||||||
"version": "5.2.11",
|
"version": "5.2.15",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"@types/react-native-vector-icons": "^6.4.5",
|
"@types/react-native-vector-icons": "^6.4.5",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"react-native": "~0.61.5",
|
"react-native": "~0.61.5",
|
||||||
"react-native-paper": "^3.10.1",
|
"react-native-paper": "^3.10.1",
|
||||||
|
"react-native-testing-library": "^2.1.0",
|
||||||
"react-native-vector-icons": "^6.6.0",
|
"react-native-vector-icons": "^6.6.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
|
|||||||
39
packages/material-bottom-tabs/src/__tests__/index.test.tsx
Normal file
39
packages/material-bottom-tabs/src/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, Text, Button } from 'react-native';
|
||||||
|
import { render, fireEvent } from 'react-native-testing-library';
|
||||||
|
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
|
||||||
|
import {
|
||||||
|
createMaterialBottomTabNavigator,
|
||||||
|
MaterialBottomTabScreenProps,
|
||||||
|
} from '../index';
|
||||||
|
|
||||||
|
it('renders a material bottom tab navigator with screens', async () => {
|
||||||
|
const Test = ({
|
||||||
|
route,
|
||||||
|
navigation,
|
||||||
|
}: MaterialBottomTabScreenProps<ParamListBase>) => (
|
||||||
|
<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 = createMaterialBottomTabNavigator();
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(
|
||||||
|
<NavigationContainer>
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name="A" component={Test} />
|
||||||
|
<Tab.Screen name="B" component={Test} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Screen A')).not.toBeNull();
|
||||||
|
expect(queryByText('Screen B')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent(await findByText('Go to B'), 'press');
|
||||||
|
|
||||||
|
expect(queryByText('Screen B')).not.toBeNull();
|
||||||
|
});
|
||||||
@@ -3,6 +3,38 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.2.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.14...@react-navigation/material-top-tabs@5.2.15) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.13...@react-navigation/material-top-tabs@5.2.14) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.12...@react-navigation/material-top-tabs@5.2.13) (2020-07-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.2.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.11...@react-navigation/material-top-tabs@5.2.12) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.10...@react-navigation/material-top-tabs@5.2.11) (2020-06-24)
|
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.10...@react-navigation/material-top-tabs@5.2.11) (2020-06-24)
|
||||||
|
|
||||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-top-tabs",
|
"name": "@react-navigation/material-top-tabs",
|
||||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||||
"version": "5.2.11",
|
"version": "5.2.15",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
"del-cli": "^3.0.1",
|
"del-cli": "^3.0.1",
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"react-native-gesture-handler": "^1.6.0",
|
"react-native-gesture-handler": "^1.6.0",
|
||||||
"react-native-reanimated": "^1.8.0",
|
"react-native-reanimated": "^1.8.0",
|
||||||
"react-native-tab-view": "^2.14.4",
|
"react-native-tab-view": "^2.14.4",
|
||||||
|
"react-native-testing-library": "^2.1.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
39
packages/material-top-tabs/src/__tests__/index.test.tsx
Normal file
39
packages/material-top-tabs/src/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, Text, Button } from 'react-native';
|
||||||
|
import { render, fireEvent } from 'react-native-testing-library';
|
||||||
|
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
|
||||||
|
import {
|
||||||
|
createMaterialTopTabNavigator,
|
||||||
|
MaterialTopTabScreenProps,
|
||||||
|
} from '../index';
|
||||||
|
|
||||||
|
it('renders a material bottom tab navigator with screens', async () => {
|
||||||
|
const Test = ({
|
||||||
|
route,
|
||||||
|
navigation,
|
||||||
|
}: MaterialTopTabScreenProps<ParamListBase>) => (
|
||||||
|
<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 { findByText, queryByText } = render(
|
||||||
|
<NavigationContainer>
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen name="A" component={Test} />
|
||||||
|
<Tab.Screen name="B" component={Test} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Screen A')).not.toBeNull();
|
||||||
|
expect(queryByText('Screen B')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent(await findByText('Go to B'), 'press');
|
||||||
|
|
||||||
|
expect(queryByText('Screen B')).not.toBeNull();
|
||||||
|
});
|
||||||
@@ -3,6 +3,48 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.7.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.1...@react-navigation/native@5.7.2) (2020-07-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/native
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.0...@react-navigation/native@5.7.1) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/native
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.6.1...@react-navigation/native@5.7.0) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensure correct document title after going back on Chrome ([8f5286e](https://github.com/react-navigation/react-navigation/commit/8f5286ef501d2e88cffbe4f7d8cdeb23a4af6cf1))
|
||||||
|
* tweak border color to match iOS default ([c665c02](https://github.com/react-navigation/react-navigation/commit/c665c027a6531cf841690940a7e2cb4ea498ba03))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a hook to update document title ([13c9d1e](https://github.com/react-navigation/react-navigation/commit/13c9d1e281b4626199671bce11ba62d83767564f))
|
||||||
|
* add support for badges to bottom tab bar ([96c7b68](https://github.com/react-navigation/react-navigation/commit/96c7b688ce773b3dd1f1cf7775367cd7080c94a2))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.6.0...@react-navigation/native@5.6.1) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/native
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.5.1...@react-navigation/native@5.6.0) (2020-06-24)
|
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.5.1...@react-navigation/native@5.6.0) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/native",
|
"name": "@react-navigation/native",
|
||||||
"description": "React Native integration for React Navigation",
|
"description": "React Native integration for React Navigation",
|
||||||
"version": "5.6.0",
|
"version": "5.7.2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native",
|
"react-native",
|
||||||
"react-navigation",
|
"react-navigation",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^5.11.0",
|
"@react-navigation/core": "^5.12.2",
|
||||||
"nanoid": "^3.1.9"
|
"nanoid": "^3.1.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import DefaultTheme from './theming/DefaultTheme';
|
|||||||
import LinkingContext from './LinkingContext';
|
import LinkingContext from './LinkingContext';
|
||||||
import useThenable from './useThenable';
|
import useThenable from './useThenable';
|
||||||
import useLinking from './useLinking';
|
import useLinking from './useLinking';
|
||||||
|
import useDocumentTitle from './useDocumentTitle';
|
||||||
import useBackButton from './useBackButton';
|
import useBackButton from './useBackButton';
|
||||||
import type { Theme, LinkingOptions } from './types';
|
import type { Theme, LinkingOptions, DocumentTitleOptions } from './types';
|
||||||
|
|
||||||
type Props = NavigationContainerProps & {
|
type Props = NavigationContainerProps & {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
linking?: LinkingOptions;
|
linking?: LinkingOptions;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
|
documentTitle?: DocumentTitleOptions;
|
||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,11 +31,19 @@ type Props = NavigationContainerProps & {
|
|||||||
* @param props.theme Theme object for the navigators.
|
* @param props.theme Theme object for the navigators.
|
||||||
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
|
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
|
||||||
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
|
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
|
||||||
|
* @param props.documentTitle Options to configure the document title on Web. Updating document title is handled by default unless `documentTitle.enabled` is `false`.
|
||||||
* @param props.children Child elements to render the content.
|
* @param props.children Child elements to render the content.
|
||||||
* @param props.ref Ref object which refers to the navigation object containing helper methods.
|
* @param props.ref Ref object which refers to the navigation object containing helper methods.
|
||||||
*/
|
*/
|
||||||
const NavigationContainer = React.forwardRef(function NavigationContainer(
|
const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||||
{ theme = DefaultTheme, linking, fallback = null, onReady, ...rest }: Props,
|
{
|
||||||
|
theme = DefaultTheme,
|
||||||
|
linking,
|
||||||
|
fallback = null,
|
||||||
|
documentTitle,
|
||||||
|
onReady,
|
||||||
|
...rest
|
||||||
|
}: Props,
|
||||||
ref?: React.Ref<NavigationContainerRef | null>
|
ref?: React.Ref<NavigationContainerRef | null>
|
||||||
) {
|
) {
|
||||||
const isLinkingEnabled = linking ? linking.enabled !== false : false;
|
const isLinkingEnabled = linking ? linking.enabled !== false : false;
|
||||||
@@ -41,6 +51,7 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
|
|||||||
const refContainer = React.useRef<NavigationContainerRef>(null);
|
const refContainer = React.useRef<NavigationContainerRef>(null);
|
||||||
|
|
||||||
useBackButton(refContainer);
|
useBackButton(refContainer);
|
||||||
|
useDocumentTitle(refContainer, documentTitle);
|
||||||
|
|
||||||
const { getInitialState } = useLinking(refContainer, {
|
const { getInitialState } = useLinking(refContainer, {
|
||||||
enabled: isLinkingEnabled,
|
enabled: isLinkingEnabled,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const removeEventListener = (type: 'popstate', listener: () => void) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
document: { title: '' },
|
||||||
location,
|
location,
|
||||||
history,
|
history,
|
||||||
addEventListener,
|
addEventListener,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const DarkTheme: Theme = {
|
|||||||
card: 'rgb(18, 18, 18)',
|
card: 'rgb(18, 18, 18)',
|
||||||
text: 'rgb(229, 229, 231)',
|
text: 'rgb(229, 229, 231)',
|
||||||
border: 'rgb(39, 39, 41)',
|
border: 'rgb(39, 39, 41)',
|
||||||
|
notification: 'rgb(255, 69, 58)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const DefaultTheme: Theme = {
|
|||||||
background: 'rgb(242, 242, 242)',
|
background: 'rgb(242, 242, 242)',
|
||||||
card: 'rgb(255, 255, 255)',
|
card: 'rgb(255, 255, 255)',
|
||||||
text: 'rgb(28, 28, 30)',
|
text: 'rgb(28, 28, 30)',
|
||||||
border: 'rgb(224, 224, 224)',
|
border: 'rgb(216, 216, 216)',
|
||||||
|
notification: 'rgb(255, 59, 48)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
getStateFromPath as getStateFromPathDefault,
|
getStateFromPath as getStateFromPathDefault,
|
||||||
getPathFromState as getPathFromStateDefault,
|
getPathFromState as getPathFromStateDefault,
|
||||||
PathConfigMap,
|
PathConfigMap,
|
||||||
|
Route,
|
||||||
} from '@react-navigation/core';
|
} from '@react-navigation/core';
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
@@ -12,6 +13,7 @@ export type Theme = {
|
|||||||
card: string;
|
card: string;
|
||||||
text: string;
|
text: string;
|
||||||
border: string;
|
border: string;
|
||||||
|
notification: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,6 +54,14 @@ export type LinkingOptions = {
|
|||||||
getPathFromState?: typeof getPathFromStateDefault;
|
getPathFromState?: typeof getPathFromStateDefault;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DocumentTitleOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
formatter?: (
|
||||||
|
options: Record<string, any> | undefined,
|
||||||
|
route: Route<string> | undefined
|
||||||
|
) => string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ServerContainerRef = {
|
export type ServerContainerRef = {
|
||||||
getCurrentOptions(): Record<string, any> | undefined;
|
getCurrentOptions(): Record<string, any> | undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
3
packages/native/src/useDocumentTitle.native.tsx
Normal file
3
packages/native/src/useDocumentTitle.native.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function useDocumentTitle() {
|
||||||
|
// Noop for React Native
|
||||||
|
}
|
||||||
37
packages/native/src/useDocumentTitle.tsx
Normal file
37
packages/native/src/useDocumentTitle.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import type { NavigationContainerRef } from '@react-navigation/core';
|
||||||
|
import type { DocumentTitleOptions } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the document title for the active screen
|
||||||
|
*/
|
||||||
|
export default function useDocumentTitle(
|
||||||
|
ref: React.RefObject<NavigationContainerRef>,
|
||||||
|
{
|
||||||
|
enabled = true,
|
||||||
|
formatter = (options, route) => options?.title ?? route?.name,
|
||||||
|
}: DocumentTitleOptions = {}
|
||||||
|
) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigation = ref.current;
|
||||||
|
|
||||||
|
if (navigation) {
|
||||||
|
const title = formatter(
|
||||||
|
navigation.getCurrentOptions(),
|
||||||
|
navigation.getCurrentRoute()
|
||||||
|
);
|
||||||
|
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigation?.addListener('options', (e) => {
|
||||||
|
const title = formatter(e.data.options, navigation?.getCurrentRoute());
|
||||||
|
|
||||||
|
document.title = title;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -116,6 +116,19 @@ const createMemoryHistory = () => {
|
|||||||
pending = true;
|
pending = true;
|
||||||
|
|
||||||
const done = () => {
|
const done = () => {
|
||||||
|
// There seems to be a bug in Chrome regarding updating the title
|
||||||
|
// If we set a title just before calling `history.go`, the title gets lost
|
||||||
|
// However the value of `document.title` is still what we set it to
|
||||||
|
// It's just not displayed in the tab bar
|
||||||
|
// To update the tab bar, we need to reset the title to something else first (e.g. '')
|
||||||
|
// And set the title to what it was before so it gets applied
|
||||||
|
// It won't work without setting it to empty string coz otherwise title isn't changing
|
||||||
|
// Which means that the browser won't do anything after setting the title
|
||||||
|
const { title } = window.document;
|
||||||
|
|
||||||
|
window.document.title = '';
|
||||||
|
window.document.title = title;
|
||||||
|
|
||||||
pending = false;
|
pending = false;
|
||||||
|
|
||||||
window.removeEventListener('popstate', done);
|
window.removeEventListener('popstate', done);
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.4.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.9...@react-navigation/routers@5.4.10) (2020-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make sure history is correct after rehydration ([b70e3fe](https://github.com/react-navigation/react-navigation/commit/b70e3fe61852502322b2cb46c5934800462b0267))
|
||||||
|
* make sure index is correct when rehydrating state for tabs ([#8638](https://github.com/react-navigation/react-navigation/issues/8638)) ([1aa8219](https://github.com/react-navigation/react-navigation/commit/1aa8219021f6c231a3e150fc9bea73f12542f85c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.4.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.8...@react-navigation/routers@5.4.9) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mark some types as read-only ([7c3a0a0](https://github.com/react-navigation/react-navigation/commit/7c3a0a0f23629da0beb956ba5a9689ab965061ce))
|
||||||
|
* only remove non-existed routes from tab history. closes [#8567](https://github.com/react-navigation/react-navigation/issues/8567) ([374b081](https://github.com/react-navigation/react-navigation/commit/374b081b1c4b2e590259a050430eb1fcdbad3557))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [5.4.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.7...@react-navigation/routers@5.4.8) (2020-06-24)
|
## [5.4.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.7...@react-navigation/routers@5.4.8) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/routers",
|
"name": "@react-navigation/routers",
|
||||||
"description": "Routers to help build custom navigators",
|
"description": "Routers to help build custom navigators",
|
||||||
"version": "5.4.8",
|
"version": "5.4.10",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
|
|||||||
@@ -196,37 +196,28 @@ export default function TabRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const index = Math.min(
|
const index = Math.min(
|
||||||
Math.max(
|
Math.max(routeNames.indexOf(state.routes[state?.index ?? 0]?.name), 0),
|
||||||
typeof state.index === 'number'
|
|
||||||
? state.index
|
|
||||||
: routeNames.indexOf(state.routes[0].name),
|
|
||||||
0
|
|
||||||
),
|
|
||||||
routes.length - 1
|
routes.length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
let history = state.history?.filter((it) =>
|
const history =
|
||||||
routes.find((r) => r.key === it.key)
|
state.history?.filter((it) => routes.find((r) => r.key === it.key)) ??
|
||||||
);
|
[];
|
||||||
|
|
||||||
if (!history?.length) {
|
return changeIndex(
|
||||||
history = getRouteHistory(
|
{
|
||||||
routes,
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: `tab-${nanoid()}`,
|
||||||
index,
|
index,
|
||||||
backBehavior,
|
routeNames,
|
||||||
initialRouteName
|
history,
|
||||||
);
|
routes,
|
||||||
}
|
},
|
||||||
|
|
||||||
return {
|
|
||||||
stale: false,
|
|
||||||
type: 'tab',
|
|
||||||
key: `tab-${nanoid()}`,
|
|
||||||
index,
|
index,
|
||||||
routeNames,
|
backBehavior,
|
||||||
history,
|
initialRouteName
|
||||||
routes,
|
);
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getStateForRouteNamesChange(state, { routeNames, routeParamList }) {
|
getStateForRouteNamesChange(state, { routeNames, routeParamList }) {
|
||||||
@@ -244,8 +235,9 @@ export default function TabRouter({
|
|||||||
routeNames.indexOf(state.routes[state.index].name)
|
routeNames.indexOf(state.routes[state.index].name)
|
||||||
);
|
);
|
||||||
|
|
||||||
let history = state.history.filter((it) =>
|
let history = state.history.filter(
|
||||||
routes.find((r) => r.key === it.key)
|
// Type will always be 'route' for tabs, but could be different in a router extending this (e.g. drawer)
|
||||||
|
(it) => it.type !== 'route' || routes.find((r) => r.key === it.key)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!history.length) {
|
if (!history.length) {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
).toEqual({
|
).toEqual({
|
||||||
index: 2,
|
index: 0,
|
||||||
key: 'drawer-test',
|
key: 'drawer-test',
|
||||||
routeNames: ['bar', 'baz', 'qux'],
|
routeNames: ['bar', 'baz', 'qux'],
|
||||||
routes: [
|
routes: [
|
||||||
@@ -158,7 +158,7 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||||
],
|
],
|
||||||
history: [{ type: 'route', key: 'qux-test' }],
|
history: [{ type: 'route', key: 'bar-test' }],
|
||||||
stale: false,
|
stale: false,
|
||||||
type: 'drawer',
|
type: 'drawer',
|
||||||
});
|
});
|
||||||
@@ -178,7 +178,7 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
).toEqual({
|
).toEqual({
|
||||||
index: 1,
|
index: 0,
|
||||||
key: 'drawer-test',
|
key: 'drawer-test',
|
||||||
routeNames: ['bar', 'baz', 'qux'],
|
routeNames: ['bar', 'baz', 'qux'],
|
||||||
routes: [
|
routes: [
|
||||||
@@ -187,8 +187,8 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||||
],
|
],
|
||||||
history: [
|
history: [
|
||||||
{ type: 'route', key: 'bar-test' },
|
|
||||||
{ type: 'route', key: 'qux-test' },
|
{ type: 'route', key: 'qux-test' },
|
||||||
|
{ type: 'route', key: 'bar-test' },
|
||||||
{ type: 'drawer' },
|
{ type: 'drawer' },
|
||||||
],
|
],
|
||||||
stale: false,
|
stale: false,
|
||||||
|
|||||||
@@ -942,3 +942,68 @@ it('changes index on focus change', () => {
|
|||||||
|
|
||||||
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
|
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges params on navigate to an existing screen', () => {
|
||||||
|
const router = StackRouter({});
|
||||||
|
const options = {
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routeParamList: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
key: 'root',
|
||||||
|
index: 2,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CommonActions.navigate('bar'),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
CommonActions.navigate('bar', { fruit: 'orange' }),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'stack',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -139,8 +139,11 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
expect(
|
expect(
|
||||||
router.getRehydratedState(
|
router.getRehydratedState(
|
||||||
{
|
{
|
||||||
index: 4,
|
index: 1,
|
||||||
routes: [],
|
routes: [
|
||||||
|
{ key: 'bar-0', name: 'bar' },
|
||||||
|
{ key: 'qux-2', name: 'qux' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
@@ -148,12 +151,34 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
index: 2,
|
index: 2,
|
||||||
key: 'tab-test',
|
key: 'tab-test',
|
||||||
routeNames: ['bar', 'baz', 'qux'],
|
routeNames: ['bar', 'baz', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'bar-0', name: 'bar' },
|
||||||
|
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||||
|
{ key: 'qux-2', name: 'qux', params: { name: 'Jane' } },
|
||||||
|
],
|
||||||
|
history: [{ type: 'route', key: 'qux-2' }],
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getRehydratedState(
|
||||||
|
{
|
||||||
|
index: 4,
|
||||||
|
routes: [],
|
||||||
|
},
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
index: 0,
|
||||||
|
key: 'tab-test',
|
||||||
|
routeNames: ['bar', 'baz', 'qux'],
|
||||||
routes: [
|
routes: [
|
||||||
{ key: 'bar-test', name: 'bar' },
|
{ key: 'bar-test', name: 'bar' },
|
||||||
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
|
||||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||||
],
|
],
|
||||||
history: [{ type: 'route', key: 'qux-test' }],
|
history: [{ type: 'route', key: 'bar-test' }],
|
||||||
stale: false,
|
stale: false,
|
||||||
type: 'tab',
|
type: 'tab',
|
||||||
});
|
});
|
||||||
@@ -172,7 +197,7 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
options
|
options
|
||||||
)
|
)
|
||||||
).toEqual({
|
).toEqual({
|
||||||
index: 1,
|
index: 0,
|
||||||
key: 'tab-test',
|
key: 'tab-test',
|
||||||
routeNames: ['bar', 'baz', 'qux'],
|
routeNames: ['bar', 'baz', 'qux'],
|
||||||
routes: [
|
routes: [
|
||||||
@@ -181,8 +206,8 @@ it('gets rehydrated state from partial state', () => {
|
|||||||
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
|
||||||
],
|
],
|
||||||
history: [
|
history: [
|
||||||
{ type: 'route', key: 'bar-test' },
|
|
||||||
{ type: 'route', key: 'qux-test' },
|
{ type: 'route', key: 'qux-test' },
|
||||||
|
{ type: 'route', key: 'bar-test' },
|
||||||
],
|
],
|
||||||
stale: false,
|
stale: false,
|
||||||
type: 'tab',
|
type: 'tab',
|
||||||
@@ -1076,3 +1101,159 @@ it('updates route key history on focus change', () => {
|
|||||||
{ type: 'route', key: 'baz-0' },
|
{ type: 'route', key: 'baz-0' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges params on navigate to an existing screen', () => {
|
||||||
|
const router = TabRouter({});
|
||||||
|
const options = {
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routeParamList: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [{ type: 'route', key: 'baz' }],
|
||||||
|
},
|
||||||
|
CommonActions.navigate('bar'),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{ type: 'route', key: 'baz' },
|
||||||
|
{ type: 'route', key: 'bar' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [{ type: 'route', key: 'baz' }],
|
||||||
|
},
|
||||||
|
CommonActions.navigate('bar', { fruit: 'orange' }),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{ type: 'route', key: 'baz' },
|
||||||
|
{ type: 'route', key: 'bar' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges params on jump to an existing screen', () => {
|
||||||
|
const router = TabRouter({});
|
||||||
|
const options = {
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routeParamList: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [{ type: 'route', key: 'baz' }],
|
||||||
|
},
|
||||||
|
TabActions.jumpTo('bar'),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{ type: 'route', key: 'baz' },
|
||||||
|
{ type: 'route', key: 'bar' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
router.getStateForAction(
|
||||||
|
{
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42 } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [{ type: 'route', key: 'baz' }],
|
||||||
|
},
|
||||||
|
TabActions.jumpTo('bar', { fruit: 'orange' }),
|
||||||
|
options
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
stale: false,
|
||||||
|
type: 'tab',
|
||||||
|
key: 'root',
|
||||||
|
index: 1,
|
||||||
|
routeNames: ['baz', 'bar', 'qux'],
|
||||||
|
routes: [
|
||||||
|
{ key: 'baz', name: 'baz' },
|
||||||
|
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
|
||||||
|
{ key: 'qux', name: 'qux' },
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{ type: 'route', key: 'baz' },
|
||||||
|
{ type: 'route', key: 'bar' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type * as CommonActions from './CommonActions';
|
|||||||
|
|
||||||
export type CommonNavigationAction = CommonActions.Action;
|
export type CommonNavigationAction = CommonActions.Action;
|
||||||
|
|
||||||
export type NavigationState = {
|
export type NavigationState = Readonly<{
|
||||||
/**
|
/**
|
||||||
* Unique key for the navigation state.
|
* Unique key for the navigation state.
|
||||||
*/
|
*/
|
||||||
@@ -35,26 +35,27 @@ export type NavigationState = {
|
|||||||
* Whether the navigation state has been rehydrated.
|
* Whether the navigation state has been rehydrated.
|
||||||
*/
|
*/
|
||||||
stale: false;
|
stale: false;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type InitialState = Partial<
|
export type InitialState = Readonly<
|
||||||
Omit<NavigationState, 'stale' | 'routes'>
|
Partial<Omit<NavigationState, 'stale' | 'routes'>> & {
|
||||||
> & {
|
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
|
||||||
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
|
}
|
||||||
};
|
>;
|
||||||
|
|
||||||
export type PartialState<State extends NavigationState> = Partial<
|
export type PartialState<State extends NavigationState> = Partial<
|
||||||
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
|
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
|
||||||
> & {
|
> &
|
||||||
stale?: true;
|
Readonly<{
|
||||||
type?: string;
|
stale?: true;
|
||||||
routes: (Omit<Route<string>, 'key'> & {
|
type?: string;
|
||||||
key?: string;
|
routes: (Omit<Route<string>, 'key'> & {
|
||||||
state?: InitialState;
|
key?: string;
|
||||||
})[];
|
state?: InitialState;
|
||||||
};
|
})[];
|
||||||
|
}>;
|
||||||
|
|
||||||
export type Route<RouteName extends string> = {
|
export type Route<RouteName extends string> = Readonly<{
|
||||||
/**
|
/**
|
||||||
* Unique key for the route.
|
* Unique key for the route.
|
||||||
*/
|
*/
|
||||||
@@ -67,11 +68,11 @@ export type Route<RouteName extends string> = {
|
|||||||
* Params for the route.
|
* Params for the route.
|
||||||
*/
|
*/
|
||||||
params?: object;
|
params?: object;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type ParamListBase = Record<string, object | undefined>;
|
export type ParamListBase = Record<string, object | undefined>;
|
||||||
|
|
||||||
export type NavigationAction = {
|
export type NavigationAction = Readonly<{
|
||||||
/**
|
/**
|
||||||
* Type of the action (e.g. `NAVIGATE`)
|
* Type of the action (e.g. `NAVIGATE`)
|
||||||
*/
|
*/
|
||||||
@@ -88,7 +89,7 @@ export type NavigationAction = {
|
|||||||
* Key of the navigator which should handle this action.
|
* Key of the navigator which should handle this action.
|
||||||
*/
|
*/
|
||||||
target?: string;
|
target?: string;
|
||||||
};
|
}>;
|
||||||
|
|
||||||
export type ActionCreators<Action extends NavigationAction> = {
|
export type ActionCreators<Action extends NavigationAction> = {
|
||||||
[key: string]: (...args: any) => Action;
|
[key: string]: (...args: any) => Action;
|
||||||
|
|||||||
@@ -3,6 +3,56 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [5.8.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.7.1...@react-navigation/stack@5.8.0) (2020-07-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow style overrides for HeaderBackButton ([#8626](https://github.com/react-navigation/react-navigation/issues/8626)) ([486c3de](https://github.com/react-navigation/react-navigation/commit/486c3defd27592bf4170af4962a1c66f4710b17a))
|
||||||
|
* emit gesture navigation events from stack view ([#8524](https://github.com/react-navigation/react-navigation/issues/8524)) ([15f9b95](https://github.com/react-navigation/react-navigation/commit/15f9b9573e52666f88b0f917396496b03218f160))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.7.0...@react-navigation/stack@5.7.1) (2020-07-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/stack
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.2...@react-navigation/stack@5.7.0) (2020-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add a `beforeRemove` event ([6925e92](https://github.com/react-navigation/react-navigation/commit/6925e92dc3e9885e3f552ca5e5eb51ae1521e54e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.1...@react-navigation/stack@5.6.2) (2020-06-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @react-navigation/stack
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.0...@react-navigation/stack@5.6.1) (2020-06-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix showing back button with headerMode=screen. fixes [#8508](https://github.com/react-navigation/react-navigation/issues/8508) ([fc95d7a](https://github.com/react-navigation/react-navigation/commit/fc95d7a256846b6a4fa999fbbe3fed2051972b42))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.5.1...@react-navigation/stack@5.6.0) (2020-06-24)
|
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.5.1...@react-navigation/stack@5.6.0) (2020-06-24)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/stack",
|
"name": "@react-navigation/stack",
|
||||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||||
"version": "5.6.0",
|
"version": "5.8.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.15.1",
|
"@react-native-community/bob": "^0.15.1",
|
||||||
"@react-native-community/masked-view": "^0.1.10",
|
"@react-native-community/masked-view": "^0.1.10",
|
||||||
"@react-navigation/native": "^5.6.0",
|
"@react-navigation/native": "^5.7.2",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.36",
|
"@types/react": "^16.9.36",
|
||||||
"@types/react-native": "^0.62.7",
|
"@types/react-native": "^0.62.7",
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"react-native-gesture-handler": "^1.6.0",
|
"react-native-gesture-handler": "^1.6.0",
|
||||||
"react-native-safe-area-context": "^1.0.0",
|
"react-native-safe-area-context": "^1.0.0",
|
||||||
"react-native-screens": "^2.7.0",
|
"react-native-screens": "^2.7.0",
|
||||||
|
"react-native-testing-library": "^2.1.0",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
33
packages/stack/src/__tests__/index.test.tsx
Normal file
33
packages/stack/src/__tests__/index.test.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { View, Text, Button } from 'react-native';
|
||||||
|
import { render, fireEvent } from 'react-native-testing-library';
|
||||||
|
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
|
||||||
|
import { createStackNavigator, StackScreenProps } from '../index';
|
||||||
|
|
||||||
|
it('renders a stack navigator with screens', async () => {
|
||||||
|
const Test = ({ route, navigation }: StackScreenProps<ParamListBase>) => (
|
||||||
|
<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 Stack = createStackNavigator();
|
||||||
|
|
||||||
|
const { findByText, queryByText } = render(
|
||||||
|
<NavigationContainer>
|
||||||
|
<Stack.Navigator>
|
||||||
|
<Stack.Screen name="A" component={Test} />
|
||||||
|
<Stack.Screen name="B" component={Test} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('Screen A')).not.toBeNull();
|
||||||
|
expect(queryByText('Screen B')).toBeNull();
|
||||||
|
|
||||||
|
fireEvent.press(await findByText('Go to B'));
|
||||||
|
|
||||||
|
expect(queryByText('Screen B')).not.toBeNull();
|
||||||
|
});
|
||||||
@@ -27,6 +27,18 @@ export type StackNavigationEventMap = {
|
|||||||
* Event which fires when a transition animation ends.
|
* Event which fires when a transition animation ends.
|
||||||
*/
|
*/
|
||||||
transitionEnd: { data: { closing: boolean } };
|
transitionEnd: { data: { closing: boolean } };
|
||||||
|
/**
|
||||||
|
* Event which fires when navigation gesture starts.
|
||||||
|
*/
|
||||||
|
gestureStart: { data: undefined };
|
||||||
|
/**
|
||||||
|
* Event which fires when navigation gesture is completed.
|
||||||
|
*/
|
||||||
|
gestureEnd: { data: undefined };
|
||||||
|
/**
|
||||||
|
* Event which fires when navigation gesture is canceled.
|
||||||
|
*/
|
||||||
|
gestureCancel: { data: undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StackNavigationHelpers = NavigationHelpers<
|
export type StackNavigationHelpers = NavigationHelpers<
|
||||||
@@ -406,6 +418,10 @@ export type StackHeaderLeftButtonProps = {
|
|||||||
* Accessibility label for the button for screen readers.
|
* Accessibility label for the button for screen readers.
|
||||||
*/
|
*/
|
||||||
accessibilityLabel?: string;
|
accessibilityLabel?: string;
|
||||||
|
/**
|
||||||
|
* Style object for the button.
|
||||||
|
*/
|
||||||
|
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StackHeaderTitleProps = {
|
export type StackHeaderTitleProps = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function HeaderBackButton({
|
|||||||
titleLayout,
|
titleLayout,
|
||||||
truncatedLabel = 'Back',
|
truncatedLabel = 'Back',
|
||||||
accessibilityLabel = label && label !== 'Back' ? `${label}, back` : 'Go back',
|
accessibilityLabel = label && label !== 'Back' ? `${label}, back` : 'Go back',
|
||||||
|
style,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { dark, colors } = useTheme();
|
const { dark, colors } = useTheme();
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ export default function HeaderBackButton({
|
|||||||
delayPressIn={0}
|
delayPressIn={0}
|
||||||
onPress={disabled ? undefined : handlePress}
|
onPress={disabled ? undefined : handlePress}
|
||||||
pressColor={pressColorAndroid}
|
pressColor={pressColorAndroid}
|
||||||
style={[styles.container, disabled && styles.disabled]}
|
style={[styles.container, disabled && styles.disabled, style]}
|
||||||
hitSlop={Platform.select({
|
hitSlop={Platform.select({
|
||||||
ios: undefined,
|
ios: undefined,
|
||||||
default: { top: 16, right: 16, bottom: 16, left: 16 },
|
default: { top: 16, right: 16, bottom: 16, left: 16 },
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export type Props = {
|
|||||||
scenes: (Scene<Route<string>> | undefined)[];
|
scenes: (Scene<Route<string>> | undefined)[];
|
||||||
getPreviousScene: (props: {
|
getPreviousScene: (props: {
|
||||||
route: Route<string>;
|
route: Route<string>;
|
||||||
index: number;
|
|
||||||
}) => Scene<Route<string>> | undefined;
|
}) => Scene<Route<string>> | undefined;
|
||||||
getFocusedRoute: () => Route<string>;
|
getFocusedRoute: () => Route<string>;
|
||||||
onContentHeightChange?: (props: {
|
onContentHeightChange?: (props: {
|
||||||
@@ -79,10 +78,7 @@ export default function HeaderContainer({
|
|||||||
|
|
||||||
const isFocused = focusedRoute.key === scene.route.key;
|
const isFocused = focusedRoute.key === scene.route.key;
|
||||||
const previous =
|
const previous =
|
||||||
getPreviousScene({
|
getPreviousScene({ route: scene.route }) ?? parentPreviousScene;
|
||||||
route: scene.route,
|
|
||||||
index: i,
|
|
||||||
}) ?? parentPreviousScene;
|
|
||||||
|
|
||||||
// If the screen is next to a headerless screen, we need to make the header appear static
|
// If the screen is next to a headerless screen, we need to make the header appear static
|
||||||
// This makes the header look like it's moving with the screen
|
// This makes the header look like it's moving with the screen
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export default class Card extends React.Component<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.animate({ closing: this.props.closing });
|
this.animate({ closing: this.props.closing });
|
||||||
|
this.isCurrentlyMounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
@@ -115,8 +116,11 @@ export default class Card extends React.Component<Props> {
|
|||||||
this.inverted.setValue(getInvertedMultiplier(gestureDirection));
|
this.inverted.setValue(getInvertedMultiplier(gestureDirection));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toValue = this.getAnimateToValue(this.props);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.getAnimateToValue(this.props) !== this.getAnimateToValue(prevProps)
|
this.getAnimateToValue(prevProps) !== toValue ||
|
||||||
|
this.lastToValue !== toValue
|
||||||
) {
|
) {
|
||||||
// We need to trigger the animation when route was closed
|
// We need to trigger the animation when route was closed
|
||||||
// Thr route might have been closed by a `POP` action or by a gesture
|
// Thr route might have been closed by a `POP` action or by a gesture
|
||||||
@@ -128,9 +132,12 @@ export default class Card extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.isCurrentlyMounted = false;
|
||||||
this.handleEndInteraction();
|
this.handleEndInteraction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isCurrentlyMounted = false;
|
||||||
|
|
||||||
private isClosing = new Animated.Value(FALSE);
|
private isClosing = new Animated.Value(FALSE);
|
||||||
|
|
||||||
private inverted = new Animated.Value(
|
private inverted = new Animated.Value(
|
||||||
@@ -148,6 +155,8 @@ export default class Card extends React.Component<Props> {
|
|||||||
|
|
||||||
private pendingGestureCallback: number | undefined;
|
private pendingGestureCallback: number | undefined;
|
||||||
|
|
||||||
|
private lastToValue: number | undefined;
|
||||||
|
|
||||||
private animate = ({
|
private animate = ({
|
||||||
closing,
|
closing,
|
||||||
velocity,
|
velocity,
|
||||||
@@ -168,6 +177,8 @@ export default class Card extends React.Component<Props> {
|
|||||||
closing,
|
closing,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.lastToValue = toValue;
|
||||||
|
|
||||||
const spec = closing ? transitionSpec.close : transitionSpec.open;
|
const spec = closing ? transitionSpec.close : transitionSpec.open;
|
||||||
|
|
||||||
const animation =
|
const animation =
|
||||||
@@ -196,6 +207,11 @@ export default class Card extends React.Component<Props> {
|
|||||||
} else {
|
} else {
|
||||||
onOpen();
|
onOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isCurrentlyMounted) {
|
||||||
|
// Make sure to re-open screen if it wasn't removed
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -301,10 +317,13 @@ export default class Card extends React.Component<Props> {
|
|||||||
if (closing) {
|
if (closing) {
|
||||||
// We call onClose with a delay to make sure that the animation has already started
|
// We call onClose with a delay to make sure that the animation has already started
|
||||||
// This will make sure that the state update caused by this doesn't affect start of animation
|
// This will make sure that the state update caused by this doesn't affect start of animation
|
||||||
this.pendingGestureCallback = (setTimeout(
|
this.pendingGestureCallback = (setTimeout(() => {
|
||||||
onClose,
|
onClose();
|
||||||
32
|
|
||||||
) as any) as number;
|
// Trigger an update after we dispatch the action to remove the screen
|
||||||
|
// This will make sure that we check if the screen didn't get removed so we can cancel the animation
|
||||||
|
this.forceUpdate();
|
||||||
|
}, 32) as any) as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
onGestureEnd?.();
|
onGestureEnd?.();
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ type Props = TransitionPreset & {
|
|||||||
cardStyle?: StyleProp<ViewStyle>;
|
cardStyle?: StyleProp<ViewStyle>;
|
||||||
getPreviousScene: (props: {
|
getPreviousScene: (props: {
|
||||||
route: Route<string>;
|
route: Route<string>;
|
||||||
index: number;
|
|
||||||
}) => Scene<Route<string>> | undefined;
|
}) => Scene<Route<string>> | undefined;
|
||||||
getFocusedRoute: () => Route<string>;
|
getFocusedRoute: () => Route<string>;
|
||||||
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
||||||
@@ -47,6 +46,9 @@ type Props = TransitionPreset & {
|
|||||||
onPageChangeStart?: () => void;
|
onPageChangeStart?: () => void;
|
||||||
onPageChangeConfirm?: () => void;
|
onPageChangeConfirm?: () => void;
|
||||||
onPageChangeCancel?: () => void;
|
onPageChangeCancel?: () => void;
|
||||||
|
onGestureStart?: (props: { route: Route<string> }) => void;
|
||||||
|
onGestureEnd?: (props: { route: Route<string> }) => void;
|
||||||
|
onGestureCancel?: (props: { route: Route<string> }) => void;
|
||||||
gestureEnabled?: boolean;
|
gestureEnabled?: boolean;
|
||||||
gestureResponseDistance?: {
|
gestureResponseDistance?: {
|
||||||
vertical?: number;
|
vertical?: number;
|
||||||
@@ -96,6 +98,9 @@ function CardContainer({
|
|||||||
onPageChangeCancel,
|
onPageChangeCancel,
|
||||||
onPageChangeConfirm,
|
onPageChangeConfirm,
|
||||||
onPageChangeStart,
|
onPageChangeStart,
|
||||||
|
onGestureCancel,
|
||||||
|
onGestureEnd,
|
||||||
|
onGestureStart,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
onTransitionStart,
|
onTransitionStart,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
@@ -121,6 +126,20 @@ function CardContainer({
|
|||||||
onCloseRoute({ route: scene.route });
|
onCloseRoute({ route: scene.route });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGestureBegin = () => {
|
||||||
|
onPageChangeStart?.();
|
||||||
|
onGestureStart?.({ route: scene.route });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGestureCanceled = () => {
|
||||||
|
onPageChangeCancel?.();
|
||||||
|
onGestureCancel?.({ route: scene.route });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGestureEnd = () => {
|
||||||
|
onGestureEnd?.({ route: scene.route });
|
||||||
|
};
|
||||||
|
|
||||||
const handleTransitionStart = ({ closing }: { closing: boolean }) => {
|
const handleTransitionStart = ({ closing }: { closing: boolean }) => {
|
||||||
if (active && closing) {
|
if (active && closing) {
|
||||||
onPageChangeConfirm?.();
|
onPageChangeConfirm?.();
|
||||||
@@ -162,7 +181,7 @@ function CardContainer({
|
|||||||
|
|
||||||
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
||||||
const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false;
|
const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false;
|
||||||
const previousScene = getPreviousScene({ route: scene.route, index });
|
const previousScene = getPreviousScene({ route: scene.route });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -180,8 +199,9 @@ function CardContainer({
|
|||||||
overlayEnabled={cardOverlayEnabled}
|
overlayEnabled={cardOverlayEnabled}
|
||||||
shadowEnabled={cardShadowEnabled}
|
shadowEnabled={cardShadowEnabled}
|
||||||
onTransitionStart={handleTransitionStart}
|
onTransitionStart={handleTransitionStart}
|
||||||
onGestureBegin={onPageChangeStart}
|
onGestureBegin={handleGestureBegin}
|
||||||
onGestureCanceled={onPageChangeCancel}
|
onGestureCanceled={handleGestureCanceled}
|
||||||
|
onGestureEnd={handleGestureEnd}
|
||||||
gestureEnabled={gestureEnabled}
|
gestureEnabled={gestureEnabled}
|
||||||
gestureResponseDistance={gestureResponseDistance}
|
gestureResponseDistance={gestureResponseDistance}
|
||||||
gestureVelocityImpact={gestureVelocityImpact}
|
gestureVelocityImpact={gestureVelocityImpact}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ type Props = {
|
|||||||
onPageChangeStart?: () => void;
|
onPageChangeStart?: () => void;
|
||||||
onPageChangeConfirm?: () => void;
|
onPageChangeConfirm?: () => void;
|
||||||
onPageChangeCancel?: () => void;
|
onPageChangeCancel?: () => void;
|
||||||
|
onGestureStart?: (props: { route: Route<string> }) => void;
|
||||||
|
onGestureEnd?: (props: { route: Route<string> }) => void;
|
||||||
|
onGestureCancel?: (props: { route: Route<string> }) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -336,31 +339,21 @@ export default class CardStack extends React.Component<Props, State> {
|
|||||||
return state.routes[state.index];
|
return state.routes[state.index];
|
||||||
};
|
};
|
||||||
|
|
||||||
private getPreviousScene = ({
|
private getPreviousScene = ({ route }: { route: Route<string> }) => {
|
||||||
route,
|
const { getPreviousRoute } = this.props;
|
||||||
index,
|
const { scenes } = this.state;
|
||||||
}: {
|
|
||||||
route: Route<string>;
|
|
||||||
index: number;
|
|
||||||
}) => {
|
|
||||||
const previousRoute = this.props.getPreviousRoute({ route });
|
|
||||||
|
|
||||||
let previous: Scene<Route<string>> | undefined;
|
const previousRoute = getPreviousRoute({ route });
|
||||||
|
|
||||||
if (previousRoute) {
|
if (previousRoute) {
|
||||||
// The previous scene will be shortly before the current scene in the array
|
const previousScene = scenes.find(
|
||||||
// So loop back from current index to avoid looping over the full array
|
(scene) => scene.route.key === previousRoute.key
|
||||||
for (let j = index - 1; j >= 0; j--) {
|
);
|
||||||
const s = this.state.scenes[j];
|
|
||||||
|
|
||||||
if (s && s.route.key === previousRoute.key) {
|
return previousScene;
|
||||||
previous = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return previous;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -382,6 +375,9 @@ export default class CardStack extends React.Component<Props, State> {
|
|||||||
onPageChangeStart,
|
onPageChangeStart,
|
||||||
onPageChangeConfirm,
|
onPageChangeConfirm,
|
||||||
onPageChangeCancel,
|
onPageChangeCancel,
|
||||||
|
onGestureStart,
|
||||||
|
onGestureEnd,
|
||||||
|
onGestureCancel,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { scenes, layout, gestures, headerHeights } = this.state;
|
const { scenes, layout, gestures, headerHeights } = this.state;
|
||||||
@@ -578,6 +574,9 @@ export default class CardStack extends React.Component<Props, State> {
|
|||||||
onPageChangeStart={onPageChangeStart}
|
onPageChangeStart={onPageChangeStart}
|
||||||
onPageChangeConfirm={onPageChangeConfirm}
|
onPageChangeConfirm={onPageChangeConfirm}
|
||||||
onPageChangeCancel={onPageChangeCancel}
|
onPageChangeCancel={onPageChangeCancel}
|
||||||
|
onGestureStart={onGestureStart}
|
||||||
|
onGestureCancel={onGestureCancel}
|
||||||
|
onGestureEnd={onGestureEnd}
|
||||||
gestureResponseDistance={gestureResponseDistance}
|
gestureResponseDistance={gestureResponseDistance}
|
||||||
headerHeight={headerHeight}
|
headerHeight={headerHeight}
|
||||||
onHeaderHeightChange={this.handleHeaderLayout}
|
onHeaderHeightChange={this.handleHeaderLayout}
|
||||||
|
|||||||
@@ -405,6 +405,27 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
target: route.key,
|
target: route.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private handleGestureStart = ({ route }: { route: Route<string> }) => {
|
||||||
|
this.props.navigation.emit({
|
||||||
|
type: 'gestureStart',
|
||||||
|
target: route.key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGestureEnd = ({ route }: { route: Route<string> }) => {
|
||||||
|
this.props.navigation.emit({
|
||||||
|
type: 'gestureEnd',
|
||||||
|
target: route.key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGestureCancel = ({ route }: { route: Route<string> }) => {
|
||||||
|
this.props.navigation.emit({
|
||||||
|
type: 'gestureCancel',
|
||||||
|
target: route.key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
@@ -451,6 +472,9 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
headerMode={headerMode}
|
headerMode={headerMode}
|
||||||
state={state}
|
state={state}
|
||||||
descriptors={descriptors}
|
descriptors={descriptors}
|
||||||
|
onGestureStart={this.handleGestureStart}
|
||||||
|
onGestureEnd={this.handleGestureEnd}
|
||||||
|
onGestureCancel={this.handleGestureCancel}
|
||||||
{...rest}
|
{...rest}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user