mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-13 17:32:55 +08:00
Compare commits
28 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
- run:
|
||||
name: Run unit tests
|
||||
command: yarn test --coverage
|
||||
command: yarn test --maxWorkers=2 --coverage
|
||||
- run:
|
||||
name: Upload test coverage
|
||||
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
||||
command: yarn codecov
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
destination: coverage
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"extends": "satya164",
|
||||
"settings": {
|
||||
"react": { "version": "16" },
|
||||
"react": {
|
||||
"version": "16"
|
||||
},
|
||||
"import/core-modules": [
|
||||
"@react-navigation/core",
|
||||
"@react-navigation/native",
|
||||
@@ -15,5 +17,11 @@
|
||||
"@react-navigation/devtools"
|
||||
]
|
||||
},
|
||||
"env": { "browser": true, "node": true }
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"react/no-unused-prop-types": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@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',
|
||||
],
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-module-resolver": "^4.0.0",
|
||||
"babel-preset-expo": "^8.2.1",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTheme, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
HeaderBackButton,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
|
||||
type AuthStackParams = {
|
||||
@@ -81,10 +81,6 @@ const HomeScreen = () => {
|
||||
|
||||
const SimpleStack = createStackNavigator<AuthStackParams>();
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
type State = {
|
||||
isLoading: boolean;
|
||||
isSignout: boolean;
|
||||
@@ -96,7 +92,9 @@ type Action =
|
||||
| { type: 'SIGN_IN'; token: string }
|
||||
| { 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>>(
|
||||
(prevState, action) => {
|
||||
switch (action.type) {
|
||||
@@ -135,9 +133,11 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const authContext = React.useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import type { StackScreenProps } from '@react-navigation/stack';
|
||||
import {
|
||||
createBottomTabNavigator,
|
||||
BottomTabNavigationProp,
|
||||
BottomTabScreenProps,
|
||||
} from '@react-navigation/bottom-tabs';
|
||||
import TouchableBounce from '../Shared/TouchableBounce';
|
||||
import Albums from '../Shared/Albums';
|
||||
@@ -36,9 +36,7 @@ const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: BottomTabNavigationProp<BottomTabParams>;
|
||||
}) => {
|
||||
}: BottomTabScreenProps<BottomTabParams>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -99,6 +97,7 @@ export default function BottomTabsScreen({
|
||||
options={{
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: getTabBarIcon('message-reply'),
|
||||
tabBarBadge: 2,
|
||||
}}
|
||||
/>
|
||||
<BottomTabs.Screen
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
@@ -126,8 +127,8 @@ const CompatStack = createCompatStackNavigator<
|
||||
StackNavigationProp<NestedStackParams>
|
||||
>(
|
||||
{
|
||||
Feed: FeedScreen,
|
||||
Article: ArticleScreen,
|
||||
Feed: { getScreen: () => FeedScreen },
|
||||
Article: { getScreen: () => ArticleScreen },
|
||||
},
|
||||
{ navigationOptions: { headerShown: false } }
|
||||
),
|
||||
@@ -143,12 +144,12 @@ const CompatStack = createCompatStackNavigator<
|
||||
|
||||
export default function CompatStackScreen({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: StackNavigationProp<{}>;
|
||||
}) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}: StackScreenProps<{}>) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return <CompatStack />;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import { Button } from 'react-native-paper';
|
||||
import {
|
||||
Link,
|
||||
StackActions,
|
||||
RouteProp,
|
||||
ParamListBase,
|
||||
useLinkProps,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
@@ -20,8 +19,6 @@ type SimpleStackParams = {
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const LinkButton = ({
|
||||
@@ -45,10 +42,7 @@ const LinkButton = ({
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -88,11 +82,7 @@ const ArticleScreen = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -124,14 +114,15 @@ const AlbumsScreen = ({
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator {...rest}>
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createDrawerNavigator,
|
||||
DrawerNavigationProp,
|
||||
DrawerScreenProps,
|
||||
DrawerContent,
|
||||
DrawerContentComponentProps,
|
||||
DrawerContentOptions,
|
||||
} from '@react-navigation/drawer';
|
||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
||||
import type { StackScreenProps } from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
@@ -24,8 +24,6 @@ type DrawerParams = {
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
|
||||
|
||||
const useIsLargeScreen = () => {
|
||||
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 (
|
||||
<>
|
||||
<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 (
|
||||
<>
|
||||
<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 (
|
||||
<>
|
||||
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
|
||||
@@ -106,15 +110,16 @@ const CustomDrawerContent = (
|
||||
|
||||
const Drawer = createDrawerNavigator<DrawerParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
|
||||
export default function DrawerScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
gestureEnabled: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
gestureEnabled: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
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 Albums from '../Shared/Albums';
|
||||
import Contacts from '../Shared/Contacts';
|
||||
@@ -14,14 +14,14 @@ type MaterialTopTabParams = {
|
||||
|
||||
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function MaterialTopTabsScreen({ navigation }: Props) {
|
||||
navigation.setOptions({
|
||||
cardStyle: { flex: 1 },
|
||||
});
|
||||
export default function MaterialTopTabsScreen({
|
||||
navigation,
|
||||
}: StackScreenProps<ParamListBase>) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
cardStyle: { flex: 1 },
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<MaterialTopTabs.Navigator>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import type { ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
StackNavigationOptions,
|
||||
TransitionPresets,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
@@ -15,17 +16,12 @@ type ModalStackParams = {
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: ModalStackNavigation;
|
||||
route: RouteProp<ModalStackParams, 'Article'>;
|
||||
}) => {
|
||||
}: StackScreenProps<ModalStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -52,7 +48,7 @@ const ArticleScreen = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
const AlbumsScreen = ({ navigation }: StackScreenProps<ModalStackParams>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -78,15 +74,16 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
|
||||
const ModalPresentationStack = createStackNavigator<ModalStackParams>();
|
||||
|
||||
type Props = {
|
||||
options?: React.ComponentProps<typeof ModalPresentationStack.Navigator>;
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
type Props = StackScreenProps<ParamListBase> & {
|
||||
options?: StackNavigationOptions;
|
||||
};
|
||||
|
||||
export default function SimpleStackScreen({ navigation, options }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<ModalPresentationStack.Navigator
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
||||
import type { StackScreenProps } from '@react-navigation/stack';
|
||||
|
||||
const NotFoundScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: StackNavigationProp<{ Home: undefined }>;
|
||||
}) => {
|
||||
}: StackScreenProps<{ Home: undefined }>) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<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 { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import type { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import type { ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
NewsFeed: undefined;
|
||||
Article: { author: string } | undefined;
|
||||
NewsFeed: { date: number };
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.replace('NewsFeed')}
|
||||
onPress={() => navigation.replace('NewsFeed', { date: Date.now() })}
|
||||
style={styles.button}
|
||||
>
|
||||
Replace with feed
|
||||
@@ -46,7 +41,7 @@ const ArticleScreen = ({
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
author={{ name: route.params?.author ?? 'Unknown' }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
</ScrollView>
|
||||
@@ -54,10 +49,9 @@ const ArticleScreen = ({
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -76,16 +70,14 @@ const NewsFeedScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -111,14 +103,14 @@ const AlbumsScreen = ({
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function SimpleStackScreen({ navigation }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
export default function SimpleStackScreen({
|
||||
navigation,
|
||||
}: StackScreenProps<ParamListBase>) {
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator>
|
||||
@@ -126,7 +118,7 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params.author}`,
|
||||
title: `Article by ${route.params?.author ?? 'Unknown'}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
} from 'react-native';
|
||||
import { Button, Appbar } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { useTheme, RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import { useTheme, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
HeaderBackground,
|
||||
useHeaderHeight,
|
||||
Header,
|
||||
@@ -27,17 +27,12 @@ type SimpleStackParams = {
|
||||
Albums: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -64,11 +59,7 @@ const ArticleScreen = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
const headerHeight = useHeaderHeight();
|
||||
|
||||
return (
|
||||
@@ -96,9 +87,8 @@ const AlbumsScreen = ({
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
|
||||
function CustomHeader(props: StackHeaderProps) {
|
||||
const { current, next } = props.scene.progress;
|
||||
@@ -120,9 +110,11 @@ function CustomHeader(props: StackHeaderProps) {
|
||||
}
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const { colors, dark } = useTheme();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { Button, Paragraph } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import { ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
|
||||
@@ -13,17 +13,12 @@ type SimpleStackParams = {
|
||||
Dialog: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
@@ -50,11 +45,7 @@ const ArticleScreen = ({
|
||||
);
|
||||
};
|
||||
|
||||
const DialogScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
const DialogScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
@@ -81,14 +72,15 @@ const DialogScreen = ({
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
|
||||
StackScreenProps<ParamListBase>;
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
React.useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator mode="modal" {...rest}>
|
||||
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
} from 'react-native-paper';
|
||||
import Color from 'color';
|
||||
|
||||
type Props = Partial<ScrollViewProps>;
|
||||
type Props = Partial<ScrollViewProps> & {
|
||||
date?: number;
|
||||
};
|
||||
|
||||
const Author = () => {
|
||||
return (
|
||||
|
||||
@@ -31,11 +31,11 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
createDrawerNavigator,
|
||||
DrawerNavigationProp,
|
||||
DrawerScreenProps,
|
||||
} from '@react-navigation/drawer';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
StackScreenProps,
|
||||
HeaderStyleInterpolators,
|
||||
} from '@react-navigation/stack';
|
||||
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
|
||||
@@ -53,9 +53,10 @@ import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||
import NotFound from './Screens/NotFound';
|
||||
import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
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';
|
||||
|
||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||
@@ -109,6 +110,10 @@ const SCREENS = {
|
||||
title: 'Auth Flow',
|
||||
component: AuthFlow,
|
||||
},
|
||||
PreventRemove: {
|
||||
title: 'Prevent removing screen',
|
||||
component: PreventRemove,
|
||||
},
|
||||
CompatAPI: {
|
||||
title: 'Compat Layer',
|
||||
component: CompatAPI,
|
||||
@@ -272,6 +277,10 @@ export default function App() {
|
||||
},
|
||||
}}
|
||||
fallback={<Text>Loading…</Text>}
|
||||
documentTitle={{
|
||||
formatter: (options, route) =>
|
||||
`${options?.title ?? route?.name} - React Navigation Example`,
|
||||
}}
|
||||
>
|
||||
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||
<Drawer.Screen
|
||||
@@ -283,11 +292,7 @@ export default function App() {
|
||||
),
|
||||
}}
|
||||
>
|
||||
{({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: DrawerNavigationProp<RootDrawerParamList>;
|
||||
}) => (
|
||||
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => (
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
|
||||
@@ -308,11 +313,7 @@ export default function App() {
|
||||
),
|
||||
}}
|
||||
>
|
||||
{({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: StackNavigationProp<RootStackParamList>;
|
||||
}) => (
|
||||
{({ navigation }: StackScreenProps<RootStackParamList>) => (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: theme.colors.background }}
|
||||
>
|
||||
@@ -361,7 +362,7 @@ export default function App() {
|
||||
<Stack.Screen
|
||||
key={name}
|
||||
name={name}
|
||||
component={SCREENS[name].component}
|
||||
getComponent={() => SCREENS[name].component}
|
||||
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;
|
||||
|
||||
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/)",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext '.js,.ts,.tsx' .",
|
||||
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
||||
"typescript": "tsc --noEmit --composite false",
|
||||
"test": "jest",
|
||||
"prerelease": "lerna run clean",
|
||||
@@ -25,24 +25,17 @@
|
||||
"example": "yarn --cwd example"
|
||||
},
|
||||
"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",
|
||||
"@types/jest": "^26.0.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"codecov": "^3.7.0",
|
||||
"commitlint": "^8.3.5",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-satya164": "^3.1.7",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.0.1",
|
||||
"lerna": "^3.22.1",
|
||||
"metro-react-native-babel-preset": "^0.59.0",
|
||||
"prettier": "^2.0.5",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
@@ -59,9 +52,6 @@
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testRegex": "/__tests__/.*\\.(test|spec)\\.(js|tsx?)$",
|
||||
"transform": {
|
||||
"^.+\\.(js|ts|tsx)$": "babel-jest"
|
||||
},
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest/setup.js"
|
||||
],
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "5.6.0",
|
||||
"version": "5.7.0",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-native": "^0.62.7",
|
||||
@@ -50,6 +50,7 @@
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-safe-area-context": "^1.0.0",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-testing-library": "^2.1.0",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"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();
|
||||
});
|
||||
@@ -79,6 +79,11 @@ export type BottomTabNavigationOptions = {
|
||||
size: number;
|
||||
}) => 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.
|
||||
* It's recommended to set this if you don't have a label for the tab.
|
||||
|
||||
83
packages/bottom-tabs/src/views/Badge.tsx
Normal file
83
packages/bottom-tabs/src/views/Badge.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
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 theme = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
Animated.timing(opacity, {
|
||||
toValue: visible ? 1 : 0,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [opacity, visible]);
|
||||
|
||||
// @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,
|
||||
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;
|
||||
};
|
||||
|
||||
const DEFAULT_TABBAR_HEIGHT = 50;
|
||||
const DEFAULT_TABBAR_HEIGHT = 49;
|
||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
@@ -152,6 +152,8 @@ export default function BottomTabBar({
|
||||
left: safeAreaInsets?.left ?? defaultInsets.left,
|
||||
};
|
||||
|
||||
const paddingBottom = Math.max(insets.bottom - 4, 0);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
@@ -165,7 +167,7 @@ export default function BottomTabBar({
|
||||
{
|
||||
translateY: visible.interpolate({
|
||||
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,
|
||||
},
|
||||
{
|
||||
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
|
||||
paddingBottom: insets.bottom,
|
||||
height: DEFAULT_TABBAR_HEIGHT + paddingBottom,
|
||||
paddingBottom,
|
||||
paddingHorizontal: Math.max(insets.left, insets.right),
|
||||
},
|
||||
style,
|
||||
@@ -245,6 +247,7 @@ export default function BottomTabBar({
|
||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||
button={options.tabBarButton}
|
||||
icon={options.tabBarIcon}
|
||||
badge={options.tabBarBadge}
|
||||
label={label}
|
||||
showLabel={showLabel}
|
||||
labelStyle={labelStyle}
|
||||
|
||||
@@ -39,6 +39,10 @@ type Props = {
|
||||
size: number;
|
||||
color: string;
|
||||
}) => React.ReactNode;
|
||||
/**
|
||||
* Text to show in a badge on the tab icon.
|
||||
*/
|
||||
badge?: number | string;
|
||||
/**
|
||||
* URL to use for the link to the tab.
|
||||
*/
|
||||
@@ -113,6 +117,7 @@ export default function BottomTabBarItem({
|
||||
route,
|
||||
label,
|
||||
icon,
|
||||
badge,
|
||||
to,
|
||||
button = ({
|
||||
children,
|
||||
@@ -220,16 +225,14 @@ export default function BottomTabBarItem({
|
||||
return (
|
||||
<TabBarIcon
|
||||
route={route}
|
||||
size={horizontal ? 17 : 24}
|
||||
horizontal={horizontal}
|
||||
badge={badge}
|
||||
activeOpacity={activeOpacity}
|
||||
inactiveOpacity={inactiveOpacity}
|
||||
activeTintColor={activeTintColor}
|
||||
inactiveTintColor={inactiveTintColor}
|
||||
renderIcon={icon}
|
||||
style={[
|
||||
horizontal ? styles.iconHorizontal : styles.iconVertical,
|
||||
iconStyle,
|
||||
]}
|
||||
style={iconStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -276,23 +279,17 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconVertical: {
|
||||
flex: 1,
|
||||
},
|
||||
iconHorizontal: {
|
||||
height: '100%',
|
||||
},
|
||||
label: {
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
labelBeneath: {
|
||||
fontSize: 11,
|
||||
marginBottom: 1.5,
|
||||
fontSize: 10,
|
||||
},
|
||||
labelBeside: {
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
marginLeft: 20,
|
||||
marginTop: 3,
|
||||
},
|
||||
button: {
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import type { Route } from '@react-navigation/native';
|
||||
import Badge from './Badge';
|
||||
|
||||
type Props = {
|
||||
route: Route<string>;
|
||||
size: number;
|
||||
horizontal: boolean;
|
||||
badge?: string | number;
|
||||
activeOpacity: number;
|
||||
inactiveOpacity: number;
|
||||
activeTintColor: string;
|
||||
@@ -18,18 +20,23 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function TabBarIcon({
|
||||
horizontal,
|
||||
badge,
|
||||
activeOpacity,
|
||||
inactiveOpacity,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
renderIcon,
|
||||
size,
|
||||
style,
|
||||
}: Props) {
|
||||
const size = 25;
|
||||
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
<View style={style}>
|
||||
<View
|
||||
style={[horizontal ? styles.iconHorizontal : styles.iconVertical, style]}
|
||||
>
|
||||
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
||||
{renderIcon({
|
||||
focused: true,
|
||||
@@ -44,6 +51,16 @@ export default function TabBarIcon({
|
||||
color: inactiveTintColor,
|
||||
})}
|
||||
</View>
|
||||
<Badge
|
||||
visible={badge != null}
|
||||
style={[
|
||||
styles.badge,
|
||||
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
|
||||
]}
|
||||
size={(size * 3) / 4}
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -62,4 +79,21 @@ const styles = StyleSheet.create({
|
||||
// Workaround for react-native >= 0.54 layout bug
|
||||
minWidth: 25,
|
||||
},
|
||||
iconVertical: {
|
||||
flex: 1,
|
||||
},
|
||||
iconHorizontal: {
|
||||
height: '100%',
|
||||
marginTop: 3,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
left: 3,
|
||||
},
|
||||
badgeVertical: {
|
||||
top: 3,
|
||||
},
|
||||
badgeHorizontal: {
|
||||
top: 7,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.1.27",
|
||||
"version": "5.2.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/react": "^16.9.36",
|
||||
"react": "~16.9.0",
|
||||
"typescript": "^3.9.5"
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import ScreenPropsContext from './ScreenPropsContext';
|
||||
import useCompatNavigation from './useCompatNavigation';
|
||||
|
||||
type Props<ParamList extends ParamListBase> = {
|
||||
navigation: NavigationProp<ParamList>;
|
||||
route: RouteProp<ParamList, string>;
|
||||
component: React.ComponentType<any>;
|
||||
type Props = {
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
};
|
||||
|
||||
function ScreenComponent<ParamList extends ParamListBase>(
|
||||
props: Props<ParamList>
|
||||
) {
|
||||
function CompatScreen({ getComponent }: Props) {
|
||||
const navigation = useCompatNavigation();
|
||||
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);
|
||||
|
||||
@@ -142,13 +142,7 @@ export default function createCompatNavigatorFactory<
|
||||
initialParams={{ ...parentRouteParams, ...initialParams }}
|
||||
options={screenOptions}
|
||||
>
|
||||
{({ navigation, route }) => (
|
||||
<CompatScreen
|
||||
navigation={navigation}
|
||||
route={route}
|
||||
component={getScreenComponent()}
|
||||
/>
|
||||
)}
|
||||
{() => <CompatScreen getComponent={getScreenComponent} />}
|
||||
</Pair.Screen>
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.11.0",
|
||||
"version": "5.12.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.4.8",
|
||||
"@react-navigation/routers": "^5.4.9",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"nanoid": "^3.1.9",
|
||||
"query-string": "^6.13.1",
|
||||
|
||||
@@ -12,12 +12,12 @@ import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import NavigationStateContext from './NavigationStateContext';
|
||||
import UnhandledActionContext from './UnhandledActionContext';
|
||||
import { ScheduleUpdateContext } from './useScheduleUpdate';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useChildListeners from './useChildListeners';
|
||||
import useKeyedChildListeners from './useKeyedChildListeners';
|
||||
import useOptionsGetters from './useOptionsGetters';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useSyncState from './useSyncState';
|
||||
import isSerializable from './isSerializable';
|
||||
import checkSerializable from './checkSerializable';
|
||||
import type {
|
||||
NavigationContainerEventMap,
|
||||
NavigationContainerRef,
|
||||
@@ -29,22 +29,26 @@ type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
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.";
|
||||
|
||||
let hasWarnedForSerialization = false;
|
||||
const serializableWarnings: string[] = [];
|
||||
|
||||
/**
|
||||
* Migration instructions for removal of devtools from core
|
||||
*/
|
||||
Object.defineProperty(
|
||||
global,
|
||||
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
|
||||
{
|
||||
set(_) {
|
||||
console.warn(
|
||||
"Redux devtools extension integration can be enabled with the '@react-navigation/devtools' package. For more details, see https://reactnavigation.org/docs/devtools"
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
try {
|
||||
/**
|
||||
* Migration instructions for removal of devtools from core
|
||||
*/
|
||||
Object.defineProperty(
|
||||
global,
|
||||
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
|
||||
{
|
||||
set(_) {
|
||||
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.
|
||||
@@ -123,29 +127,26 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
navigatorKeyRef.current = key;
|
||||
}, []);
|
||||
|
||||
const {
|
||||
listeners,
|
||||
addListener: addFocusedListener,
|
||||
} = useFocusedListeners();
|
||||
const { listeners, addListener } = useChildListeners();
|
||||
|
||||
const { getStateForRoute, addStateGetter } = useStateGetters();
|
||||
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
||||
|
||||
const dispatch = (
|
||||
action: NavigationAction | ((state: NavigationState) => NavigationAction)
|
||||
) => {
|
||||
if (listeners[0] == null) {
|
||||
if (listeners.focus[0] == null) {
|
||||
throw new Error(NOT_INITIALIZED_ERROR);
|
||||
}
|
||||
|
||||
listeners[0]((navigation) => navigation.dispatch(action));
|
||||
listeners.focus[0]((navigation) => navigation.dispatch(action));
|
||||
};
|
||||
|
||||
const canGoBack = () => {
|
||||
if (listeners[0] == null) {
|
||||
if (listeners.focus[0] == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { result, handled } = listeners[0]((navigation) =>
|
||||
const { result, handled } = listeners.focus[0]((navigation) =>
|
||||
navigation.canGoBack()
|
||||
);
|
||||
|
||||
@@ -164,8 +165,8 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
);
|
||||
|
||||
const getRootState = React.useCallback(() => {
|
||||
return getStateForRoute('root');
|
||||
}, [getStateForRoute]);
|
||||
return keyedListeners.getState.root?.();
|
||||
}, [keyedListeners.getState]);
|
||||
|
||||
const getCurrentRoute = React.useCallback(() => {
|
||||
let state = getRootState();
|
||||
@@ -213,8 +214,16 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
[emitter]
|
||||
);
|
||||
|
||||
const lastEmittedOptionsRef = React.useRef<object | undefined>();
|
||||
|
||||
const onOptionsChange = React.useCallback(
|
||||
(options) => {
|
||||
if (lastEmittedOptionsRef.current === options) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastEmittedOptionsRef.current = options;
|
||||
|
||||
emitter.emit({
|
||||
type: 'options',
|
||||
data: { options },
|
||||
@@ -225,12 +234,12 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
|
||||
const builderContext = React.useMemo(
|
||||
() => ({
|
||||
addFocusedListener,
|
||||
addStateGetter,
|
||||
addListener,
|
||||
addKeyedListener,
|
||||
onDispatchAction,
|
||||
onOptionsChange,
|
||||
}),
|
||||
[addFocusedListener, addStateGetter, onDispatchAction, onOptionsChange]
|
||||
[addListener, addKeyedListener, onDispatchAction, onOptionsChange]
|
||||
);
|
||||
|
||||
const scheduleContext = React.useMemo(
|
||||
@@ -258,16 +267,55 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (
|
||||
state !== undefined &&
|
||||
!isSerializable(state) &&
|
||||
!hasWarnedForSerialization
|
||||
) {
|
||||
hasWarnedForSerialization = true;
|
||||
if (state !== undefined) {
|
||||
const result = checkSerializable(state);
|
||||
|
||||
console.warn(
|
||||
"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."
|
||||
);
|
||||
if (!result.serializable) {
|
||||
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';
|
||||
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 = (
|
||||
action: NavigationAction,
|
||||
visitedNavigators?: Set<string>
|
||||
@@ -19,7 +40,9 @@ export type FocusedNavigationListener = <T>(
|
||||
callback: FocusedNavigationCallback<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.
|
||||
@@ -29,11 +52,10 @@ const NavigationBuilderContext = React.createContext<{
|
||||
action: NavigationAction,
|
||||
visitedNavigators?: Set<string>
|
||||
) => boolean;
|
||||
addActionListener?: (listener: ChildActionListener) => void;
|
||||
addFocusedListener?: (listener: FocusedNavigationListener) => void;
|
||||
addListener?: AddListener;
|
||||
addKeyedListener?: AddKeyedListener;
|
||||
onRouteFocus?: (key: string) => void;
|
||||
onDispatchAction: (action: NavigationAction, noop: boolean) => void;
|
||||
addStateGetter?: (key: string, getter: NavigatorStateGetter) => void;
|
||||
onOptionsChange: (options: object) => void;
|
||||
}>({
|
||||
onDispatchAction: () => undefined,
|
||||
|
||||
@@ -9,8 +9,6 @@ import NavigationStateContext from './NavigationStateContext';
|
||||
import StaticContainer from './StaticContainer';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import useOptionsGetters from './useOptionsGetters';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import useFocusEffect from './useFocusEffect';
|
||||
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
|
||||
|
||||
type Props<
|
||||
@@ -45,26 +43,14 @@ export default function SceneView<
|
||||
options,
|
||||
}: Props<State, ScreenOptions, EventMap>) {
|
||||
const navigatorKeyRef = React.useRef<string | undefined>();
|
||||
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
|
||||
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,
|
||||
getState,
|
||||
getOptions,
|
||||
options,
|
||||
navigation,
|
||||
});
|
||||
|
||||
const optionsChange = React.useCallback(() => {
|
||||
optionsRef.current = options;
|
||||
if (!hasAnyChildListener) {
|
||||
onOptionsChange(options);
|
||||
}
|
||||
}, [onOptionsChange, options, hasAnyChildListener]);
|
||||
|
||||
useFocusEffect(optionsChange);
|
||||
|
||||
const setKey = React.useCallback((key: string) => {
|
||||
navigatorKeyRef.current = key;
|
||||
}, []);
|
||||
@@ -109,19 +95,22 @@ export default function SceneView<
|
||||
]
|
||||
);
|
||||
|
||||
const ScreenComponent = screen.getComponent
|
||||
? screen.getComponent()
|
||||
: screen.component;
|
||||
|
||||
return (
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<EnsureSingleNavigator>
|
||||
<StaticContainer
|
||||
name={screen.name}
|
||||
// @ts-expect-error: these properties exist on screen, but TS is confused
|
||||
render={screen.component || screen.children}
|
||||
render={ScreenComponent || screen.children}
|
||||
navigation={navigation}
|
||||
route={route}
|
||||
>
|
||||
{'component' in screen && screen.component !== undefined ? (
|
||||
<screen.component navigation={navigation} route={route} />
|
||||
) : 'children' in screen && screen.children !== undefined ? (
|
||||
{ScreenComponent !== undefined ? (
|
||||
<ScreenComponent navigation={navigation} route={route} />
|
||||
) : screen.children !== undefined ? (
|
||||
screen.children({ navigation, route })
|
||||
) : null}
|
||||
</StaticContainer>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { act, render } from 'react-native-testing-library';
|
||||
import type {
|
||||
import {
|
||||
DefaultRouterOptions,
|
||||
NavigationState,
|
||||
Router,
|
||||
StackRouter,
|
||||
TabRouter,
|
||||
} from '@react-navigation/routers';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
import NavigationStateContext from '../NavigationStateContext';
|
||||
@@ -430,9 +432,9 @@ it('emits state events when the state changes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('emits state events when options change', () => {
|
||||
it('emits option events when options change with tab router', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
const { state, descriptors } = useNavigationBuilder(TabRouter, props);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -455,7 +457,10 @@ it('emits state events when options change', () => {
|
||||
<Screen name="baz" options={{ v: 3 }}>
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="foo" options={{ g: 5 }}>
|
||||
<Screen name="qux" options={{ g: 5 }}>
|
||||
{() => null}
|
||||
</Screen>
|
||||
<Screen name="quxx" options={{ h: 9 }}>
|
||||
{() => null}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
@@ -474,19 +479,105 @@ it('emits state events when options change', () => {
|
||||
ref.current?.navigate('bar');
|
||||
});
|
||||
|
||||
expect(listener.mock.calls[0][0].data.options).toEqual({
|
||||
y: 2,
|
||||
});
|
||||
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('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', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import isSerializable from '../isSerializable';
|
||||
import checkSerializable from '../checkSerializable';
|
||||
|
||||
it('returns true for serializable object', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
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', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
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', () => {
|
||||
@@ -59,7 +90,11 @@ it('returns false for circular references', () => {
|
||||
x.b.b2 = x;
|
||||
x.c = x.b;
|
||||
|
||||
expect(isSerializable(x)).toBe(false);
|
||||
expect(checkSerializable(x)).toEqual({
|
||||
serializable: false,
|
||||
location: ['b', 'b2'],
|
||||
reason: 'Circular reference',
|
||||
});
|
||||
|
||||
const y: any = [
|
||||
{
|
||||
@@ -72,7 +107,11 @@ it('returns false for circular references', () => {
|
||||
y[0].children[0].parent = y[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 = {
|
||||
name: 'sun',
|
||||
@@ -81,14 +120,18 @@ it('returns false for circular references', () => {
|
||||
|
||||
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", () => {
|
||||
const o = { foo: 'bar' };
|
||||
|
||||
expect(
|
||||
isSerializable({
|
||||
checkSerializable({
|
||||
baz: 'bax',
|
||||
first: o,
|
||||
second: o,
|
||||
@@ -96,5 +139,5 @@ it("doesn't fail if same object used multiple times", () => {
|
||||
b: o,
|
||||
},
|
||||
})
|
||||
).toBe(true);
|
||||
).toEqual({ serializable: true });
|
||||
});
|
||||
@@ -1403,6 +1403,7 @@ it('throws if both children and component are passed', () => {
|
||||
const element = (
|
||||
<BaseNavigationContainer>
|
||||
<TestNavigator>
|
||||
{/* @ts-ignore */}
|
||||
<Screen name="foo" component={jest.fn()}>
|
||||
{jest.fn()}
|
||||
</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', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
@@ -1430,7 +1473,7 @@ it('throws descriptive error for undefined screen component', () => {
|
||||
);
|
||||
|
||||
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', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-native-testing-library';
|
||||
import type {
|
||||
import { act, render } from 'react-native-testing-library';
|
||||
import {
|
||||
Router,
|
||||
DefaultRouterOptions,
|
||||
NavigationState,
|
||||
StackRouter,
|
||||
} from '@react-navigation/routers';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import BaseNavigationContainer from '../BaseNavigationContainer';
|
||||
@@ -12,8 +13,19 @@ import MockRouter, {
|
||||
MockActions,
|
||||
MockRouterKey,
|
||||
} 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", () => {
|
||||
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", () => {
|
||||
const CurrentParentRouter = MockRouter;
|
||||
|
||||
@@ -379,3 +532,649 @@ it('logs error if no navigator handled the action', () => {
|
||||
|
||||
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) {
|
||||
// @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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
blur: { data: undefined };
|
||||
state: { data: { state: State } };
|
||||
beforeRemove: { data: { action: NavigationAction }; canPreventDefault: true };
|
||||
};
|
||||
|
||||
export type EventArg<
|
||||
@@ -61,7 +62,9 @@ export type EventArg<
|
||||
preventDefault(): void;
|
||||
}
|
||||
: {}) &
|
||||
(undefined extends Data ? {} : { readonly data: Data });
|
||||
(undefined extends Data
|
||||
? { readonly data?: Readonly<Data> }
|
||||
: { readonly data: Readonly<Data> });
|
||||
|
||||
export type EventListenerCallback<
|
||||
EventMap extends EventMapBase,
|
||||
@@ -108,7 +111,7 @@ export type EventEmitter<EventMap extends EventMapBase> = {
|
||||
? { canPreventDefault: true }
|
||||
: {}) &
|
||||
(undefined extends EventMap[EventName]['data']
|
||||
? {}
|
||||
? { data?: EventMap[EventName]['data'] }
|
||||
: { data: EventMap[EventName]['data'] })
|
||||
): EventArg<
|
||||
EventName,
|
||||
@@ -276,13 +279,18 @@ export type RouteProp<
|
||||
RouteName extends keyof ParamList
|
||||
> = Omit<Route<Extract<RouteName, string>>, 'params'> &
|
||||
(undefined extends ParamList[RouteName]
|
||||
? {}
|
||||
: {
|
||||
? Readonly<{
|
||||
/**
|
||||
* Params for this route
|
||||
*/
|
||||
params: ParamList[RouteName];
|
||||
});
|
||||
params?: Readonly<ParamList[RouteName]>;
|
||||
}>
|
||||
: Readonly<{
|
||||
/**
|
||||
* Params for this route
|
||||
*/
|
||||
params: Readonly<ParamList[RouteName]>;
|
||||
}>);
|
||||
|
||||
export type CompositeNavigationProp<
|
||||
A extends NavigationProp<ParamListBase, string, any, any>,
|
||||
@@ -398,6 +406,16 @@ export type RouteConfig<
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<any>;
|
||||
getComponent?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Lazily get a React component to render for this screen.
|
||||
*/
|
||||
getComponent: () => React.ComponentType<any>;
|
||||
component?: never;
|
||||
children?: never;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
@@ -407,6 +425,8 @@ export type RouteConfig<
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}) => 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';
|
||||
import SceneView from './SceneView';
|
||||
import NavigationBuilderContext, {
|
||||
ChildActionListener,
|
||||
FocusedNavigationListener,
|
||||
NavigatorStateGetter,
|
||||
AddListener,
|
||||
AddKeyedListener,
|
||||
} from './NavigationBuilderContext';
|
||||
import type { NavigationEventEmitter } from './useEventEmitter';
|
||||
import useNavigationCache from './useNavigationCache';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import type {
|
||||
Descriptor,
|
||||
NavigationHelpers,
|
||||
@@ -20,8 +21,6 @@ import type {
|
||||
RouteProp,
|
||||
EventMapBase,
|
||||
} from './types';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
|
||||
type Options<
|
||||
State extends NavigationState,
|
||||
@@ -46,9 +45,8 @@ type Options<
|
||||
) => boolean;
|
||||
getState: () => State;
|
||||
setState: (state: State) => void;
|
||||
addActionListener: (listener: ChildActionListener) => void;
|
||||
addFocusedListener: (listener: FocusedNavigationListener) => void;
|
||||
addStateGetter: (key: string, getter: NavigatorStateGetter) => void;
|
||||
addListener: AddListener;
|
||||
addKeyedListener: AddKeyedListener;
|
||||
onRouteFocus: (key: string) => void;
|
||||
router: Router<State, NavigationAction>;
|
||||
emitter: NavigationEventEmitter<any>;
|
||||
@@ -74,9 +72,8 @@ export default function useDescriptors<
|
||||
onAction,
|
||||
getState,
|
||||
setState,
|
||||
addActionListener,
|
||||
addFocusedListener,
|
||||
addStateGetter,
|
||||
addListener,
|
||||
addKeyedListener,
|
||||
onRouteFocus,
|
||||
router,
|
||||
emitter,
|
||||
@@ -90,21 +87,19 @@ export default function useDescriptors<
|
||||
() => ({
|
||||
navigation,
|
||||
onAction,
|
||||
addActionListener,
|
||||
addFocusedListener,
|
||||
addStateGetter,
|
||||
addListener,
|
||||
addKeyedListener,
|
||||
onRouteFocus,
|
||||
onDispatchAction,
|
||||
onOptionsChange,
|
||||
}),
|
||||
[
|
||||
addActionListener,
|
||||
addFocusedListener,
|
||||
addStateGetter,
|
||||
navigation,
|
||||
onAction,
|
||||
onDispatchAction,
|
||||
addListener,
|
||||
addKeyedListener,
|
||||
onRouteFocus,
|
||||
onDispatchAction,
|
||||
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,
|
||||
focusedListeners,
|
||||
}: Options) {
|
||||
const { addFocusedListener } = React.useContext(NavigationBuilderContext);
|
||||
const { addListener } = React.useContext(NavigationBuilderContext);
|
||||
|
||||
const listener = React.useCallback(
|
||||
(callback: FocusedNavigationCallback<any>) => {
|
||||
@@ -39,8 +39,8 @@ export default function useFocusedListenersChildrenAdapter({
|
||||
[focusedListeners, navigation]
|
||||
);
|
||||
|
||||
React.useEffect(() => addFocusedListener?.(listener), [
|
||||
addFocusedListener,
|
||||
React.useEffect(() => addListener?.('focus', listener), [
|
||||
addListener,
|
||||
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 useFocusEvents from './useFocusEvents';
|
||||
import useOnRouteFocus from './useOnRouteFocus';
|
||||
import useChildActionListeners from './useChildActionListeners';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useChildListeners from './useChildListeners';
|
||||
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
|
||||
import {
|
||||
DefaultNavigatorOptions,
|
||||
@@ -31,7 +30,7 @@ import {
|
||||
EventMapBase,
|
||||
EventMapCore,
|
||||
} from './types';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import useKeyedChildListeners from './useKeyedChildListeners';
|
||||
import useOnGetState from './useOnGetState';
|
||||
import useScheduleUpdate from './useScheduleUpdate';
|
||||
import useCurrentRender from './useCurrentRender';
|
||||
@@ -103,7 +102,7 @@ const getRouteConfigsFromChildren = <
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configs.forEach((config) => {
|
||||
const { name, children, component } = config as any;
|
||||
const { name, children, component, getComponent } = config;
|
||||
|
||||
if (typeof name !== 'string' || !name) {
|
||||
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) {
|
||||
throw new Error(
|
||||
`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') {
|
||||
throw new Error(
|
||||
`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') {
|
||||
// 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
|
||||
@@ -142,7 +163,7 @@ const getRouteConfigsFromChildren = <
|
||||
}
|
||||
} else {
|
||||
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.`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -430,28 +451,22 @@ export default function useNavigationBuilder<
|
||||
emitter.emit({ type: 'state', data: { state } });
|
||||
}, [emitter, state]);
|
||||
|
||||
const {
|
||||
listeners: actionListeners,
|
||||
addListener: addActionListener,
|
||||
} = useChildActionListeners();
|
||||
const { listeners: childListeners, addListener } = useChildListeners();
|
||||
|
||||
const {
|
||||
listeners: focusedListeners,
|
||||
addListener: addFocusedListener,
|
||||
} = useFocusedListeners();
|
||||
|
||||
const { getStateForRoute, addStateGetter } = useStateGetters();
|
||||
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
|
||||
|
||||
const onAction = useOnAction({
|
||||
router,
|
||||
getState,
|
||||
setState,
|
||||
key: route?.key,
|
||||
listeners: actionListeners,
|
||||
actionListeners: childListeners.action,
|
||||
beforeRemoveListeners: keyedListeners.beforeRemove,
|
||||
routerConfigOptions: {
|
||||
routeNames,
|
||||
routeParamList,
|
||||
},
|
||||
emitter,
|
||||
});
|
||||
|
||||
const onRouteFocus = useOnRouteFocus({
|
||||
@@ -470,12 +485,12 @@ export default function useNavigationBuilder<
|
||||
|
||||
useFocusedListenersChildrenAdapter({
|
||||
navigation,
|
||||
focusedListeners,
|
||||
focusedListeners: childListeners.focus,
|
||||
});
|
||||
|
||||
useOnGetState({
|
||||
getState,
|
||||
getStateForRoute,
|
||||
getStateListeners: keyedListeners.getState,
|
||||
});
|
||||
|
||||
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
|
||||
@@ -487,9 +502,8 @@ export default function useNavigationBuilder<
|
||||
getState,
|
||||
setState,
|
||||
onRouteFocus,
|
||||
addActionListener,
|
||||
addFocusedListener,
|
||||
addStateGetter,
|
||||
addListener,
|
||||
addKeyedListener,
|
||||
router,
|
||||
emitter,
|
||||
});
|
||||
|
||||
@@ -8,15 +8,21 @@ import type {
|
||||
} from '@react-navigation/routers';
|
||||
import NavigationBuilderContext, {
|
||||
ChildActionListener,
|
||||
ChildBeforeRemoveListener,
|
||||
} from './NavigationBuilderContext';
|
||||
import useOnPreventRemove, { shouldPreventRemove } from './useOnPreventRemove';
|
||||
import type { NavigationEventEmitter } from './useEventEmitter';
|
||||
import type { EventMapCore } from './types';
|
||||
|
||||
type Options = {
|
||||
router: Router<NavigationState, NavigationAction>;
|
||||
key?: string;
|
||||
getState: () => NavigationState;
|
||||
setState: (state: NavigationState | PartialState<NavigationState>) => void;
|
||||
listeners: ChildActionListener[];
|
||||
actionListeners: ChildActionListener[];
|
||||
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
|
||||
routerConfigOptions: RouterConfigOptions;
|
||||
emitter: NavigationEventEmitter<EventMapCore<any>>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -33,13 +39,15 @@ export default function useOnAction({
|
||||
getState,
|
||||
setState,
|
||||
key,
|
||||
listeners,
|
||||
actionListeners,
|
||||
beforeRemoveListeners,
|
||||
routerConfigOptions,
|
||||
emitter,
|
||||
}: Options) {
|
||||
const {
|
||||
onAction: onActionParent,
|
||||
onRouteFocus: onRouteFocusParent,
|
||||
addActionListener: addActionListenerParent,
|
||||
addListener: addListenerParent,
|
||||
onDispatchAction,
|
||||
} = React.useContext(NavigationBuilderContext);
|
||||
|
||||
@@ -66,38 +74,56 @@ export default function useOnAction({
|
||||
|
||||
visitedNavigators.add(state.key);
|
||||
|
||||
if (typeof action.target === 'string' && action.target !== state.key) {
|
||||
return false;
|
||||
}
|
||||
if (typeof action.target !== 'string' || action.target === state.key) {
|
||||
let result = router.getStateForAction(
|
||||
state,
|
||||
action,
|
||||
routerConfigOptionsRef.current
|
||||
);
|
||||
|
||||
let result = router.getStateForAction(
|
||||
state,
|
||||
action,
|
||||
routerConfigOptionsRef.current
|
||||
);
|
||||
// If a target is specified and set to current navigator, the action shouldn't bubble
|
||||
// So instead of `null`, we use the state object for such cases to signal that action was handled
|
||||
result =
|
||||
result === null && action.target === state.key ? state : result;
|
||||
|
||||
// If a target is specified and set to current navigator, the action shouldn't bubble
|
||||
// So instead of `null`, we use the state object for such cases to signal that action was handled
|
||||
result = result === null && action.target === state.key ? state : result;
|
||||
if (result !== null) {
|
||||
onDispatchAction(action, state === result);
|
||||
|
||||
if (result !== null) {
|
||||
onDispatchAction(action, state === result);
|
||||
if (state !== result) {
|
||||
const nextRouteKeys = (result.routes as any[]).map(
|
||||
(route: { key?: string }) => route.key
|
||||
);
|
||||
|
||||
if (state !== result) {
|
||||
setState(result);
|
||||
}
|
||||
const removedRoutes = state.routes.filter(
|
||||
(route) => !nextRouteKeys.includes(route.key)
|
||||
);
|
||||
|
||||
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);
|
||||
const isPrevented = shouldPreventRemove(
|
||||
emitter,
|
||||
beforeRemoveListeners,
|
||||
removedRoutes,
|
||||
action
|
||||
);
|
||||
|
||||
if (shouldFocus && key !== undefined) {
|
||||
onRouteFocusParent(key);
|
||||
if (isPrevented) {
|
||||
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) {
|
||||
@@ -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
|
||||
for (let i = listeners.length - 1; i >= 0; i--) {
|
||||
const listener = listeners[i];
|
||||
for (let i = actionListeners.length - 1; i >= 0; i--) {
|
||||
const listener = actionListeners[i];
|
||||
|
||||
if (listener(action, visitedNavigators)) {
|
||||
return true;
|
||||
@@ -119,19 +145,27 @@ export default function useOnAction({
|
||||
return false;
|
||||
},
|
||||
[
|
||||
actionListeners,
|
||||
beforeRemoveListeners,
|
||||
emitter,
|
||||
getState,
|
||||
router,
|
||||
key,
|
||||
onActionParent,
|
||||
onDispatchAction,
|
||||
onRouteFocusParent,
|
||||
router,
|
||||
setState,
|
||||
key,
|
||||
listeners,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(() => addActionListenerParent?.(onAction), [
|
||||
addActionListenerParent,
|
||||
useOnPreventRemove({
|
||||
getState,
|
||||
emitter,
|
||||
beforeRemoveListeners,
|
||||
});
|
||||
|
||||
React.useEffect(() => addListenerParent?.('action', onAction), [
|
||||
addListenerParent,
|
||||
onAction,
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import type { NavigationState } from '@react-navigation/routers';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import NavigationBuilderContext, {
|
||||
GetStateListener,
|
||||
} from './NavigationBuilderContext';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import isArrayEqual from './isArrayEqual';
|
||||
|
||||
export default function useOnGetState({
|
||||
getStateForRoute,
|
||||
getState,
|
||||
}: {
|
||||
getStateForRoute: (routeName: string) => NavigationState | undefined;
|
||||
type Options = {
|
||||
getState: () => NavigationState;
|
||||
}) {
|
||||
const { addStateGetter } = React.useContext(NavigationBuilderContext);
|
||||
getStateListeners: Record<string, GetStateListener | undefined>;
|
||||
};
|
||||
|
||||
export default function useOnGetState({
|
||||
getState,
|
||||
getStateListeners,
|
||||
}: Options) {
|
||||
const { addKeyedListener } = React.useContext(NavigationBuilderContext);
|
||||
const route = React.useContext(NavigationRouteContext);
|
||||
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
|
||||
const routes = state.routes.map((route) => {
|
||||
const childState = getStateForRoute(route.key);
|
||||
const childState = getStateListeners[route.key]?.();
|
||||
|
||||
if (route.state === childState) {
|
||||
return route;
|
||||
@@ -34,9 +38,9 @@ export default function useOnGetState({
|
||||
}
|
||||
|
||||
return { ...state, routes };
|
||||
}, [getState, getStateForRoute]);
|
||||
}, [getState, getStateListeners]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return addStateGetter?.(key, getRehydratedState);
|
||||
}, [addStateGetter, getRehydratedState, key]);
|
||||
return addKeyedListener?.('getState', key, getRehydratedState);
|
||||
}, [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 type { ParamListBase, NavigationState } from '@react-navigation/routers';
|
||||
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({
|
||||
key,
|
||||
getOptions,
|
||||
getState,
|
||||
}: {
|
||||
key?: string;
|
||||
getOptions?: () => object | undefined;
|
||||
getState?: () => NavigationState;
|
||||
}) {
|
||||
let [
|
||||
numberOfChildrenListeners,
|
||||
setNumberOfChildrenListeners,
|
||||
] = React.useState(0);
|
||||
const optionsGettersFromChild = React.useRef<
|
||||
options,
|
||||
navigation,
|
||||
}: Options) {
|
||||
const optionsRef = React.useRef<object | undefined>(options);
|
||||
const optionsGettersFromChildRef = React.useRef<
|
||||
Record<string, () => object | undefined | null>
|
||||
>({});
|
||||
|
||||
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
|
||||
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
|
||||
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(() => {
|
||||
for (let key in optionsGettersFromChild.current) {
|
||||
if (optionsGettersFromChild.current.hasOwnProperty(key)) {
|
||||
const result = optionsGettersFromChild.current[key]?.();
|
||||
for (let key in optionsGettersFromChildRef.current) {
|
||||
if (optionsGettersFromChildRef.current.hasOwnProperty(key)) {
|
||||
const result = optionsGettersFromChildRef.current[key]?.();
|
||||
|
||||
// null means unfocused route
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const getCurrentOptions = React.useCallback(() => {
|
||||
if (getState) {
|
||||
const state = getState();
|
||||
if (state.routes[state.index].key !== key) {
|
||||
// null means unfocused route
|
||||
return null;
|
||||
}
|
||||
const isFocused = navigation?.isFocused() ?? true;
|
||||
|
||||
if (!isFocused) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const optionsFromListener = getOptionsFromListener();
|
||||
|
||||
if (optionsFromListener !== null) {
|
||||
return optionsFromListener;
|
||||
}
|
||||
return getOptions?.() ?? undefined;
|
||||
}, [getState, getOptionsFromListener, getOptions, key]);
|
||||
|
||||
return optionsRef.current;
|
||||
}, [navigation, getOptionsFromListener]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return parentAddOptionsGetter?.(key!, getCurrentOptions);
|
||||
@@ -58,26 +78,20 @@ export default function useOptionsGetters({
|
||||
|
||||
const addOptionsGetter = React.useCallback(
|
||||
(key: string, getter: () => object | undefined | null) => {
|
||||
optionsGettersFromChild.current[key] = getter;
|
||||
setNumberOfChildrenListeners((prev) => prev + 1);
|
||||
optionsGettersFromChildRef.current[key] = getter;
|
||||
optionsChangeListener();
|
||||
|
||||
return () => {
|
||||
setNumberOfChildrenListeners((prev) => prev - 1);
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete optionsGettersFromChild.current[key];
|
||||
delete optionsGettersFromChildRef.current[key];
|
||||
optionsChangeListener();
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const hasAnyChildListener = React.useMemo(
|
||||
() => numberOfChildrenListeners > 0,
|
||||
[numberOfChildrenListeners]
|
||||
[optionsChangeListener]
|
||||
);
|
||||
|
||||
return {
|
||||
addOptionsGetter,
|
||||
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,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/devtools",
|
||||
"description": "Developer tools for React Navigation",
|
||||
"version": "5.1.0",
|
||||
"version": "5.1.2",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -36,7 +36,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.11.0",
|
||||
"@react-navigation/core": "^5.12.0",
|
||||
"deep-equal": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "5.8.3",
|
||||
"version": "5.8.5",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"del-cli": "^3.0.1",
|
||||
@@ -56,6 +56,7 @@
|
||||
"react-native-reanimated": "^1.8.0",
|
||||
"react-native-safe-area-context": "^1.0.0",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-testing-library": "^2.1.0",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"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();
|
||||
});
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "5.2.11",
|
||||
"version": "5.2.13",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
@@ -50,6 +50,7 @@
|
||||
"react": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-paper": "^3.10.1",
|
||||
"react-native-testing-library": "^2.1.0",
|
||||
"react-native-vector-icons": "^6.6.0",
|
||||
"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,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-top-tabs",
|
||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||
"version": "5.2.11",
|
||||
"version": "5.2.13",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-native": "^0.62.7",
|
||||
"del-cli": "^3.0.1",
|
||||
@@ -54,6 +54,7 @@
|
||||
"react-native-gesture-handler": "^1.6.0",
|
||||
"react-native-reanimated": "^1.8.0",
|
||||
"react-native-tab-view": "^2.14.4",
|
||||
"react-native-testing-library": "^2.1.0",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"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,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"description": "React Native integration for React Navigation",
|
||||
"version": "5.6.0",
|
||||
"version": "5.7.0",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -37,7 +37,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.11.0",
|
||||
"@react-navigation/core": "^5.12.0",
|
||||
"nanoid": "^3.1.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -9,13 +9,15 @@ import DefaultTheme from './theming/DefaultTheme';
|
||||
import LinkingContext from './LinkingContext';
|
||||
import useThenable from './useThenable';
|
||||
import useLinking from './useLinking';
|
||||
import useDocumentTitle from './useDocumentTitle';
|
||||
import useBackButton from './useBackButton';
|
||||
import type { Theme, LinkingOptions } from './types';
|
||||
import type { Theme, LinkingOptions, DocumentTitleOptions } from './types';
|
||||
|
||||
type Props = NavigationContainerProps & {
|
||||
theme?: Theme;
|
||||
linking?: LinkingOptions;
|
||||
fallback?: React.ReactNode;
|
||||
documentTitle?: DocumentTitleOptions;
|
||||
onReady?: () => void;
|
||||
};
|
||||
|
||||
@@ -29,11 +31,19 @@ type Props = NavigationContainerProps & {
|
||||
* @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.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.ref Ref object which refers to the navigation object containing helper methods.
|
||||
*/
|
||||
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>
|
||||
) {
|
||||
const isLinkingEnabled = linking ? linking.enabled !== false : false;
|
||||
@@ -41,6 +51,7 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||
const refContainer = React.useRef<NavigationContainerRef>(null);
|
||||
|
||||
useBackButton(refContainer);
|
||||
useDocumentTitle(refContainer, documentTitle);
|
||||
|
||||
const { getInitialState } = useLinking(refContainer, {
|
||||
enabled: isLinkingEnabled,
|
||||
|
||||
@@ -62,6 +62,7 @@ const removeEventListener = (type: 'popstate', listener: () => void) => {
|
||||
};
|
||||
|
||||
export default {
|
||||
document: { title: '' },
|
||||
location,
|
||||
history,
|
||||
addEventListener,
|
||||
|
||||
@@ -8,6 +8,7 @@ const DarkTheme: Theme = {
|
||||
card: 'rgb(18, 18, 18)',
|
||||
text: 'rgb(229, 229, 231)',
|
||||
border: 'rgb(39, 39, 41)',
|
||||
notification: 'rgb(255, 69, 58)',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ const DefaultTheme: Theme = {
|
||||
background: 'rgb(242, 242, 242)',
|
||||
card: 'rgb(255, 255, 255)',
|
||||
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,
|
||||
getPathFromState as getPathFromStateDefault,
|
||||
PathConfigMap,
|
||||
Route,
|
||||
} from '@react-navigation/core';
|
||||
|
||||
export type Theme = {
|
||||
@@ -12,6 +13,7 @@ export type Theme = {
|
||||
card: string;
|
||||
text: string;
|
||||
border: string;
|
||||
notification: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,6 +54,14 @@ export type LinkingOptions = {
|
||||
getPathFromState?: typeof getPathFromStateDefault;
|
||||
};
|
||||
|
||||
export type DocumentTitleOptions = {
|
||||
enabled?: boolean;
|
||||
formatter?: (
|
||||
options: Record<string, any> | undefined,
|
||||
route: Route<string> | undefined
|
||||
) => string;
|
||||
};
|
||||
|
||||
export type ServerContainerRef = {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
window.removeEventListener('popstate', done);
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/routers",
|
||||
"description": "Routers to help build custom navigators",
|
||||
"version": "5.4.8",
|
||||
"version": "5.4.9",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
|
||||
@@ -244,8 +244,9 @@ export default function TabRouter({
|
||||
routeNames.indexOf(state.routes[state.index].name)
|
||||
);
|
||||
|
||||
let history = state.history.filter((it) =>
|
||||
routes.find((r) => r.key === it.key)
|
||||
let history = state.history.filter(
|
||||
// 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) {
|
||||
|
||||
@@ -942,3 +942,68 @@ it('changes index on focus change', () => {
|
||||
|
||||
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' } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1076,3 +1076,159 @@ it('updates route key history on focus change', () => {
|
||||
{ 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 NavigationState = {
|
||||
export type NavigationState = Readonly<{
|
||||
/**
|
||||
* Unique key for the navigation state.
|
||||
*/
|
||||
@@ -35,26 +35,27 @@ export type NavigationState = {
|
||||
* Whether the navigation state has been rehydrated.
|
||||
*/
|
||||
stale: false;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type InitialState = Partial<
|
||||
Omit<NavigationState, 'stale' | 'routes'>
|
||||
> & {
|
||||
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
|
||||
};
|
||||
export type InitialState = Readonly<
|
||||
Partial<Omit<NavigationState, 'stale' | 'routes'>> & {
|
||||
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type PartialState<State extends NavigationState> = Partial<
|
||||
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
|
||||
> & {
|
||||
stale?: true;
|
||||
type?: string;
|
||||
routes: (Omit<Route<string>, 'key'> & {
|
||||
key?: string;
|
||||
state?: InitialState;
|
||||
})[];
|
||||
};
|
||||
> &
|
||||
Readonly<{
|
||||
stale?: true;
|
||||
type?: string;
|
||||
routes: (Omit<Route<string>, 'key'> & {
|
||||
key?: string;
|
||||
state?: InitialState;
|
||||
})[];
|
||||
}>;
|
||||
|
||||
export type Route<RouteName extends string> = {
|
||||
export type Route<RouteName extends string> = Readonly<{
|
||||
/**
|
||||
* Unique key for the route.
|
||||
*/
|
||||
@@ -67,11 +68,11 @@ export type Route<RouteName extends string> = {
|
||||
* Params for the route.
|
||||
*/
|
||||
params?: object;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ParamListBase = Record<string, object | undefined>;
|
||||
|
||||
export type NavigationAction = {
|
||||
export type NavigationAction = Readonly<{
|
||||
/**
|
||||
* Type of the action (e.g. `NAVIGATE`)
|
||||
*/
|
||||
@@ -88,7 +89,7 @@ export type NavigationAction = {
|
||||
* Key of the navigator which should handle this action.
|
||||
*/
|
||||
target?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ActionCreators<Action extends NavigationAction> = {
|
||||
[key: string]: (...args: any) => Action;
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/stack",
|
||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||
"version": "5.6.0",
|
||||
"version": "5.7.0",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -46,7 +46,7 @@
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.15.1",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-navigation/native": "^5.6.0",
|
||||
"@react-navigation/native": "^5.7.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-native": "^0.62.7",
|
||||
@@ -56,6 +56,7 @@
|
||||
"react-native-gesture-handler": "^1.6.0",
|
||||
"react-native-safe-area-context": "^1.0.0",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-testing-library": "^2.1.0",
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"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();
|
||||
});
|
||||
@@ -32,7 +32,6 @@ export type Props = {
|
||||
scenes: (Scene<Route<string>> | undefined)[];
|
||||
getPreviousScene: (props: {
|
||||
route: Route<string>;
|
||||
index: number;
|
||||
}) => Scene<Route<string>> | undefined;
|
||||
getFocusedRoute: () => Route<string>;
|
||||
onContentHeightChange?: (props: {
|
||||
@@ -79,10 +78,7 @@ export default function HeaderContainer({
|
||||
|
||||
const isFocused = focusedRoute.key === scene.route.key;
|
||||
const previous =
|
||||
getPreviousScene({
|
||||
route: scene.route,
|
||||
index: i,
|
||||
}) ?? parentPreviousScene;
|
||||
getPreviousScene({ route: scene.route }) ?? parentPreviousScene;
|
||||
|
||||
// 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
|
||||
|
||||
@@ -97,6 +97,7 @@ export default class Card extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
this.animate({ closing: this.props.closing });
|
||||
this.isCurrentlyMounted = true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
@@ -115,8 +116,11 @@ export default class Card extends React.Component<Props> {
|
||||
this.inverted.setValue(getInvertedMultiplier(gestureDirection));
|
||||
}
|
||||
|
||||
const toValue = this.getAnimateToValue(this.props);
|
||||
|
||||
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
|
||||
// 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() {
|
||||
this.isCurrentlyMounted = false;
|
||||
this.handleEndInteraction();
|
||||
}
|
||||
|
||||
private isCurrentlyMounted = false;
|
||||
|
||||
private isClosing = new Animated.Value(FALSE);
|
||||
|
||||
private inverted = new Animated.Value(
|
||||
@@ -148,6 +155,8 @@ export default class Card extends React.Component<Props> {
|
||||
|
||||
private pendingGestureCallback: number | undefined;
|
||||
|
||||
private lastToValue: number | undefined;
|
||||
|
||||
private animate = ({
|
||||
closing,
|
||||
velocity,
|
||||
@@ -168,6 +177,8 @@ export default class Card extends React.Component<Props> {
|
||||
closing,
|
||||
});
|
||||
|
||||
this.lastToValue = toValue;
|
||||
|
||||
const spec = closing ? transitionSpec.close : transitionSpec.open;
|
||||
|
||||
const animation =
|
||||
@@ -196,6 +207,11 @@ export default class Card extends React.Component<Props> {
|
||||
} else {
|
||||
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) {
|
||||
// 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.pendingGestureCallback = (setTimeout(
|
||||
onClose,
|
||||
32
|
||||
) as any) as number;
|
||||
this.pendingGestureCallback = (setTimeout(() => {
|
||||
onClose();
|
||||
|
||||
// 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?.();
|
||||
|
||||
@@ -32,7 +32,6 @@ type Props = TransitionPreset & {
|
||||
cardStyle?: StyleProp<ViewStyle>;
|
||||
getPreviousScene: (props: {
|
||||
route: Route<string>;
|
||||
index: number;
|
||||
}) => Scene<Route<string>> | undefined;
|
||||
getFocusedRoute: () => Route<string>;
|
||||
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
||||
@@ -162,7 +161,7 @@ function CardContainer({
|
||||
|
||||
const isParentHeaderShown = React.useContext(HeaderShownContext);
|
||||
const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false;
|
||||
const previousScene = getPreviousScene({ route: scene.route, index });
|
||||
const previousScene = getPreviousScene({ route: scene.route });
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
||||
@@ -336,31 +336,21 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
return state.routes[state.index];
|
||||
};
|
||||
|
||||
private getPreviousScene = ({
|
||||
route,
|
||||
index,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
index: number;
|
||||
}) => {
|
||||
const previousRoute = this.props.getPreviousRoute({ route });
|
||||
private getPreviousScene = ({ route }: { route: Route<string> }) => {
|
||||
const { getPreviousRoute } = this.props;
|
||||
const { scenes } = this.state;
|
||||
|
||||
let previous: Scene<Route<string>> | undefined;
|
||||
const previousRoute = getPreviousRoute({ route });
|
||||
|
||||
if (previousRoute) {
|
||||
// The previous scene will be shortly before the current scene in the array
|
||||
// So loop back from current index to avoid looping over the full array
|
||||
for (let j = index - 1; j >= 0; j--) {
|
||||
const s = this.state.scenes[j];
|
||||
const previousScene = scenes.find(
|
||||
(scene) => scene.route.key === previousRoute.key
|
||||
);
|
||||
|
||||
if (s && s.route.key === previousRoute.key) {
|
||||
previous = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return previousScene;
|
||||
}
|
||||
|
||||
return previous;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
79
yarn.lock
79
yarn.lock
@@ -422,6 +422,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127"
|
||||
integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==
|
||||
|
||||
"@babel/helper-plugin-utils@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
|
||||
integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
|
||||
|
||||
"@babel/helper-regex@^7.10.1":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96"
|
||||
@@ -897,6 +902,13 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.1"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
|
||||
integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
|
||||
"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897"
|
||||
@@ -904,7 +916,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.3"
|
||||
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator@^7.0.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
|
||||
integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
|
||||
@@ -939,7 +951,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.0"
|
||||
|
||||
"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
|
||||
"@babel/plugin-syntax-optional-chaining@^7.0.0", "@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
|
||||
integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
|
||||
@@ -1383,6 +1395,14 @@
|
||||
"@babel/helper-plugin-utils" "^7.10.1"
|
||||
"@babel/plugin-syntax-jsx" "^7.10.1"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@^7.0.0":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369"
|
||||
integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
"@babel/plugin-syntax-jsx" "^7.10.4"
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@^7.10.1":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz#22143e14388d72eb88649606bb9e46f421bc3821"
|
||||
@@ -1825,13 +1845,6 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.10.2":
|
||||
version "7.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
|
||||
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
@@ -5854,7 +5867,7 @@ babel-jest@^26.0.1:
|
||||
graceful-fs "^4.2.4"
|
||||
slash "^3.0.0"
|
||||
|
||||
babel-loader@8.1.0:
|
||||
babel-loader@8.1.0, babel-loader@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
|
||||
integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
|
||||
@@ -7575,7 +7588,7 @@ core-js@^2.2.2, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.6.5:
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
|
||||
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
|
||||
|
||||
core-js@^3.2.1, core-js@^3.6.5:
|
||||
core-js@^3.2.1:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||
@@ -13745,6 +13758,50 @@ metro-react-native-babel-preset@^0.56.0, metro-react-native-babel-preset@^0.56.4
|
||||
"@babel/template" "^7.0.0"
|
||||
react-refresh "^0.4.0"
|
||||
|
||||
metro-react-native-babel-preset@^0.59.0:
|
||||
version "0.59.0"
|
||||
resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.59.0.tgz#20e020bc6ac9849e1477de1333d303ed42aba225"
|
||||
integrity sha512-BoO6ncPfceIDReIH8pQ5tQptcGo5yRWQXJGVXfANbiKLq4tfgdZB1C1e2rMUJ6iypmeJU9dzl+EhPmIFKtgREg==
|
||||
dependencies:
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
"@babel/plugin-proposal-export-default-from" "^7.0.0"
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0"
|
||||
"@babel/plugin-proposal-object-rest-spread" "^7.0.0"
|
||||
"@babel/plugin-proposal-optional-catch-binding" "^7.0.0"
|
||||
"@babel/plugin-proposal-optional-chaining" "^7.0.0"
|
||||
"@babel/plugin-syntax-dynamic-import" "^7.0.0"
|
||||
"@babel/plugin-syntax-export-default-from" "^7.0.0"
|
||||
"@babel/plugin-syntax-flow" "^7.2.0"
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0"
|
||||
"@babel/plugin-syntax-optional-chaining" "^7.0.0"
|
||||
"@babel/plugin-transform-arrow-functions" "^7.0.0"
|
||||
"@babel/plugin-transform-block-scoping" "^7.0.0"
|
||||
"@babel/plugin-transform-classes" "^7.0.0"
|
||||
"@babel/plugin-transform-computed-properties" "^7.0.0"
|
||||
"@babel/plugin-transform-destructuring" "^7.0.0"
|
||||
"@babel/plugin-transform-exponentiation-operator" "^7.0.0"
|
||||
"@babel/plugin-transform-flow-strip-types" "^7.0.0"
|
||||
"@babel/plugin-transform-for-of" "^7.0.0"
|
||||
"@babel/plugin-transform-function-name" "^7.0.0"
|
||||
"@babel/plugin-transform-literals" "^7.0.0"
|
||||
"@babel/plugin-transform-modules-commonjs" "^7.0.0"
|
||||
"@babel/plugin-transform-object-assign" "^7.0.0"
|
||||
"@babel/plugin-transform-parameters" "^7.0.0"
|
||||
"@babel/plugin-transform-react-display-name" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
|
||||
"@babel/plugin-transform-regenerator" "^7.0.0"
|
||||
"@babel/plugin-transform-runtime" "^7.0.0"
|
||||
"@babel/plugin-transform-shorthand-properties" "^7.0.0"
|
||||
"@babel/plugin-transform-spread" "^7.0.0"
|
||||
"@babel/plugin-transform-sticky-regex" "^7.0.0"
|
||||
"@babel/plugin-transform-template-literals" "^7.0.0"
|
||||
"@babel/plugin-transform-typescript" "^7.5.0"
|
||||
"@babel/plugin-transform-unicode-regex" "^7.0.0"
|
||||
"@babel/template" "^7.0.0"
|
||||
react-refresh "^0.4.0"
|
||||
|
||||
metro-react-native-babel-transformer@^0.56.0:
|
||||
version "0.56.4"
|
||||
resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.56.4.tgz#3c6e48b605c305362ee624e45ff338656e35fc1d"
|
||||
|
||||
Reference in New Issue
Block a user