mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-14 09:32:32 +08:00
Compare commits
37 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e7cfc4ac0 | ||
|
|
5751e7f97a | ||
|
|
179e807a64 | ||
|
|
2f1f0af862 | ||
|
|
9976a888a0 | ||
|
|
16c64e7298 | ||
|
|
f1fe951cf9 | ||
|
|
14250851d1 | ||
|
|
42586462fd | ||
|
|
3dede316cc | ||
|
|
63988e0da8 | ||
|
|
67b2ecfcfc | ||
|
|
68ed8a7259 | ||
|
|
6c2acbb304 | ||
|
|
84d75b37e7 | ||
|
|
65e5147910 | ||
|
|
321fa653ad | ||
|
|
2a76dc4d3c | ||
|
|
0a982ee698 | ||
|
|
1da4a6437f | ||
|
|
f1df4a0808 | ||
|
|
14ae3738cf | ||
|
|
32a2206513 | ||
|
|
38520a97ff | ||
|
|
3bf5ddde2a | ||
|
|
43d2c456be | ||
|
|
fe82276b1f | ||
|
|
1e53821d52 | ||
|
|
23ab45aceb | ||
|
|
d9059b56d8 | ||
|
|
ad4eaff1e9 | ||
|
|
da67e134d2 | ||
|
|
ee381a4ba3 | ||
|
|
3c5b8c4992 | ||
|
|
a912323c1d | ||
|
|
805e5e8636 | ||
|
|
65a5dac2bf |
@@ -16,14 +16,15 @@
|
||||
"color": "^3.1.2",
|
||||
"expo": "^36.0.2",
|
||||
"expo-asset": "~8.0.0",
|
||||
"expo-blur": "^8.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-dom": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-gesture-handler": "~1.5.3",
|
||||
"react-native-paper": "^3.4.0",
|
||||
"react-native-paper": "^3.5.0",
|
||||
"react-native-reanimated": "^1.4.0",
|
||||
"react-native-safe-area-context": "^0.6.2",
|
||||
"react-native-screens": "^2.0.0-alpha.22",
|
||||
"react-native-screens": "^2.0.0-alpha.25",
|
||||
"react-native-tab-view": "2.11.0",
|
||||
"react-native-unimodules": "^0.7.0",
|
||||
"react-native-web": "^0.11.7"
|
||||
|
||||
56
example/src/Screens/DynamicTabs.tsx
Normal file
56
example/src/Screens/DynamicTabs.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Title, Button } from 'react-native-paper';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
|
||||
type BottomTabParams = {
|
||||
[key: string]: undefined;
|
||||
};
|
||||
|
||||
const BottomTabs = createBottomTabNavigator<BottomTabParams>();
|
||||
|
||||
export default function BottomTabsScreen() {
|
||||
const [tabs, setTabs] = React.useState([0, 1]);
|
||||
|
||||
return (
|
||||
<BottomTabs.Navigator>
|
||||
{tabs.map(i => (
|
||||
<BottomTabs.Screen
|
||||
key={i}
|
||||
name={`tab-${i}`}
|
||||
options={{
|
||||
title: `Tab ${i}`,
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Feather name="octagon" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<View style={styles.container}>
|
||||
<Title>Tab {i}</Title>
|
||||
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}>
|
||||
Add a tab
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() =>
|
||||
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs))
|
||||
}
|
||||
>
|
||||
Remove a tab
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</BottomTabs.Screen>
|
||||
))}
|
||||
</BottomTabs.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ const ArticleScreen = ({
|
||||
route: RouteProp<ModalStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -42,14 +42,14 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} />
|
||||
</React.Fragment>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -66,8 +66,8 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const AlbumsScreen = ({
|
||||
}: {
|
||||
navigation: NativeStackNavigation;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -154,8 +154,8 @@ const AlbumsScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const NativeStack = createNativeStackNavigator<NativeStackParams>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
} 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;
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
@@ -24,14 +26,42 @@ const ArticleScreen = ({
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Album')}
|
||||
onPress={() => navigation.replace('NewsFeed')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
Replace with feed
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.navigate('Album')}
|
||||
style={styles.button}
|
||||
>
|
||||
Navigate to album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
@@ -41,8 +71,8 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} />
|
||||
</React.Fragment>
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +82,7 @@ const AlbumsScreen = ({
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -63,14 +93,14 @@ const AlbumsScreen = ({
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
onPress={() => navigation.pop(2)}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
Pop by 2
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,6 +125,11 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
|
||||
158
example/src/Screens/StackHeaderCustomization.tsx
Normal file
158
example/src/Screens/StackHeaderCustomization.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
|
||||
import { Button, Appbar } from 'react-native-paper';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
HeaderBackground,
|
||||
useHeaderHeight,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Album')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
const headerHeight = useHeaderHeight();
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ paddingTop: headerHeight }}>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const SimpleStack = createStackNavigator<SimpleStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SimpleStack.Navigator {...rest}>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params?.author}`,
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: { backgroundColor: '#ff005d' },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleAlign: 'center',
|
||||
headerBackImage: ({ tintColor }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-left-circle-outline"
|
||||
color={tintColor}
|
||||
size={24}
|
||||
style={{ marginHorizontal: Platform.OS === 'ios' ? 8 : 0 }}
|
||||
/>
|
||||
),
|
||||
headerRight: ({ tintColor }) => (
|
||||
<Appbar.Action
|
||||
color={tintColor}
|
||||
icon="dots-horizontal-circle-outline"
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
'Never gonna give you up!',
|
||||
'Never gonna let you down! Never gonna run around and desert you!'
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
title: 'Album',
|
||||
headerBackTitle: 'Back',
|
||||
headerTransparent: true,
|
||||
headerBackground: () => (
|
||||
<HeaderBackground style={{ backgroundColor: 'transparent' }}>
|
||||
<BlurView
|
||||
tint="light"
|
||||
intensity={75}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</HeaderBackground>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,13 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native';
|
||||
import {
|
||||
Image,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop } from '@react-navigation/native';
|
||||
|
||||
const COVERS = [
|
||||
@@ -15,7 +21,7 @@ const COVERS = [
|
||||
require('../../assets/album-art-8.jpg'),
|
||||
];
|
||||
|
||||
export default function Albums() {
|
||||
export default function Albums(props: Partial<ScrollViewProps>) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
@@ -25,6 +31,7 @@ export default function Albums() {
|
||||
ref={ref}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.content}
|
||||
{...props}
|
||||
>
|
||||
{COVERS.map((source, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { View, Text, Image, ScrollView, StyleSheet } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
|
||||
type Props = {
|
||||
type Props = Partial<ScrollViewProps> & {
|
||||
date?: string;
|
||||
author?: {
|
||||
name: string;
|
||||
@@ -14,6 +21,7 @@ export default function Article({
|
||||
author = {
|
||||
name: 'Knowledge Bot',
|
||||
},
|
||||
...rest
|
||||
}: Props) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
@@ -26,6 +34,7 @@ export default function Article({
|
||||
ref={ref}
|
||||
style={{ backgroundColor: colors.card }}
|
||||
contentContainerStyle={styles.content}
|
||||
{...rest}
|
||||
>
|
||||
<View style={styles.author}>
|
||||
<Image
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TextInput,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
import Color from 'color';
|
||||
@@ -17,7 +18,7 @@ const MESSAGES = [
|
||||
'make me a sandwich',
|
||||
];
|
||||
|
||||
export default function Chat() {
|
||||
export default function Chat(props: Partial<ScrollViewProps>) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
@@ -29,6 +30,7 @@ export default function Chat() {
|
||||
<ScrollView
|
||||
style={styles.inverted}
|
||||
contentContainerStyle={styles.content}
|
||||
{...props}
|
||||
>
|
||||
{MESSAGES.map((text, i) => {
|
||||
const odd = i % 2;
|
||||
|
||||
146
example/src/Shared/NewsFeed.tsx
Normal file
146
example/src/Shared/NewsFeed.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Avatar,
|
||||
Subheading,
|
||||
IconButton,
|
||||
Divider,
|
||||
} from 'react-native-paper';
|
||||
import Color from 'color';
|
||||
|
||||
type Props = Partial<ScrollViewProps>;
|
||||
|
||||
const Author = () => {
|
||||
return (
|
||||
<View style={[styles.row, styles.attribution]}>
|
||||
<Avatar.Image source={require('../../assets/avatar-1.png')} size={32} />
|
||||
<Subheading style={styles.author}>Joke bot</Subheading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<IconButton style={styles.icon} size={16} icon="heart-outline" />
|
||||
<IconButton style={styles.icon} size={16} icon="comment-outline" />
|
||||
<IconButton style={styles.icon} size={16} icon="share-outline" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function NewsFeed(props: Props) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<ScrollView ref={ref} {...props}>
|
||||
<Card style={styles.card}>
|
||||
<TextInput
|
||||
placeholder="What's on your mind?"
|
||||
placeholderTextColor={Color(colors.text)
|
||||
.alpha(0.5)
|
||||
.rgb()
|
||||
.string()}
|
||||
style={styles.input}
|
||||
/>
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
If you aren't impressed with the picture of the first Black
|
||||
Hole, you clearly don't understand the gravity of the
|
||||
situation.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
I went to the zoo and I saw a baguette in a cage. I asked the
|
||||
zookeeper about it and he said it was bread in captivity.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Image source={require('../../assets/book.jpg')} style={styles.cover} />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>Why didn't 4 ask 5 out? Because he was 2².</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
What did Master Yoda say when he first saw himself in 4k? HDMI.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
Someone broke into my house and stole 20% of my couch. Ouch!
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
padding: 16,
|
||||
backgroundColor: 'transparent',
|
||||
margin: 0,
|
||||
},
|
||||
card: {
|
||||
marginVertical: 8,
|
||||
borderRadius: 0,
|
||||
},
|
||||
cover: {
|
||||
height: 160,
|
||||
borderRadius: 0,
|
||||
},
|
||||
content: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
attribution: {
|
||||
margin: 12,
|
||||
},
|
||||
author: {
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
26
example/src/Shared/SettingsItem.tsx
Normal file
26
example/src/Shared/SettingsItem.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Subheading, Switch } from 'react-native-paper';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: () => void;
|
||||
};
|
||||
|
||||
export default function SettingsItem({ label, value, onValueChange }: Props) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
}}
|
||||
>
|
||||
<Subheading>{label}</Subheading>
|
||||
<Switch value={value} onValueChange={onValueChange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
AsyncStorage,
|
||||
YellowBox,
|
||||
Platform,
|
||||
StatusBar,
|
||||
I18nManager,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import {
|
||||
Provider as PaperProvider,
|
||||
DefaultTheme as PaperLightTheme,
|
||||
DarkTheme as PaperDarkTheme,
|
||||
Subheading,
|
||||
Appbar,
|
||||
List,
|
||||
Switch,
|
||||
Divider,
|
||||
} from 'react-native-paper';
|
||||
import { Asset } from 'expo-asset';
|
||||
@@ -42,11 +40,15 @@ import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import NativeStack from './Screens/NativeStack';
|
||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
||||
import BottomTabs from './Screens/BottomTabs';
|
||||
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||
import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import { Updates } from 'expo';
|
||||
|
||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||
|
||||
@@ -68,6 +70,10 @@ const SCREENS = {
|
||||
title: 'Modal Presentation Stack',
|
||||
component: ModalPresentationStack,
|
||||
},
|
||||
StackHeaderCustomization: {
|
||||
title: 'Header Customization in Stack',
|
||||
component: StackHeaderCustomization,
|
||||
},
|
||||
BottomTabs: { title: 'Bottom Tabs', component: BottomTabs },
|
||||
MaterialTopTabs: {
|
||||
title: 'Material Top Tabs',
|
||||
@@ -77,6 +83,10 @@ const SCREENS = {
|
||||
title: 'Material Bottom Tabs',
|
||||
component: MaterialBottomTabs,
|
||||
},
|
||||
DynamicTabs: {
|
||||
title: 'Dynamic Tabs',
|
||||
component: DynamicTabs,
|
||||
},
|
||||
AuthFlow: {
|
||||
title: 'Auth Flow',
|
||||
component: AuthFlow,
|
||||
@@ -233,27 +243,27 @@ export default function App() {
|
||||
<ScrollView
|
||||
style={{ backgroundColor: theme.colors.background }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
<SettingsItem
|
||||
label="Right to left"
|
||||
value={I18nManager.isRTL}
|
||||
onValueChange={() => {
|
||||
I18nManager.forceRTL(!I18nManager.isRTL);
|
||||
Updates.reloadFromCache();
|
||||
}}
|
||||
>
|
||||
<Subheading>Dark theme</Subheading>
|
||||
<Switch
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
/>
|
||||
<Divider />
|
||||
<SettingsItem
|
||||
label="Dark theme"
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
|
||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
name => (
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.39](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.38...@react-navigation/bottom-tabs@5.0.0-alpha.39) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use layout instead of dimensions for determining tab bar layout ([f1fe951](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/f1fe951cf9d602e1b6d4932e3c6c77bbeaaec5c0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.38](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.37...@react-navigation/bottom-tabs@5.0.0-alpha.38) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't use native driver on web ([0a982ee](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/0a982ee6984b24c0ba053a30223e255f3835e050))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.37](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.36...@react-navigation/bottom-tabs@5.0.0-alpha.37) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.36](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.35...@react-navigation/bottom-tabs@5.0.0-alpha.36) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.35](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.34...@react-navigation/bottom-tabs@5.0.0-alpha.35) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"android",
|
||||
"tab"
|
||||
],
|
||||
"version": "5.0.0-alpha.35",
|
||||
"version": "5.0.0-alpha.39",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -21,6 +21,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -29,12 +30,12 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23",
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-native": "^0.60.30",
|
||||
@@ -56,7 +57,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ export type BottomTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
/**
|
||||
* Event which fires on long press on the tab in the tab bar.
|
||||
*/
|
||||
tabLongPress: undefined;
|
||||
tabLongPress: { data: undefined };
|
||||
};
|
||||
|
||||
export type LabelPosition = 'beside-icon' | 'below-icon';
|
||||
@@ -176,14 +176,9 @@ export type BottomTabBarOptions = {
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
/**
|
||||
* Whether the label is renderd below the icon or beside the icon.
|
||||
* When a function is passed, it receives the device dimensions to render the label differently.
|
||||
* By default, in `vertical` orinetation, label is rendered below and in `horizontal` orientation, it's renderd beside.
|
||||
*/
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: {
|
||||
dimensions: { height: number; width: number };
|
||||
}) => LabelPosition);
|
||||
labelPosition?: LabelPosition;
|
||||
/**
|
||||
* Whether the label position should adapt to the orientation.
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,8 @@ type Props = BottomTabBarProps & {
|
||||
const DEFAULT_TABBAR_HEIGHT = 50;
|
||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
export default function BottomTabBar({
|
||||
state,
|
||||
navigation,
|
||||
@@ -48,7 +50,10 @@ export default function BottomTabBar({
|
||||
const { colors } = useTheme();
|
||||
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
const [layout, setLayout] = React.useState({ height: 0, width: 0 });
|
||||
const [layout, setLayout] = React.useState({
|
||||
height: 0,
|
||||
width: dimensions.width,
|
||||
});
|
||||
const [keyboardShown, setKeyboardShown] = React.useState(false);
|
||||
|
||||
const [visible] = React.useState(() => new Animated.Value(0));
|
||||
@@ -60,7 +65,7 @@ export default function BottomTabBar({
|
||||
Animated.timing(visible, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
}).start();
|
||||
}
|
||||
}, [keyboardShown, visible]);
|
||||
@@ -76,7 +81,7 @@ export default function BottomTabBar({
|
||||
Animated.timing(visible, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
}).start(({ finished }) => {
|
||||
if (finished) {
|
||||
setKeyboardShown(false);
|
||||
@@ -122,27 +127,15 @@ export default function BottomTabBar({
|
||||
};
|
||||
|
||||
const shouldUseHorizontalLabels = () => {
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
if (labelPosition) {
|
||||
let position;
|
||||
|
||||
if (typeof labelPosition === 'string') {
|
||||
position = labelPosition;
|
||||
} else {
|
||||
position = labelPosition({ dimensions });
|
||||
}
|
||||
|
||||
if (position) {
|
||||
return position === 'beside-icon';
|
||||
}
|
||||
return labelPosition === 'beside-icon';
|
||||
}
|
||||
|
||||
if (!adaptive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dimensions.width >= 768) {
|
||||
if (layout.width >= 768) {
|
||||
// Screen size matches a tablet
|
||||
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
|
||||
@@ -156,8 +149,10 @@ export default function BottomTabBar({
|
||||
}
|
||||
}
|
||||
|
||||
return routes.length * maxTabItemWidth <= dimensions.width;
|
||||
return routes.length * maxTabItemWidth <= layout.width;
|
||||
} else {
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
return isLandscape;
|
||||
}
|
||||
};
|
||||
@@ -205,6 +200,7 @@ export default function BottomTabBar({
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (!focused && !event.defaultPrevented) {
|
||||
|
||||
6
packages/bottom-tabs/tsconfig.build.json
Normal file
6
packages/bottom-tabs/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,45 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.27...@react-navigation/compat@5.0.0-alpha.28) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.26...@react-navigation/compat@5.0.0-alpha.27) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure re-render on isFirstRouteInParent change in compat layer ([14ae373](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/14ae3738cf46088e082bd1c60b9dcc6dacacd1bf))
|
||||
* improvements to the compat layer ([2a76dc4](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/2a76dc4d3c4cc0365a3afcff6ac321145efed026))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.25...@react-navigation/compat@5.0.0-alpha.26) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.24...@react-navigation/compat@5.0.0-alpha.25) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.24](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.23...@react-navigation/compat@5.0.0-alpha.24) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.0.0-alpha.24",
|
||||
"version": "5.0.0-alpha.28",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/compat",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -12,6 +12,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -20,7 +21,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23"
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
@@ -37,7 +38,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import ScreenPropsContext from './ScreenPropsContext';
|
||||
import createCompatNavigationProp from './createCompatNavigationProp';
|
||||
import useCompatNavigation from './useCompatNavigation';
|
||||
|
||||
type Props<ParamList extends ParamListBase> = {
|
||||
navigation: NavigationProp<ParamList>;
|
||||
@@ -16,12 +16,7 @@ type Props<ParamList extends ParamListBase> = {
|
||||
function ScreenComponent<ParamList extends ParamListBase>(
|
||||
props: Props<ParamList>
|
||||
) {
|
||||
const navigation = React.useMemo(
|
||||
() =>
|
||||
createCompatNavigationProp(props.navigation as any, props.route as any),
|
||||
[props.navigation, props.route]
|
||||
);
|
||||
|
||||
const navigation = useCompatNavigation();
|
||||
const screenProps = React.useContext(ScreenPropsContext);
|
||||
|
||||
return <props.component navigation={navigation} screenProps={screenProps} />;
|
||||
|
||||
@@ -19,7 +19,7 @@ export function replace({
|
||||
key?: string;
|
||||
newKey?: string;
|
||||
action?: never;
|
||||
}): CommonActions.Action {
|
||||
}): StackActionType {
|
||||
if (action !== undefined) {
|
||||
throw new Error(
|
||||
'Sub-actions are not supported for `replace`. Remove the `action` key from the options.'
|
||||
|
||||
@@ -8,11 +8,17 @@ import {
|
||||
import * as helpers from './helpers';
|
||||
import { CompatNavigationProp } from './types';
|
||||
|
||||
type EventName = 'willFocus' | 'willBlur' | 'didFocus' | 'didBlur' | 'refocus';
|
||||
type EventName =
|
||||
| 'action'
|
||||
| 'willFocus'
|
||||
| 'willBlur'
|
||||
| 'didFocus'
|
||||
| 'didBlur'
|
||||
| 'refocus';
|
||||
|
||||
const focusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
const blurSubscriptions = new WeakMap<() => void, () => void>();
|
||||
const refocusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const focusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const blurSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const refocusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
|
||||
export default function createCompatNavigationProp<
|
||||
NavigationPropType extends NavigationProp<ParamListBase>,
|
||||
@@ -28,8 +34,17 @@ export default function createCompatNavigationProp<
|
||||
state?: NavigationState | PartialState<NavigationState>;
|
||||
})
|
||||
| NavigationState
|
||||
| PartialState<NavigationState>
|
||||
| PartialState<NavigationState>,
|
||||
context: Record<string, any>,
|
||||
isFirstRouteInParent?: boolean
|
||||
): CompatNavigationProp<NavigationPropType> {
|
||||
context.parent = context.parent || {};
|
||||
context.subscriptions = context.subscriptions || {
|
||||
didFocus: new Map<() => void, () => void>(),
|
||||
didBlur: new Map<() => void, () => void>(),
|
||||
refocus: new Map<() => void, () => void>(),
|
||||
};
|
||||
|
||||
return {
|
||||
...navigation,
|
||||
...Object.entries(helpers).reduce<{
|
||||
@@ -61,7 +76,7 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('transitionEnd', listener);
|
||||
focusSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.didFocus.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'didBlur': {
|
||||
@@ -73,7 +88,7 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('transitionEnd', listener);
|
||||
blurSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.didBlur.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'refocus': {
|
||||
@@ -85,9 +100,11 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('tabPress', listener);
|
||||
refocusSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.refocus.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'action':
|
||||
throw new Error("Listening to 'action' events is not supported.");
|
||||
default:
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener(type, callback);
|
||||
@@ -100,6 +117,8 @@ export default function createCompatNavigationProp<
|
||||
return subscription;
|
||||
},
|
||||
removeListener(type: EventName, callback: () => void) {
|
||||
context.subscriptions = context.subscriptions || {};
|
||||
|
||||
switch (type) {
|
||||
case 'willFocus':
|
||||
navigation.removeListener('focus', callback);
|
||||
@@ -108,20 +127,22 @@ export default function createCompatNavigationProp<
|
||||
navigation.removeListener('blur', callback);
|
||||
break;
|
||||
case 'didFocus': {
|
||||
const unsubscribe = focusSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.didFocus.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'didBlur': {
|
||||
const unsubscribe = blurSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.didBlur.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'refocus': {
|
||||
const unsubscribe = refocusSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.refocus.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'action':
|
||||
throw new Error("Listening to 'action' events is not supported.");
|
||||
default:
|
||||
// @ts-ignore
|
||||
navigation.removeListener(type, callback);
|
||||
@@ -174,6 +195,10 @@ export default function createCompatNavigationProp<
|
||||
return defaultValue;
|
||||
},
|
||||
isFirstRouteInParent(): boolean {
|
||||
if (typeof isFirstRouteInParent === 'boolean') {
|
||||
return isFirstRouteInParent;
|
||||
}
|
||||
|
||||
const { routes } = navigation.dangerouslyGetState();
|
||||
|
||||
// @ts-ignore
|
||||
@@ -185,7 +210,8 @@ export default function createCompatNavigationProp<
|
||||
if (parent) {
|
||||
return createCompatNavigationProp(
|
||||
parent,
|
||||
navigation.dangerouslyGetState()
|
||||
navigation.dangerouslyGetState(),
|
||||
context.parent
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function createCompatNavigatorFactory<
|
||||
? {
|
||||
navigation: createCompatNavigationProp<
|
||||
NavigationPropType
|
||||
>(navigation, route),
|
||||
>(navigation, route, {}),
|
||||
navigationOptions: defaultNavigationOptions || {},
|
||||
screenProps,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useRoute,
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
useNavigationState,
|
||||
} from '@react-navigation/native';
|
||||
import createCompatNavigationProp from './createCompatNavigationProp';
|
||||
import { CompatNavigationProp } from './types';
|
||||
@@ -14,12 +15,20 @@ export default function useCompatNavigation<
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
|
||||
const isFirstRouteInParent = useNavigationState(
|
||||
state => state.routes[0].key === route.key
|
||||
);
|
||||
|
||||
const context = React.useRef<Record<string, any>>({});
|
||||
|
||||
return React.useMemo(
|
||||
() =>
|
||||
createCompatNavigationProp(
|
||||
navigation,
|
||||
route as any
|
||||
route as any,
|
||||
context.current,
|
||||
isFirstRouteInParent
|
||||
) as CompatNavigationProp<T>,
|
||||
[navigation, route]
|
||||
[isFirstRouteInParent, navigation, route]
|
||||
);
|
||||
}
|
||||
|
||||
6
packages/compat/tsconfig.build.json
Normal file
6
packages/compat/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,63 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.37](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.36...@react-navigation/core@5.0.0-alpha.37) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add error message when trying to use v4 API with v5 ([179e807](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/179e807a64a7d031d671c2c4b12edaee3c3440c5))
|
||||
* validate screen configs ([2f1f0af](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/2f1f0af862ef8625da4c2aaf463d45fe17a4ac88))
|
||||
* warn if non-serializable values found in state ([5751e7f](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/5751e7f97a1731a5c71862174dfd931b6ffe13e2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.36](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.35...@react-navigation/core@5.0.0-alpha.36) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disallow canPreventDefault option if not present in types ([d9059b5](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/d9059b56d8a89b39fec43d38a7b0514d41c0b550))
|
||||
* don't add ?if query params is empty ([3bf5ddd](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/3bf5ddde2ac1ba45f1123752d37532175f18a3d9))
|
||||
* fix types for useFocusEffect ([23ab45a](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/23ab45aceb72cc27ebfacdedfbf60d0c540fecfb)), closes [#270](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/270)
|
||||
* make sure that we return correct value if selector changes ([6c2acbb](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/6c2acbb304a9f11789b45a410b6c41911eca3947)), closes [/github.com/react-navigation/navigation-ex/pull/273#issuecomment-576581225](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/issuecomment-576581225)
|
||||
* use protected for private value store ([ad4eaff](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/ad4eaff1e99e4f9fca3a193764fd0f26efa41341))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add useNavigationState hook ([32a2206](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/32a2206513bc084d8da07187385d11db498f1e2a))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
* support nested config in getPathFromState ([#266](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/266)) ([1e53821](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/1e53821d52be182369add07a86c72221c5dba53e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.35](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.34...@react-navigation/core@5.0.0-alpha.35) (2020-01-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix intellisense for CompositeNavigationProp ([a912323](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/a912323c1dfa0c3564ca82c448a86f85d1658f7f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.34](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.33...@react-navigation/core@5.0.0-alpha.34) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.33](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.32...@react-navigation/core@5.0.0-alpha.33) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/core
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"react-native",
|
||||
"react-navigation"
|
||||
],
|
||||
"version": "5.0.0-alpha.33",
|
||||
"version": "5.0.0-alpha.37",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/core",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -27,14 +27,17 @@
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"query-string": "^6.9.0",
|
||||
"react-is": "^16.12.0",
|
||||
"shortid": "^2.2.15",
|
||||
"use-subscription": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-is": "^16.7.1",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/use-subscription": "^1.0.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-native-testing-library": "^1.12.0",
|
||||
@@ -50,7 +53,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import shortid from 'shortid';
|
||||
import { CommonAction, NavigationState, PartialState } from './types';
|
||||
|
||||
/**
|
||||
@@ -11,35 +10,6 @@ const BaseRouter = {
|
||||
action: CommonAction
|
||||
): State | PartialState<State> | null {
|
||||
switch (action.type) {
|
||||
case 'REPLACE': {
|
||||
const index = action.source
|
||||
? state.routes.findIndex(r => r.key === action.source)
|
||||
: state.index;
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, key, params } = action.payload;
|
||||
|
||||
if (!state.routeNames.includes(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
routes: state.routes.map((route, i) =>
|
||||
i === index
|
||||
? {
|
||||
key: key !== undefined ? key : `${name}-${shortid()}`,
|
||||
name,
|
||||
params,
|
||||
}
|
||||
: route
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_PARAMS': {
|
||||
const index = action.source
|
||||
? state.routes.findIndex(r => r.key === action.source)
|
||||
|
||||
@@ -14,12 +14,6 @@ export type Action =
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
| {
|
||||
type: 'REPLACE';
|
||||
payload: { name: string; key?: string; params?: object };
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
| {
|
||||
type: 'RESET';
|
||||
payload: PartialState<NavigationState>;
|
||||
@@ -59,10 +53,6 @@ export function navigate(...args: any): Action {
|
||||
}
|
||||
}
|
||||
|
||||
export function replace(name: string, params?: object): Action {
|
||||
return { type: 'REPLACE', payload: { name, params } };
|
||||
}
|
||||
|
||||
export function reset(state: PartialState<NavigationState>): Action {
|
||||
return { type: 'RESET', payload: state };
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as React from 'react';
|
||||
import * as CommonActions from './CommonActions';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import ResetRootContext from './ResetRootContext';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useDevTools from './useDevTools';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import isSerializable from './isSerializable';
|
||||
|
||||
import {
|
||||
Route,
|
||||
@@ -45,6 +45,8 @@ export const NavigationStateContext = React.createContext<{
|
||||
},
|
||||
});
|
||||
|
||||
let hasWarnedForSerialization = false;
|
||||
|
||||
/**
|
||||
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
|
||||
*
|
||||
@@ -242,6 +244,20 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (
|
||||
state !== undefined &&
|
||||
!isSerializable(state) &&
|
||||
!hasWarnedForSerialization
|
||||
) {
|
||||
hasWarnedForSerialization = true;
|
||||
|
||||
console.warn(
|
||||
"We found non-serializable values 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 functions in your options, you can use 'navigation.setOptions' instead."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (skipTrackingRef.current) {
|
||||
skipTrackingRef.current = false;
|
||||
} else {
|
||||
@@ -261,9 +277,7 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
return (
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<ResetRootContext.Provider value={resetRoot}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</ResetRootContext.Provider>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationState, PartialState } from './types';
|
||||
|
||||
/**
|
||||
* Context which holds the method to reset root navigator state.
|
||||
*/
|
||||
const ResetRootContext = React.createContext<
|
||||
(state: PartialState<NavigationState> | NavigationState) => void
|
||||
>(() => {
|
||||
throw new Error(
|
||||
"We couldn't find a way to reset root state. Have you wrapped your app with 'NavigationContainer'?"
|
||||
);
|
||||
});
|
||||
|
||||
export default ResetRootContext;
|
||||
@@ -16,64 +16,6 @@ const STATE = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
};
|
||||
|
||||
it('replaces focused screen with REPLACE', () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
CommonActions.replace('qux', { answer: 42 })
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces source screen with REPLACE', () => {
|
||||
const result = BaseRouter.getStateForAction(STATE, {
|
||||
...CommonActions.replace('qux', { answer: 42 }),
|
||||
source: 'baz',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't handle REPLACE if source key isn't present", () => {
|
||||
const result = BaseRouter.getStateForAction(STATE, {
|
||||
...CommonActions.replace('qux', { answer: 42 }),
|
||||
source: 'magic',
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it("doesn't handle REPLACE if screen to replace with isn't present", () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
CommonActions.replace('nonexistent', { answer: 42 })
|
||||
);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('sets params for the focused screen with SET_PARAMS', () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
|
||||
12
packages/core/src/__tests__/createNavigatorFactory.test.tsx
Normal file
12
packages/core/src/__tests__/createNavigatorFactory.test.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import createNavigatorFactory from '../createNavigatorFactory';
|
||||
|
||||
it('throws descriptive error if an argument is passed', () => {
|
||||
const createDummyNavigator = createNavigatorFactory(() => null);
|
||||
|
||||
expect(() => createDummyNavigator()).not.toThrowError();
|
||||
|
||||
// @ts-ignore
|
||||
expect(() => createDummyNavigator({})).toThrowError(
|
||||
"Creating a navigator doesn't take an argument."
|
||||
);
|
||||
});
|
||||
@@ -1,89 +1,374 @@
|
||||
import getPathFromState from '../getPathFromState';
|
||||
import getStateFromPath from '../getStateFromPath';
|
||||
|
||||
it('converts state to path string', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { fruit: 'apple' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { fruit: 'apple' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toMatchInlineSnapshot(`"/foo/bar/baz%20qux?author=jane&valid=true"`);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const path = '/foo/bar/baz%20qux?author=jane&valid=true';
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('converts state to path string with config', () => {
|
||||
expect(
|
||||
getPathFromState(
|
||||
{
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet', avaliable: false },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { author: 'Jane', valid: true, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
const path = '/few/bar/sweet/apple/baz/jane?id=x10&valid=true';
|
||||
const config = {
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
id: (id: string) => Number(id.replace(/^x/, '')),
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
id: (id: number) => `x${id}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
stringify: {
|
||||
author: author => author.toLowerCase(),
|
||||
id: id => `x${id}`,
|
||||
},
|
||||
name: 'Foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet', avaliable: false },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { author: 'Jane', valid: true, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
).toMatchInlineSnapshot(`"/few/bar/sweet/apple/baz/jane?id=x10&valid=true"`);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
routes: [{ name: 'bar' }],
|
||||
},
|
||||
const path = '/foo/bar';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
routes: [{ name: 'bar' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe('/foo/bar');
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it("doesn't add query param for empty params", () => {
|
||||
const path = '/foo';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens', () => {
|
||||
const path = '/few/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
id: (id: number) => `x${id}`,
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: '10',
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and unused configs', () => {
|
||||
const path = '/few/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()),
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object with stringify in it', () => {
|
||||
const path = '/bar/sweet/apple/few/bis/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and and path and stringify in roots', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import getStateFromPath from '../getStateFromPath';
|
||||
import getPathFromState from '../getPathFromState';
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
expect(
|
||||
getStateFromPath('foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
|
||||
).toEqual({
|
||||
const path = 'foo/bar/baz%20qux?author=jane%20%26%20co&valid=true';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -24,28 +28,31 @@ it('converts path string to initial state', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(state);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
const path = '/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -72,7 +79,12 @@ it('converts path string to initial state with config', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles leading slash when converting', () => {
|
||||
@@ -112,7 +124,8 @@ it('handles ending slash when converting', () => {
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(getStateFromPath('foo/bar')).toEqual({
|
||||
const path = 'foo/bar';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -121,34 +134,33 @@ it('handles route without param', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(state);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
const path = '/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -182,26 +194,32 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens and unused configs', () => {
|
||||
expect(
|
||||
getStateFromPath('/few/baz/jane?count=10&answer=42&valid=true', {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
it('converts path string to initial state with config with nested screens and unused parse functions', () => {
|
||||
const path = '/few/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
id: Boolean,
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -227,32 +245,36 @@ it('converts path string to initial state with config with nested screens and un
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object with parse in it', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/bar/sweet/apple/few/bis/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
it('handles nested object with unused configs and with parse in it', () => {
|
||||
const path = '/bar/sweet/apple/few/bis/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
@@ -293,21 +315,27 @@ it('handles parse in nested object with parse in it', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth', () => {
|
||||
expect(
|
||||
getStateFromPath('/baz', {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -323,28 +351,40 @@ it('handles parse in nested object for second route depth', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth and and path and parse in roots', () => {
|
||||
expect(
|
||||
getStateFromPath('/baz', {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
Baz: 'baz',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -360,5 +400,10 @@ it('handles parse in nested object for second route depth and and path and parse
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -936,3 +936,100 @@ it('switches rendered navigators', () => {
|
||||
'Another navigator is already registered for this container.'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if both children and component are passed', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={jest.fn()}>
|
||||
{jest.fn()}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We got both 'component' and 'children' props for 'Screen'. You must pass only one of them."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for undefined screen component', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={undefined as any} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We couldn't find a 'component' or 'children' prop for 'Screen'"
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for invalid screen component', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={{} as any} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We got an invalid value for 'component' prop for 'Screen'. It must be a a valid React Component."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for invalid children', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">{[] as any}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"We got an invalid value for 'children' prop for 'Screen'. It must be a function returning a React Element."
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't throw if children is null", () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={jest.fn()}>
|
||||
{null as any}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).not.toThrowError();
|
||||
});
|
||||
|
||||
@@ -391,6 +391,8 @@ it('fires custom events', () => {
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
@@ -400,3 +402,62 @@ it('fires custom events', () => {
|
||||
expect(secondCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('has option to prevent default', () => {
|
||||
expect.assertions(5);
|
||||
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const callback = (e: any) => {
|
||||
expect(e.type).toBe('someSuperCoolEvent');
|
||||
expect(e.data).toBe(42);
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(e.preventDefault).not.toBe(undefined);
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
};
|
||||
|
||||
const Test = ({ navigation }: any) => {
|
||||
React.useEffect(() => navigation.addListener(eventName, callback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen name="first" component={Test} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
data: 42,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
58
packages/core/src/__tests__/useNavigationCache.test.tsx
Normal file
58
packages/core/src/__tests__/useNavigationCache.test.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-native-testing-library';
|
||||
import useEventEmitter from '../useEventEmitter';
|
||||
import useNavigationCache from '../useNavigationCache';
|
||||
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
||||
|
||||
beforeEach(() => (MockRouterKey.current = 0));
|
||||
|
||||
it('preserves reference for navigation objects', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const state = {
|
||||
type: 'tab',
|
||||
stale: false as const,
|
||||
index: 1,
|
||||
key: 'State',
|
||||
routeNames: ['Foo', 'Bar'],
|
||||
routes: [
|
||||
{ key: 'Foo', name: 'Foo' },
|
||||
{ key: 'Bar', name: 'Bar' },
|
||||
],
|
||||
};
|
||||
|
||||
const getState = () => state;
|
||||
const navigation = {} as any;
|
||||
const setOptions = (() => {}) as any;
|
||||
const router = MockRouter({});
|
||||
|
||||
const Test = () => {
|
||||
const previous = React.useRef<any>();
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
const navigations = useNavigationCache({
|
||||
state,
|
||||
getState,
|
||||
navigation,
|
||||
setOptions,
|
||||
router,
|
||||
emitter,
|
||||
});
|
||||
|
||||
if (previous.current) {
|
||||
Object.keys(navigations).forEach(key => {
|
||||
expect(navigations[key]).toBe(previous.current[key]);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
previous.current = navigations;
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const root = render(<Test />);
|
||||
|
||||
root.update(<Test />);
|
||||
});
|
||||
156
packages/core/src/__tests__/useNavigationState.test.tsx
Normal file
156
packages/core/src/__tests__/useNavigationState.test.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import useNavigationState from '../useNavigationState';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter from './__fixtures__/MockRouter';
|
||||
import { NavigationState } from '../types';
|
||||
|
||||
it('gets the current navigation state', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const state = useNavigationState(state => state);
|
||||
|
||||
callback(state);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0].index).toBe(0);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0].index).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('third'));
|
||||
|
||||
expect(callback).toBeCalledTimes(3);
|
||||
expect(callback.mock.calls[2][0].index).toBe(2);
|
||||
|
||||
act(() => navigation.current.navigate('second', { answer: 42 }));
|
||||
|
||||
expect(callback).toBeCalledTimes(4);
|
||||
expect(callback.mock.calls[3][0].index).toBe(1);
|
||||
expect(callback.mock.calls[3][0].routes[1].params).toEqual({ answer: 42 });
|
||||
});
|
||||
|
||||
it('gets the current navigation state with selector', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const index = useNavigationState(state => state.index);
|
||||
|
||||
callback(index);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toBe(0);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0]).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('third'));
|
||||
|
||||
expect(callback).toBeCalledTimes(3);
|
||||
expect(callback.mock.calls[1][0]).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(4);
|
||||
expect(callback.mock.calls[3][0]).toBe(1);
|
||||
});
|
||||
|
||||
it('gets the correct value if selector changes', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const SelectorContext = React.createContext<any>(null);
|
||||
|
||||
const Test = () => {
|
||||
const selector = React.useContext(SelectorContext);
|
||||
const result = useNavigationState(selector);
|
||||
|
||||
callback(result);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const App = ({ selector }: { selector: (state: NavigationState) => any }) => {
|
||||
return (
|
||||
<SelectorContext.Provider value={selector}>
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
</SelectorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const root = render(<App selector={state => state.index} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toBe(0);
|
||||
|
||||
root.update(<App selector={state => state.routes[state.index].name} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0]).toBe('first');
|
||||
});
|
||||
@@ -13,11 +13,17 @@ export default function createNavigatorFactory<
|
||||
ScreenOptions extends object,
|
||||
NavigatorComponent extends React.ComponentType<any>
|
||||
>(Navigator: NavigatorComponent) {
|
||||
return <ParamList extends ParamListBase>(): TypedNavigator<
|
||||
return function<ParamList extends ParamListBase>(): TypedNavigator<
|
||||
ParamList,
|
||||
ScreenOptions,
|
||||
typeof Navigator
|
||||
> => {
|
||||
> {
|
||||
if (arguments[0] !== undefined) {
|
||||
throw new Error(
|
||||
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5?"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
Navigator,
|
||||
Screen,
|
||||
|
||||
@@ -6,7 +6,10 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
type StringifyConfig = Record<string, (value: any) => string>;
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; stringify?: StringifyConfig };
|
||||
[routeName: string]:
|
||||
| string
|
||||
| { path: string; stringify?: StringifyConfig }
|
||||
| Options;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,7 +40,7 @@ type Options = {
|
||||
* @returns Path representing the state, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getPathFromState(
|
||||
state: State,
|
||||
state?: State,
|
||||
options: Options = {}
|
||||
): string {
|
||||
let path = '/';
|
||||
@@ -45,14 +48,35 @@ export default function getPathFromState(
|
||||
let current: State | undefined = state;
|
||||
|
||||
while (current) {
|
||||
const index = typeof current.index === 'number' ? current.index : 0;
|
||||
const route = current.routes[index] as Route<string> & {
|
||||
let index = typeof current.index === 'number' ? current.index : 0;
|
||||
let route = current.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
};
|
||||
let currentOptions = options;
|
||||
let pattern = route.name;
|
||||
|
||||
while (route.name in currentOptions) {
|
||||
if (typeof currentOptions[route.name] === 'string') {
|
||||
pattern = currentOptions[route.name] as string;
|
||||
break;
|
||||
} else if (typeof currentOptions[route.name] === 'object') {
|
||||
if (route.state === undefined) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
break;
|
||||
} else {
|
||||
currentOptions = currentOptions[route.name] as Options;
|
||||
index = typeof route.state.index === 'number' ? route.state.index : 0;
|
||||
route = route.state.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config =
|
||||
options[route.name] !== undefined
|
||||
? (options[route.name] as { stringify?: StringifyConfig }).stringify
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
.stringify
|
||||
: undefined;
|
||||
|
||||
const params = route.params
|
||||
@@ -65,12 +89,7 @@ export default function getPathFromState(
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
if (options[route.name] !== undefined) {
|
||||
const pattern =
|
||||
typeof options[route.name] === 'string'
|
||||
? (options[route.name] as string)
|
||||
: (options[route.name] as { path: string }).path;
|
||||
|
||||
if (currentOptions[route.name] !== undefined) {
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map(p => {
|
||||
@@ -95,7 +114,11 @@ export default function getPathFromState(
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (params) {
|
||||
path += `?${queryString.stringify(params)}`;
|
||||
const query = queryString.stringify(params);
|
||||
|
||||
if (query) {
|
||||
path += `?${query}`;
|
||||
}
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
|
||||
@@ -12,7 +12,7 @@ type RouteConfig = {
|
||||
match: RegExp;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: Record<string, (value: string) => any> | undefined;
|
||||
parse: ParseConfig | undefined;
|
||||
};
|
||||
|
||||
type ResultState = PartialState<NavigationState> & {
|
||||
|
||||
@@ -14,6 +14,7 @@ export { default as useNavigation } from './useNavigation';
|
||||
export { default as useRoute } from './useRoute';
|
||||
export { default as useFocusEffect } from './useFocusEffect';
|
||||
export { default as useIsFocused } from './useIsFocused';
|
||||
export { default as useNavigationState } from './useNavigationState';
|
||||
|
||||
export { default as getStateFromPath } from './getStateFromPath';
|
||||
export { default as getPathFromState } from './getPathFromState';
|
||||
|
||||
34
packages/core/src/isSerializable.tsx
Normal file
34
packages/core/src/isSerializable.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
export default function isSerializable(o: { [key: string]: 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 (Array.isArray(o)) {
|
||||
for (const it of o) {
|
||||
if (!isSerializable(it)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in o) {
|
||||
if (!isSerializable(o[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -207,31 +207,52 @@ export type Router<
|
||||
|
||||
export type ParamListBase = Record<string, object | undefined>;
|
||||
|
||||
export type EventMapBase = {
|
||||
focus: undefined;
|
||||
blur: undefined;
|
||||
export type EventMapBase = Record<
|
||||
string,
|
||||
{ data?: any; canPreventDefault?: boolean }
|
||||
>;
|
||||
|
||||
export type EventMapCore = {
|
||||
focus: { data: undefined };
|
||||
blur: { data: undefined };
|
||||
state: { data: { state: NavigationState } };
|
||||
};
|
||||
|
||||
export type EventArg<EventName extends string, Data = undefined> = {
|
||||
export type EventArg<
|
||||
EventName extends string,
|
||||
CanPreventDefault extends boolean | undefined = false,
|
||||
Data = undefined
|
||||
> = {
|
||||
/**
|
||||
* Type of the event (e.g. `focus`, `blur`)
|
||||
*/
|
||||
readonly type: EventName;
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
} & (Data extends undefined ? {} : { readonly data: Data });
|
||||
} & (CanPreventDefault extends true
|
||||
? {
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
}
|
||||
: {}) &
|
||||
(Data extends undefined ? {} : { readonly data: Data });
|
||||
|
||||
export type EventListenerCallback<EventName extends string, Data> = (
|
||||
e: EventArg<EventName, Data>
|
||||
export type EventListenerCallback<
|
||||
EventMap extends EventMapBase,
|
||||
EventName extends keyof EventMap
|
||||
> = (
|
||||
e: EventArg<
|
||||
Extract<EventName, string>,
|
||||
EventMap[EventName]['canPreventDefault'],
|
||||
EventMap[EventName]['data']
|
||||
>
|
||||
) => void;
|
||||
|
||||
export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
export type EventConsumer<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Subscribe to events from the parent navigator.
|
||||
*
|
||||
@@ -240,15 +261,15 @@ export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
*/
|
||||
addListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): () => void;
|
||||
removeListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): void;
|
||||
};
|
||||
|
||||
export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
export type EventEmitter<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Emit an event to child screens.
|
||||
*
|
||||
@@ -261,23 +282,31 @@ export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
options: {
|
||||
type: EventName;
|
||||
target?: string;
|
||||
} & (EventMap[EventName] extends undefined
|
||||
? {}
|
||||
: { data: EventMap[EventName] })
|
||||
): EventArg<EventName, EventMap[EventName]>;
|
||||
} & (EventMap[EventName]['canPreventDefault'] extends true
|
||||
? { canPreventDefault: true }
|
||||
: {}) &
|
||||
(EventMap[EventName]['data'] extends undefined
|
||||
? {}
|
||||
: { data: EventMap[EventName]['data'] })
|
||||
): EventArg<
|
||||
EventName,
|
||||
EventMap[EventName]['canPreventDefault'],
|
||||
EventMap[EventName]['data']
|
||||
>;
|
||||
};
|
||||
|
||||
export class PrivateValueStore<A, B, C> {
|
||||
/**
|
||||
* TypeScript requires a type to be actually used to be able to infer it.
|
||||
* This is a hacky way of storing type in a property without surfacing it in intellisense.
|
||||
* UGLY HACK! DO NOT USE THE TYPE!!!
|
||||
*
|
||||
* TypeScript requires a type to be used to be able to infer it.
|
||||
* The type should exist as its own without any operations such as union.
|
||||
* So we need to figure out a way to store this type in a property.
|
||||
* The problem with a normal property is that it shows up in intelliSense.
|
||||
* Adding private keyword works, but the annotation is stripped away in declaration.
|
||||
* Turns out if we use an empty string, it doesn't show up in intelliSense.
|
||||
*/
|
||||
// @ts-ignore
|
||||
private __private_value_type_a?: A;
|
||||
// @ts-ignore
|
||||
private __private_value_type_b?: B;
|
||||
// @ts-ignore
|
||||
private __private_value_type_c?: C;
|
||||
protected ''?: { a: A; b: B; c: C };
|
||||
}
|
||||
|
||||
type NavigationHelpersCommon<
|
||||
@@ -336,13 +365,6 @@ type NavigationHelpersCommon<
|
||||
*/
|
||||
reset(state: PartialState<State> | State): void;
|
||||
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||
|
||||
/**
|
||||
* Go back to the previous route in history.
|
||||
*/
|
||||
@@ -365,7 +387,7 @@ type NavigationHelpersCommon<
|
||||
|
||||
export type NavigationHelpers<
|
||||
ParamList extends ParamListBase,
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList> &
|
||||
EventEmitter<EventMap> & {
|
||||
/**
|
||||
@@ -405,7 +427,7 @@ export type NavigationProp<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList, State> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
@@ -436,7 +458,7 @@ export type NavigationProp<
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
dangerouslyGetState(): State;
|
||||
} & EventConsumer<EventMap & EventMapBase> &
|
||||
} & EventConsumer<EventMap & EventMapCore> &
|
||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||
|
||||
export type RouteProp<
|
||||
@@ -490,7 +512,7 @@ export type Descriptor<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = {
|
||||
/**
|
||||
* Render the component associated with this route.
|
||||
@@ -543,10 +565,7 @@ export type RouteConfig<
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<{
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}>;
|
||||
component: React.ComponentType<any>;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
@@ -567,6 +586,9 @@ export type NavigationContainerRef =
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||
/**
|
||||
* Get the rehydrated navigation state of the navigation tree.
|
||||
*/
|
||||
getRootState(): NavigationState;
|
||||
})
|
||||
| undefined
|
||||
|
||||
@@ -43,7 +43,17 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
}, []);
|
||||
|
||||
const emit = React.useCallback(
|
||||
({ type, data, target }: { type: string; data?: any; target?: string }) => {
|
||||
({
|
||||
type,
|
||||
data,
|
||||
target,
|
||||
canPreventDefault,
|
||||
}: {
|
||||
type: string;
|
||||
data?: any;
|
||||
target?: string;
|
||||
canPreventDefault?: boolean;
|
||||
}) => {
|
||||
const items = listeners.current[type] || {};
|
||||
|
||||
// Copy the current list of callbacks in case they are mutated during execution
|
||||
@@ -52,26 +62,40 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
? items[target] && items[target].slice()
|
||||
: ([] as Listeners).concat(...Object.keys(items).map(t => items[t]));
|
||||
|
||||
let defaultPrevented = false;
|
||||
|
||||
const event: EventArg<any, any> = {
|
||||
const event: EventArg<any, any, any> = {
|
||||
get type() {
|
||||
return type;
|
||||
},
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
get defaultPrevented() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
preventDefault() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
};
|
||||
|
||||
if (data !== undefined) {
|
||||
Object.defineProperty(event, 'data', {
|
||||
get() {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (canPreventDefault) {
|
||||
let defaultPrevented = false;
|
||||
|
||||
Object.defineProperties(event, {
|
||||
defaultPrevented: {
|
||||
get() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
},
|
||||
preventDefault: {
|
||||
value() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
callbacks?.forEach(cb => cb(event));
|
||||
|
||||
return event;
|
||||
return event as any;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import useNavigation from './useNavigation';
|
||||
|
||||
type EffectCallback = (() => undefined) | (() => () => void);
|
||||
type EffectCallback = () => undefined | void | (() => void);
|
||||
|
||||
/**
|
||||
* Hook to run an effect in a focused screen, similar to `React.useEffect`.
|
||||
@@ -15,7 +15,7 @@ export default function useFocusEffect(callback: EffectCallback) {
|
||||
|
||||
React.useEffect(() => {
|
||||
let isFocused = false;
|
||||
let cleanup: (() => void) | undefined;
|
||||
let cleanup: undefined | void | (() => void);
|
||||
|
||||
// We need to run the effect on intial render/dep changes if the screen is focused
|
||||
if (navigation.isFocused()) {
|
||||
@@ -30,19 +30,28 @@ export default function useFocusEffect(callback: EffectCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup?.();
|
||||
if (cleanup !== undefined) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
cleanup = callback();
|
||||
isFocused = true;
|
||||
});
|
||||
|
||||
const unsubscribeBlur = navigation.addListener('blur', () => {
|
||||
cleanup?.();
|
||||
if (cleanup !== undefined) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
cleanup = undefined;
|
||||
isFocused = false;
|
||||
});
|
||||
|
||||
return () => {
|
||||
cleanup?.();
|
||||
if (cleanup !== undefined) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
unsubscribeFocus();
|
||||
unsubscribeBlur();
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
|
||||
const currentFocusedKey = state.routes[state.index].key;
|
||||
|
||||
// When the parent screen changes its focus state, we also need to change child's focus
|
||||
// Coz the child screen can't be focused if the parent screen is out of fcous
|
||||
// Coz the child screen can't be focused if the parent screen is out of focus
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation?.addListener('focus', () =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { isValidElementType } from 'react-is';
|
||||
import { NavigationStateContext } from './NavigationContainer';
|
||||
import NavigationRouteContext from './NavigationRouteContext';
|
||||
import Screen from './Screen';
|
||||
@@ -53,8 +54,8 @@ const isArrayEqual = (a: any[], b: any[]) =>
|
||||
*/
|
||||
const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
children: React.ReactNode
|
||||
) =>
|
||||
React.Children.toArray(children).reduce<
|
||||
) => {
|
||||
const configs = React.Children.toArray(children).reduce<
|
||||
RouteConfig<ParamListBase, string, ScreenOptions>[]
|
||||
>((acc, child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
@@ -85,6 +86,39 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
configs.forEach(config => {
|
||||
const { children, component } = config as any;
|
||||
|
||||
if (children != null || component !== undefined) {
|
||||
if (children != null && component !== undefined) {
|
||||
throw new Error(
|
||||
"We got both 'component' and 'children' props for 'Screen'. You must pass only one of them."
|
||||
);
|
||||
}
|
||||
|
||||
if (children != null && typeof children !== 'function') {
|
||||
throw new Error(
|
||||
`We got an invalid value for 'children' prop for 'Screen'. It must be a function returning a React Element.`
|
||||
);
|
||||
}
|
||||
|
||||
if (component !== undefined && !isValidElementType(component)) {
|
||||
throw new Error(
|
||||
`We got an invalid value for 'component' prop for 'Screen'. It must be a a valid React Component.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"We couldn't find a 'component' or 'children' prop for 'Screen'. 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."
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for building navigators.
|
||||
*
|
||||
@@ -293,6 +327,10 @@ export default function useNavigationBuilder<
|
||||
|
||||
useFocusEvents({ state, emitter });
|
||||
|
||||
React.useEffect(() => {
|
||||
emitter.emit({ type: 'state', data: { state } });
|
||||
}, [emitter, state]);
|
||||
|
||||
const {
|
||||
listeners: actionListeners,
|
||||
addListener: addActionListener,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import * as CommonActions from './CommonActions';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import ResetRootContext from './ResetRootContext';
|
||||
import { NavigationStateContext } from './NavigationContainer';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import {
|
||||
@@ -37,7 +36,6 @@ export default function useNavigationHelpers<
|
||||
Action extends NavigationAction,
|
||||
EventMap extends Record<string, any>
|
||||
>({ onAction, getState, emitter, router }: Options<State, Action>) {
|
||||
const resetRoot = React.useContext(ResetRootContext);
|
||||
const parentNavigationHelpers = React.useContext(NavigationContext);
|
||||
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||
|
||||
@@ -76,7 +74,6 @@ export default function useNavigationHelpers<
|
||||
return {
|
||||
...parentNavigationHelpers,
|
||||
...helpers,
|
||||
resetRoot,
|
||||
dispatch,
|
||||
emit: emitter.emit,
|
||||
isFocused: parentNavigationHelpers
|
||||
@@ -100,7 +97,6 @@ export default function useNavigationHelpers<
|
||||
router,
|
||||
getState,
|
||||
parentNavigationHelpers,
|
||||
resetRoot,
|
||||
emitter.emit,
|
||||
performTransaction,
|
||||
onAction,
|
||||
|
||||
37
packages/core/src/useNavigationState.tsx
Normal file
37
packages/core/src/useNavigationState.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import useNavigation from './useNavigation';
|
||||
import { NavigationState } from './types';
|
||||
|
||||
type Selector<T> = (state: NavigationState) => T;
|
||||
|
||||
/**
|
||||
* Hook to get a value from the current navigation state using a selector.
|
||||
*
|
||||
* @param selector Selector function to get a value from the state.
|
||||
*/
|
||||
export default function useNavigationState<T>(selector: Selector<T>): T {
|
||||
const navigation = useNavigation();
|
||||
|
||||
// We don't care about the state value, we run the selector again at the end
|
||||
// The state is only to make sure that there's a re-render when we have a new value
|
||||
const [, setResult] = React.useState(() =>
|
||||
selector(navigation.dangerouslyGetState())
|
||||
);
|
||||
|
||||
// We store the selector in a ref to avoid re-subscribing listeners every render
|
||||
const selectorRef = React.useRef(selector);
|
||||
|
||||
React.useEffect(() => {
|
||||
selectorRef.current = selector;
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubscribe = navigation.addListener('state', e => {
|
||||
setResult(selectorRef.current(e.data.state));
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
return selector(navigation.dangerouslyGetState());
|
||||
}
|
||||
6
packages/core/tsconfig.build.json
Normal file
6
packages/core/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,45 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.41](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.0-alpha.40...@react-navigation/drawer@5.0.0-alpha.41) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.40](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.0-alpha.39...@react-navigation/drawer@5.0.0-alpha.40) (2020-01-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* emit appear and dismiss events for native stack ([f1df4a0](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/commit/f1df4a080877b3642e748a41a5ffc2da8c449a8c))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.39](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.0-alpha.38...@react-navigation/drawer@5.0.0-alpha.39) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.38](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.0-alpha.37...@react-navigation/drawer@5.0.0-alpha.38) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.37](https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer/compare/@react-navigation/drawer@5.0.0-alpha.36...@react-navigation/drawer@5.0.0-alpha.37) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"material",
|
||||
"drawer"
|
||||
],
|
||||
"version": "5.0.0-alpha.37",
|
||||
"version": "5.0.0-alpha.41",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/drawer",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -22,6 +22,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -30,12 +31,12 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23",
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"del-cli": "^3.0.0",
|
||||
@@ -44,7 +45,7 @@
|
||||
"react-native-gesture-handler": "^1.5.3",
|
||||
"react-native-reanimated": "^1.4.0",
|
||||
"react-native-safe-area-context": "^0.6.2",
|
||||
"react-native-screens": "^2.0.0-alpha.22",
|
||||
"react-native-screens": "^2.0.0-alpha.25",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -62,7 +63,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,11 +173,11 @@ export type DrawerNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when the drawer opens.
|
||||
*/
|
||||
drawerOpen: undefined;
|
||||
drawerOpen: { data: undefined };
|
||||
/**
|
||||
* Event which fires when the drawer closes.
|
||||
*/
|
||||
drawerClose: undefined;
|
||||
drawerClose: { data: undefined };
|
||||
};
|
||||
|
||||
export type DrawerNavigationHelpers = NavigationHelpers<
|
||||
|
||||
6
packages/drawer/tsconfig.build.json
Normal file
6
packages/drawer/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,45 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.36](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.35...@react-navigation/material-bottom-tabs@5.0.0-alpha.36) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.35](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.34...@react-navigation/material-bottom-tabs@5.0.0-alpha.35) (2020-01-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add preventDefault functionality in material bottom tabs ([3dede31](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/commit/3dede316ccab3b2403a475f60ce20b5c4e4cc068))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.34](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.33...@react-navigation/material-bottom-tabs@5.0.0-alpha.34) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.33](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.32...@react-navigation/material-bottom-tabs@5.0.0-alpha.33) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.32](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.31...@react-navigation/material-bottom-tabs@5.0.0-alpha.32) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"material",
|
||||
"tab"
|
||||
],
|
||||
"version": "5.0.0-alpha.32",
|
||||
"version": "5.0.0-alpha.36",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/material-bottom-tabs",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -22,6 +22,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -30,17 +31,17 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23"
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
"del-cli": "^3.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-paper": "^3.4.0",
|
||||
"react-native-paper": "^3.5.0",
|
||||
"react-native-vector-icons": "^6.6.0",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
@@ -48,7 +49,7 @@
|
||||
"@react-navigation/native": "^5.0.0-alpha.0",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-paper": "^3.0.0",
|
||||
"react-native-paper": "^3.5.0",
|
||||
"react-native-vector-icons": "^6.0.0"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
@@ -57,7 +58,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export type MaterialBottomTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
};
|
||||
|
||||
export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -89,11 +89,16 @@ export default function MaterialBottomTabView({
|
||||
descriptors[route.key].options.tabBarAccessibilityLabel
|
||||
}
|
||||
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
|
||||
onTabPress={({ route }) => {
|
||||
navigation.emit({
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
preventDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
6
packages/material-bottom-tabs/tsconfig.build.json
Normal file
6
packages/material-bottom-tabs/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.35](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.0-alpha.34...@react-navigation/material-top-tabs@5.0.0-alpha.35) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.34](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.0-alpha.33...@react-navigation/material-top-tabs@5.0.0-alpha.34) (2020-01-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.33](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.0-alpha.32...@react-navigation/material-top-tabs@5.0.0-alpha.33) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.32](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.0-alpha.31...@react-navigation/material-top-tabs@5.0.0-alpha.32) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.31](https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.0.0-alpha.30...@react-navigation/material-top-tabs@5.0.0-alpha.31) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"material",
|
||||
"tab"
|
||||
],
|
||||
"version": "5.0.0-alpha.31",
|
||||
"version": "5.0.0-alpha.35",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/material-top-tabs",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -22,6 +22,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -30,11 +31,11 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23",
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27",
|
||||
"color": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"del-cli": "^3.0.0",
|
||||
@@ -59,7 +60,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ export type MaterialTopTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
/**
|
||||
* Event which fires on long press on the tab in the tab bar.
|
||||
*/
|
||||
tabLongPress: undefined;
|
||||
tabLongPress: { data: undefined };
|
||||
/**
|
||||
* Event which fires when a swipe gesture starts, i.e. finger touches the screen.
|
||||
*/
|
||||
swipeStart: undefined;
|
||||
swipeStart: { data: undefined };
|
||||
/**
|
||||
* Event which fires when a swipe gesture ends, i.e. finger leaves the screen.
|
||||
*/
|
||||
swipeEnd: undefined;
|
||||
swipeEnd: { data: undefined };
|
||||
};
|
||||
|
||||
export type MaterialTopTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function TabBarTop(props: MaterialTopTabBarProps) {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
|
||||
6
packages/material-top-tabs/tsconfig.build.json
Normal file
6
packages/material-top-tabs/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/compare/@react-navigation/native-stack@5.0.0-alpha.28...@react-navigation/native-stack@5.0.0-alpha.29) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native-stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/compare/@react-navigation/native-stack@5.0.0-alpha.27...@react-navigation/native-stack@5.0.0-alpha.28) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix types for native stack ([1da4a64](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/commit/1da4a6437f4607c1d4547d26dd5068615631982e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* emit appear and dismiss events for native stack ([f1df4a0](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/commit/f1df4a080877b3642e748a41a5ffc2da8c449a8c))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/compare/@react-navigation/native-stack@5.0.0-alpha.26...@react-navigation/native-stack@5.0.0-alpha.27) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native-stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/compare/@react-navigation/native-stack@5.0.0-alpha.25...@react-navigation/native-stack@5.0.0-alpha.26) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack/compare/@react-navigation/native-stack@5.0.0-alpha.24...@react-navigation/native-stack@5.0.0-alpha.25) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native-stack
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"react-native",
|
||||
"react-navigation"
|
||||
],
|
||||
"version": "5.0.0-alpha.25",
|
||||
"version": "5.0.0-alpha.29",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/native-stack",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -17,6 +17,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -25,19 +26,19 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23"
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"react-native-screens": "^2.0.0-alpha.22",
|
||||
"react-native-screens": "^2.0.0-alpha.25",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^5.0.0-alpha.0",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-screens": "^2.0.0-alpha.8"
|
||||
"react-native-screens": "^2.0.0-alpha.25"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"source": "src",
|
||||
@@ -45,7 +46,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import NativeStackView from '../views/NativeStackView';
|
||||
import {
|
||||
NativeStackNavigatorProps,
|
||||
NativeStackNavigationOptions,
|
||||
NativeStackNavigationEventMap,
|
||||
} from '../types';
|
||||
|
||||
function NativeStackNavigator(props: NativeStackNavigatorProps) {
|
||||
@@ -34,7 +35,7 @@ function NativeStackNavigator(props: NativeStackNavigatorProps) {
|
||||
StackNavigationState,
|
||||
StackRouterOptions,
|
||||
NativeStackNavigationOptions,
|
||||
{}
|
||||
NativeStackNavigationEventMap
|
||||
>(StackRouter, {
|
||||
initialRouteName,
|
||||
children,
|
||||
@@ -44,13 +45,17 @@ function NativeStackNavigator(props: NativeStackNavigatorProps) {
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
|
||||
navigation.addListener('tabPress', e => {
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
// Run the operation in the next frame so we're sure all listeners have been run
|
||||
// This is necessary to know if preventDefault() has been called
|
||||
requestAnimationFrame(() => {
|
||||
if (state.index > 0 && isFocused && !e.defaultPrevented) {
|
||||
if (
|
||||
state.index > 0 &&
|
||||
isFocused &&
|
||||
!(e as EventArg<'tabPress', true>).defaultPrevented
|
||||
) {
|
||||
// When user taps on already focused tab and we're inside the tab,
|
||||
// reset the stack to replicate native behaviour
|
||||
navigation.dispatch({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenProps } from 'react-native-screens';
|
||||
import {
|
||||
DefaultNavigatorOptions,
|
||||
Descriptor,
|
||||
@@ -12,6 +14,17 @@ import {
|
||||
StackRouterOptions,
|
||||
} from '@react-navigation/routers';
|
||||
|
||||
export type NativeStackNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when the screen appears.
|
||||
*/
|
||||
appear: { data: undefined };
|
||||
/**
|
||||
* Event which fires when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down).
|
||||
*/
|
||||
dismiss: { data: undefined };
|
||||
};
|
||||
|
||||
export type NativeStackNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
@@ -20,7 +33,7 @@ export type NativeStackNavigationProp<
|
||||
RouteName,
|
||||
StackNavigationState,
|
||||
NativeStackNavigationOptions,
|
||||
{}
|
||||
NativeStackNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Push a new screen onto the stack.
|
||||
@@ -45,7 +58,10 @@ export type NativeStackNavigationProp<
|
||||
popToTop(): void;
|
||||
};
|
||||
|
||||
export type NativeStackNavigationHelpers = NavigationHelpers<ParamListBase, {}>;
|
||||
export type NativeStackNavigationHelpers = NavigationHelpers<
|
||||
ParamListBase,
|
||||
NativeStackNavigationEventMap
|
||||
>;
|
||||
|
||||
export type NativeStackNavigationConfig = {};
|
||||
|
||||
@@ -168,15 +184,21 @@ export type NativeStackNavigationOptions = {
|
||||
gestureEnabled?: boolean;
|
||||
/**
|
||||
* How should the screen be presented.
|
||||
* The following values are currently supported:
|
||||
* - "push" – the new screen will be pushed onto a stack which on iOS means that the default animation will be slide from the side, the animation on Android may vary depending on the OS version and theme.
|
||||
* - "modal" – the new screen will be presented modally. In addition this allow for a nested stack to be rendered inside such screens
|
||||
* - "transparentModal" – the new screen will be presented modally but in addition the second to last screen will remain attached to the stack container such that if the top screen is non opaque the content below can still be seen. If "modal" is used instead the below screen will get unmounted as soon as the transition ends.
|
||||
*/
|
||||
presentation?: 'modal' | 'transparentModal' | 'push';
|
||||
stackPresentation?: ScreenProps['stackPresentation'];
|
||||
/**
|
||||
* How should the screen should be animated.
|
||||
* Only supported on Android.
|
||||
*
|
||||
* @platform android
|
||||
* How the screen should appear/disappear when pushed or popped at the top of the stack.
|
||||
* The following values are currently supported:
|
||||
* - "default" – uses a platform default animation
|
||||
* - "fade" – fades screen in or out
|
||||
* - "flip" – flips the screen, requires stackPresentation: "modal" (iOS only)
|
||||
* - "none" – the screen appears/dissapears without an animation
|
||||
*/
|
||||
animation?: 'default' | 'fade' | 'none';
|
||||
stackAnimation?: ScreenProps['stackAnimation'];
|
||||
};
|
||||
|
||||
export type NativeStackNavigatorProps = DefaultNavigatorOptions<
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
// @ts-ignore
|
||||
ScreenStackHeaderConfig,
|
||||
// @ts-ignore
|
||||
ScreenStackHeaderRightView,
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
} from 'react-native-screens';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { View, StyleSheet } from 'react-native';
|
||||
import { StackNavigationState, StackActions } from '@react-navigation/routers';
|
||||
|
||||
import {
|
||||
// @ts-ignore
|
||||
ScreenStack,
|
||||
Screen as ScreenComponent,
|
||||
ScreenProps,
|
||||
@@ -16,13 +15,7 @@ import {
|
||||
NativeStackDescriptorMap,
|
||||
} from '../types';
|
||||
|
||||
const Screen = (ScreenComponent as unknown) as React.ComponentType<
|
||||
ScreenProps & {
|
||||
stackPresentation?: 'push' | 'modal' | 'transparentModal';
|
||||
stackAnimation?: 'default' | 'fade' | 'none';
|
||||
onDismissed?: () => void;
|
||||
}
|
||||
>;
|
||||
const Screen = (ScreenComponent as unknown) as React.ComponentType<ScreenProps>;
|
||||
|
||||
type Props = {
|
||||
state: StackNavigationState;
|
||||
@@ -41,15 +34,30 @@ export default function NativeStackView({
|
||||
<ScreenStack style={styles.container}>
|
||||
{state.routes.map(route => {
|
||||
const { options, render: renderScene } = descriptors[route.key];
|
||||
const { presentation = 'push', animation, contentStyle } = options;
|
||||
const {
|
||||
stackPresentation = 'push',
|
||||
stackAnimation,
|
||||
contentStyle,
|
||||
} = options;
|
||||
|
||||
return (
|
||||
<Screen
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
stackPresentation={presentation}
|
||||
stackAnimation={animation}
|
||||
stackPresentation={stackPresentation}
|
||||
stackAnimation={stackAnimation}
|
||||
onAppear={() => {
|
||||
navigation.emit({
|
||||
type: 'appear',
|
||||
target: route.key,
|
||||
});
|
||||
}}
|
||||
onDismissed={() => {
|
||||
navigation.emit({
|
||||
type: 'dismiss',
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
|
||||
6
packages/native-stack/tsconfig.build.json
Normal file
6
packages/native-stack/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/compare/@react-navigation/native@5.0.0-alpha.28...@react-navigation/native@5.0.0-alpha.29) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/compare/@react-navigation/native@5.0.0-alpha.27...@react-navigation/native@5.0.0-alpha.28) (2020-01-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/compare/@react-navigation/native@5.0.0-alpha.26...@react-navigation/native@5.0.0-alpha.27) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/compare/@react-navigation/native@5.0.0-alpha.25...@react-navigation/native@5.0.0-alpha.26) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/tree/master/packages/native/compare/@react-navigation/native@5.0.0-alpha.24...@react-navigation/native@5.0.0-alpha.25) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"version": "5.0.0-alpha.25",
|
||||
"version": "5.0.0-alpha.29",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/native",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -18,6 +18,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -26,10 +27,10 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.0.0-alpha.33"
|
||||
"@react-navigation/core": "^5.0.0-alpha.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-native": "^0.60.30",
|
||||
"del-cli": "^3.0.0",
|
||||
@@ -47,7 +48,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function useScrollToTop(
|
||||
// in addition, there are multiple tab implementations
|
||||
// @ts-ignore
|
||||
'tabPress',
|
||||
(e: EventArg<'tabPress'>) => {
|
||||
(e: EventArg<'tabPress', true>) => {
|
||||
// We should scroll to top only when the screen is focused
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
|
||||
6
packages/native/tsconfig.build.json
Normal file
6
packages/native/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/compare/@react-navigation/routers@5.0.0-alpha.26...@react-navigation/routers@5.0.0-alpha.27) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/routers
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/compare/@react-navigation/routers@5.0.0-alpha.25...@react-navigation/routers@5.0.0-alpha.26) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle popping more than available screens in stack ([68ed8a7](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/commit/68ed8a725950f39228847ab10b3dd7f3ebd2e2dc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/compare/@react-navigation/routers@5.0.0-alpha.24...@react-navigation/routers@5.0.0-alpha.25) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/routers
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.24](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/compare/@react-navigation/routers@5.0.0-alpha.23...@react-navigation/routers@5.0.0-alpha.24) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.23](https://github.com/react-navigation/navigation-ex/tree/master/packages/routers/compare/@react-navigation/routers@5.0.0-alpha.22...@react-navigation/routers@5.0.0-alpha.23) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/routers
|
||||
|
||||
@@ -538,6 +538,32 @@ it('handles pop action', () => {
|
||||
routes: [{ key: 'baz', name: 'baz' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 2,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [
|
||||
{ key: 'baz', name: 'baz' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'qux', name: 'qux' },
|
||||
],
|
||||
},
|
||||
StackActions.pop(4),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 0,
|
||||
routeNames: ['baz', 'bar', 'qux'],
|
||||
routes: [{ key: 'baz', name: 'baz' }],
|
||||
});
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
@@ -657,6 +683,145 @@ it('handles pop to top action', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces focused screen with replace', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
},
|
||||
StackActions.replace('qux', { answer: 42 }),
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces source screen with replace', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
},
|
||||
{
|
||||
...StackActions.replace('qux', { answer: 42 }),
|
||||
source: 'baz',
|
||||
},
|
||||
options
|
||||
)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't handle replace if source key isn't present", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
},
|
||||
{
|
||||
...StackActions.replace('qux', { answer: 42 }),
|
||||
source: 'magic',
|
||||
},
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("doesn't handle replace if screen to replace with isn't present", () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
router.getStateForAction(
|
||||
{
|
||||
stale: false,
|
||||
type: 'stack',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
},
|
||||
{
|
||||
...StackActions.replace('nonexistent', { answer: 42 }),
|
||||
source: 'magic',
|
||||
},
|
||||
options
|
||||
)
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('handles push action', () => {
|
||||
const router = StackRouter({});
|
||||
const options = {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"react-native",
|
||||
"react-navigation"
|
||||
],
|
||||
"version": "5.0.0-alpha.23",
|
||||
"version": "5.0.0-alpha.27",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/routers",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -17,6 +17,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -25,11 +26,11 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.0.0-alpha.33",
|
||||
"@react-navigation/core": "^5.0.0-alpha.37",
|
||||
"shortid": "^2.2.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
@@ -39,7 +40,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
} from '@react-navigation/core';
|
||||
|
||||
export type StackActionType =
|
||||
| {
|
||||
type: 'REPLACE';
|
||||
payload: { name: string; key?: string | undefined; params?: object };
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
| {
|
||||
type: 'PUSH';
|
||||
payload: { name: string; key?: string | undefined; params?: object };
|
||||
@@ -37,6 +43,9 @@ export type StackNavigationState = NavigationState & {
|
||||
};
|
||||
|
||||
export const StackActions = {
|
||||
replace(name: string, params?: object): StackActionType {
|
||||
return { type: 'REPLACE', payload: { name, params } };
|
||||
},
|
||||
push(name: string, params?: object): StackActionType {
|
||||
return { type: 'PUSH', payload: { name, params } };
|
||||
},
|
||||
@@ -169,6 +178,35 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
const { routeParamList } = options;
|
||||
|
||||
switch (action.type) {
|
||||
case 'REPLACE': {
|
||||
const index = action.source
|
||||
? state.routes.findIndex(r => r.key === action.source)
|
||||
: state.index;
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, key, params } = action.payload;
|
||||
|
||||
if (!state.routeNames.includes(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
routes: state.routes.map((route, i) =>
|
||||
i === index
|
||||
? {
|
||||
key: key !== undefined ? key : `${name}-${shortid()}`,
|
||||
name,
|
||||
params,
|
||||
}
|
||||
: route
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case 'PUSH':
|
||||
if (state.routeNames.includes(action.payload.name)) {
|
||||
return {
|
||||
@@ -203,7 +241,7 @@ export default function StackRouter(options: StackRouterOptions) {
|
||||
: state.index;
|
||||
|
||||
if (index > 0) {
|
||||
const count = Math.max(index - action.payload.count + 1, 0);
|
||||
const count = Math.max(index - action.payload.count + 1, 1);
|
||||
const routes = state.routes
|
||||
.slice(0, count)
|
||||
.concat(state.routes.slice(index + 1));
|
||||
|
||||
6
packages/routers/tsconfig.build.json
Normal file
6
packages/routers/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,57 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.63](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/compare/@react-navigation/stack@5.0.0-alpha.62...@react-navigation/stack@5.0.0-alpha.63) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pass correct previous scene to header with headerMode: screen ([16c64e7](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/16c64e729896a157b2b5bb96d6e3eead827626a0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.62](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/compare/@react-navigation/stack@5.0.0-alpha.61...@react-navigation/stack@5.0.0-alpha.62) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't use native driver on web ([0a982ee](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/0a982ee6984b24c0ba053a30223e255f3835e050))
|
||||
* handle header translation for horizontal-inverted ([321fa65](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/321fa653add8366b7f24fb9de9a950064421dfc1))
|
||||
* position inactivscreensws offscreen by default ([38520a9](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/38520a97ff90af0a2f89f95676487a54104068d3))
|
||||
* slide the header up to hide it for vertical animation ([43d2c45](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/43d2c456beb58a8a57104ac308559cbd62998a52))
|
||||
* use a fade animation for header in all presets ([fe82276](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/fe82276b1f0d1a991744e642dcfa9034fb767caf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* emit appear and dismiss events for native stack ([f1df4a0](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/f1df4a080877b3642e748a41a5ffc2da8c449a8c))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.61](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/compare/@react-navigation/stack@5.0.0-alpha.60...@react-navigation/stack@5.0.0-alpha.61) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.60](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/compare/@react-navigation/stack@5.0.0-alpha.59...@react-navigation/stack@5.0.0-alpha.60) (2020-01-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure paths aren't aliased when building definitions ([65a5dac](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/commit/65a5dac2bf887f4ba081ab15bd4c9870bb15697f)), closes [#265](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/issues/265)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.59](https://github.com/react-navigation/navigation-ex/tree/master/packages/stack/compare/@react-navigation/stack@5.0.0-alpha.58...@react-navigation/stack@5.0.0-alpha.59) (2020-01-13)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"android",
|
||||
"stack"
|
||||
],
|
||||
"version": "5.0.0-alpha.59",
|
||||
"version": "5.0.0-alpha.63",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/stack",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -21,6 +21,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -29,12 +30,12 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.23",
|
||||
"@react-navigation/routers": "^5.0.0-alpha.27",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.7.1",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@react-native-community/masked-view": "^0.1.5",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.17",
|
||||
@@ -44,7 +45,7 @@
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-gesture-handler": "^1.5.3",
|
||||
"react-native-safe-area-context": "^0.6.2",
|
||||
"react-native-screens": "^2.0.0-alpha.22",
|
||||
"react-native-screens": "^2.0.0-alpha.25",
|
||||
"typescript": "^3.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -62,7 +63,12 @@
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
"typescript"
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +145,9 @@ export function forFade({
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple translate animation to translate the header along with the sliding screen.
|
||||
* Simple translate animation to translate the header to left.
|
||||
*/
|
||||
export function forSlide({
|
||||
export function forSlideLeft({
|
||||
current,
|
||||
next,
|
||||
layouts: { screen },
|
||||
@@ -184,6 +184,83 @@ export function forSlide({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple translate animation to translate the header to right.
|
||||
*/
|
||||
export function forSlideRight({
|
||||
current,
|
||||
next,
|
||||
layouts: { screen },
|
||||
}: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle {
|
||||
const progress = add(
|
||||
current.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
next
|
||||
? next.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
})
|
||||
: 0
|
||||
);
|
||||
|
||||
const translateX = progress.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [screen.width, 0, -screen.width]
|
||||
: [-screen.width, 0, screen.width],
|
||||
});
|
||||
|
||||
const transform = [{ translateX }];
|
||||
|
||||
return {
|
||||
leftButtonStyle: { transform },
|
||||
rightButtonStyle: { transform },
|
||||
titleStyle: { transform },
|
||||
backgroundStyle: { transform },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple translate animation to translate the header to slide up.
|
||||
*/
|
||||
export function forSlideUp({
|
||||
current,
|
||||
next,
|
||||
}: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle {
|
||||
const progress = add(
|
||||
current.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
next
|
||||
? next.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: 'clamp',
|
||||
})
|
||||
: 0
|
||||
);
|
||||
|
||||
const translateY = progress.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: ['-100%', '0%', '-100%'],
|
||||
});
|
||||
|
||||
const transform = [{ translateY }];
|
||||
|
||||
return {
|
||||
leftButtonStyle: { transform },
|
||||
rightButtonStyle: { transform },
|
||||
titleStyle: { transform },
|
||||
backgroundStyle: { transform },
|
||||
};
|
||||
}
|
||||
|
||||
export function forNoAnimation(): StackHeaderInterpolatedStyle {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
forFadeFromBottomAndroid,
|
||||
forModalPresentationIOS,
|
||||
} from './CardStyleInterpolators';
|
||||
import { forNoAnimation, forFade } from './HeaderStyleInterpolators';
|
||||
import { forFade } from './HeaderStyleInterpolators';
|
||||
import {
|
||||
TransitionIOSSpec,
|
||||
ScaleFromCenterAndroidSpec,
|
||||
@@ -42,7 +42,7 @@ export const ModalSlideFromBottomIOS: TransitionPreset = {
|
||||
close: TransitionIOSSpec,
|
||||
},
|
||||
cardStyleInterpolator: forVerticalIOS,
|
||||
headerStyleInterpolator: forNoAnimation,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export const ModalPresentationIOS: TransitionPreset = {
|
||||
close: TransitionIOSSpec,
|
||||
},
|
||||
cardStyleInterpolator: forModalPresentationIOS,
|
||||
headerStyleInterpolator: forNoAnimation,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,7 +68,7 @@ export const FadeFromBottomAndroid: TransitionPreset = {
|
||||
close: FadeOutToBottomAndroidSpec,
|
||||
},
|
||||
cardStyleInterpolator: forFadeFromBottomAndroid,
|
||||
headerStyleInterpolator: forNoAnimation,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ export const RevealFromBottomAndroid: TransitionPreset = {
|
||||
close: RevealFromBottomAndroidSpec,
|
||||
},
|
||||
cardStyleInterpolator: forRevealFromBottomAndroid,
|
||||
headerStyleInterpolator: forNoAnimation,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,7 +94,7 @@ export const ScaleFromCenterAndroid: TransitionPreset = {
|
||||
close: ScaleFromCenterAndroidSpec,
|
||||
},
|
||||
cardStyleInterpolator: forScaleFromCenterAndroid,
|
||||
headerStyleInterpolator: forNoAnimation,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ export { default as StackView } from './views/Stack/StackView';
|
||||
export { default as Header } from './views/Header/Header';
|
||||
export { default as HeaderTitle } from './views/Header/HeaderTitle';
|
||||
export { default as HeaderBackButton } from './views/Header/HeaderBackButton';
|
||||
export { default as HeaderBackground } from './views/Header/HeaderBackground';
|
||||
|
||||
/**
|
||||
* Transition presets
|
||||
|
||||
@@ -42,13 +42,17 @@ function StackNavigator({
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
|
||||
navigation.addListener('tabPress', e => {
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
// Run the operation in the next frame so we're sure all listeners have been run
|
||||
// This is necessary to know if preventDefault() has been called
|
||||
requestAnimationFrame(() => {
|
||||
if (state.index > 0 && isFocused && !e.defaultPrevented) {
|
||||
if (
|
||||
state.index > 0 &&
|
||||
isFocused &&
|
||||
!(e as EventArg<'tabPress', true>).defaultPrevented
|
||||
) {
|
||||
// When user taps on already focused tab and we're inside the tab,
|
||||
// reset the stack to replicate native behaviour
|
||||
navigation.dispatch({
|
||||
|
||||
@@ -20,11 +20,11 @@ export type StackNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when a transition animation starts.
|
||||
*/
|
||||
transitionStart: { closing: boolean };
|
||||
transitionStart: { data: { closing: boolean } };
|
||||
/**
|
||||
* Event which fires when a transition animation ends.
|
||||
*/
|
||||
transitionEnd: { closing: boolean };
|
||||
transitionEnd: { data: { closing: boolean } };
|
||||
};
|
||||
|
||||
export type StackNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -8,6 +8,8 @@ type Props = React.ComponentProps<typeof BaseButton> & {
|
||||
activeOpacity: number;
|
||||
};
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
export default class BorderlessButton extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
activeOpacity: 0.3,
|
||||
@@ -26,7 +28,7 @@ export default class BorderlessButton extends React.Component<Props> {
|
||||
restDisplacementThreshold: 0.01,
|
||||
restSpeedThreshold: 0.01,
|
||||
toValue: active ? this.props.activeOpacity : 1,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import * as React from 'react';
|
||||
import { Animated, StyleSheet, Platform, ViewProps } from 'react-native';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
export default function HeaderBackground({ style, ...rest }: ViewProps) {
|
||||
type Props = ViewProps & {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function HeaderBackground({ style, ...rest }: Props) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,14 +10,17 @@ import { EdgeInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import Header from './Header';
|
||||
import {
|
||||
forSlide,
|
||||
forSlideLeft,
|
||||
forSlideUp,
|
||||
forNoAnimation,
|
||||
forSlideRight,
|
||||
} from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||
import {
|
||||
Layout,
|
||||
Scene,
|
||||
StackHeaderStyleInterpolator,
|
||||
StackNavigationProp,
|
||||
GestureDirection,
|
||||
} from '../../types';
|
||||
|
||||
export type Props = {
|
||||
@@ -34,6 +37,7 @@ export type Props = {
|
||||
height: number;
|
||||
}) => void;
|
||||
styleInterpolator: StackHeaderStyleInterpolator;
|
||||
gestureDirection: GestureDirection;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
@@ -45,6 +49,7 @@ export default function HeaderContainer({
|
||||
state,
|
||||
getPreviousRoute,
|
||||
onContentHeightChange,
|
||||
gestureDirection,
|
||||
styleInterpolator,
|
||||
style,
|
||||
}: Props) {
|
||||
@@ -100,7 +105,12 @@ export default function HeaderContainer({
|
||||
styleInterpolator:
|
||||
mode === 'float'
|
||||
? isHeaderStatic
|
||||
? forSlide
|
||||
? gestureDirection === 'vertical' ||
|
||||
gestureDirection === 'vertical-inverted'
|
||||
? forSlideUp
|
||||
: gestureDirection === 'horizontal-inverted'
|
||||
? forSlideRight
|
||||
: forSlideLeft
|
||||
: styleInterpolator
|
||||
: forNoAnimation,
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ const FALSE = 0;
|
||||
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50;
|
||||
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
export default class Card extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
overlayEnabled: Platform.OS !== 'ios',
|
||||
@@ -164,7 +166,7 @@ export default class Card extends React.Component<Props> {
|
||||
...spec.config,
|
||||
velocity,
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
isInteraction: false,
|
||||
}).start(({ finished }) => {
|
||||
this.handleEndInteraction();
|
||||
@@ -442,7 +444,7 @@ export default class Card extends React.Component<Props> {
|
||||
: { translationX: gesture },
|
||||
},
|
||||
],
|
||||
{ useNativeDriver: true }
|
||||
{ useNativeDriver }
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ function CardContainer({
|
||||
scenes: [previousScene, scene],
|
||||
state,
|
||||
getPreviousRoute,
|
||||
gestureDirection,
|
||||
styleInterpolator: headerStyleInterpolator,
|
||||
onContentHeightChange: onHeaderHeightChange,
|
||||
})
|
||||
|
||||
@@ -75,39 +75,78 @@ type State = {
|
||||
};
|
||||
|
||||
const EPSILON = 1e-5;
|
||||
const FAR_FAR_AWAY = 9000;
|
||||
|
||||
const dimensions = Dimensions.get('window');
|
||||
const layout = { width: dimensions.width, height: dimensions.height };
|
||||
|
||||
const MaybeScreenContainer = ({
|
||||
enabled,
|
||||
style,
|
||||
...rest
|
||||
}: ViewProps & {
|
||||
enabled: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
if (Platform.OS !== 'ios' && enabled && screensEnabled()) {
|
||||
return <ScreenContainer {...rest} />;
|
||||
if (enabled && screensEnabled()) {
|
||||
return <ScreenContainer style={style} {...rest} />;
|
||||
}
|
||||
|
||||
return <View {...rest} />;
|
||||
return (
|
||||
<View
|
||||
collapsable={!enabled}
|
||||
removeClippedSubviews={Platform.OS !== 'ios' && enabled}
|
||||
style={[style, { overflow: 'hidden' }]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MaybeScreen = ({
|
||||
enabled,
|
||||
active,
|
||||
style,
|
||||
...rest
|
||||
}: ViewProps & {
|
||||
enabled: boolean;
|
||||
active: number | Animated.AnimatedInterpolation;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
if (Platform.OS !== 'ios' && enabled && screensEnabled()) {
|
||||
if (enabled && screensEnabled()) {
|
||||
// @ts-ignore
|
||||
return <Screen active={active} {...rest} />;
|
||||
return <Screen active={active} style={style} {...rest} />;
|
||||
}
|
||||
|
||||
return <View {...rest} />;
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
overflow: 'hidden',
|
||||
// Position the screen offscreen to take advantage of offscreen perf optimization
|
||||
// https://facebook.github.io/react-native/docs/view#removeclippedsubviews
|
||||
// This can be useful if screens is not enabled
|
||||
// It's buggy on iOS, so we don't enable it there
|
||||
transform: [
|
||||
{
|
||||
translateY:
|
||||
Platform.OS !== 'ios' && enabled
|
||||
? typeof active === 'number'
|
||||
? active
|
||||
? 0
|
||||
: FAR_FAR_AWAY
|
||||
: active.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [FAR_FAR_AWAY, 0],
|
||||
})
|
||||
: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
|
||||
@@ -396,10 +435,14 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
left = insets.left,
|
||||
} = focusedOptions.safeAreaInsets || {};
|
||||
|
||||
// Screens is buggy on iOS, so we don't enable it there
|
||||
// For modals, usually we want the screen underneath to be visible, so also disable it there
|
||||
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MaybeScreenContainer
|
||||
enabled={mode !== 'modal'}
|
||||
enabled={isScreensEnabled}
|
||||
style={styles.container}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
@@ -490,11 +533,28 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
left: safeAreaInsetLeft = insets.left,
|
||||
} = safeAreaInsets || {};
|
||||
|
||||
const previousRoute = getPreviousRoute({ route: scene.route });
|
||||
|
||||
let previousScene = scenes[index - 1];
|
||||
|
||||
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 = scenes[j];
|
||||
|
||||
if (s && s.route.key === previousRoute.key) {
|
||||
previousScene = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MaybeScreen
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
enabled={mode !== 'modal'}
|
||||
enabled={isScreensEnabled}
|
||||
active={isScreenActive}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
@@ -506,7 +566,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
layout={layout}
|
||||
gesture={gesture}
|
||||
scene={scene}
|
||||
previousScene={scenes[index - 1]}
|
||||
previousScene={previousScene}
|
||||
state={state}
|
||||
safeAreaInsetTop={safeAreaInsetTop}
|
||||
safeAreaInsetRight={safeAreaInsetRight}
|
||||
@@ -548,6 +608,10 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
state,
|
||||
getPreviousRoute,
|
||||
onContentHeightChange: this.handleHeaderLayout,
|
||||
gestureDirection:
|
||||
focusedOptions.gestureDirection !== undefined
|
||||
? focusedOptions.gestureDirection
|
||||
: defaultTransitionPreset.gestureDirection,
|
||||
styleInterpolator:
|
||||
focusedOptions.headerStyleInterpolator !== undefined
|
||||
? focusedOptions.headerStyleInterpolator
|
||||
|
||||
@@ -244,6 +244,7 @@ class StackView extends React.Component<Props, State> {
|
||||
(!closingRouteKeys.includes(r.key) &&
|
||||
!replacingRouteKeys.includes(r.key))
|
||||
);
|
||||
|
||||
const index = routes.findIndex(r => r.key === route.key);
|
||||
|
||||
return routes[index - 1];
|
||||
|
||||
6
packages/stack/tsconfig.build.json
Normal file
6
packages/stack/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@react-navigation/*": [
|
||||
"./packages/*/src"
|
||||
],
|
||||
"use-subscription": ["./typings/use-subscription.d"]
|
||||
"@react-navigation/*": ["./packages/*/src"]
|
||||
},
|
||||
"composite": true,
|
||||
"allowUnreachableCode": false,
|
||||
|
||||
6
typings/use-subscription.d.ts
vendored
6
typings/use-subscription.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
declare module 'use-subscription' {
|
||||
export function useSubscription<T>(options: {
|
||||
getCurrentValue: () => T;
|
||||
subscribe: (callback: (value: T) => void) => () => void;
|
||||
}): T;
|
||||
}
|
||||
45
yarn.lock
45
yarn.lock
@@ -2749,10 +2749,10 @@
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@react-native-community/bob@^0.7.1":
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/bob/-/bob-0.7.1.tgz#481bf46704c5846ab5a9551c90d25a59bfd5bb0c"
|
||||
integrity sha512-lTTRVfk1crR3HT6vxT+JFrhWATy/HVtbMxewkfWqaxiiYtnIc19y/WuguvSyOGpGgDniuGBU62fUVnlD8n7pWg==
|
||||
"@react-native-community/bob@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/bob/-/bob-0.8.0.tgz#8eb33d18393584b2769adde55a042337c5311c80"
|
||||
integrity sha512-b4I3YY/3RrJtsBN7BBE56QGH2/jEoEKeEJqnkXiK4hbEEOnHl6/4mEQTZRM1OX8UUhQy9xHdmFfne+xjhSvs9g==
|
||||
dependencies:
|
||||
"@babel/core" "^7.7.7"
|
||||
chalk "^3.0.0"
|
||||
@@ -3081,6 +3081,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.0.tgz#2a5fa918786d07d3725726f7f650527e1cfeaffd"
|
||||
integrity sha512-c4zji5CjWv1tJxIZkz1oUtGcdOlsH3aza28Nqmm+uNDWBRHoMsjooBEN4czZp1V3iXPihE/VRUOBqg+4Xq0W4g==
|
||||
|
||||
"@types/react-is@^16.7.1":
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-16.7.1.tgz#d3f1c68c358c00ce116b55ef5410cf486dd08539"
|
||||
integrity sha512-dMLFD2cCsxtDgMkTydQCM0PxDq8vwc6uN5M/jRktDfYvH3nQj6pjC9OrCXS2lKlYoYTNJorI/dI8x9dpLshexQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-native-vector-icons@^6.4.5":
|
||||
version "6.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.5.tgz#74cbfc564bd8435e43ad6728572a0e5b49c335d1"
|
||||
@@ -3152,6 +3159,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/untildify/-/untildify-3.0.0.tgz#cd3e6624e46ccf292d3823fb48fa90dda0deaec0"
|
||||
integrity sha512-FTktI3Y1h+gP9GTjTvXBP5v8xpH4RU6uS9POoBcGy4XkS2Np6LNtnP1eiNNth4S7P+qw2c/rugkwBasSHFzJEg==
|
||||
|
||||
"@types/use-subscription@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/use-subscription/-/use-subscription-1.0.0.tgz#d146f8d834f70f50d48bd8246a481d096f11db19"
|
||||
integrity sha512-0WWZ5GUDKMXUY/1zy4Ur5/zsC0s/B+JjXfHdkvx6JgDNZzZV5eW+KKhDqsTGyqX56uh99gwGwbsKbVwkcVIKQA==
|
||||
|
||||
"@types/uuid-js@^0.7.1":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid-js/-/uuid-js-0.7.2.tgz#5b5552fcbaaf4acf026fb6dc66f7e5bd6b4be92f"
|
||||
@@ -7145,6 +7157,13 @@ expo-asset@~8.0.0:
|
||||
path-browserify "^1.0.0"
|
||||
url-parse "^1.4.4"
|
||||
|
||||
expo-blur@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-8.0.0.tgz#79df808b266bda8b6a66d54567abda5738355ad0"
|
||||
integrity sha512-aZhfT2clEc4D3y0tnCb8/mM1c1Gm8Fqk8xHwgJKV869+5lvtRcUKhlMhZTynspVmPzwHgWV7Q9VcOkzGs7N/5g==
|
||||
dependencies:
|
||||
prop-types "^15.6.0"
|
||||
|
||||
expo-cli@^3.11.5:
|
||||
version "3.11.5"
|
||||
resolved "https://registry.yarnpkg.com/expo-cli/-/expo-cli-3.11.5.tgz#428576a5dbacbb94dda18927184bb3ba37a584f6"
|
||||
@@ -13523,7 +13542,7 @@ react-error-overlay@^6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.4.tgz#0d165d6d27488e660bc08e57bdabaad741366f7a"
|
||||
integrity sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA==
|
||||
|
||||
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||
version "16.12.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||
@@ -13543,10 +13562,10 @@ react-native-iphone-x-helper@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.1.tgz#645e2ffbbb49e80844bb4cbbe34a126fda1e6772"
|
||||
integrity sha512-/VbpIEp8tSNNHIvstuA3Swx610whci1Zpc9mqNkqn14DkMbw+ORviln2u0XyHG1kPvvwTNGZY6QpeFwxYaSdbQ==
|
||||
|
||||
react-native-paper@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-3.4.0.tgz#b8d3643b33177ff41a2505aaa801d313defaf707"
|
||||
integrity sha512-QVd6vZ4iJbUqp1OI1DY/0HwBS4y0SVixa2IeBIVyYM4ZZeoeOOlWCXdgtQk7tlf1ju5t1MJunyRGPOXOXTLGNg==
|
||||
react-native-paper@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-3.5.0.tgz#d74db26fa1714911ec1c9187c430726c680f4b52"
|
||||
integrity sha512-W347plzhV/EsVzC8aT8se6s/jmIx1kSF7E4fpbptA950OHxb4gPFFruHZkffHTByLUiZEgKl3nXKqXkRDDp1UA==
|
||||
dependencies:
|
||||
"@callstack/react-theme-provider" "^3.0.5"
|
||||
color "^3.1.2"
|
||||
@@ -13569,10 +13588,10 @@ react-native-safe-area-view@^0.14.6:
|
||||
dependencies:
|
||||
hoist-non-react-statics "^2.3.1"
|
||||
|
||||
react-native-screens@^2.0.0-alpha.22:
|
||||
version "2.0.0-alpha.22"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.0.0-alpha.22.tgz#67cde460153985e400e0280ed3a0e5be07b0dc5a"
|
||||
integrity sha512-2U++QrTf8H989ekHbgFuia8LLd8/+SbXra+rqDAOihCNRLFi91+y5QGgc7DP4Ic9MtHTaYRtWopyfyUo4ybD0A==
|
||||
react-native-screens@^2.0.0-alpha.25:
|
||||
version "2.0.0-alpha.25"
|
||||
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.0.0-alpha.25.tgz#790d273b41d8dde37aa3e43bc662444aff18cd20"
|
||||
integrity sha512-IxKOqPxIWwyJhFOvfkxU/NSFzM5PRiyWWL8g0WCPozVU1KNEtJQp7j0sONkTLGQDkGwLbDu0kuGawT1zXMnE5A==
|
||||
dependencies:
|
||||
debounce "^1.2.0"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user