Compare commits

..

25 Commits

Author SHA1 Message Date
Satyajit Sahoo
ef0f5d6567 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.29
 - @react-navigation/compat@5.0.0-alpha.20
 - @react-navigation/core@5.0.0-alpha.30
 - @react-navigation/drawer@5.0.0-alpha.31
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.28
 - @react-navigation/material-top-tabs@5.0.0-alpha.26
 - @react-navigation/native-stack@5.0.0-alpha.20
 - @react-navigation/native@5.0.0-alpha.22
 - @react-navigation/routers@5.0.0-alpha.19
 - @react-navigation/stack@5.0.0-alpha.47
2020-01-01 13:33:50 +01:00
Satyajit Sahoo
499f66dba4 chore: fix type error 2020-01-01 13:32:50 +01:00
Satyajit Sahoo
2ef2f1a86f refactor: navigate instead of reset when we can when handling links 2020-01-01 13:28:31 +01:00
Satyajit Sahoo
0252bdc222 fix: show error if an action was not handled 2020-01-01 13:12:41 +01:00
Satyajit Sahoo
282f62c258 refactor: use animated instead of reanimated 2020-01-01 12:53:01 +01:00
Satyajit Sahoo
f462d67270 fix: cleanup transaction even if action wasn't handled 2019-12-25 01:41:14 +01:00
Satyajit Sahoo
873afec9fe chore: tweak route names in example 2019-12-19 23:25:58 +01:00
Satyajit Sahoo
878297e52f chore: update eslint config 2019-12-19 23:13:23 +01:00
Satyajit Sahoo
1ea9b4524d chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.28
 - @react-navigation/compat@5.0.0-alpha.19
 - @react-navigation/core@5.0.0-alpha.29
 - @react-navigation/drawer@5.0.0-alpha.30
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.27
 - @react-navigation/material-top-tabs@5.0.0-alpha.25
 - @react-navigation/native-stack@5.0.0-alpha.19
 - @react-navigation/native@5.0.0-alpha.21
 - @react-navigation/routers@5.0.0-alpha.18
 - @react-navigation/stack@5.0.0-alpha.46
2019-12-19 14:45:13 +01:00
Satyajit Sahoo
c52a8c46a8 fix: fix typescript issues 2019-12-19 14:39:35 +01:00
Satyajit Sahoo
0635365483 fix: set screen background in drawer from theme 2019-12-17 00:01:47 +01:00
Satyajit Sahoo
9843b92e05 chore: upgrade deps 2019-12-16 23:42:19 +01:00
Satyajit Sahoo
ebd145a09d fix: fix backgroundColor in sceneContainerStyle overriden by theme
Closes #215
2019-12-16 23:00:12 +01:00
Satyajit Sahoo
9fc1af02c2 refactor: remove extra prop 2019-12-16 15:43:38 +01:00
Satyajit Sahoo
68a334cc93 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.27
 - @react-navigation/compat@5.0.0-alpha.18
 - @react-navigation/core@5.0.0-alpha.28
 - @react-navigation/drawer@5.0.0-alpha.29
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.26
 - @react-navigation/material-top-tabs@5.0.0-alpha.24
 - @react-navigation/native-stack@5.0.0-alpha.18
 - @react-navigation/native@5.0.0-alpha.20
 - @react-navigation/routers@5.0.0-alpha.17
 - @react-navigation/stack@5.0.0-alpha.45
2019-12-16 15:27:11 +01:00
Satyajit Sahoo
c110570d4c fix: disable style interpolation for card when animation is disabled 2019-12-16 15:25:19 +01:00
Satyajit Sahoo
d57226fd8b refactor: remove cardTransparent in favor of cardStyle 2019-12-16 15:09:10 +01:00
Oliver Winter
c3d3748143 fix: use Partial type for initialParam (#206) 2019-12-16 13:58:37 +01:00
Wojciech Lewicki
8002d51795 feat: add nested config in deep linking (#210)
Fixes #154
2019-12-16 13:30:28 +01:00
Erivelton Elias
31cf19912b chore: update README expo and native-stack (#212) 2019-12-16 12:29:31 +01:00
Satyajit Sahoo
d0e4e1f6fb chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.26
 - @react-navigation/drawer@5.0.0-alpha.28
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.25
 - @react-navigation/material-top-tabs@5.0.0-alpha.23
 - @react-navigation/native-stack@5.0.0-alpha.17
 - @react-navigation/native@5.0.0-alpha.19
 - @react-navigation/stack@5.0.0-alpha.44
2019-12-14 22:43:24 +01:00
Satyajit Sahoo
00fc616de0 feat: add custom theme support (#211) 2019-12-14 22:25:25 +01:00
Satyajit Sahoo
703edb3569 chore: fix loading back icon on Android 2019-12-14 06:37:21 +01:00
Satyajit Sahoo
38a38b021a refactor: use function component for bottom tab bar 2019-12-13 17:11:50 +01:00
Satyajit Sahoo
42bc37d2ff chore: update auth flow example 2019-12-12 13:36:43 +01:00
111 changed files with 3115 additions and 2576 deletions

View File

@@ -13,5 +13,8 @@ module.exports = {
'@babel/preset-react', '@babel/preset-react',
'@babel/preset-typescript', '@babel/preset-typescript',
], ],
plugins: ['@babel/plugin-proposal-class-properties'], plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
}; };

View File

@@ -54,6 +54,23 @@ module.exports = {
}, {}), }, {}),
}, },
server: {
enhanceMiddleware: middleware => {
return (req, res, next) => {
const assets = '/packages/stack/src/views/assets';
if (req.url.startsWith(assets)) {
req.url = req.url.replace(
assets,
'/assets/../packages/stack/src/views/assets'
);
}
return middleware(req, res, next);
};
},
},
transformer: { transformer: {
getTransformOptions: async () => ({ getTransformOptions: async () => ({
transform: { transform: {

View File

@@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.0.0",
"@react-native-community/masked-view": "0.1.5", "@react-native-community/masked-view": "0.1.5",
"color": "^3.1.2",
"expo": "^36.0.0", "expo": "^36.0.0",
"expo-asset": "~8.0.0", "expo-asset": "~8.0.0",
"query-string": "^6.9.0", "query-string": "^6.9.0",
@@ -27,24 +28,24 @@
"react-dom": "~16.9.0", "react-dom": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "~1.5.0", "react-native-gesture-handler": "~1.5.0",
"react-native-paper": "^3.2.1", "react-native-paper": "^3.3.0",
"react-native-reanimated": "^1.4.0", "react-native-reanimated": "^1.4.0",
"react-native-safe-area-context": "^0.6.0", "react-native-safe-area-context": "^0.6.0",
"react-native-screens": "^2.0.0-alpha.12", "react-native-screens": "^2.0.0-alpha.19",
"react-native-tab-view": "2.11.0", "react-native-tab-view": "2.11.0",
"react-native-unimodules": "^0.6.0", "react-native-unimodules": "^0.7.0",
"react-native-web": "^0.11.7", "react-native-web": "^0.11.7",
"scheduler": "^0.18.0", "scheduler": "^0.18.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"use-subscription": "^1.3.0" "use-subscription": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.2", "@babel/core": "^7.7.5",
"@expo/webpack-config": "^0.10.1", "@expo/webpack-config": "^0.10.6",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.60.25",
"babel-preset-expo": "^7.1.0", "babel-preset-expo": "^8.0.0",
"expo-cli": "^3.8.0", "expo-cli": "^3.11.1",
"typescript": "^3.7.2" "typescript": "^3.7.3"
} }
} }

View File

@@ -9,11 +9,27 @@ import {
} from '@react-navigation/stack'; } from '@react-navigation/stack';
type AuthStackParams = { type AuthStackParams = {
splash: undefined; Splash: undefined;
home: undefined; Home: undefined;
'sign-in': undefined; SignIn: undefined;
PostSignOut: undefined;
}; };
const AUTH_CONTEXT_ERROR =
'Authentication context not found. Have your wrapped your components with AuthContext.Consumer?';
const AuthContext = React.createContext<{
signIn: () => void;
signOut: () => void;
}>({
signIn: () => {
throw new Error(AUTH_CONTEXT_ERROR);
},
signOut: () => {
throw new Error(AUTH_CONTEXT_ERROR);
},
});
const SplashScreen = () => { const SplashScreen = () => {
return ( return (
<View style={styles.content}> <View style={styles.content}>
@@ -22,27 +38,27 @@ const SplashScreen = () => {
); );
}; };
const SignInScreen = ({ onSignIn }: { onSignIn: (token: string) => void }) => { const SignInScreen = () => {
const { signIn } = React.useContext(AuthContext);
return ( return (
<View style={styles.content}> <View style={styles.content}>
<TextInput placeholder="Username" style={styles.input} /> <TextInput placeholder="Username" style={styles.input} />
<TextInput placeholder="Password" secureTextEntry style={styles.input} /> <TextInput placeholder="Password" secureTextEntry style={styles.input} />
<Button <Button mode="contained" onPress={signIn} style={styles.button}>
mode="contained"
onPress={() => onSignIn('token')}
style={styles.button}
>
Sign in Sign in
</Button> </Button>
</View> </View>
); );
}; };
const HomeScreen = ({ onSignOut }: { onSignOut: () => void }) => { const HomeScreen = () => {
const { signOut } = React.useContext(AuthContext);
return ( return (
<View style={styles.content}> <View style={styles.content}>
<Title style={styles.text}>Signed in successfully 🎉</Title> <Title style={styles.text}>Signed in successfully 🎉</Title>
<Button onPress={onSignOut} style={styles.button}> <Button onPress={signOut} style={styles.button}>
Sign out Sign out
</Button> </Button>
</View> </View>
@@ -105,36 +121,44 @@ export default function SimpleStackScreen({ navigation }: Props) {
headerShown: false, headerShown: false,
}); });
const authContext = React.useMemo(
() => ({
signIn: () => dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' }),
signOut: () => dispatch({ type: 'SIGN_OUT' }),
}),
[]
);
return ( return (
<SimpleStack.Navigator <AuthContext.Provider value={authContext}>
screenOptions={{ <SimpleStack.Navigator
headerLeft: () => ( screenOptions={{
<HeaderBackButton onPress={() => navigation.goBack()} /> headerLeft: () => (
), <HeaderBackButton onPress={() => navigation.goBack()} />
}} ),
> }}
{state.isLoading ? ( >
<SimpleStack.Screen {state.isLoading ? (
name="splash" <SimpleStack.Screen
component={SplashScreen} name="Splash"
options={{ title: `Auth Flow` }} component={SplashScreen}
/> options={{ title: 'Auth Flow' }}
) : state.userToken === undefined ? ( />
<SimpleStack.Screen name="sign-in" options={{ title: `Sign in` }}> ) : state.userToken === undefined ? (
{() => ( <SimpleStack.Screen
<SignInScreen name="SignIn"
onSignIn={token => dispatch({ type: 'SIGN_IN', token })} options={{ title: 'Sign in' }}
/> component={SignInScreen}
)} />
</SimpleStack.Screen> ) : (
) : ( <SimpleStack.Screen
<SimpleStack.Screen name="home" options={{ title: 'Home' }}> name="Home"
{() => ( options={{ title: 'Home' }}
<HomeScreen onSignOut={() => dispatch({ type: 'SIGN_OUT' })} /> component={HomeScreen}
)} />
</SimpleStack.Screen> )}
)} </SimpleStack.Navigator>
</SimpleStack.Navigator> </AuthContext.Provider>
); );
} }

View File

@@ -16,10 +16,10 @@ const getTabBarIcon = (name: string) => ({
}) => <MaterialCommunityIcons name={name} color={color} size={size} />; }) => <MaterialCommunityIcons name={name} color={color} size={size} />;
type BottomTabParams = { type BottomTabParams = {
article: undefined; Article: undefined;
albums: undefined; Albums: undefined;
contacts: undefined; Contacts: undefined;
chat: undefined; Chat: undefined;
}; };
const BottomTabs = createBottomTabNavigator<BottomTabParams>(); const BottomTabs = createBottomTabNavigator<BottomTabParams>();
@@ -32,7 +32,7 @@ export default function BottomTabsScreen() {
}} }}
> >
<BottomTabs.Screen <BottomTabs.Screen
name="article" name="Article"
options={{ options={{
title: 'Article', title: 'Article',
tabBarIcon: getTabBarIcon('file-document-box'), tabBarIcon: getTabBarIcon('file-document-box'),
@@ -41,7 +41,7 @@ export default function BottomTabsScreen() {
{props => <SimpleStackScreen {...props} headerMode="none" />} {props => <SimpleStackScreen {...props} headerMode="none" />}
</BottomTabs.Screen> </BottomTabs.Screen>
<BottomTabs.Screen <BottomTabs.Screen
name="chat" name="Chat"
component={Chat} component={Chat}
options={{ options={{
tabBarLabel: 'Chat', tabBarLabel: 'Chat',
@@ -49,7 +49,7 @@ export default function BottomTabsScreen() {
}} }}
/> />
<BottomTabs.Screen <BottomTabs.Screen
name="contacts" name="Contacts"
component={Contacts} component={Contacts}
options={{ options={{
title: 'Contacts', title: 'Contacts',
@@ -57,7 +57,7 @@ export default function BottomTabsScreen() {
}} }}
/> />
<BottomTabs.Screen <BottomTabs.Screen
name="albums" name="Albums"
component={Albums} component={Albums}
options={{ options={{
title: 'Albums', title: 'Albums',

View File

@@ -7,10 +7,10 @@ import Chat from '../Shared/Chat';
import SimpleStackScreen from './SimpleStack'; import SimpleStackScreen from './SimpleStack';
type MaterialBottomTabParams = { type MaterialBottomTabParams = {
article: undefined; Article: undefined;
albums: undefined; Albums: undefined;
contacts: undefined; Contacts: undefined;
chat: undefined; Chat: undefined;
}; };
const MaterialBottomTabs = createMaterialBottomTabNavigator< const MaterialBottomTabs = createMaterialBottomTabNavigator<
@@ -21,7 +21,7 @@ export default function MaterialBottomTabsScreen() {
return ( return (
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}> <MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="article" name="Article"
options={{ options={{
tabBarLabel: 'Article', tabBarLabel: 'Article',
tabBarIcon: 'file-document-box', tabBarIcon: 'file-document-box',
@@ -31,7 +31,7 @@ export default function MaterialBottomTabsScreen() {
{props => <SimpleStackScreen {...props} headerMode="none" />} {props => <SimpleStackScreen {...props} headerMode="none" />}
</MaterialBottomTabs.Screen> </MaterialBottomTabs.Screen>
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="chat" name="Chat"
component={Chat} component={Chat}
options={{ options={{
tabBarLabel: 'Chat', tabBarLabel: 'Chat',
@@ -41,7 +41,7 @@ export default function MaterialBottomTabsScreen() {
}} }}
/> />
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="contacts" name="Contacts"
component={Contacts} component={Contacts}
options={{ options={{
tabBarLabel: 'Contacts', tabBarLabel: 'Contacts',
@@ -50,7 +50,7 @@ export default function MaterialBottomTabsScreen() {
}} }}
/> />
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="albums" name="Albums"
component={Albums} component={Albums}
options={{ options={{
tabBarLabel: 'Albums', tabBarLabel: 'Albums',

View File

@@ -1,54 +1,35 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet } from 'react-native';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts'; import Contacts from '../Shared/Contacts';
import Chat from '../Shared/Chat'; import Chat from '../Shared/Chat';
type MaterialTopTabParams = { type MaterialTopTabParams = {
albums: undefined; Albums: undefined;
contacts: undefined; Contacts: undefined;
chat: undefined; Chat: undefined;
}; };
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>(); const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
export default function MaterialTopTabsScreen() { export default function MaterialTopTabsScreen() {
return ( return (
<MaterialTopTabs.Navigator <MaterialTopTabs.Navigator>
tabBarOptions={{
style: styles.tabBar,
labelStyle: styles.tabLabel,
indicatorStyle: styles.tabIndicator,
}}
>
<MaterialTopTabs.Screen <MaterialTopTabs.Screen
name="chat" name="Chat"
component={Chat} component={Chat}
options={{ title: 'Chat' }} options={{ title: 'Chat' }}
/> />
<MaterialTopTabs.Screen <MaterialTopTabs.Screen
name="contacts" name="Contacts"
component={Contacts} component={Contacts}
options={{ title: 'Contacts' }} options={{ title: 'Contacts' }}
/> />
<MaterialTopTabs.Screen <MaterialTopTabs.Screen
name="albums" name="Albums"
component={Albums} component={Albums}
options={{ title: 'Albums' }} options={{ title: 'Albums' }}
/> />
</MaterialTopTabs.Navigator> </MaterialTopTabs.Navigator>
); );
} }
const styles = StyleSheet.create({
tabBar: {
backgroundColor: 'white',
},
tabLabel: {
color: 'black',
},
tabIndicator: {
backgroundColor: 'tomato',
},
});

View File

@@ -11,19 +11,19 @@ import {
import Article from '../Shared/Article'; import Article from '../Shared/Article';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
type SimpleStackParams = { type ModalStackParams = {
article: { author: string }; Article: { author: string };
album: undefined; Album: undefined;
}; };
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
}: { }: {
navigation: SimpleStackNavigation; navigation: ModalStackNavigation;
route: RouteProp<SimpleStackParams, 'article'>; route: RouteProp<ModalStackParams, 'Article'>;
}) => { }) => {
const insets = useSafeArea(); const insets = useSafeArea();
@@ -32,7 +32,7 @@ const ArticleScreen = ({
<View style={[styles.buttons, { marginTop: insets.top }]}> <View style={[styles.buttons, { marginTop: insets.top }]}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('album')} onPress={() => navigation.push('Album')}
style={styles.button} style={styles.button}
> >
Push album Push album
@@ -50,11 +50,7 @@ const ArticleScreen = ({
); );
}; };
const AlbumsScreen = ({ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
const insets = useSafeArea(); const insets = useSafeArea();
return ( return (
@@ -62,7 +58,7 @@ const AlbumsScreen = ({
<View style={[styles.buttons, { marginTop: insets.top }]}> <View style={[styles.buttons, { marginTop: insets.top }]}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('article', { author: 'Babel fish' })} onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button} style={styles.button}
> >
Push article Push article
@@ -80,7 +76,7 @@ const AlbumsScreen = ({
); );
}; };
const ModalPresentationStack = createStackNavigator<SimpleStackParams>(); const ModalPresentationStack = createStackNavigator<ModalStackParams>();
type Props = { type Props = {
options?: React.ComponentProps<typeof ModalPresentationStack.Navigator>; options?: React.ComponentProps<typeof ModalPresentationStack.Navigator>;
@@ -104,7 +100,7 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
{...options} {...options}
> >
<ModalPresentationStack.Screen <ModalPresentationStack.Screen
name="article" name="Article"
component={ArticleScreen} component={ArticleScreen}
options={({ route }) => ({ options={({ route }) => ({
title: `Article by ${route.params.author}`, title: `Article by ${route.params.author}`,
@@ -112,7 +108,7 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
initialParams={{ author: 'Gandalf' }} initialParams={{ author: 'Gandalf' }}
/> />
<ModalPresentationStack.Screen <ModalPresentationStack.Screen
name="album" name="Album"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Album' }}
/> />

View File

@@ -7,6 +7,7 @@ import {
RouteProp, RouteProp,
ParamListBase, ParamListBase,
useFocusEffect, useFocusEffect,
useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { DrawerNavigationProp } from '@react-navigation/drawer'; import { DrawerNavigationProp } from '@react-navigation/drawer';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
@@ -17,96 +18,119 @@ import {
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
type NativeStackParams = { type NativeStackParams = {
article: { author: string }; Article: { author: string };
album: undefined; Album: undefined;
}; };
type NativeStackNavigation = NativeStackNavigationProp<NativeStackParams>; type NativeStackNavigation = NativeStackNavigationProp<NativeStackParams>;
const Title = ({ children }: { children: React.ReactNode }) => {
const { colors } = useTheme();
return <Text style={[styles.title, { color: colors.text }]}>{children}</Text>;
};
const Paragraph = ({ children }: { children: React.ReactNode }) => {
const { colors } = useTheme();
return (
<Text style={[styles.paragraph, { color: colors.text }]}>{children}</Text>
);
};
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
}: { }: {
navigation: NativeStackNavigation; navigation: NativeStackNavigation;
route: RouteProp<NativeStackParams, 'article'>; route: RouteProp<NativeStackParams, 'Article'>;
}) => ( }) => {
<ScrollView style={styles.container} contentContainerStyle={styles.content}> const { colors } = useTheme();
<View style={styles.buttons}>
<Button return (
mode="contained" <ScrollView
onPress={() => navigation.push('album')} style={{ backgroundColor: colors.card }}
style={styles.button} contentContainerStyle={styles.content}
> >
Push album <View style={styles.buttons}>
</Button> <Button
<Button mode="contained"
mode="outlined" onPress={() => navigation.push('Album')}
onPress={() => navigation.goBack()} style={styles.button}
style={styles.button} >
> Push album
Go back </Button>
</Button> <Button
</View> mode="outlined"
<Text style={styles.title}>What is Lorem Ipsum?</Text> onPress={() => navigation.goBack()}
<Text style={styles.paragraph}> style={styles.button}
Lorem Ipsum is simply dummy text of the printing and typesetting industry. >
Lorem Ipsum has been the industry&apos;s standard dummy text ever since Go back
the 1500s, when an unknown printer took a galley of type and scrambled it </Button>
to make a type specimen book. It has survived not only five centuries, but </View>
also the leap into electronic typesetting, remaining essentially <Title>What is Lorem Ipsum?</Title>
unchanged. It was popularised in the 1960s with the release of Letraset <Paragraph>
sheets containing Lorem Ipsum passages, and more recently with desktop Lorem Ipsum is simply dummy text of the printing and typesetting
publishing software like Aldus PageMaker including versions of Lorem industry. Lorem Ipsum has been the industry&apos;s standard dummy text
Ipsum. ever since the 1500s, when an unknown printer took a galley of type and
</Text> scrambled it to make a type specimen book. It has survived not only five
<Text style={styles.title}>Where does it come from?</Text> centuries, but also the leap into electronic typesetting, remaining
<Text style={styles.paragraph}> essentially unchanged. It was popularised in the 1960s with the release
Contrary to popular belief, Lorem Ipsum is not simply random text. It has of Letraset sheets containing Lorem Ipsum passages, and more recently
roots in a piece of classical Latin literature from 45 BC, making it over with desktop publishing software like Aldus PageMaker including versions
2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney of Lorem Ipsum.
College in Virginia, looked up one of the more obscure Latin words, </Paragraph>
consectetur, from a Lorem Ipsum passage, and going through the cites of <Title>Where does it come from?</Title>
the word in classical literature, discovered the undoubtable source. Lorem <Paragraph>
Ipsum comes from sections 1.10.32 and 1.10.33 of &quot;de Finibus Bonorum Contrary to popular belief, Lorem Ipsum is not simply random text. It
et Malorum&quot; (The Extremes of Good and Evil) by Cicero, written in 45 has roots in a piece of classical Latin literature from 45 BC, making it
BC. This book is a treatise on the theory of ethics, very popular during over 2000 years old. Richard McClintock, a Latin professor at
the Renaissance. The first line of Lorem Ipsum, &quot;Lorem ipsum dolor Hampden-Sydney College in Virginia, looked up one of the more obscure
sit amet..&quot;, comes from a line in section 1.10.32. Latin words, consectetur, from a Lorem Ipsum passage, and going through
</Text> the cites of the word in classical literature, discovered the
<Text style={styles.paragraph}> undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below of &quot;de Finibus Bonorum et Malorum&quot; (The Extremes of Good and
for those interested. Sections 1.10.32 and 1.10.33 from &quot;de Finibus Evil) by Cicero, written in 45 BC. This book is a treatise on the theory
Bonorum et Malorum&quot; by Cicero are also reproduced in their exact of ethics, very popular during the Renaissance. The first line of Lorem
original form, accompanied by English versions from the 1914 translation Ipsum, &quot;Lorem ipsum dolor sit amet..&quot;, comes from a line in
by H. Rackham. section 1.10.32.
</Text> </Paragraph>
<Text style={styles.title}>Why do we use it?</Text> <Paragraph>
<Text style={styles.paragraph}> The standard chunk of Lorem Ipsum used since the 1500s is reproduced
It is a long established fact that a reader will be distracted by the below for those interested. Sections 1.10.32 and 1.10.33 from &quot;de
readable content of a page when looking at its layout. The point of using Finibus Bonorum et Malorum&quot; by Cicero are also reproduced in their
Lorem Ipsum is that it has a more-or-less normal distribution of letters, exact original form, accompanied by English versions from the 1914
as opposed to using &quot;Content here, content here&quot;, making it look translation by H. Rackham.
like readable English. Many desktop publishing packages and web page </Paragraph>
editors now use Lorem Ipsum as their default model text, and a search for <Title>Why do we use it?</Title>
&quot;lorem ipsum&quot; will uncover many web sites still in their <Paragraph>
infancy. Various versions have evolved over the years, sometimes by It is a long established fact that a reader will be distracted by the
accident, sometimes on purpose (injected humour and the like). readable content of a page when looking at its layout. The point of
</Text> using Lorem Ipsum is that it has a more-or-less normal distribution of
<Text style={styles.title}>Where can I get some?</Text> letters, as opposed to using &quot;Content here, content here&quot;,
<Text style={styles.paragraph}> making it look like readable English. Many desktop publishing packages
There are many variations of passages of Lorem Ipsum available, but the and web page editors now use Lorem Ipsum as their default model text,
majority have suffered alteration in some form, by injected humour, or and a search for &quot;lorem ipsum&quot; will uncover many web sites
randomised words which don&apos;t look even slightly believable. If you still in their infancy. Various versions have evolved over the years,
are going to use a passage of Lorem Ipsum, you need to be sure there sometimes by accident, sometimes on purpose (injected humour and the
isn&apos;t anything embarrassing hidden in the middle of text. All the like).
Lorem Ipsum generators on the Internet tend to repeat predefined chunks as </Paragraph>
necessary, making this the first true generator on the Internet. It uses a <Title>Where can I get some?</Title>
dictionary of over 200 Latin words, combined with a handful of model <Paragraph>
sentence structures, to generate Lorem Ipsum which looks reasonable. The There are many variations of passages of Lorem Ipsum available, but the
generated Lorem Ipsum is therefore always free from repetition, injected majority have suffered alteration in some form, by injected humour, or
humour, or non-characteristic words etc. randomised words which don&apos;t look even slightly believable. If you
</Text> are going to use a passage of Lorem Ipsum, you need to be sure there
</ScrollView> isn&apos;t anything embarrassing hidden in the middle of text. All the
); Lorem Ipsum generators on the Internet tend to repeat predefined chunks
as necessary, making this the first true generator on the Internet. It
uses a dictionary of over 200 Latin words, combined with a handful of
model sentence structures, to generate Lorem Ipsum which looks
reasonable. The generated Lorem Ipsum is therefore always free from
repetition, injected humour, or non-characteristic words etc.
</Paragraph>
</ScrollView>
);
};
const AlbumsScreen = ({ const AlbumsScreen = ({
navigation, navigation,
@@ -117,7 +141,7 @@ const AlbumsScreen = ({
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('article', { author: 'Babel fish' })} onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button} style={styles.button}
> >
Push article Push article
@@ -164,7 +188,7 @@ export default function NativeStackScreen({ navigation }: Props) {
return ( return (
<NativeStack.Navigator> <NativeStack.Navigator>
<NativeStack.Screen <NativeStack.Screen
name="article" name="Article"
component={ArticleScreen} component={ArticleScreen}
options={{ options={{
title: 'Lorem Ipsum', title: 'Lorem Ipsum',
@@ -173,7 +197,7 @@ export default function NativeStackScreen({ navigation }: Props) {
}} }}
/> />
<NativeStack.Screen <NativeStack.Screen
name="album" name="Album"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Album' }}
/> />
@@ -191,21 +215,16 @@ const styles = StyleSheet.create({
button: { button: {
margin: 8, margin: 8,
}, },
container: {
backgroundColor: 'white',
},
content: { content: {
paddingVertical: 16, paddingVertical: 16,
}, },
title: { title: {
color: '#000',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 24, fontSize: 24,
marginVertical: 8, marginVertical: 8,
marginHorizontal: 16, marginHorizontal: 16,
}, },
paragraph: { paragraph: {
color: '#000',
fontSize: 16, fontSize: 16,
lineHeight: 24, lineHeight: 24,
marginVertical: 8, marginVertical: 8,

View File

@@ -10,8 +10,8 @@ import Article from '../Shared/Article';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
type SimpleStackParams = { type SimpleStackParams = {
article: { author: string }; Article: { author: string };
album: undefined; Album: undefined;
}; };
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
@@ -21,14 +21,14 @@ const ArticleScreen = ({
route, route,
}: { }: {
navigation: SimpleStackNavigation; navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'article'>; route: RouteProp<SimpleStackParams, 'Article'>;
}) => { }) => {
return ( return (
<React.Fragment> <React.Fragment>
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('album')} onPress={() => navigation.push('Album')}
style={styles.button} style={styles.button}
> >
Push album Push album
@@ -56,7 +56,7 @@ const AlbumsScreen = ({
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('article', { author: 'Babel fish' })} onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button} style={styles.button}
> >
Push article Push article
@@ -88,7 +88,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
return ( return (
<SimpleStack.Navigator {...rest}> <SimpleStack.Navigator {...rest}>
<SimpleStack.Screen <SimpleStack.Screen
name="article" name="Article"
component={ArticleScreen} component={ArticleScreen}
options={({ route }) => ({ options={({ route }) => ({
title: `Article by ${route.params.author}`, title: `Article by ${route.params.author}`,
@@ -96,7 +96,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
initialParams={{ author: 'Gandalf' }} initialParams={{ author: 'Gandalf' }}
/> />
<SimpleStack.Screen <SimpleStack.Screen
name="album" name="Album"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Album' }}
/> />

View File

@@ -1,3 +1,5 @@
/* eslint-disable import/no-commonjs */
import * as React from 'react'; import * as React from 'react';
import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native'; import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native';
import { useScrollToTop } from '@react-navigation/native'; import { useScrollToTop } from '@react-navigation/native';
@@ -38,7 +40,7 @@ export default function Albums() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
backgroundColor: '#343C46', backgroundColor: '#000',
}, },
content: { content: {
flexDirection: 'row', flexDirection: 'row',

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { View, Text, Image, ScrollView, StyleSheet } from 'react-native'; import { View, Text, Image, ScrollView, StyleSheet } from 'react-native';
import { useScrollToTop } from '@react-navigation/native'; import { useScrollToTop, useTheme } from '@react-navigation/native';
type Props = { type Props = {
date?: string; date?: string;
@@ -19,10 +19,12 @@ export default function Article({
useScrollToTop(ref); useScrollToTop(ref);
const { colors } = useTheme();
return ( return (
<ScrollView <ScrollView
ref={ref} ref={ref}
style={styles.container} style={{ backgroundColor: colors.card }}
contentContainerStyle={styles.content} contentContainerStyle={styles.content}
> >
<View style={styles.author}> <View style={styles.author}>
@@ -31,24 +33,26 @@ export default function Article({
source={require('../../assets/avatar-1.png')} source={require('../../assets/avatar-1.png')}
/> />
<View style={styles.meta}> <View style={styles.meta}>
<Text style={styles.name}>{author.name}</Text> <Text style={[styles.name, { color: colors.text }]}>
<Text style={styles.timestamp}>{date}</Text> {author.name}
</Text>
<Text style={[styles.timestamp, { color: colors.text }]}>{date}</Text>
</View> </View>
</View> </View>
<Text style={styles.title}>Lorem Ipsum</Text> <Text style={[styles.title, { color: colors.text }]}>Lorem Ipsum</Text>
<Text style={styles.paragraph}> <Text style={[styles.paragraph, { color: colors.text }]}>
Contrary to popular belief, Lorem Ipsum is not simply random text. It Contrary to popular belief, Lorem Ipsum is not simply random text. It
has roots in a piece of classical Latin literature from 45 BC, making it has roots in a piece of classical Latin literature from 45 BC, making it
over 2000 years old. over 2000 years old.
</Text> </Text>
<Image style={styles.image} source={require('../../assets/book.jpg')} /> <Image style={styles.image} source={require('../../assets/book.jpg')} />
<Text style={styles.paragraph}> <Text style={[styles.paragraph, { color: colors.text }]}>
Richard McClintock, a Latin professor at Hampden-Sydney College in Richard McClintock, a Latin professor at Hampden-Sydney College in
Virginia, looked up one of the more obscure Latin words, consectetur, Virginia, looked up one of the more obscure Latin words, consectetur,
from a Lorem Ipsum passage, and going through the cites of the word in from a Lorem Ipsum passage, and going through the cites of the word in
classical literature, discovered the undoubtable source. classical literature, discovered the undoubtable source.
</Text> </Text>
<Text style={styles.paragraph}> <Text style={[styles.paragraph, { color: colors.text }]}>
Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of &quot;de Finibus Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of &quot;de Finibus
Bonorum et Malorum&quot; (The Extremes of Good and Evil) by Cicero, Bonorum et Malorum&quot; (The Extremes of Good and Evil) by Cicero,
written in 45 BC. This book is a treatise on the theory of ethics, very written in 45 BC. This book is a treatise on the theory of ethics, very
@@ -61,9 +65,6 @@ export default function Article({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
},
content: { content: {
paddingVertical: 16, paddingVertical: 16,
}, },
@@ -77,13 +78,12 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
}, },
name: { name: {
color: '#000',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 16, fontSize: 16,
lineHeight: 24, lineHeight: 24,
}, },
timestamp: { timestamp: {
color: '#999', opacity: 0.5,
fontSize: 14, fontSize: 14,
lineHeight: 21, lineHeight: 21,
}, },
@@ -93,14 +93,12 @@ const styles = StyleSheet.create({
borderRadius: 24, borderRadius: 24,
}, },
title: { title: {
color: '#000',
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 36, fontSize: 36,
marginVertical: 8, marginVertical: 8,
marginHorizontal: 16, marginHorizontal: 16,
}, },
paragraph: { paragraph: {
color: '#000',
fontSize: 16, fontSize: 16,
lineHeight: 24, lineHeight: 24,
marginVertical: 8, marginVertical: 8,

View File

@@ -7,7 +7,8 @@ import {
ScrollView, ScrollView,
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import { useScrollToTop } from '@react-navigation/native'; import { useScrollToTop, useTheme } from '@react-navigation/native';
import Color from 'color';
const MESSAGES = [ const MESSAGES = [
'okay', 'okay',
@@ -21,6 +22,8 @@ export default function Chat() {
useScrollToTop(ref); useScrollToTop(ref);
const { colors } = useTheme();
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView <ScrollView
@@ -45,9 +48,12 @@ export default function Chat() {
} }
/> />
<View <View
style={[styles.bubble, odd ? styles.received : styles.sent]} style={[
styles.bubble,
{ backgroundColor: odd ? colors.primary : colors.card },
]}
> >
<Text style={odd ? styles.receivedText : styles.sentText}> <Text style={{ color: odd ? 'white' : colors.text }}>
{text} {text}
</Text> </Text>
</View> </View>
@@ -56,7 +62,14 @@ export default function Chat() {
})} })}
</ScrollView> </ScrollView>
<TextInput <TextInput
style={styles.input} style={[
styles.input,
{ backgroundColor: colors.card, color: colors.text },
]}
placeholderTextColor={Color(colors.text)
.alpha(0.5)
.rgb()
.string()}
placeholder="Write a message" placeholder="Write a message"
underlineColorAndroid="transparent" underlineColorAndroid="transparent"
/> />
@@ -67,7 +80,6 @@ export default function Chat() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#eceff1',
}, },
inverted: { inverted: {
transform: [{ scaleY: -1 }], transform: [{ scaleY: -1 }],
@@ -97,22 +109,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
borderRadius: 20, borderRadius: 20,
}, },
sent: {
backgroundColor: '#cfd8dc',
},
received: {
backgroundColor: '#2196F3',
},
sentText: {
color: 'black',
},
receivedText: {
color: 'white',
},
input: { input: {
height: 48, height: 48,
paddingVertical: 12, paddingVertical: 12,
paddingHorizontal: 24, paddingHorizontal: 24,
backgroundColor: 'white',
}, },
}); });

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native'; import { View, Text, FlatList, StyleSheet } from 'react-native';
import { useScrollToTop } from '@react-navigation/native'; import { useScrollToTop, useTheme } from '@react-navigation/native';
type Item = { name: string; number: number }; type Item = { name: string; number: number };
@@ -57,27 +57,35 @@ const CONTACTS: Item[] = [
{ name: 'Vincent Sandoval', number: 2606111495 }, { name: 'Vincent Sandoval', number: 2606111495 },
]; ];
class ContactItem extends React.PureComponent<{ const ContactItem = React.memo(
item: { name: string; number: number }; ({ item }: { item: { name: string; number: number } }) => {
}> { const { colors } = useTheme();
render() {
const { item } = this.props;
return ( return (
<View style={styles.item}> <View style={[styles.item, { backgroundColor: colors.card }]}>
<View style={styles.avatar}> <View style={styles.avatar}>
<Text style={styles.letter}> <Text style={styles.letter}>
{item.name.slice(0, 1).toUpperCase()} {item.name.slice(0, 1).toUpperCase()}
</Text> </Text>
</View> </View>
<View style={styles.details}> <View style={styles.details}>
<Text style={styles.name}>{item.name}</Text> <Text style={[styles.name, { color: colors.text }]}>{item.name}</Text>
<Text style={styles.number}>{item.number}</Text> <Text style={[styles.number, { color: colors.text, opacity: 0.5 }]}>
{item.number}
</Text>
</View> </View>
</View> </View>
); );
} }
} );
const ItemSeparator = () => {
const { colors } = useTheme();
return (
<View style={[styles.separator, { backgroundColor: colors.border }]} />
);
};
export default function Contacts() { export default function Contacts() {
const ref = React.useRef<FlatList<Item>>(null); const ref = React.useRef<FlatList<Item>>(null);
@@ -86,8 +94,6 @@ export default function Contacts() {
const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />; const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />;
const ItemSeparator = () => <View style={styles.separator} />;
return ( return (
<FlatList <FlatList
ref={ref} ref={ref}
@@ -101,7 +107,6 @@ export default function Contacts() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
item: { item: {
backgroundColor: 'white',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 8, padding: 8,
@@ -124,14 +129,11 @@ const styles = StyleSheet.create({
name: { name: {
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 14, fontSize: 14,
color: 'black',
}, },
number: { number: {
fontSize: 12, fontSize: 12,
color: '#999',
}, },
separator: { separator: {
height: StyleSheet.hairlineWidth, height: StyleSheet.hairlineWidth,
backgroundColor: 'rgba(0, 0, 0, .08)',
}, },
}); });

View File

@@ -1,14 +1,31 @@
import * as React from 'react'; import * as React from 'react';
import { ScrollView, AsyncStorage, YellowBox } from 'react-native'; import {
View,
ScrollView,
AsyncStorage,
YellowBox,
Platform,
StatusBar,
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { Appbar, List } from 'react-native-paper'; import {
Provider as PaperProvider,
DefaultTheme as PaperLightTheme,
DarkTheme as PaperDarkTheme,
Subheading,
Appbar,
List,
Switch,
Divider,
} from 'react-native-paper';
import { Asset } from 'expo-asset'; import { Asset } from 'expo-asset';
import { import {
InitialState, InitialState,
getStateFromPath,
useLinking, useLinking,
NavigationContainerRef, NavigationContainerRef,
NavigationNativeContainer, NavigationNativeContainer,
DefaultTheme,
DarkTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { import {
createDrawerNavigator, createDrawerNavigator,
@@ -72,7 +89,8 @@ const SCREENS = {
const Drawer = createDrawerNavigator<RootDrawerParamList>(); const Drawer = createDrawerNavigator<RootDrawerParamList>();
const Stack = createStackNavigator<RootStackParamList>(); const Stack = createStackNavigator<RootStackParamList>();
const PERSISTENCE_KEY = 'NAVIGATION_STATE'; const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets); Asset.loadAsync(StackAssets);
@@ -85,23 +103,24 @@ export default function App() {
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`) // The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
const { getInitialState } = useLinking(containerRef, { const { getInitialState } = useLinking(containerRef, {
prefixes: LinkingPrefixes, prefixes: LinkingPrefixes,
getStateFromPath: path => { config: {
const state = getStateFromPath(path); Root: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
acc[name] = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
return { return acc;
routes: [ },
{ {}
name: 'root', ),
state: {
...state,
routes: [{ name: 'home' }, ...(state ? state.routes : [])],
},
},
],
};
}, },
}); });
const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState< const [initialState, setInitialState] = React.useState<
InitialState | undefined InitialState | undefined
@@ -113,7 +132,9 @@ export default function App() {
let state = await getInitialState(); let state = await getInitialState();
if (state === undefined) { if (state === undefined) {
const savedState = await AsyncStorage.getItem(PERSISTENCE_KEY); const savedState = await AsyncStorage.getItem(
NAVIGATION_PERSISTENCE_KEY
);
state = savedState ? JSON.parse(savedState) : undefined; state = savedState ? JSON.parse(savedState) : undefined;
} }
@@ -121,6 +142,14 @@ export default function App() {
setInitialState(state); setInitialState(state);
} }
} finally { } finally {
try {
const themeName = await AsyncStorage.getItem(THEME_PERSISTENCE_KEY);
setTheme(themeName === 'dark' ? DarkTheme : DefaultTheme);
} catch (e) {
// Ignore
}
setIsReady(true); setIsReady(true);
} }
}; };
@@ -128,78 +157,126 @@ export default function App() {
restoreState(); restoreState();
}, [getInitialState]); }, [getInitialState]);
const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
return {
...t,
colors: {
...t.colors,
...theme.colors,
surface: theme.colors.card,
accent: theme.dark ? 'rgb(255, 55, 95)' : 'rgb(255, 45, 85)',
},
};
}, [theme.colors, theme.dark]);
if (!isReady) { if (!isReady) {
return null; return null;
} }
return ( return (
<NavigationNativeContainer <PaperProvider theme={paperTheme}>
ref={containerRef} {Platform.OS === 'ios' && (
initialState={initialState} <StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
onStateChange={state => )}
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state)) <NavigationNativeContainer
} ref={containerRef}
> initialState={initialState}
<Drawer.Navigator> onStateChange={state =>
<Drawer.Screen AsyncStorage.setItem(
name="Root" NAVIGATION_PERSISTENCE_KEY,
options={{ JSON.stringify(state)
title: 'Examples', )
drawerIcon: ({ size, color }) => ( }
<MaterialIcons size={size} color={color} name="folder" /> theme={theme}
), >
}} <Drawer.Navigator>
> <Drawer.Screen
{({ name="Root"
navigation, options={{
}: { title: 'Examples',
navigation: DrawerNavigationProp<RootDrawerParamList>; drawerIcon: ({ size, color }) => (
}) => ( <MaterialIcons size={size} color={color} name="folder" />
<Stack.Navigator> ),
<Stack.Screen }}
name="Home" >
options={{ {({
title: 'Examples', navigation,
headerLeft: () => ( }: {
<Appbar.Action navigation: DrawerNavigationProp<RootDrawerParamList>;
icon="menu" }) => (
onPress={() => navigation.toggleDrawer()} <Stack.Navigator>
/> <Stack.Screen
), name="Home"
}} options={{
> title: 'Examples',
{({ headerLeft: () => (
navigation, <Appbar.Action
}: { color={theme.colors.text}
navigation: StackNavigationProp<RootStackParamList>; icon="menu"
}) => ( onPress={() => navigation.toggleDrawer()}
<ScrollView> />
{(Object.keys(SCREENS) as Array<keyof typeof SCREENS>).map( ),
name => ( }}
<List.Item >
key={name} {({
title={SCREENS[name].title} navigation,
onPress={() => navigation.push(name)} }: {
navigation: StackNavigationProp<RootStackParamList>;
}) => (
<ScrollView
style={{ backgroundColor: theme.colors.background }}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 16,
}}
>
<Subheading>Dark theme</Subheading>
<Switch
value={theme.dark}
onValueChange={() => {
AsyncStorage.setItem(
THEME_PERSISTENCE_KEY,
theme.dark ? 'light' : 'dark'
);
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
}}
/> />
) </View>
)} <Divider />
</ScrollView> {(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => (
<List.Item
key={name}
title={SCREENS[name].title}
onPress={() => navigation.push(name)}
/>
)
)}
</ScrollView>
)}
</Stack.Screen>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
name => (
<Stack.Screen
key={name}
name={name}
component={SCREENS[name].component}
options={{ title: SCREENS[name].title }}
/>
)
)} )}
</Stack.Screen> </Stack.Navigator>
{(Object.keys(SCREENS) as Array<keyof typeof SCREENS>).map( )}
name => ( </Drawer.Screen>
<Stack.Screen </Drawer.Navigator>
key={name} </NavigationNativeContainer>
name={name} </PaperProvider>
component={SCREENS[name].component}
options={{ title: SCREENS[name].title }}
/>
)
)}
</Stack.Navigator>
)}
</Drawer.Screen>
</Drawer.Navigator>
</NavigationNativeContainer>
); );
} }

View File

@@ -2,7 +2,7 @@ const error = console.error;
console.error = (...args) => console.error = (...args) =>
// Supress error messages regarding error boundary in tests // Supress error messages regarding error boundary in tests
/Consider adding an error boundary to your tree to customize error handling behavior/m.test( /(Consider adding an error boundary to your tree to customize error handling behavior|React will try to recreate this component tree from scratch using the error boundary you provided|Error boundaries should implement getDerivedStateFromError)/m.test(
args[0] args[0]
) )
? void 0 ? void 0

View File

@@ -24,22 +24,23 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.7.0", "@babel/plugin-proposal-class-properties": "^7.7.0",
"@babel/preset-env": "^7.7.1", "@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@babel/preset-env": "^7.7.6",
"@babel/preset-react": "^7.7.0", "@babel/preset-react": "^7.7.0",
"@babel/preset-typescript": "^7.7.2", "@babel/preset-typescript": "^7.7.2",
"@babel/runtime": "^7.7.2", "@babel/runtime": "^7.7.6",
"@commitlint/config-conventional": "^8.2.0", "@commitlint/config-conventional": "^8.2.0",
"@types/jest": "^24.0.23", "@types/jest": "^24.0.23",
"codecov": "^3.6.1", "codecov": "^3.6.1",
"commitlint": "^8.2.0", "commitlint": "^8.2.0",
"core-js": "^3.4.1", "core-js": "^3.5.0",
"eslint": "^6.6.0", "eslint": "^6.7.2",
"eslint-config-satya164": "^3.1.2", "eslint-config-satya164": "^3.1.5",
"husky": "^3.0.9", "husky": "^3.0.9",
"jest": "^24.8.0", "jest": "^24.8.0",
"lerna": "^3.18.4", "lerna": "^3.18.4",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"resolutions": { "resolutions": {
"react": "~16.9.0", "react": "~16.9.0",

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.28...@react-navigation/bottom-tabs@5.0.0-alpha.29) (2020-01-01)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.27...@react-navigation/bottom-tabs@5.0.0-alpha.28) (2019-12-19)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.26...@react-navigation/bottom-tabs@5.0.0-alpha.27) (2019-12-16)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.25...@react-navigation/bottom-tabs@5.0.0-alpha.26) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.24...@react-navigation/bottom-tabs@5.0.0-alpha.25) (2019-12-11) # [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.24...@react-navigation/bottom-tabs@5.0.0-alpha.25) (2019-12-11)
**Note:** Version bump only for package @react-navigation/bottom-tabs **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -10,7 +10,7 @@
"android", "android",
"tab" "tab"
], ],
"version": "5.0.0-alpha.25", "version": "5.0.0-alpha.29",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -33,17 +33,19 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19",
"color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/color": "^3.0.0",
"@types/react-native": "^0.60.22", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.25",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-safe-area-context": "^0.6.0", "react-native-safe-area-context": "^0.6.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",

View File

@@ -10,22 +10,15 @@ import {
Dimensions, Dimensions,
} from 'react-native'; } from 'react-native';
import { import {
Route,
NavigationContext, NavigationContext,
CommonActions, CommonActions,
useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { SafeAreaConsumer } from 'react-native-safe-area-context'; import { SafeAreaConsumer } from 'react-native-safe-area-context';
import BottomTabItem from './BottomTabItem'; import BottomTabItem from './BottomTabItem';
import { BottomTabBarProps } from '../types'; import { BottomTabBarProps } from '../types';
type State = {
dimensions: { height: number; width: number };
layout: { height: number; width: number };
keyboard: boolean;
visible: Animated.Value;
};
type Props = BottomTabBarProps & { type Props = BottomTabBarProps & {
activeTintColor?: string; activeTintColor?: string;
inactiveTintColor?: string; inactiveTintColor?: string;
@@ -34,97 +27,101 @@ type Props = BottomTabBarProps & {
const DEFAULT_TABBAR_HEIGHT = 50; const DEFAULT_TABBAR_HEIGHT = 50;
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125; const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
export default class BottomTabBar extends React.Component<Props, State> { export default function BottomTabBar({
static defaultProps = { state,
keyboardHidesTabBar: false, navigation,
adaptive: true, descriptors,
}; activeBackgroundColor,
activeTintColor,
adaptive = true,
allowFontScaling,
inactiveBackgroundColor,
inactiveTintColor,
keyboardHidesTabBar = false,
labelPosition,
labelStyle,
showIcon,
showLabel,
style,
tabStyle,
}: Props) {
const { colors } = useTheme();
state = { const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
dimensions: Dimensions.get('window'), const [layout, setLayout] = React.useState({ height: 0, width: 0 });
layout: { height: 0, width: 0 }, const [keyboardShown, setKeyboardShown] = React.useState(false);
keyboard: false,
visible: new Animated.Value(1),
};
componentDidMount() { const [visible] = React.useState(() => new Animated.Value(0));
Dimensions.addEventListener('change', this.handleOrientationChange);
if (Platform.OS === 'ios') { const { routes } = state;
Keyboard.addListener('keyboardWillShow', this.handleKeyboardShow);
Keyboard.addListener('keyboardWillHide', this.handleKeyboardHide);
} else {
Keyboard.addListener('keyboardDidShow', this.handleKeyboardShow);
Keyboard.addListener('keyboardDidHide', this.handleKeyboardHide);
}
}
componentWillUnmount() { React.useEffect(() => {
Dimensions.removeEventListener('change', this.handleOrientationChange); if (keyboardShown) {
Animated.timing(visible, {
if (Platform.OS === 'ios') {
Keyboard.removeListener('keyboardWillShow', this.handleKeyboardShow);
Keyboard.removeListener('keyboardWillHide', this.handleKeyboardHide);
} else {
Keyboard.removeListener('keyboardDidShow', this.handleKeyboardShow);
Keyboard.removeListener('keyboardDidHide', this.handleKeyboardHide);
}
}
private handleOrientationChange = ({ window }: { window: ScaledSize }) => {
this.setState({ dimensions: window });
};
private handleKeyboardShow = () =>
this.setState({ keyboard: true }, () =>
Animated.timing(this.state.visible, {
toValue: 0, toValue: 0,
duration: 200, duration: 200,
useNativeDriver: true, useNativeDriver: true,
}).start() }).start();
); }
}, [keyboardShown, visible]);
private handleKeyboardHide = () => React.useEffect(() => {
Animated.timing(this.state.visible, { const handleOrientationChange = ({ window }: { window: ScaledSize }) => {
toValue: 1, setDimensions(window);
duration: 250, };
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
this.setState({ keyboard: false });
}
});
private handleLayout = (e: LayoutChangeEvent) => { const handleKeyboardShow = () => setKeyboardShown(true);
const { layout } = this.state;
const { height, width } = e.nativeEvent.layout;
if (height === layout.height && width === layout.width) { const handleKeyboardHide = () =>
return; Animated.timing(visible, {
toValue: 1,
duration: 250,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished) {
setKeyboardShown(false);
}
});
Dimensions.addEventListener('change', handleOrientationChange);
if (Platform.OS === 'ios') {
Keyboard.addListener('keyboardWillShow', handleKeyboardShow);
Keyboard.addListener('keyboardWillHide', handleKeyboardHide);
} else {
Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
} }
this.setState({ return () => {
layout: { Dimensions.removeEventListener('change', handleOrientationChange);
height,
width, if (Platform.OS === 'ios') {
}, Keyboard.removeListener('keyboardWillShow', handleKeyboardShow);
Keyboard.removeListener('keyboardWillHide', handleKeyboardHide);
} else {
Keyboard.removeListener('keyboardDidShow', handleKeyboardShow);
Keyboard.removeListener('keyboardDidHide', handleKeyboardHide);
}
};
}, [visible]);
const handleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
setLayout(layout => {
if (height === layout.height && width === layout.width) {
return layout;
} else {
return {
height,
width,
};
}
}); });
}; };
private getLabelText = ({ route }: { route: Route<string> }) => { const shouldUseHorizontalLabels = () => {
const { options } = this.props.descriptors[route.key];
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
};
private shouldUseHorizontalLabels = () => {
const { state, adaptive, tabStyle, labelPosition } = this.props;
const { dimensions } = this.state;
const { routes } = state;
const isLandscape = dimensions.width > dimensions.height; const isLandscape = dimensions.width > dimensions.height;
if (labelPosition) { if (labelPosition) {
@@ -165,126 +162,114 @@ export default class BottomTabBar extends React.Component<Props, State> {
} }
}; };
render() { return (
const { <SafeAreaConsumer>
state, {insets => (
navigation, <Animated.View
descriptors, style={[
keyboardHidesTabBar, styles.tabBar,
activeTintColor, {
inactiveTintColor, backgroundColor: colors.card,
activeBackgroundColor, borderTopColor: colors.border,
inactiveBackgroundColor, },
labelStyle, keyboardHidesTabBar
showIcon, ? {
showLabel, // When the keyboard is shown, slide down the tab bar
allowFontScaling, transform: [
style, {
tabStyle, translateY: visible.interpolate({
} = this.props; inputRange: [0, 1],
const { routes } = state; outputRange: [layout.height, 0],
}),
},
],
// Absolutely position the tab bar so that the content is below it
// This is needed to avoid gap at bottom when the tab bar is hidden
position: keyboardShown ? 'absolute' : null,
}
: null,
{
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0),
paddingBottom: insets ? insets.bottom : 0,
},
style,
]}
pointerEvents={keyboardHidesTabBar && keyboardShown ? 'none' : 'auto'}
>
<View style={styles.content} onLayout={handleLayout}>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
return ( const onPress = () => {
<SafeAreaConsumer> const event = navigation.emit({
{insets => ( type: 'tabPress',
<Animated.View target: route.key,
style={[ });
styles.tabBar,
keyboardHidesTabBar
? {
// When the keyboard is shown, slide down the tab bar
transform: [
{
translateY: this.state.visible.interpolate({
inputRange: [0, 1],
outputRange: [this.state.layout.height, 0],
}),
},
],
// Absolutely position the tab bar so that the content is below it
// This is needed to avoid gap at bottom when the tab bar is hidden
position: this.state.keyboard ? 'absolute' : null,
}
: null,
{
height: DEFAULT_TABBAR_HEIGHT + (insets ? insets.bottom : 0),
paddingBottom: insets ? insets.bottom : 0,
},
style,
]}
pointerEvents={
keyboardHidesTabBar && this.state.keyboard ? 'none' : 'auto'
}
>
<View style={styles.content} onLayout={this.handleLayout}>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
const onPress = () => { if (!focused && !event.defaultPrevented) {
const event = navigation.emit({ navigation.dispatch({
type: 'tabPress', ...CommonActions.navigate(route.name),
target: route.key, target: state.key,
}); });
}
};
if (!focused && !event.defaultPrevented) { const onLongPress = () => {
navigation.dispatch({ navigation.emit({
...CommonActions.navigate(route.name), type: 'tabLongPress',
target: state.key, target: route.key,
}); });
} };
};
const onLongPress = () => { const label =
navigation.emit({ options.tabBarLabel !== undefined
type: 'tabLongPress', ? options.tabBarLabel
target: route.key, : options.title !== undefined
}); ? options.title
}; : route.name;
const label = this.getLabelText({ route }); const accessibilityLabel =
const accessibilityLabel = options.tabBarAccessibilityLabel !== undefined
options.tabBarAccessibilityLabel !== undefined ? options.tabBarAccessibilityLabel
? options.tabBarAccessibilityLabel : typeof label === 'string'
: typeof label === 'string' ? `${label}, tab, ${index + 1} of ${routes.length}`
? `${label}, tab, ${index + 1} of ${routes.length}` : undefined;
: undefined;
return ( return (
<NavigationContext.Provider <NavigationContext.Provider
key={route.key} key={route.key}
value={descriptors[route.key].navigation} value={descriptors[route.key].navigation}
> >
<BottomTabItem <BottomTabItem
route={route} route={route}
focused={focused} focused={focused}
horizontal={this.shouldUseHorizontalLabels()} horizontal={shouldUseHorizontalLabels()}
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
testID={options.tabBarTestID} testID={options.tabBarTestID}
allowFontScaling={allowFontScaling} allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor} activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor} inactiveTintColor={inactiveTintColor}
activeBackgroundColor={activeBackgroundColor} activeBackgroundColor={activeBackgroundColor}
inactiveBackgroundColor={inactiveBackgroundColor} inactiveBackgroundColor={inactiveBackgroundColor}
button={options.tabBarButton} button={options.tabBarButton}
icon={options.tabBarIcon} icon={options.tabBarIcon}
label={label} label={label}
showIcon={showIcon} showIcon={showIcon}
showLabel={showLabel} showLabel={showLabel}
labelStyle={labelStyle} labelStyle={labelStyle}
style={tabStyle} style={tabStyle}
/> />
</NavigationContext.Provider> </NavigationContext.Provider>
); );
})} })}
</View> </View>
</Animated.View> </Animated.View>
)} )}
</SafeAreaConsumer> </SafeAreaConsumer>
); );
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -292,9 +277,7 @@ const styles = StyleSheet.create({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: '#fff',
borderTopWidth: StyleSheet.hairlineWidth, borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(0, 0, 0, .3)',
elevation: 8, elevation: 8,
}, },
content: { content: {

View File

@@ -8,7 +8,8 @@ import {
ViewStyle, ViewStyle,
TextStyle, TextStyle,
} from 'react-native'; } from 'react-native';
import { Route } from '@react-navigation/native'; import { Route, useTheme } from '@react-navigation/native';
import Color from 'color';
import TabBarIcon from './TabBarIcon'; import TabBarIcon from './TabBarIcon';
import { BottomTabBarButtonProps } from '../types'; import { BottomTabBarButtonProps } from '../types';
@@ -113,8 +114,8 @@ export default function BottomTabBarItem({
onPress, onPress,
onLongPress, onLongPress,
horizontal, horizontal,
activeTintColor = '#007AFF', activeTintColor: customActiveTintColor,
inactiveTintColor = '#8E8E93', inactiveTintColor: customInactiveTintColor,
activeBackgroundColor = 'transparent', activeBackgroundColor = 'transparent',
inactiveBackgroundColor = 'transparent', inactiveBackgroundColor = 'transparent',
showLabel = true, showLabel = true,
@@ -123,6 +124,20 @@ export default function BottomTabBarItem({
labelStyle, labelStyle,
style, style,
}: Props) { }: Props) {
const { colors } = useTheme();
const activeTintColor =
customActiveTintColor === undefined
? colors.primary
: customActiveTintColor;
const inactiveTintColor =
customInactiveTintColor === undefined
? Color(colors.text)
.mix(Color(colors.card), 0.5)
.hex()
: customInactiveTintColor;
const renderLabel = ({ focused }: { focused: boolean }) => { const renderLabel = ({ focused }: { focused: boolean }) => {
if (showLabel === false) { if (showLabel === false) {
return null; return null;

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import { TabNavigationState } from '@react-navigation/routers'; import { TabNavigationState } from '@react-navigation/routers';
import { useTheme } from '@react-navigation/native';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
@@ -25,6 +26,26 @@ type State = {
loaded: number[]; loaded: number[];
}; };
function SceneContent({
isFocused,
children,
}: {
isFocused: boolean;
children: React.ReactNode;
}) {
const { colors } = useTheme();
return (
<View
accessibilityElementsHidden={!isFocused}
importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'}
style={[styles.content, { backgroundColor: colors.background }]}
>
{children}
</View>
);
}
export default class BottomTabView extends React.Component<Props, State> { export default class BottomTabView extends React.Component<Props, State> {
static defaultProps = { static defaultProps = {
lazy: true, lazy: true,
@@ -97,15 +118,9 @@ export default class BottomTabView extends React.Component<Props, State> {
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
isVisible={isFocused} isVisible={isFocused}
> >
<View <SceneContent isFocused={isFocused}>
accessibilityElementsHidden={!isFocused}
importantForAccessibility={
isFocused ? 'auto' : 'no-hide-descendants'
}
style={styles.content}
>
{descriptors[route.key].render()} {descriptors[route.key].render()}
</View> </SceneContent>
</ResourceSavingScene> </ResourceSavingScene>
); );
})} })}

View File

@@ -14,7 +14,7 @@ const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view o
export default class ResourceSavingScene extends React.Component<Props> { export default class ResourceSavingScene extends React.Component<Props> {
render() { render() {
if (screensEnabled && screensEnabled()) { if (screensEnabled?.()) {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-ignore // @ts-ignore
return <Screen active={isVisible ? 1 : 0} {...rest} />; return <Screen active={isVisible ? 1 : 0} {...rest} />;

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.20](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/compat@5.0.0-alpha.19...@react-navigation/compat@5.0.0-alpha.20) (2020-01-01)
**Note:** Version bump only for package @react-navigation/compat
# [5.0.0-alpha.19](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/compat@5.0.0-alpha.18...@react-navigation/compat@5.0.0-alpha.19) (2019-12-19)
**Note:** Version bump only for package @react-navigation/compat
# [5.0.0-alpha.18](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/compat@5.0.0-alpha.17...@react-navigation/compat@5.0.0-alpha.18) (2019-12-16)
**Note:** Version bump only for package @react-navigation/compat
# [5.0.0-alpha.17](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/compat@5.0.0-alpha.16...@react-navigation/compat@5.0.0-alpha.17) (2019-12-11) # [5.0.0-alpha.17](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/compat@5.0.0-alpha.16...@react-navigation/compat@5.0.0-alpha.17) (2019-12-11)
**Note:** Version bump only for package @react-navigation/compat **Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.0.0-alpha.17", "version": "5.0.0-alpha.20",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -24,12 +24,12 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"react": "~16.9.0", "react": "~16.9.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",

View File

@@ -25,7 +25,7 @@ export function navigate({
} }
export function back(options?: { key?: null | string }) { export function back(options?: { key?: null | string }) {
return options && options.key != null return options?.key != null
? (state: NavigationState) => ({ ? (state: NavigationState) => ({
...CommonActions.goBack(), ...CommonActions.goBack(),
source: options.key, source: options.key,

View File

@@ -109,17 +109,17 @@ export default function createCompatNavigationProp<
break; break;
case 'didFocus': { case 'didFocus': {
const unsubscribe = focusSubscriptions.get(callback); const unsubscribe = focusSubscriptions.get(callback);
unsubscribe && unsubscribe(); unsubscribe?.();
break; break;
} }
case 'didBlur': { case 'didBlur': {
const unsubscribe = blurSubscriptions.get(callback); const unsubscribe = blurSubscriptions.get(callback);
unsubscribe && unsubscribe(); unsubscribe?.();
break; break;
} }
case 'refocus': { case 'refocus': {
const unsubscribe = refocusSubscriptions.get(callback); const unsubscribe = refocusSubscriptions.get(callback);
unsubscribe && unsubscribe(); unsubscribe?.();
break; break;
} }
default: default:

View File

@@ -47,7 +47,7 @@ export default function createCompatNavigatorFactory<
>( >(
routeConfig: CompatRouteConfig<NavigationPropType>, routeConfig: CompatRouteConfig<NavigationPropType>,
navigationConfig: Partial<Omit<NavigationConfig, 'screenOptions'>> & { navigationConfig: Partial<Omit<NavigationConfig, 'screenOptions'>> & {
order?: Array<Extract<keyof ParamList, string>>; order?: Extract<keyof ParamList, string>[];
defaultNavigationOptions?: ScreenOptions; defaultNavigationOptions?: ScreenOptions;
navigationOptions?: Record<string, any>; navigationOptions?: Record<string, any>;
} = {} } = {}

View File

@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.30](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.29...@react-navigation/core@5.0.0-alpha.30) (2020-01-01)
### Bug Fixes
* cleanup transaction even if action wasn't handled ([f462d67](https://github.com/react-navigation/navigation-ex/commit/f462d672708cabfb0477c3a48505bd194ea626fd))
* show error if an action was not handled ([0252bdc](https://github.com/react-navigation/navigation-ex/commit/0252bdc2222ebe7410a0ed593bf03b2bdf5dc7ca))
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.28...@react-navigation/core@5.0.0-alpha.29) (2019-12-19)
**Note:** Version bump only for package @react-navigation/core
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.27...@react-navigation/core@5.0.0-alpha.28) (2019-12-16)
### Bug Fixes
* use Partial type for initialParam ([#206](https://github.com/react-navigation/navigation-ex/issues/206)) ([c3d3748](https://github.com/react-navigation/navigation-ex/commit/c3d374814308b0bd6d259099444f0f24593f4d7e))
### Features
* add nested config in deep linking ([#210](https://github.com/react-navigation/navigation-ex/issues/210)) ([8002d51](https://github.com/react-navigation/navigation-ex/commit/8002d5179524a7211c37760a4ed45e8c12af4358)), closes [#154](https://github.com/react-navigation/navigation-ex/issues/154)
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.26...@react-navigation/core@5.0.0-alpha.27) (2019-12-10) # [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.26...@react-navigation/core@5.0.0-alpha.27) (2019-12-10)
**Note:** Version bump only for package @react-navigation/core **Note:** Version bump only for package @react-navigation/core

View File

@@ -6,7 +6,7 @@
"react-native", "react-native",
"react-navigation" "react-navigation"
], ],
"version": "5.0.0-alpha.27", "version": "5.0.0-alpha.30",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -35,15 +35,15 @@
"use-subscription": "^1.3.0" "use-subscription": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.7.2", "@babel/core": "^7.7.5",
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/shortid": "^0.0.29", "@types/shortid": "^0.0.29",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native-testing-library": "^1.9.1", "react-native-testing-library": "^1.12.0",
"react-test-renderer": "~16.9.0", "react-test-renderer": "~16.9.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "~16.9.0" "react": "~16.9.0"

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-cycle
import { NavigationState, PartialState } from './types'; import { NavigationState, PartialState } from './types';
export type Action = export type Action =

View File

@@ -123,9 +123,11 @@ const Container = React.forwardRef(function NavigationContainer(
isTransactionActiveRef.current = true; isTransactionActiveRef.current = true;
transactionStateRef.current = navigationState; transactionStateRef.current = navigationState;
callback(); try {
callback();
isTransactionActiveRef.current = false; } finally {
isTransactionActiveRef.current = false;
}
return transactionStateRef.current; return transactionStateRef.current;
}); });
@@ -202,7 +204,7 @@ const Container = React.forwardRef(function NavigationContainer(
}, [getStateForRoute]); }, [getStateForRoute]);
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
...(Object.keys(CommonActions) as Array<keyof typeof CommonActions>).reduce< ...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce<
any any
>((acc, name) => { >((acc, name) => {
acc[name] = (...args: any[]) => acc[name] = (...args: any[]) =>

View File

@@ -0,0 +1,75 @@
import getActionFromState from '../getActionFromState';
it('gets navigate action from state', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
params: { answer: 42 },
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
params: {
answer: 42,
params: {
author: 'jane',
},
screen: 'qux',
},
screen: 'bar',
},
},
type: 'NAVIGATE',
});
});
it('gets reset action from state', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: state,
type: 'RESET_ROOT',
});
});

View File

@@ -127,3 +127,238 @@ it('handles route without param', () => {
it('returns undefined for invalid path', () => { it('returns undefined for invalid path', () => {
expect(getStateFromPath('//')).toBe(undefined); expect(getStateFromPath('//')).toBe(undefined);
}); });
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({
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,
},
},
],
},
},
],
},
},
],
},
},
],
});
});
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',
},
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, c => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
})
).toEqual({
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
});
});
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',
},
Bar: 'bar/:type/:fruit',
Baz: {
Bis: {
path: 'bis/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, c => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
}
)
).toEqual({
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,
},
},
],
},
},
],
},
},
],
},
},
],
},
},
],
});
});
it('handles parse in nested object for second route depth', () => {
expect(
getStateFromPath('/baz', {
Foo: {
path: 'foo',
Foe: 'foe',
Bar: {
Baz: 'baz',
},
},
})
).toEqual({
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
});
});
it('handles parse in nested object for second route depth and and path and parse in roots', () => {
expect(
getStateFromPath('/baz', {
Foo: {
path: 'foo/:id',
parse: {
id: Number,
},
Foe: 'foe',
Bar: {
path: 'bar/:id',
parse: {
id: Number,
},
Baz: 'baz',
},
},
})
).toEqual({
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
});
});

View File

@@ -357,6 +357,8 @@ it("doesn't update state if action wasn't handled", () => {
const onStateChange = jest.fn(); const onStateChange = jest.fn();
const spy = jest.spyOn(console, 'error').mockImplementation();
render( render(
<NavigationContainer onStateChange={onStateChange}> <NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo"> <TestNavigator initialRouteName="foo">
@@ -367,6 +369,12 @@ it("doesn't update state if action wasn't handled", () => {
); );
expect(onStateChange).toBeCalledTimes(0); expect(onStateChange).toBeCalledTimes(0);
expect(spy.mock.calls[0][0]).toMatch(
"The action 'INVALID' with payload 'undefined' was not handled by any navigator."
);
spy.mockRestore();
}); });
it('cleans up state when the navigator unmounts', () => { it('cleans up state when the navigator unmounts', () => {

View File

@@ -305,8 +305,7 @@ it("action doesn't bubble if target is specified", () => {
expect(onStateChange).not.toBeCalled(); expect(onStateChange).not.toBeCalled();
}); });
// eslint-disable-next-line jest/expect-expect it('logs error if no navigator handled the action', () => {
it("doesn't crash if no navigator handled the action", () => {
const TestRouter = MockRouter; const TestRouter = MockRouter;
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
@@ -366,5 +365,13 @@ it("doesn't crash if no navigator handled the action", () => {
</NavigationContainer> </NavigationContainer>
); );
const spy = jest.spyOn(console, 'error').mockImplementation();
render(element).update(element); render(element).update(element);
expect(spy.mock.calls[0][0]).toMatch(
"The action 'UNKNOWN' with payload 'undefined' was not handled by any navigator."
);
spy.mockRestore();
}); });

View File

@@ -19,7 +19,7 @@ it('gets route prop from context', () => {
const Test = () => { const Test = () => {
const route = useRoute<RouteProp<{ sample: { x: string } }, 'sample'>>(); const route = useRoute<RouteProp<{ sample: { x: string } }, 'sample'>>();
expect(route && route.params && route.params.x).toEqual(1); expect(route?.params?.x).toEqual(1);
return null; return null;
}; };

View File

@@ -0,0 +1,67 @@
import { PartialState, NavigationState } from './types';
type NavigateParams = {
screen?: string;
params?: NavigateParams;
};
type Action =
| {
type: 'NAVIGATE';
payload: { name: string; params: NavigateParams };
}
| {
type: 'RESET_ROOT';
payload: PartialState<NavigationState>;
};
export default function getActionFromState(
state: PartialState<NavigationState>
): Action {
let payload: { name: string; params: NavigateParams } | undefined;
if (state.routes.length === 1) {
// Try to construct payload for a `NAVIGATE` action from the state
// This lets us preserve the navigation state and not lose it
let route = state.routes[0];
payload = {
name: route.name,
params: { ...route.params },
};
let current = state.routes[0].state;
let params = payload.params;
while (current) {
if (current.routes.length === 1) {
route = current.routes[0];
params.screen = route.name;
if (route.state) {
params.params = { ...route.params };
params = params.params;
} else {
params.params = route.params;
}
current = route.state;
} else {
payload = undefined;
break;
}
}
}
if (payload) {
return {
type: 'NAVIGATE',
payload,
};
}
return {
type: 'RESET_ROOT',
payload: state,
};
}

View File

@@ -60,7 +60,7 @@ export default function getPathFromState(
Object.entries(route.params).reduce<{ Object.entries(route.params).reduce<{
[key: string]: string; [key: string]: string;
}>((acc, [key, value]) => { }>((acc, [key, value]) => {
acc[key] = config && config[key] ? config[key](value) : String(value); acc[key] = config?.[key] ? config[key](value) : String(value);
return acc; return acc;
}, {}) }, {})
: undefined; : undefined;
@@ -80,6 +80,7 @@ export default function getPathFromState(
if (params && name in params && p.startsWith(':')) { if (params && name in params && p.startsWith(':')) {
const value = params[name]; const value = params[name];
// Remove the used value from the params object since we'll use the rest for query string // Remove the used value from the params object since we'll use the rest for query string
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[name]; delete params[name];
return encodeURIComponent(value); return encodeURIComponent(value);
} }

View File

@@ -1,11 +1,22 @@
import escape from 'escape-string-regexp'; import escape from 'escape-string-regexp';
import queryString from 'query-string'; import queryString from 'query-string';
import { NavigationState, PartialState } from './types'; import { NavigationState, PartialState, InitialState } from './types';
type ParseConfig = Record<string, (value: string) => any>; type ParseConfig = Record<string, (value: string) => any>;
type Options = { type Options = {
[routeName: string]: string | { path: string; parse?: ParseConfig }; [routeName: string]: string | { path: string; parse?: ParseConfig } | Options;
};
type RouteConfig = {
match: RegExp;
pattern: string;
routeNames: string[];
parse: Record<string, (value: string) => any> | undefined;
};
type ResultState = PartialState<NavigationState> & {
state?: ResultState;
}; };
/** /**
@@ -30,28 +41,11 @@ type Options = {
export default function getStateFromPath( export default function getStateFromPath(
path: string, path: string,
options: Options = {} options: Options = {}
): PartialState<NavigationState> | undefined { ): ResultState | undefined {
// Create a normalized config array which will be easier to use // Create a normalized configs array which will be easier to use
const routeConfig = Object.keys(options).map(key => { const configs = ([] as RouteConfig[]).concat(
const pattern = ...Object.keys(options).map(key => createNormalizedConfigs(key, options))
typeof options[key] === 'string' );
? (options[key] as string)
: (options[key] as { path: string }).path;
// Create a regex from the provided path pattern
// With the pattern, we can match segements containing params and extract them
const match = new RegExp(
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
);
return {
match,
pattern,
routeName: key,
// @ts-ignore
parse: options[key].parse,
};
});
let result: PartialState<NavigationState> | undefined; let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined; let current: PartialState<NavigationState> | undefined;
@@ -62,16 +56,16 @@ export default function getStateFromPath(
.replace(/\?.*/, ''); // Remove query params which we will handle later .replace(/\?.*/, ''); // Remove query params which we will handle later
while (remaining) { while (remaining) {
let routeName; let routeNames;
let params; let params;
// Go through all configs, and see if the next path segment matches our regex // Go through all configs, and see if the next path segment matches our regex
for (const config of routeConfig) { for (const config of configs) {
const match = remaining.match(config.match); const match = remaining.match(config.match);
// If our regex matches, we need to extract params from the path // If our regex matches, we need to extract params from the path
if (match) { if (match) {
routeName = config.routeName; routeNames = config.routeNames;
const paramPatterns = config.pattern const paramPatterns = config.pattern
.split('/') .split('/')
@@ -99,20 +93,54 @@ export default function getStateFromPath(
} }
// If we hadn't matched any segments earlier, use the path as route name // If we hadn't matched any segments earlier, use the path as route name
if (routeName === undefined) { if (routeNames === undefined) {
const segments = remaining.split('/'); const segments = remaining.split('/');
routeName = decodeURIComponent(segments[0]); routeNames = [decodeURIComponent(segments[0])];
segments.shift(); segments.shift();
remaining = segments.join('/'); remaining = segments.join('/');
} }
const state = { let state: InitialState;
routes: [{ name: routeName, params }],
}; if (routeNames.length === 1) {
state = {
routes: [
{ name: routeNames.shift() as string, ...(params && { params }) },
],
};
} else {
state = {
routes: [{ name: routeNames.shift() as string, state: { routes: [] } }],
};
let helper = state.routes[0].state as InitialState;
let routeName;
while ((routeName = routeNames.shift())) {
if (routeNames.length === 0) {
helper.routes.push({
name: routeName,
...(params && { params }),
});
} else {
helper.routes[0] = {
name: routeName,
state: {
routes: [],
},
};
helper = helper.routes[0].state as InitialState;
}
}
}
if (current) { if (current) {
// The state should be nested inside the route we parsed before // The state should be nested inside the deepest route we parsed before
while (current.routes[0].state) {
current = current.routes[0].state;
}
current.routes[0].state = state; current.routes[0].state = state;
} else { } else {
result = state; result = state;
@@ -128,17 +156,20 @@ export default function getStateFromPath(
const query = path.split('?')[1]; const query = path.split('?')[1];
if (query) { if (query) {
while (current.routes[0].state) {
// The query params apply to the deepest route
current = current.routes[0].state;
}
const route = current.routes[0]; const route = current.routes[0];
const params = queryString.parse(query); const params = queryString.parse(query);
const config = options[route.name] const parseFunction = findParseConfigForRoute(route.name, options);
? (options[route.name] as { parse?: ParseConfig }).parse
: undefined;
if (config) { if (parseFunction) {
Object.keys(params).forEach(name => { Object.keys(params).forEach(name => {
if (config[name] && typeof params[name] === 'string') { if (parseFunction[name] && typeof params[name] === 'string') {
params[name] = config[name](params[name] as string); params[name] = parseFunction[name](params[name] as string);
} }
}); });
} }
@@ -148,3 +179,89 @@ export default function getStateFromPath(
return result; return result;
} }
function createNormalizedConfigs(
key: string,
routeConfig: Options,
routeNames: string[] = []
): RouteConfig[] {
const configs = [];
routeNames.push(key);
const value = routeConfig[key];
if (typeof value === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
configs.push(createConfigItem(routeNames, value));
} else if (typeof value === 'object') {
// if an object is specified as the value (e.g. Foo: { ... }),
// it could have config object and optionally nested config
Object.keys(value).forEach(nestedKey => {
if (nestedKey === 'path') {
configs.push(
createConfigItem(
routeNames,
value[nestedKey] as string,
value.parse ? (value.parse as ParseConfig) : undefined
)
);
} else if (nestedKey === 'parse') {
// We handle custom parse function when a `path` is specified (in nestedKey === path)
} else {
// If the name of the key is not `path` or `parse`, it's a nested config for route
// So we need to traverse into it and collect the configs
const result = createNormalizedConfigs(
nestedKey,
routeConfig[key] as Options,
routeNames
);
configs.push(...result);
}
});
}
routeNames.pop();
return configs;
}
function createConfigItem(
routeNames: string[],
pattern: string,
parse?: ParseConfig
): RouteConfig {
const match = new RegExp(
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
);
return {
match,
pattern,
// The routeNames array is mutated, so copy it to keep the current state
routeNames: [...routeNames],
parse,
};
}
function findParseConfigForRoute(
routeName: string,
config: Options
): ParseConfig | undefined {
if (config[routeName]) {
return (config[routeName] as { parse?: ParseConfig }).parse;
}
for (const name in config) {
if (typeof config[name] === 'object') {
const parse = findParseConfigForRoute(routeName, config[name] as Options);
if (parse) {
return parse;
}
}
}
return undefined;
}

View File

@@ -17,5 +17,6 @@ export { default as useIsFocused } from './useIsFocused';
export { default as getStateFromPath } from './getStateFromPath'; export { default as getStateFromPath } from './getStateFromPath';
export { default as getPathFromState } from './getPathFromState'; export { default as getPathFromState } from './getPathFromState';
export { default as getActionFromState } from './getActionFromState';
export * from './types'; export * from './types';

View File

@@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-cycle
import * as CommonActions from './CommonActions'; import * as CommonActions from './CommonActions';
import * as React from 'react'; import * as React from 'react';
@@ -20,9 +19,9 @@ export type NavigationState = {
/** /**
* List of rendered routes. * List of rendered routes.
*/ */
routes: Array< routes: (Route<string> & {
Route<string> & { state?: NavigationState | PartialState<NavigationState> } state?: NavigationState | PartialState<NavigationState>;
>; })[];
/** /**
* Custom type for the state, whether it's for tab, stack, drawer etc. * Custom type for the state, whether it's for tab, stack, drawer etc.
* During rehydration, the state will be discarded if type doesn't match with router type. * During rehydration, the state will be discarded if type doesn't match with router type.
@@ -38,7 +37,7 @@ export type NavigationState = {
export type InitialState = Partial< export type InitialState = Partial<
Omit<NavigationState, 'stale' | 'routes'> Omit<NavigationState, 'stale' | 'routes'>
> & { > & {
routes: Array<Omit<Route<string>, 'key'> & { state?: InitialState }>; routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
}; };
export type PartialState<State extends NavigationState> = Partial< export type PartialState<State extends NavigationState> = Partial<
@@ -46,9 +45,10 @@ export type PartialState<State extends NavigationState> = Partial<
> & { > & {
stale?: true; stale?: true;
type?: string; type?: string;
routes: Array< routes: (Omit<Route<string>, 'key'> & {
Omit<Route<string>, 'key'> & { key?: string; state?: InitialState } key?: string;
>; state?: InitialState;
})[];
}; };
export type Route<RouteName extends string> = { export type Route<RouteName extends string> = {
@@ -71,6 +71,10 @@ export type NavigationAction = {
* Type of the action (e.g. `NAVIGATE`) * Type of the action (e.g. `NAVIGATE`)
*/ */
type: string; type: string;
/**
* Additional data for the action
*/
payload?: object;
/** /**
* Key of the route which dispatched this action. * Key of the route which dispatched this action.
*/ */
@@ -533,7 +537,7 @@ export type RouteConfig<
/** /**
* Initial params object for the route. * Initial params object for the route.
*/ */
initialParams?: ParamList[RouteName]; initialParams?: Partial<ParamList[RouteName]>;
} & ( } & (
| { | {
/** /**

View File

@@ -44,16 +44,15 @@ export default function useDevTools({ name, reset, state }: Options) {
const devTools = devToolsRef.current; const devTools = devToolsRef.current;
const lastStateRef = React.useRef<State>(state); const lastStateRef = React.useRef<State>(state);
const actions = React.useRef<Array<NavigationAction | string>>([]); const actions = React.useRef<(NavigationAction | string)[]>([]);
React.useEffect(() => { React.useEffect(() => {
devTools && devTools.init(lastStateRef.current); devTools?.init(lastStateRef.current);
}, [devTools]); }, [devTools]);
React.useEffect( React.useEffect(
() => () =>
devTools && devTools?.subscribe(message => {
devTools.subscribe(message => {
if (message.type === 'DISPATCH' && message.state) { if (message.type === 'DISPATCH' && message.state) {
reset(JSON.parse(message.state)); reset(JSON.parse(message.state));
} }

View File

@@ -5,7 +5,7 @@ export type NavigationEventEmitter = EventEmitter<Record<string, any>> & {
create: (target: string) => EventConsumer<Record<string, any>>; create: (target: string) => EventConsumer<Record<string, any>>;
}; };
type Listeners = Array<(data: any) => void>; type Listeners = ((data: any) => void)[];
/** /**
* Hook to manage the event system used by the navigator to notify screens of various events. * Hook to manage the event system used by the navigator to notify screens of various events.
@@ -69,7 +69,7 @@ export default function useEventEmitter(): NavigationEventEmitter {
}, },
}; };
callbacks && callbacks.forEach(cb => cb(event)); callbacks?.forEach(cb => cb(event));
return event; return event;
}, },

View File

@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import useNavigation from './useNavigation'; import useNavigation from './useNavigation';
type EffectCallback = (() => void) | (() => () => void); type EffectCallback = (() => undefined) | (() => () => void);
/** /**
* Hook to run an effect in a focused screen, similar to `React.useEffect`. * 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(() => { React.useEffect(() => {
let isFocused = false; let isFocused = false;
let cleanup: (() => void) | void; let cleanup: (() => void) | undefined;
// We need to run the effect on intial render/dep changes if the screen is focused // We need to run the effect on intial render/dep changes if the screen is focused
if (navigation.isFocused()) { if (navigation.isFocused()) {
@@ -30,19 +30,19 @@ export default function useFocusEffect(callback: EffectCallback) {
return; return;
} }
cleanup && cleanup(); cleanup?.();
cleanup = callback(); cleanup = callback();
isFocused = true; isFocused = true;
}); });
const unsubscribeBlur = navigation.addListener('blur', () => { const unsubscribeBlur = navigation.addListener('blur', () => {
cleanup && cleanup(); cleanup?.();
cleanup = undefined; cleanup = undefined;
isFocused = false; isFocused = false;
}); });
return () => { return () => {
cleanup && cleanup(); cleanup?.();
unsubscribeFocus(); unsubscribeFocus();
unsubscribeBlur(); unsubscribeBlur();
}; };

View File

@@ -21,8 +21,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
// 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 fcous
React.useEffect( React.useEffect(
() => () =>
navigation && navigation?.addListener('focus', () =>
navigation.addListener('focus', () =>
emitter.emit({ type: 'focus', target: currentFocusedKey }) emitter.emit({ type: 'focus', target: currentFocusedKey })
), ),
[currentFocusedKey, emitter, navigation] [currentFocusedKey, emitter, navigation]
@@ -30,8 +29,7 @@ export default function useFocusEvents({ state, emitter }: Options) {
React.useEffect( React.useEffect(
() => () =>
navigation && navigation?.addListener('blur', () =>
navigation.addListener('blur', () =>
emitter.emit({ type: 'blur', target: currentFocusedKey }) emitter.emit({ type: 'blur', target: currentFocusedKey })
), ),
[currentFocusedKey, emitter, navigation] [currentFocusedKey, emitter, navigation]

View File

@@ -38,7 +38,7 @@ export default function useFocusedListenersChildrenAdapter({
[focusedListeners, navigation] [focusedListeners, navigation]
); );
React.useEffect(() => addFocusedListener && addFocusedListener(listener), [ React.useEffect(() => addFocusedListener?.(listener), [
addFocusedListener, addFocusedListener,
listener, listener,
]); ]);

View File

@@ -117,7 +117,7 @@ export default function useNavigationBuilder<
const { current: router } = React.useRef<Router<State, any>>( const { current: router } = React.useRef<Router<State, any>>(
createRouter({ createRouter({
...((rest as unknown) as RouterOptions), ...((rest as unknown) as RouterOptions),
...(route && route.params && typeof route.params.screen === 'string' ...(route?.params && typeof route.params.screen === 'string'
? { initialRouteName: route.params.screen } ? { initialRouteName: route.params.screen }
: null), : null),
}) })
@@ -141,7 +141,7 @@ export default function useNavigationBuilder<
(acc, curr) => { (acc, curr) => {
const { initialParams } = screens[curr]; const { initialParams } = screens[curr];
const initialParamsFromParams = const initialParamsFromParams =
route && route.params && route.params.screen === curr route?.params && route.params.screen === curr
? route.params.params ? route.params.params
: undefined; : undefined;

View File

@@ -42,13 +42,22 @@ export default function useNavigationHelpers<
const { performTransaction } = React.useContext(NavigationStateContext); const { performTransaction } = React.useContext(NavigationStateContext);
return React.useMemo(() => { return React.useMemo(() => {
const dispatch = (action: Action | ((state: State) => Action)) => const dispatch = (action: Action | ((state: State) => Action)) => {
performTransaction(() => { performTransaction(() => {
const payload = const payload =
typeof action === 'function' ? action(getState()) : action; typeof action === 'function' ? action(getState()) : action;
onAction(payload); const handled = onAction(payload);
if (!handled && process.env.NODE_ENV !== 'production') {
console.error(
`The action '${payload.type}' with payload '${JSON.stringify(
payload.payload
)}' was not handled by any navigator.`
);
}
}); });
};
const actions = { const actions = {
...router.actionCreators, ...router.actionCreators,
@@ -81,7 +90,7 @@ export default function useNavigationHelpers<
routeNames: state.routeNames, routeNames: state.routeNames,
routeParamList: {}, routeParamList: {},
}) !== null || }) !== null ||
(parentNavigationHelpers && parentNavigationHelpers.canGoBack()) || parentNavigationHelpers?.canGoBack() ||
false false
); );
}, },

View File

@@ -130,10 +130,10 @@ export default function useOnAction({
] ]
); );
React.useEffect( React.useEffect(() => addActionListenerParent?.(onAction), [
() => addActionListenerParent && addActionListenerParent(onAction), addActionListenerParent,
[addActionListenerParent, onAction] onAction,
); ]);
return onAction; return onAction;
} }

View File

@@ -26,6 +26,6 @@ export default function useOnGetState({
}, [getState, getStateForRoute]); }, [getState, getStateForRoute]);
React.useEffect(() => { React.useEffect(() => {
return addStateGetter && addStateGetter(key, getRehydratedState); return addStateGetter?.(key, getRehydratedState);
}, [addStateGetter, getRehydratedState, key]); }, [addStateGetter, getRehydratedState, key]);
} }

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.31](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.30...@react-navigation/drawer@5.0.0-alpha.31) (2020-01-01)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.30](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.29...@react-navigation/drawer@5.0.0-alpha.30) (2019-12-19)
### Bug Fixes
* set screen background in drawer from theme ([0635365](https://github.com/react-navigation/navigation-ex/commit/0635365483bf5ac38e75191b4ba8f52cf6d73896))
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.28...@react-navigation/drawer@5.0.0-alpha.29) (2019-12-16)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.27...@react-navigation/drawer@5.0.0-alpha.28) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.26...@react-navigation/drawer@5.0.0-alpha.27) (2019-12-11) # [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.26...@react-navigation/drawer@5.0.0-alpha.27) (2019-12-11)
**Note:** Version bump only for package @react-navigation/drawer **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -11,7 +11,7 @@
"material", "material",
"drawer" "drawer"
], ],
"version": "5.0.0-alpha.27", "version": "5.0.0-alpha.31",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -34,20 +34,21 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19",
"color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.60.25",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.5.0", "react-native-gesture-handler": "^1.5.0",
"react-native-reanimated": "^1.4.0", "react-native-reanimated": "^1.4.0",
"react-native-safe-area-context": "^0.6.0", "react-native-safe-area-context": "^0.6.0",
"react-native-screens": "^2.0.0-alpha.11", "react-native-screens": "^2.0.0-alpha.19",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",
@@ -55,7 +56,7 @@
"react-native": "*", "react-native": "*",
"react-native-gesture-handler": "^1.0.0", "react-native-gesture-handler": "^1.0.0",
"react-native-reanimated": "^1.0.0", "react-native-reanimated": "^1.0.0",
"react-native-safe-area-context": "^0.3.6", "react-native-safe-area-context": "^0.6.0",
"react-native-screens": "^1.0.0-alpha.0 || ^2.0.0-alpha.0" "react-native-screens": "^1.0.0-alpha.0 || ^2.0.0-alpha.0"
}, },
"@react-native-community/bob": { "@react-native-community/bob": {

View File

@@ -10,6 +10,7 @@ export { default as DrawerView } from './views/DrawerView';
export { default as DrawerItem } from './views/DrawerItem'; export { default as DrawerItem } from './views/DrawerItem';
export { default as DrawerItemList } from './views/DrawerItemList'; export { default as DrawerItemList } from './views/DrawerItemList';
export { default as DrawerContent } from './views/DrawerContent'; export { default as DrawerContent } from './views/DrawerContent';
export { default as DrawerContentScrollView } from './views/DrawerContentScrollView';
/** /**
* Utilities * Utilities

View File

@@ -19,7 +19,7 @@ import {
type Props = DefaultNavigatorOptions<DrawerNavigationOptions> & type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
DrawerRouterOptions & DrawerRouterOptions &
Partial<DrawerNavigationConfig>; DrawerNavigationConfig;
function DrawerNavigator({ function DrawerNavigator({
initialRouteName, initialRouteName,

View File

@@ -20,14 +20,14 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/** /**
* Position of the drawer on the screen. Defaults to `left`. * Position of the drawer on the screen. Defaults to `left`.
*/ */
drawerPosition: 'left' | 'right'; drawerPosition?: 'left' | 'right';
/** /**
* Type of the drawer. It determines how the drawer looks and animates. * Type of the drawer. It determines how the drawer looks and animates.
* - `front`: Traditional drawer which covers the screen with a overlay behind it. * - `front`: Traditional drawer which covers the screen with a overlay behind it.
* - `back`: The drawer is revealed behind the screen on swipe. * - `back`: The drawer is revealed behind the screen on swipe.
* - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer. * - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
*/ */
drawerType: 'front' | 'back' | 'slide'; drawerType?: 'front' | 'back' | 'slide';
/** /**
* How far from the edge of the screen the swipe gesture should activate. * How far from the edge of the screen the swipe gesture should activate.
*/ */
@@ -35,12 +35,12 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/** /**
* Whether the statusbar should be hidden when the drawer is pulled or opens, * Whether the statusbar should be hidden when the drawer is pulled or opens,
*/ */
hideStatusBar: boolean; hideStatusBar?: boolean;
/** /**
* Whether the keyboard should be dismissed when the swipe gesture begins. * Whether the keyboard should be dismissed when the swipe gesture begins.
* Defaults to `'on-drag'`. Set to `'none'` to disable keyboard handling. * Defaults to `'on-drag'`. Set to `'none'` to disable keyboard handling.
*/ */
keyboardDismissMode: 'on-drag' | 'none'; keyboardDismissMode?: 'on-drag' | 'none';
/** /**
* Minimum swipe distance threshold that should activate opening the drawer. * Minimum swipe distance threshold that should activate opening the drawer.
*/ */
@@ -53,7 +53,7 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/** /**
* Animation of the statusbar when hiding it. use in combination with `hideStatusBar`. * Animation of the statusbar when hiding it. use in combination with `hideStatusBar`.
*/ */
statusBarAnimation: 'slide' | 'none' | 'fade'; statusBarAnimation?: 'slide' | 'none' | 'fade';
/** /**
* Props to pass to the underlying pan gesture handler. * Props to pass to the underlying pan gesture handler.
*/ */
@@ -62,7 +62,7 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
* Whether the screens should render the first time they are accessed. Defaults to `true`. * Whether the screens should render the first time they are accessed. Defaults to `true`.
* Set it to `false` if you want to render all screens on initial render. * Set it to `false` if you want to render all screens on initial render.
*/ */
lazy: boolean; lazy?: boolean;
/** /**
* Whether a screen should be unmounted when navigating away from it. * Whether a screen should be unmounted when navigating away from it.
* Defaults to `false`. * Defaults to `false`.
@@ -72,7 +72,7 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
* Function that returns React element to render as the content of the drawer, for example, navigation items. * Function that returns React element to render as the content of the drawer, for example, navigation items.
* Defaults to `DrawerContent`. * Defaults to `DrawerContent`.
*/ */
drawerContent: (props: DrawerContentComponentProps<T>) => React.ReactNode; drawerContent?: (props: DrawerContentComponentProps<T>) => React.ReactNode;
/** /**
* Options for the content component which will be passed as props. * Options for the content component which will be passed as props.
*/ */
@@ -121,7 +121,7 @@ export type DrawerNavigationOptions = {
export type DrawerContentComponentProps<T = DrawerContentOptions> = T & { export type DrawerContentComponentProps<T = DrawerContentOptions> = T & {
state: DrawerNavigationState; state: DrawerNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
/** /**
* Animated node which represents the current progress of the drawer's open state. * Animated node which represents the current progress of the drawer's open state.

View File

@@ -288,7 +288,7 @@ export default class DrawerView extends React.PureComponent<Props> {
set(this.offsetX, 0), set(this.offsetX, 0),
// When the animation finishes, stop the clock // When the animation finishes, stop the clock
stopClock(this.clock), stopClock(this.clock),
call([this.isOpen], ([value]: ReadonlyArray<Binary>) => { call([this.isOpen], ([value]: readonly Binary[]) => {
const open = Boolean(value); const open = Boolean(value);
if (open !== this.props.open) { if (open !== this.props.open) {
@@ -304,7 +304,7 @@ export default class DrawerView extends React.PureComponent<Props> {
private dragX = block([ private dragX = block([
onChange( onChange(
this.isOpen, this.isOpen,
call([this.isOpen], ([value]: ReadonlyArray<Binary>) => { call([this.isOpen], ([value]: readonly Binary[]) => {
const open = Boolean(value); const open = Boolean(value);
this.currentOpenValue = open; this.currentOpenValue = open;
@@ -344,7 +344,7 @@ export default class DrawerView extends React.PureComponent<Props> {
// Listen to updates for this value only when it changes // Listen to updates for this value only when it changes
// Without `onChange`, this will fire even if the value didn't change // Without `onChange`, this will fire even if the value didn't change
// We don't want to call the listeners if the value didn't change // We don't want to call the listeners if the value didn't change
call([this.isSwiping], ([value]: ReadonlyArray<Binary>) => { call([this.isSwiping], ([value]: readonly Binary[]) => {
const { keyboardDismissMode } = this.props; const { keyboardDismissMode } = this.props;
if (value === TRUE) { if (value === TRUE) {

View File

@@ -1,36 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
import { useSafeArea } from 'react-native-safe-area-context';
import DrawerItemList from './DrawerItemList'; import DrawerItemList from './DrawerItemList';
import { DrawerContentComponentProps } from '../types'; import { DrawerContentComponentProps } from '../types';
import DrawerContentScrollView from './DrawerContentScrollView';
export default function DrawerContent({ export default function DrawerContent(props: DrawerContentComponentProps) {
contentContainerStyle,
style,
drawerPosition,
...rest
}: DrawerContentComponentProps) {
const insets = useSafeArea();
return ( return (
<ScrollView <DrawerContentScrollView {...props}>
contentContainerStyle={[ <DrawerItemList {...props} />
{ </DrawerContentScrollView>
paddingTop: insets.top + 4,
paddingLeft: drawerPosition === 'left' ? insets.left : 0,
paddingRight: drawerPosition === 'right' ? insets.right : 0,
},
contentContainerStyle,
]}
style={[styles.container, style]}
>
<DrawerItemList {...rest} />
</ScrollView>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import { ScrollView, StyleSheet, ScrollViewProps } from 'react-native';
import { useSafeArea } from 'react-native-safe-area-context';
type Props = ScrollViewProps & {
drawerPosition: 'left' | 'right';
children: React.ReactNode;
};
export default function DrawerContentScrollView({
contentContainerStyle,
style,
drawerPosition,
children,
...rest
}: Props) {
const insets = useSafeArea();
return (
<ScrollView
{...rest}
contentContainerStyle={[
{
paddingTop: insets.top + 4,
paddingLeft: drawerPosition === 'left' ? insets.left : 0,
paddingRight: drawerPosition === 'right' ? insets.right : 0,
},
contentContainerStyle,
]}
style={[styles.container, style]}
>
{children}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -7,6 +7,8 @@ import {
ViewStyle, ViewStyle,
TextStyle, TextStyle,
} from 'react-native'; } from 'react-native';
import { useTheme } from '@react-navigation/native';
import Color from 'color';
import TouchableItem from './TouchableItem'; import TouchableItem from './TouchableItem';
type Props = { type Props = {
@@ -61,19 +63,29 @@ type Props = {
/** /**
* A component used to show an action item with an icon and a label in a navigation drawer. * A component used to show an action item with an icon and a label in a navigation drawer.
*/ */
export default function DrawerItem({ export default function DrawerItem(props: Props) {
icon, const { colors } = useTheme();
label,
labelStyle, const {
focused = false, icon,
activeTintColor = '#6200ee', label,
inactiveTintColor = 'rgba(0, 0, 0, .68)', labelStyle,
activeBackgroundColor = 'rgba(98, 0, 238, 0.12)', focused = false,
inactiveBackgroundColor = 'transparent', activeTintColor = colors.primary,
style, inactiveTintColor = Color(colors.text)
onPress, .alpha(0.68)
...rest .rgb()
}: Props) { .string(),
activeBackgroundColor = Color(activeTintColor)
.alpha(0.12)
.rgb()
.string(),
inactiveBackgroundColor = 'transparent',
style,
onPress,
...rest
} = props;
const { borderRadius = 4 } = StyleSheet.flatten(style || {}); const { borderRadius = 4 } = StyleSheet.flatten(style || {});
const color = focused ? activeTintColor : inactiveTintColor; const color = focused ? activeTintColor : inactiveTintColor;
const backgroundColor = focused const backgroundColor = focused

View File

@@ -13,6 +13,7 @@ import {
DrawerNavigationState, DrawerNavigationState,
DrawerActions, DrawerActions,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import { useTheme } from '@react-navigation/native';
import DrawerGestureContext from '../utils/DrawerGestureContext'; import DrawerGestureContext from '../utils/DrawerGestureContext';
import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import SafeAreaProviderCompat from './SafeAreaProviderCompat';
@@ -26,16 +27,10 @@ import {
DrawerContentComponentProps, DrawerContentComponentProps,
} from '../types'; } from '../types';
type Props = Omit<DrawerNavigationConfig, 'overlayColor'> & { type Props = DrawerNavigationConfig & {
state: DrawerNavigationState; state: DrawerNavigationState;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
overlayColor: string;
};
type State = {
loaded: number[];
drawerWidth: number;
}; };
const getDefaultDrawerWidth = ({ const getDefaultDrawerWidth = ({
@@ -62,49 +57,52 @@ const getDefaultDrawerWidth = ({
/** /**
* Component that renders the drawer. * Component that renders the drawer.
*/ */
export default class DrawerView extends React.PureComponent<Props, State> { export default function DrawerView({
static defaultProps = { state,
lazy: true, navigation,
drawerContent: (props: DrawerContentComponentProps) => ( descriptors,
<DrawerContent {...props} /> lazy = true,
), drawerContent = (props: DrawerContentComponentProps) => (
drawerPosition: I18nManager.isRTL ? 'right' : 'left', <DrawerContent {...props} />
keyboardDismissMode: 'on-drag', ),
overlayColor: 'rgba(0, 0, 0, 0.5)', drawerPosition = I18nManager.isRTL ? 'right' : 'left',
drawerType: 'front', keyboardDismissMode = 'on-drag',
hideStatusBar: false, overlayColor = 'rgba(0, 0, 0, 0.5)',
statusBarAnimation: 'slide', drawerType = 'front',
}; hideStatusBar = false,
statusBarAnimation = 'slide',
drawerContentOptions,
drawerStyle,
edgeWidth,
gestureHandlerProps,
minSwipeDistance,
sceneContainerStyle,
unmountInactiveScreens,
}: Props) {
const [loaded, setLoaded] = React.useState([state.index]);
const [drawerWidth, setDrawerWidth] = React.useState(() =>
getDefaultDrawerWidth(Dimensions.get('window'))
);
static getDerivedStateFromProps(nextProps: Props, prevState: State) { const drawerGestureRef = React.useRef<PanGestureHandler>(null);
const { index } = nextProps.state;
return { const { colors } = useTheme();
// Set the current tab to be loaded if it was not loaded before
loaded: prevState.loaded.includes(index) React.useEffect(() => {
? prevState.loaded const updateWidth = ({ window }: { window: ScaledSize }) => {
: [...prevState.loaded, index], setDrawerWidth(getDefaultDrawerWidth(window));
}; };
Dimensions.addEventListener('change', updateWidth);
return () => Dimensions.removeEventListener('change', updateWidth);
}, []);
if (!loaded.includes(state.index)) {
setLoaded([...loaded, state.index]);
} }
state: State = { const handleDrawerOpen = () => {
loaded: [this.props.state.index],
drawerWidth: getDefaultDrawerWidth(Dimensions.get('window')),
};
componentDidMount() {
Dimensions.addEventListener('change', this.updateWidth);
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this.updateWidth);
}
private drawerGestureRef = React.createRef<PanGestureHandler>();
private handleDrawerOpen = () => {
const { state, navigation } = this.props;
navigation.dispatch({ navigation.dispatch({
...DrawerActions.openDrawer(), ...DrawerActions.openDrawer(),
target: state.key, target: state.key,
@@ -113,9 +111,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
navigation.emit({ type: 'drawerOpen' }); navigation.emit({ type: 'drawerOpen' });
}; };
private handleDrawerClose = () => { const handleDrawerClose = () => {
const { state, navigation } = this.props;
navigation.dispatch({ navigation.dispatch({
...DrawerActions.closeDrawer(), ...DrawerActions.closeDrawer(),
target: state.key, target: state.key,
@@ -124,24 +120,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
navigation.emit({ type: 'drawerClose' }); navigation.emit({ type: 'drawerClose' });
}; };
private updateWidth = ({ window }: { window: ScaledSize }) => { const renderNavigationView = ({ progress }: any) => {
const drawerWidth = getDefaultDrawerWidth(window);
if (this.state.drawerWidth !== drawerWidth) {
this.setState({ drawerWidth });
}
};
private renderNavigationView = ({ progress }: any) => {
const {
state,
navigation,
descriptors,
drawerPosition,
drawerContent,
drawerContentOptions,
} = this.props;
return drawerContent({ return drawerContent({
...drawerContentOptions, ...drawerContentOptions,
progress: progress, progress: progress,
@@ -152,11 +131,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
}); });
}; };
private renderContent = () => { const renderContent = () => {
let { lazy, state, descriptors, unmountInactiveScreens } = this.props;
const { loaded } = this.state;
return ( return (
<ScreenContainer style={styles.content}> <ScreenContainer style={styles.content}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
@@ -164,7 +139,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
return null; return null;
} }
if (lazy && !loaded.includes(index)) { if (lazy && !loaded.includes(index) && index !== state.index) {
// Don't render a screen if we've never navigated to it // Don't render a screen if we've never navigated to it
return null; return null;
} }
@@ -186,58 +161,45 @@ export default class DrawerView extends React.PureComponent<Props, State> {
); );
}; };
private setDrawerGestureRef = (ref: PanGestureHandler | null) => { const activeKey = state.routes[state.index].key;
// @ts-ignore const { gestureEnabled } = descriptors[activeKey].options;
this.drawerGestureRef.current = ref;
};
render() { return (
const { <SafeAreaProviderCompat>
state, <DrawerGestureContext.Provider value={drawerGestureRef}>
descriptors, <Drawer
drawerType, open={state.isDrawerOpen}
drawerPosition, gestureEnabled={gestureEnabled !== false}
overlayColor, onOpen={handleDrawerOpen}
sceneContainerStyle, onClose={handleDrawerClose}
drawerStyle, onGestureRef={ref => {
edgeWidth, // @ts-ignore
minSwipeDistance, drawerGestureRef.current = ref;
hideStatusBar, }}
statusBarAnimation, gestureHandlerProps={gestureHandlerProps}
gestureHandlerProps, drawerType={drawerType}
} = this.props; drawerPosition={drawerPosition}
sceneContainerStyle={[
const { drawerWidth } = this.state; { backgroundColor: colors.background },
sceneContainerStyle,
const activeKey = state.routes[state.index].key; ]}
const { gestureEnabled } = descriptors[activeKey].options; drawerStyle={[
{ width: drawerWidth, backgroundColor: colors.card },
return ( drawerStyle,
<SafeAreaProviderCompat> ]}
<DrawerGestureContext.Provider value={this.drawerGestureRef}> overlayStyle={{ backgroundColor: overlayColor }}
<Drawer swipeEdgeWidth={edgeWidth}
open={state.isDrawerOpen} swipeDistanceThreshold={minSwipeDistance}
gestureEnabled={gestureEnabled !== false} hideStatusBar={hideStatusBar}
onOpen={this.handleDrawerOpen} statusBarAnimation={statusBarAnimation}
onClose={this.handleDrawerClose} renderDrawerContent={renderNavigationView}
onGestureRef={this.setDrawerGestureRef} renderSceneContent={renderContent}
gestureHandlerProps={gestureHandlerProps} keyboardDismissMode={keyboardDismissMode}
drawerType={drawerType} drawerPostion={drawerPosition}
drawerPosition={drawerPosition} />
sceneContainerStyle={sceneContainerStyle} </DrawerGestureContext.Provider>
drawerStyle={[{ width: drawerWidth }, drawerStyle]} </SafeAreaProviderCompat>
overlayStyle={{ backgroundColor: overlayColor }} );
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}
statusBarAnimation={statusBarAnimation}
renderDrawerContent={this.renderNavigationView}
renderSceneContent={this.renderContent}
/>
</DrawerGestureContext.Provider>
</SafeAreaProviderCompat>
);
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -13,7 +13,7 @@ const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view o
export default class ResourceSavingScene extends React.Component<Props> { export default class ResourceSavingScene extends React.Component<Props> {
render() { render() {
if (screensEnabled && screensEnabled()) { if (screensEnabled?.()) {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-ignore // @ts-ignore

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.27...@react-navigation/material-bottom-tabs@5.0.0-alpha.28) (2020-01-01)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.26...@react-navigation/material-bottom-tabs@5.0.0-alpha.27) (2019-12-19)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.25...@react-navigation/material-bottom-tabs@5.0.0-alpha.26) (2019-12-16)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.24...@react-navigation/material-bottom-tabs@5.0.0-alpha.25) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.24](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.23...@react-navigation/material-bottom-tabs@5.0.0-alpha.24) (2019-12-11) # [5.0.0-alpha.24](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.23...@react-navigation/material-bottom-tabs@5.0.0-alpha.24) (2019-12-11)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -11,7 +11,7 @@
"material", "material",
"tab" "tab"
], ],
"version": "5.0.0-alpha.24", "version": "5.0.0-alpha.28",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -34,19 +34,19 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.60.25",
"@types/react-native-vector-icons": "^6.4.4", "@types/react-native-vector-icons": "^6.4.4",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-paper": "^3.2.1", "react-native-paper": "^3.3.0",
"react-native-vector-icons": "^6.6.0", "react-native-vector-icons": "^6.6.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",

View File

@@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { BottomNavigation } from 'react-native-paper'; import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Route } from '@react-navigation/native'; import { Route, useTheme } from '@react-navigation/native';
import { TabNavigationState, TabActions } from '@react-navigation/routers'; import { TabNavigationState, TabActions } from '@react-navigation/routers';
import { import {
@@ -25,9 +25,25 @@ export default function MaterialBottomTabView({
descriptors, descriptors,
...rest ...rest
}: Props) { }: Props) {
const { dark, colors } = useTheme();
const theme = React.useMemo(() => {
const t = dark ? DarkTheme : DefaultTheme;
return {
...t,
colors: {
...t.colors,
...colors,
surface: colors.card,
},
};
}, [colors, dark]);
return ( return (
<BottomNavigation <BottomNavigation
{...rest} {...rest}
theme={theme}
navigationState={state} navigationState={state}
onIndexChange={(index: number) => onIndexChange={(index: number) =>
navigation.dispatch({ navigation.dispatch({

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.25...@react-navigation/material-top-tabs@5.0.0-alpha.26) (2020-01-01)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.0.0-alpha.25](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.24...@react-navigation/material-top-tabs@5.0.0-alpha.25) (2019-12-19)
### Bug Fixes
* fix backgroundColor in sceneContainerStyle overriden by theme ([ebd145a](https://github.com/react-navigation/navigation-ex/commit/ebd145a09d80f119070a14a8d4940b5757b5e7fb)), closes [#215](https://github.com/react-navigation/navigation-ex/issues/215)
# [5.0.0-alpha.24](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.23...@react-navigation/material-top-tabs@5.0.0-alpha.24) (2019-12-16)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.0.0-alpha.23](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.22...@react-navigation/material-top-tabs@5.0.0-alpha.23) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.22](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.21...@react-navigation/material-top-tabs@5.0.0-alpha.22) (2019-12-11) # [5.0.0-alpha.22](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.21...@react-navigation/material-top-tabs@5.0.0-alpha.22) (2019-12-11)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -11,7 +11,7 @@
"material", "material",
"tab" "tab"
], ],
"version": "5.0.0-alpha.22", "version": "5.0.0-alpha.26",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -34,19 +34,20 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19",
"color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.60.25",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.5.0", "react-native-gesture-handler": "^1.5.0",
"react-native-reanimated": "^1.4.0", "react-native-reanimated": "^1.4.0",
"react-native-tab-view": "^2.11.0", "react-native-tab-view": "^2.11.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",

View File

@@ -1,29 +1,46 @@
import * as React from 'react'; import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import { TabBar } from 'react-native-tab-view'; import { TabBar } from 'react-native-tab-view';
import { Route } from '@react-navigation/native'; import { Route, useTheme } from '@react-navigation/native';
import Color from 'color';
import { MaterialTopTabBarProps } from '../types'; import { MaterialTopTabBarProps } from '../types';
export default function TabBarTop({ export default function TabBarTop(props: MaterialTopTabBarProps) {
state, const { colors } = useTheme();
navigation,
descriptors, const {
activeTintColor = 'rgba(255, 255, 255, 1)', state,
inactiveTintColor = 'rgba(255, 255, 255, 0.7)', navigation,
allowFontScaling = true, descriptors,
iconStyle, activeTintColor = colors.text,
labelStyle, inactiveTintColor = Color(activeTintColor)
showIcon = false, .alpha(0.5)
showLabel = true, .rgb()
...rest .string(),
}: MaterialTopTabBarProps) { allowFontScaling = true,
showIcon = false,
showLabel = true,
pressColor = Color(activeTintColor)
.alpha(0.08)
.rgb()
.string(),
iconStyle,
labelStyle,
indicatorStyle,
style,
...rest
} = props;
return ( return (
<TabBar <TabBar
{...rest} {...rest}
navigationState={state} navigationState={state}
activeColor={activeTintColor} activeColor={activeTintColor}
inactiveColor={inactiveTintColor} inactiveColor={inactiveTintColor}
indicatorStyle={[{ backgroundColor: colors.primary }, indicatorStyle]}
style={[{ backgroundColor: colors.card }, style]}
pressColor={pressColor}
getAccessibilityLabel={({ route }) => getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel descriptors[route.key].options.tabBarAccessibilityLabel
} }

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { TabView, SceneRendererProps } from 'react-native-tab-view'; import { TabView, SceneRendererProps } from 'react-native-tab-view';
import { Route } from '@react-navigation/native'; import { Route, useTheme } from '@react-navigation/native';
import { TabNavigationState, TabActions } from '@react-navigation/routers'; import { TabNavigationState, TabActions } from '@react-navigation/routers';
import MaterialTopTabBar from './MaterialTopTabBar'; import MaterialTopTabBar from './MaterialTopTabBar';
@@ -15,17 +15,22 @@ type Props = MaterialTopTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState;
navigation: MaterialTopTabNavigationHelpers; navigation: MaterialTopTabNavigationHelpers;
descriptors: MaterialTopTabDescriptorMap; descriptors: MaterialTopTabDescriptorMap;
tabBarPosition: 'top' | 'bottom'; tabBarPosition?: 'top' | 'bottom';
}; };
export default class MaterialTopTabView extends React.PureComponent<Props> { export default function MaterialTopTabView({
static defaultProps = { lazyPlaceholder,
tabBarPosition: 'top', tabBar = (props: MaterialTopTabBarProps) => <MaterialTopTabBar {...props} />,
}; tabBarOptions,
state,
private renderLazyPlaceholder = (props: { route: Route<string> }) => { navigation,
const { lazyPlaceholder } = this.props; descriptors,
sceneContainerStyle,
...rest
}: Props) {
const { colors } = useTheme();
const renderLazyPlaceholder = (props: { route: Route<string> }) => {
if (lazyPlaceholder != null) { if (lazyPlaceholder != null) {
return lazyPlaceholder(props); return lazyPlaceholder(props);
} }
@@ -33,21 +38,12 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
return null; return null;
}; };
private renderTabBar = (props: SceneRendererProps) => { const renderTabBar = (props: SceneRendererProps) => {
const { state, descriptors } = this.props;
const route = state.routes[state.index]; const route = state.routes[state.index];
const options = descriptors[route.key].options; const options = descriptors[route.key].options;
const tabBarVisible = options.tabBarVisible !== false; const tabBarVisible = options.tabBarVisible !== false;
const {
navigation,
tabBar = (props: MaterialTopTabBarProps) => (
<MaterialTopTabBar {...props} />
),
tabBarOptions,
} = this.props;
if (tabBarVisible === false) { if (tabBarVisible === false) {
return null; return null;
} }
@@ -61,45 +57,25 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
}); });
}; };
private handleSwipeStart = () => return (
this.props.navigation.emit({ <TabView
type: 'swipeStart', {...rest}
}); onIndexChange={index =>
navigation.dispatch({
private handleSwipeEnd = () => ...TabActions.jumpTo(state.routes[index].name),
this.props.navigation.emit({ target: state.key,
type: 'swipeEnd', })
}); }
renderScene={({ route }) => descriptors[route.key].render()}
render() { navigationState={state}
const { renderTabBar={renderTabBar}
/* eslint-disable @typescript-eslint/no-unused-vars */ renderLazyPlaceholder={renderLazyPlaceholder}
lazyPlaceholder, onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
tabBar, onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
tabBarOptions, sceneContainerStyle={[
/* eslint-enable @typescript-eslint/no-unused-vars */ { backgroundColor: colors.background },
state, sceneContainerStyle,
navigation, ]}
descriptors, />
...rest );
} = this.props;
return (
<TabView
{...rest}
onIndexChange={index =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={this.renderTabBar}
renderLazyPlaceholder={this.renderLazyPlaceholder}
onSwipeStart={this.handleSwipeStart}
onSwipeEnd={this.handleSwipeEnd}
/>
);
}
} }

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.20](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.19...@react-navigation/native-stack@5.0.0-alpha.20) (2020-01-01)
**Note:** Version bump only for package @react-navigation/native-stack
# [5.0.0-alpha.19](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.18...@react-navigation/native-stack@5.0.0-alpha.19) (2019-12-19)
**Note:** Version bump only for package @react-navigation/native-stack
# [5.0.0-alpha.18](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.17...@react-navigation/native-stack@5.0.0-alpha.18) (2019-12-16)
**Note:** Version bump only for package @react-navigation/native-stack
# [5.0.0-alpha.17](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.16...@react-navigation/native-stack@5.0.0-alpha.17) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.16](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.15...@react-navigation/native-stack@5.0.0-alpha.16) (2019-12-11) # [5.0.0-alpha.16](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native-stack@5.0.0-alpha.15...@react-navigation/native-stack@5.0.0-alpha.16) (2019-12-11)
**Note:** Version bump only for package @react-navigation/native-stack **Note:** Version bump only for package @react-navigation/native-stack

View File

@@ -2,7 +2,7 @@
Stack navigator for React Native using native primitives for navigation. Uses [`react-native-screens`](https://github.com/kmagiera/react-native-screens) under the hood. Stack navigator for React Native using native primitives for navigation. Uses [`react-native-screens`](https://github.com/kmagiera/react-native-screens) under the hood.
Expo is currently not supported as it includes an older version of `react-native-screens`. Expo SDK 35 and lower is not supported as it includes an older version of `react-native-screens`.
## Installation ## Installation
@@ -11,12 +11,30 @@ Open a Terminal in your project's folder and run,
```sh ```sh
yarn add @react-navigation/native @react-navigation/native-stack yarn add @react-navigation/native @react-navigation/native-stack
``` ```
Or with npm
Now we need to install [`react-native-screens`](https://github.com/kmagiera/react-native-screens). ```sh
npm install --save @react-navigation/native @react-navigation/native-stack
```
If you are using Expo, to ensure that you get the compatible versions of the libraries, run:
```sh
expo install react-native-screens
```
If you are not using Expo, run the following:
```sh ```sh
yarn add react-native-screens yarn add react-native-screens
``` ```
Or with npm
```sh
npm install --save react-native-screens
```
If you are using Expo, you are done. Otherwise, continue to the next steps.
To complete the linking on iOS, make sure you have [Cocoapods](https://cocoapods.org/) installed. Then run: To complete the linking on iOS, make sure you have [Cocoapods](https://cocoapods.org/) installed. Then run:
@@ -36,9 +54,9 @@ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'
Make sure to enable `react-native-screens`. This needs to be done before our app renders. To do it, add the following code in your entry file (e.g. `App.js`): Make sure to enable `react-native-screens`. This needs to be done before our app renders. To do it, add the following code in your entry file (e.g. `App.js`):
```js ```js
import { useScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
useScreens(); enableScreens();
``` ```
## Usage ## Usage

View File

@@ -6,7 +6,7 @@
"react-native", "react-native",
"react-navigation" "react-navigation"
], ],
"version": "5.0.0-alpha.16", "version": "5.0.0-alpha.20",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -29,13 +29,13 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react-native-screens": "^2.0.0-alpha.11", "react-native-screens": "^2.0.0-alpha.19",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-navigation/native": "^5.0.0-alpha.0", "@react-navigation/native": "^5.0.0-alpha.0",

View File

@@ -6,7 +6,7 @@ import {
ScreenStackHeaderRightView, ScreenStackHeaderRightView,
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
} from 'react-native-screens'; } from 'react-native-screens';
import { Route } from '@react-navigation/native'; import { Route, useTheme } from '@react-navigation/native';
import { NativeStackNavigationOptions } from '../types'; import { NativeStackNavigationOptions } from '../types';
type Props = NativeStackNavigationOptions & { type Props = NativeStackNavigationOptions & {
@@ -14,6 +14,7 @@ type Props = NativeStackNavigationOptions & {
}; };
export default function HeaderConfig(props: Props) { export default function HeaderConfig(props: Props) {
const { colors } = useTheme();
const { const {
route, route,
title, title,
@@ -52,17 +53,23 @@ export default function HeaderConfig(props: Props) {
titleColor={ titleColor={
headerTitleStyle.color !== undefined headerTitleStyle.color !== undefined
? headerTitleStyle.color ? headerTitleStyle.color
: headerTintColor : headerTintColor !== undefined
? headerTintColor
: colors.text
} }
backTitle={headerBackTitleVisible ? headerBackTitle : ''} backTitle={headerBackTitleVisible ? headerBackTitle : ''}
backTitleFontFamily={headerBackTitleStyle.fontFamily} backTitleFontFamily={headerBackTitleStyle.fontFamily}
backTitleFontSize={headerBackTitleStyle.fontSize} backTitleFontSize={headerBackTitleStyle.fontSize}
color={headerTintColor} color={headerTintColor !== undefined ? headerTintColor : colors.primary}
gestureEnabled={gestureEnabled === undefined ? true : gestureEnabled} gestureEnabled={gestureEnabled === undefined ? true : gestureEnabled}
largeTitle={headerLargeTitle} largeTitle={headerLargeTitle}
largeTitleFontFamily={headerLargeTitleStyle.fontFamily} largeTitleFontFamily={headerLargeTitleStyle.fontFamily}
largeTitleFontSize={headerLargeTitleStyle.fontSize} largeTitleFontSize={headerLargeTitleStyle.fontSize}
backgroundColor={headerStyle.backgroundColor} backgroundColor={
headerStyle.backgroundColor !== undefined
? headerStyle.backgroundColor
: colors.card
}
> >
{headerRight !== undefined ? ( {headerRight !== undefined ? (
<ScreenStackHeaderRightView>{headerRight()}</ScreenStackHeaderRightView> <ScreenStackHeaderRightView>{headerRight()}</ScreenStackHeaderRightView>

View File

@@ -9,6 +9,7 @@ import {
ScreenProps, ScreenProps,
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
} from 'react-native-screens'; } from 'react-native-screens';
import { useTheme } from '@react-navigation/native';
import HeaderConfig from './HeaderConfig'; import HeaderConfig from './HeaderConfig';
import { import {
NativeStackNavigationHelpers, NativeStackNavigationHelpers,
@@ -34,8 +35,10 @@ export default function NativeStackView({
navigation, navigation,
descriptors, descriptors,
}: Props) { }: Props) {
const { colors } = useTheme();
return ( return (
<ScreenStack style={styles.scenes}> <ScreenStack style={styles.container}>
{state.routes.map(route => { {state.routes.map(route => {
const { options, render: renderScene } = descriptors[route.key]; const { options, render: renderScene } = descriptors[route.key];
const { presentation = 'push', animation, contentStyle } = options; const { presentation = 'push', animation, contentStyle } = options;
@@ -55,7 +58,15 @@ export default function NativeStackView({
}} }}
> >
<HeaderConfig {...options} route={route} /> <HeaderConfig {...options} route={route} />
<View style={[styles.content, contentStyle]}>{renderScene()}</View> <View
style={[
styles.container,
{ backgroundColor: colors.background },
contentStyle,
]}
>
{renderScene()}
</View>
</Screen> </Screen>
); );
})} })}
@@ -64,11 +75,7 @@ export default function NativeStackView({
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
content: { container: {
flex: 1,
backgroundColor: '#eee',
},
scenes: {
flex: 1, flex: 1,
}, },
}); });

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.22](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.21...@react-navigation/native@5.0.0-alpha.22) (2020-01-01)
**Note:** Version bump only for package @react-navigation/native
# [5.0.0-alpha.21](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.20...@react-navigation/native@5.0.0-alpha.21) (2019-12-19)
**Note:** Version bump only for package @react-navigation/native
# [5.0.0-alpha.20](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.19...@react-navigation/native@5.0.0-alpha.20) (2019-12-16)
### Features
* add nested config in deep linking ([#210](https://github.com/react-navigation/navigation-ex/issues/210)) ([8002d51](https://github.com/react-navigation/navigation-ex/commit/8002d5179524a7211c37760a4ed45e8c12af4358)), closes [#154](https://github.com/react-navigation/navigation-ex/issues/154)
# [5.0.0-alpha.19](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.18...@react-navigation/native@5.0.0-alpha.19) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.18](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.17...@react-navigation/native@5.0.0-alpha.18) (2019-12-11) # [5.0.0-alpha.18](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.17...@react-navigation/native@5.0.0-alpha.18) (2019-12-11)
**Note:** Version bump only for package @react-navigation/native **Note:** Version bump only for package @react-navigation/native

View File

@@ -7,7 +7,7 @@
"ios", "ios",
"android" "android"
], ],
"version": "5.0.0-alpha.18", "version": "5.0.0-alpha.22",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -30,16 +30,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.0.0-alpha.27" "@react-navigation/core": "^5.0.0-alpha.30"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@types/react": "^16.9.11", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.60.25",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",

View File

@@ -4,7 +4,14 @@ import {
NavigationContainerProps, NavigationContainerProps,
NavigationContainerRef, NavigationContainerRef,
} from '@react-navigation/core'; } from '@react-navigation/core';
import ThemeProvider from './theming/ThemeProvider';
import DefaultTheme from './theming/DefaultTheme';
import useBackButton from './useBackButton'; import useBackButton from './useBackButton';
import { Theme } from './types';
type Props = NavigationContainerProps & {
theme?: Theme;
};
/** /**
* Container component which holds the navigation state * Container component which holds the navigation state
@@ -13,11 +20,12 @@ import useBackButton from './useBackButton';
* *
* @param props.initialState Initial state object for the navigation tree. * @param props.initialState Initial state object for the navigation tree.
* @param props.onStateChange Callback which is called with the latest navigation state when it changes. * @param props.onStateChange Callback which is called with the latest navigation state when it changes.
* @param props.theme Theme object for the navigators.
* @param props.children Child elements to render the content. * @param props.children Child elements to render the content.
* @param props.ref Ref object which refers to the navigation object containing helper methods. * @param props.ref Ref object which refers to the navigation object containing helper methods.
*/ */
const NavigationNativeContainer = React.forwardRef(function NativeContainer( const NavigationNativeContainer = React.forwardRef(function NativeContainer(
props: NavigationContainerProps, { theme = DefaultTheme, ...rest }: Props,
ref: React.Ref<NavigationContainerRef> ref: React.Ref<NavigationContainerRef>
) { ) {
const refContainer = React.useRef<NavigationContainerRef>(null); const refContainer = React.useRef<NavigationContainerRef>(null);
@@ -27,11 +35,9 @@ const NavigationNativeContainer = React.forwardRef(function NativeContainer(
React.useImperativeHandle(ref, () => refContainer.current); React.useImperativeHandle(ref, () => refContainer.current);
return ( return (
<NavigationContainer <ThemeProvider value={theme}>
{...props} <NavigationContainer {...rest} ref={refContainer} />
ref={refContainer} </ThemeProvider>
children={props.children}
/>
); );
}); });

View File

@@ -5,3 +5,8 @@ export { default as NavigationNativeContainer } from './NavigationNativeContaine
export { default as useBackButton } from './useBackButton'; export { default as useBackButton } from './useBackButton';
export { default as useLinking } from './useLinking'; export { default as useLinking } from './useLinking';
export { default as useScrollToTop } from './useScrollToTop'; export { default as useScrollToTop } from './useScrollToTop';
export { default as DefaultTheme } from './theming/DefaultTheme';
export { default as DarkTheme } from './theming/DarkTheme';
export { default as ThemeProvider } from './theming/ThemeProvider';
export { default as useTheme } from './theming/useTheme';

View File

@@ -0,0 +1,14 @@
import { Theme } from '../types';
const DarkTheme: Theme = {
dark: true,
colors: {
primary: 'rgb(10, 132, 255)',
background: 'rgb(1, 1, 1)',
card: 'rgb(18, 18, 18)',
text: 'rgb(229, 229, 231)',
border: 'rgb(39, 39, 41)',
},
};
export default DarkTheme;

View File

@@ -0,0 +1,14 @@
import { Theme } from '../types';
const DefaultTheme: Theme = {
dark: false,
colors: {
primary: 'rgb(0, 122, 255)',
background: 'rgb(242, 242, 242)',
card: 'rgb(255, 255, 255)',
text: 'rgb(28, 28, 30)',
border: 'rgb(199, 199, 204)',
},
};
export default DefaultTheme;

View File

@@ -0,0 +1,7 @@
import * as React from 'react';
import DefaultTheme from './DefaultTheme';
import { Theme } from '../types';
const ThemeContext = React.createContext<Theme>(DefaultTheme);
export default ThemeContext;

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import ThemeContext from './ThemeContext';
import { Theme } from '../types';
type Props = {
value: Theme;
children: React.ReactNode;
};
export default function ThemeProvider({ value, children }: Props) {
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}

View File

@@ -0,0 +1,8 @@
import * as React from 'react';
import ThemeContext from './ThemeContext';
export default function useTheme() {
const theme = React.useContext(ThemeContext);
return theme;
}

View File

@@ -0,0 +1,10 @@
export type Theme = {
dark: boolean;
colors: {
primary: string;
background: string;
card: string;
text: string;
border: string;
};
};

View File

@@ -1,17 +1,14 @@
import * as React from 'react'; import * as React from 'react';
import { Linking } from 'react-native'; import { Linking } from 'react-native';
import { import {
getActionFromState,
getStateFromPath as getStateFromPathDefault, getStateFromPath as getStateFromPathDefault,
NavigationContainerRef, NavigationContainerRef,
NavigationState,
PartialState,
} from '@react-navigation/core'; } from '@react-navigation/core';
type Config = { type GetStateFromPath = typeof getStateFromPathDefault;
[routeName: string]:
| string type Config = Parameters<GetStateFromPath>[1];
| { path: string; parse?: Record<string, (value: string) => any> };
};
type Options = { type Options = {
/** /**
@@ -36,10 +33,7 @@ type Options = {
/** /**
* Custom function to parse the URL object to a valid navigation state (advanced). * Custom function to parse the URL object to a valid navigation state (advanced).
*/ */
getStateFromPath?: ( getStateFromPath?: GetStateFromPath;
path: string,
options?: Config
) => PartialState<NavigationState> | undefined;
}; };
export default function useLinking( export default function useLinking(
@@ -89,7 +83,13 @@ export default function useLinking(
const state = getStateFromPathRef.current(path, configRef.current); const state = getStateFromPathRef.current(path, configRef.current);
if (state) { if (state) {
navigation.resetRoot(state); const action = getActionFromState(state);
if (action.type === 'RESET_ROOT') {
navigation.resetRoot(action.payload);
} else {
navigation.dispatch(action);
}
} }
} }
}; };

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.19](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.18...@react-navigation/routers@5.0.0-alpha.19) (2020-01-01)
**Note:** Version bump only for package @react-navigation/routers
# [5.0.0-alpha.18](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.17...@react-navigation/routers@5.0.0-alpha.18) (2019-12-19)
**Note:** Version bump only for package @react-navigation/routers
# [5.0.0-alpha.17](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.16...@react-navigation/routers@5.0.0-alpha.17) (2019-12-16)
**Note:** Version bump only for package @react-navigation/routers
# [5.0.0-alpha.16](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.15...@react-navigation/routers@5.0.0-alpha.16) (2019-12-11) # [5.0.0-alpha.16](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.15...@react-navigation/routers@5.0.0-alpha.16) (2019-12-11)
**Note:** Version bump only for package @react-navigation/routers **Note:** Version bump only for package @react-navigation/routers

View File

@@ -6,7 +6,7 @@
"react-native", "react-native",
"react-navigation" "react-navigation"
], ],
"version": "5.0.0-alpha.16", "version": "5.0.0-alpha.19",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -29,13 +29,13 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.0.0-alpha.27", "@react-navigation/core": "^5.0.0-alpha.30",
"shortid": "^2.2.15" "shortid": "^2.2.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"@react-native-community/bob": { "@react-native-community/bob": {
"source": "src", "source": "src",

View File

@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.47](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.46...@react-navigation/stack@5.0.0-alpha.47) (2020-01-01)
**Note:** Version bump only for package @react-navigation/stack
# [5.0.0-alpha.46](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.45...@react-navigation/stack@5.0.0-alpha.46) (2019-12-19)
### Bug Fixes
* fix typescript issues ([c52a8c4](https://github.com/react-navigation/navigation-ex/commit/c52a8c46a8906812651e5259a850207fc448590e))
# [5.0.0-alpha.45](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.44...@react-navigation/stack@5.0.0-alpha.45) (2019-12-16)
### Bug Fixes
* disable style interpolation for card when animation is disabled ([c110570](https://github.com/react-navigation/navigation-ex/commit/c110570d4c89a38336f19403e6f2d0870868620e))
# [5.0.0-alpha.44](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.43...@react-navigation/stack@5.0.0-alpha.44) (2019-12-14)
### Features
* add custom theme support ([#211](https://github.com/react-navigation/navigation-ex/issues/211)) ([00fc616](https://github.com/react-navigation/navigation-ex/commit/00fc616de0572bade8aa85052cdc8290360b1d7f))
# [5.0.0-alpha.43](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.42...@react-navigation/stack@5.0.0-alpha.43) (2019-12-11) # [5.0.0-alpha.43](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.42...@react-navigation/stack@5.0.0-alpha.43) (2019-12-11)
**Note:** Version bump only for package @react-navigation/stack **Note:** Version bump only for package @react-navigation/stack

View File

@@ -10,18 +10,18 @@ Open a Terminal in your project's folder and run,
yarn add @react-navigation/native @react-navigation/stack @react-native-community/masked-view yarn add @react-navigation/native @react-navigation/stack @react-native-community/masked-view
``` ```
Now we need to install [`react-native-gesture-handler`](https://github.com/kmagiera/react-native-gesture-handler), [`react-native-reanimated`](https://github.com/kmagiera/react-native-reanimated), [`react-native-screens`](https://github.com/kmagiera/react-native-screens) and [`react-native-safe-area-context`](https://github.com/th3rdwave/react-native-safe-area-context). Now we need to install [`react-native-gesture-handler`](https://github.com/kmagiera/react-native-gesture-handler), [`react-native-screens`](https://github.com/kmagiera/react-native-screens) and [`react-native-safe-area-context`](https://github.com/th3rdwave/react-native-safe-area-context).
If you are using Expo, to ensure that you get the compatible versions of the libraries, run: If you are using Expo, to ensure that you get the compatible versions of the libraries, run:
```sh ```sh
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context expo install react-native-gesture-handler react-native-screens react-native-safe-area-context
``` ```
If you are not using Expo, run the following: If you are not using Expo, run the following:
```sh ```sh
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context yarn add react-native-gesture-handler react-native-screens react-native-safe-area-context
``` ```
If you are using Expo, you are done. Otherwise, continue to the next steps. If you are using Expo, you are done. Otherwise, continue to the next steps.

View File

@@ -10,7 +10,7 @@
"android", "android",
"stack" "stack"
], ],
"version": "5.0.0-alpha.43", "version": "5.0.0-alpha.47",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -33,21 +33,22 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.16" "@react-navigation/routers": "^5.0.0-alpha.19",
"color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",
"@react-native-community/masked-view": "^0.1.5", "@react-native-community/masked-view": "^0.1.5",
"@types/react": "^16.9.11", "@types/color": "^3.0.0",
"@types/react-native": "^0.60.22", "@types/react": "^16.9.16",
"@types/react-native": "^0.60.25",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.5.0", "react-native-gesture-handler": "^1.5.0",
"react-native-reanimated": "^1.4.0",
"react-native-safe-area-context": "^0.6.0", "react-native-safe-area-context": "^0.6.0",
"react-native-screens": "^2.0.0-alpha.11", "react-native-screens": "^2.0.0-alpha.19",
"typescript": "^3.7.2" "typescript": "^3.7.3"
}, },
"peerDependencies": { "peerDependencies": {
"@react-native-community/masked-view": "^0.1.1", "@react-native-community/masked-view": "^0.1.1",
@@ -55,8 +56,7 @@
"react": "*", "react": "*",
"react-native": "*", "react-native": "*",
"react-native-gesture-handler": "^1.0.0", "react-native-gesture-handler": "^1.0.0",
"react-native-reanimated": "^1.0.0", "react-native-safe-area-context": "^0.6.0",
"react-native-safe-area-context": "^0.3.6",
"react-native-screens": "^1.0.0-alpha.0 || ^2.0.0-alpha.0" "react-native-screens": "^1.0.0-alpha.0 || ^2.0.0-alpha.0"
}, },
"@react-native-community/bob": { "@react-native-community/bob": {

View File

@@ -1,10 +1,11 @@
import Animated from 'react-native-reanimated'; import { Animated } from 'react-native';
import conditional from '../utils/conditional';
import { import {
StackCardInterpolationProps, StackCardInterpolationProps,
StackCardInterpolatedStyle, StackCardInterpolatedStyle,
} from '../types'; } from '../types';
const { cond, add, multiply, interpolate } = Animated; const { add, multiply } = Animated;
/** /**
* Standard iOS-style slide in from the right. * Standard iOS-style slide in from the right.
@@ -16,7 +17,7 @@ export function forHorizontalIOS({
layouts: { screen }, layouts: { screen },
}: StackCardInterpolationProps): StackCardInterpolatedStyle { }: StackCardInterpolationProps): StackCardInterpolatedStyle {
const translateFocused = multiply( const translateFocused = multiply(
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [screen.width, 0], outputRange: [screen.width, 0],
}), }),
@@ -25,20 +26,20 @@ export function forHorizontalIOS({
const translateUnfocused = next const translateUnfocused = next
? multiply( ? multiply(
interpolate(next.progress, { next.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, multiply(screen.width, -0.3)], outputRange: [0, screen.width * -0.3],
}), }),
inverted inverted
) )
: 0; : 0;
const overlayOpacity = interpolate(current.progress, { const overlayOpacity = current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 0.07], outputRange: [0, 0.07],
}); });
const shadowOpacity = interpolate(current.progress, { const shadowOpacity = current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 0.3], outputRange: [0, 0.3],
}); });
@@ -66,7 +67,7 @@ export function forVerticalIOS({
layouts: { screen }, layouts: { screen },
}: StackCardInterpolationProps): StackCardInterpolatedStyle { }: StackCardInterpolationProps): StackCardInterpolatedStyle {
const translateY = multiply( const translateY = multiply(
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [screen.height, 0], outputRange: [screen.height, 0],
}), }),
@@ -102,7 +103,7 @@ export function forModalPresentationIOS({
const progress = add(current.progress, next ? next.progress : 0); const progress = add(current.progress, next ? next.progress : 0);
const translateY = multiply( const translateY = multiply(
interpolate(progress, { progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [ outputRange: [
screen.height, screen.height,
@@ -113,14 +114,14 @@ export function forModalPresentationIOS({
inverted inverted
); );
const overlayOpacity = interpolate(progress, { const overlayOpacity = progress.interpolate({
inputRange: [0, 1, 1.0001, 2], inputRange: [0, 1, 1.0001, 2],
outputRange: [0, 0.3, 1, 1], outputRange: [0, 0.3, 1, 1],
}); });
const scale = isLandscape const scale = isLandscape
? 1 ? 1
: interpolate(progress, { : progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [ outputRange: [
1, 1,
@@ -132,7 +133,7 @@ export function forModalPresentationIOS({
const borderRadius = isLandscape const borderRadius = isLandscape
? 0 ? 0
: index === 0 : index === 0
? interpolate(progress, { ? progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [0, 0, 10], outputRange: [0, 0, 10],
}) })
@@ -160,17 +161,17 @@ export function forFadeFromBottomAndroid({
closing, closing,
}: StackCardInterpolationProps): StackCardInterpolatedStyle { }: StackCardInterpolationProps): StackCardInterpolatedStyle {
const translateY = multiply( const translateY = multiply(
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [multiply(screen.height, 0.08), 0], outputRange: [screen.height * 0.08, 0],
}), }),
inverted inverted
); );
const opacity = cond( const opacity = conditional(
closing, closing,
current.progress, current.progress,
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 0.5, 0.9, 1], inputRange: [0, 0.5, 0.9, 1],
outputRange: [0, 0.25, 0.7, 1], outputRange: [0, 0.25, 0.7, 1],
}) })
@@ -194,7 +195,7 @@ export function forRevealFromBottomAndroid({
layouts: { screen }, layouts: { screen },
}: StackCardInterpolationProps): StackCardInterpolatedStyle { }: StackCardInterpolationProps): StackCardInterpolatedStyle {
const containerTranslateY = multiply( const containerTranslateY = multiply(
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [screen.height, 0], outputRange: [screen.height, 0],
}), }),
@@ -202,24 +203,24 @@ export function forRevealFromBottomAndroid({
); );
const cardTranslateYFocused = multiply( const cardTranslateYFocused = multiply(
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [multiply(screen.height, 95.9 / 100, -1), 0], outputRange: [screen.height * (95.9 / 100) * -1, 0],
}), }),
inverted inverted
); );
const cardTranslateYUnfocused = next const cardTranslateYUnfocused = next
? multiply( ? multiply(
interpolate(next.progress, { next.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, multiply(screen.height, 2 / 100, -1)], outputRange: [0, screen.height * (2 / 100) * -1],
}), }),
inverted inverted
) )
: 0; : 0;
const overlayOpacity = interpolate(current.progress, { const overlayOpacity = current.progress.interpolate({
inputRange: [0, 0.36, 1], inputRange: [0, 0.36, 1],
outputRange: [0, 0.1, 0.1], outputRange: [0, 0.1, 0.1],
}); });
@@ -249,18 +250,18 @@ export function forScaleFromCenterAndroid({
}: StackCardInterpolationProps): StackCardInterpolatedStyle { }: StackCardInterpolationProps): StackCardInterpolatedStyle {
const progress = add(current.progress, next ? next.progress : 0); const progress = add(current.progress, next ? next.progress : 0);
const opacity = interpolate(progress, { const opacity = progress.interpolate({
inputRange: [0, 0.8, 1, 1.2, 2], inputRange: [0, 0.8, 1, 1.2, 2],
outputRange: [0, 0.5, 1, 0.33, 0], outputRange: [0, 0.5, 1, 0.33, 0],
}); });
const scale = cond( const scale = conditional(
closing, closing,
interpolate(current.progress, { current.progress.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0.9, 1], outputRange: [0.9, 1],
}), }),
interpolate(progress, { progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [0.85, 1, 1.1], outputRange: [0.85, 1, 1.1],
}) })
@@ -273,3 +274,7 @@ export function forScaleFromCenterAndroid({
}, },
}; };
} }
export function forNoAnimation(): StackCardInterpolatedStyle {
return {};
}

View File

@@ -1,11 +1,10 @@
import { I18nManager } from 'react-native'; import { Animated, I18nManager } from 'react-native';
import Animated from 'react-native-reanimated';
import { import {
StackHeaderInterpolationProps, StackHeaderInterpolationProps,
StackHeaderInterpolatedStyle, StackHeaderInterpolatedStyle,
} from '../types'; } from '../types';
const { interpolate, add } = Animated; const { add } = Animated;
/** /**
* Standard UIKit style animation for the header where the title fades into the back button label. * Standard UIKit style animation for the header where the title fades into the back button label.
@@ -38,7 +37,7 @@ export function forUIKit({
return { return {
leftButtonStyle: { leftButtonStyle: {
opacity: interpolate(progress, { opacity: progress.interpolate({
inputRange: [0.3, 1, 1.5], inputRange: [0.3, 1, 1.5],
outputRange: [0, 1, 0], outputRange: [0, 1, 0],
}), }),
@@ -46,7 +45,7 @@ export function forUIKit({
leftLabelStyle: { leftLabelStyle: {
transform: [ transform: [
{ {
translateX: interpolate(progress, { translateX: progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: I18nManager.isRTL outputRange: I18nManager.isRTL
? [-rightOffset, 0, leftLabelOffset] ? [-rightOffset, 0, leftLabelOffset]
@@ -56,19 +55,19 @@ export function forUIKit({
], ],
}, },
rightButtonStyle: { rightButtonStyle: {
opacity: interpolate(progress, { opacity: progress.interpolate({
inputRange: [0.3, 1, 1.5], inputRange: [0.3, 1, 1.5],
outputRange: [0, 1, 0], outputRange: [0, 1, 0],
}), }),
}, },
titleStyle: { titleStyle: {
opacity: interpolate(progress, { opacity: progress.interpolate({
inputRange: [0, 0.4, 1, 1.5], inputRange: [0, 0.4, 1, 1.5],
outputRange: [0, 0.1, 1, 0], outputRange: [0, 0.1, 1, 0],
}), }),
transform: [ transform: [
{ {
translateX: interpolate(progress, { translateX: progress.interpolate({
inputRange: [0.5, 1, 2], inputRange: [0.5, 1, 2],
outputRange: I18nManager.isRTL outputRange: I18nManager.isRTL
? [-titleLeftOffset, 0, rightOffset] ? [-titleLeftOffset, 0, rightOffset]
@@ -80,7 +79,7 @@ export function forUIKit({
backgroundStyle: { backgroundStyle: {
transform: [ transform: [
{ {
translateX: interpolate(progress, { translateX: progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: I18nManager.isRTL outputRange: I18nManager.isRTL
? [-layouts.screen.width, 0, layouts.screen.width] ? [-layouts.screen.width, 0, layouts.screen.width]
@@ -100,7 +99,7 @@ export function forFade({
next, next,
}: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle { }: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle {
const progress = add(current.progress, next ? next.progress : 0); const progress = add(current.progress, next ? next.progress : 0);
const opacity = interpolate(progress, { const opacity = progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [0, 1, 0], outputRange: [0, 1, 0],
}); });
@@ -110,7 +109,7 @@ export function forFade({
rightButtonStyle: { opacity }, rightButtonStyle: { opacity },
titleStyle: { opacity }, titleStyle: { opacity },
backgroundStyle: { backgroundStyle: {
opacity: interpolate(progress, { opacity: progress.interpolate({
inputRange: [0, 1, 1.9, 2], inputRange: [0, 1, 1.9, 2],
outputRange: [0, 1, 1, 0], outputRange: [0, 1, 1, 0],
}), }),
@@ -127,7 +126,7 @@ export function forStatic({
layouts: { screen }, layouts: { screen },
}: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle { }: StackHeaderInterpolationProps): StackHeaderInterpolatedStyle {
const progress = add(current.progress, next ? next.progress : 0); const progress = add(current.progress, next ? next.progress : 0);
const translateX = interpolate(progress, { const translateX = progress.interpolate({
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: I18nManager.isRTL outputRange: I18nManager.isRTL
? [-screen.width, 0, screen.width] ? [-screen.width, 0, screen.width]

View File

@@ -1,4 +1,4 @@
import { Easing } from 'react-native-reanimated'; import { Easing } from 'react-native';
import { TransitionSpec } from '../types'; import { TransitionSpec } from '../types';
/** /**

View File

@@ -9,7 +9,9 @@ import * as TransitionPresets from './TransitionConfigs/TransitionPresets';
export { default as createStackNavigator } from './navigators/createStackNavigator'; export { default as createStackNavigator } from './navigators/createStackNavigator';
export const Assets = [ export const Assets = [
// eslint-disable-next-line import/no-commonjs
require('./views/assets/back-icon.png'), require('./views/assets/back-icon.png'),
// eslint-disable-next-line import/no-commonjs
require('./views/assets/back-icon-mask.png'), require('./views/assets/back-icon-mask.png'),
]; ];

View File

@@ -1,10 +1,11 @@
import { import {
Animated,
EasingFunction,
StyleProp, StyleProp,
TextStyle, TextStyle,
ViewStyle, ViewStyle,
LayoutChangeEvent, LayoutChangeEvent,
} from 'react-native'; } from 'react-native';
import Animated from 'react-native-reanimated';
import { EdgeInsets } from 'react-native-safe-area-context'; import { EdgeInsets } from 'react-native-safe-area-context';
import { import {
NavigationProp, NavigationProp,
@@ -88,22 +89,24 @@ export type Scene<T> = {
/** /**
* Progress value of the current screen. * Progress value of the current screen.
*/ */
current: Animated.Node<number>; current: Animated.AnimatedInterpolation;
/** /**
* Progress value for the screen after this one in the stack. * Progress value for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one. * This can be `undefined` in case the screen animating is the last one.
*/ */
next?: Animated.Node<number>; next?: Animated.AnimatedInterpolation;
/** /**
* Progress value for the screen before this one in the stack. * Progress value for the screen before this one in the stack.
* This can be `undefined` in case the screen animating is the first one. * This can be `undefined` in case the screen animating is the first one.
*/ */
previous?: Animated.Node<number>; previous?: Animated.AnimatedInterpolation;
}; };
}; };
export type StackHeaderMode = 'float' | 'screen' | 'none'; export type StackHeaderMode = 'float' | 'screen' | 'none';
export type StackCardMode = 'card' | 'modal';
export type StackHeaderOptions = { export type StackHeaderOptions = {
/** /**
* String or a function that returns a React Element to be used by the header. * String or a function that returns a React Element to be used by the header.
@@ -271,18 +274,14 @@ export type StackNavigationOptions = StackHeaderOptions &
* Defaults to `true` on Android and `false` on iOS. * Defaults to `true` on Android and `false` on iOS.
*/ */
cardOverlayEnabled?: boolean; cardOverlayEnabled?: boolean;
/**
* Whether to use a transparent background for the card instead of a white one.
* This is useful to implement things like modal dialogs where the previous scene should still be visible underneath the current one.
* Defaults to `false`.
*
* If you use [`react-native-screens`](https://github.com/kmagiera/react-native-screens),
* you should also specify `mode: 'modal'` in the stack view config so previous screens aren't detached.
*/
cardTransparent?: boolean;
/** /**
* Style object for the card in stack. * Style object for the card in stack.
* You can provide a custom background color to use instead of the default background here. * You can provide a custom background color to use instead of the default background here.
*
* You can also specify `{ backgroundColor: 'transparent' }` to make the previous screen visible underneath.
* This is useful to implement things like modal dialogs.
* If you use [`react-native-screens`](https://github.com/kmagiera/react-native-screens), you should also specify `mode: 'modal'`
* in the stack view config when using a transparent background so previous screens aren't detached.
*/ */
cardStyle?: StyleProp<ViewStyle>; cardStyle?: StyleProp<ViewStyle>;
/** /**
@@ -326,7 +325,7 @@ export type StackNavigationOptions = StackHeaderOptions &
}; };
export type StackNavigationConfig = { export type StackNavigationConfig = {
mode?: 'card' | 'modal'; mode?: StackCardMode;
headerMode?: StackHeaderMode; headerMode?: StackHeaderMode;
/** /**
* If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen. * If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen.
@@ -406,6 +405,10 @@ export type StackHeaderTitleProps = {
* Whether title font should scale to respect Text Size accessibility settings. * Whether title font should scale to respect Text Size accessibility settings.
*/ */
allowFontScaling?: boolean; allowFontScaling?: boolean;
/**
* Tint color for the header.
*/
tintColor?: string;
/** /**
* Content of the title element. Usually the title string. * Content of the title element. Usually the title string.
*/ */
@@ -433,7 +436,7 @@ export type SpringConfig = {
export type TimingConfig = { export type TimingConfig = {
duration: number; duration: number;
easing: Animated.EasingFunction; easing: EasingFunction;
}; };
export type TransitionSpec = export type TransitionSpec =
@@ -448,7 +451,7 @@ export type StackCardInterpolationProps = {
/** /**
* Animated node representing the progress value of the current screen. * Animated node representing the progress value of the current screen.
*/ */
progress: Animated.Node<number>; progress: Animated.AnimatedInterpolation;
}; };
/** /**
* Values for the current screen the screen after this one in the stack. * Values for the current screen the screen after this one in the stack.
@@ -458,24 +461,24 @@ export type StackCardInterpolationProps = {
/** /**
* Animated node representing the progress value of the next screen. * Animated node representing the progress value of the next screen.
*/ */
progress: Animated.Node<number>; progress: Animated.AnimatedInterpolation;
}; };
/** /**
* The index of the card in the stack. * The index of the card in the stack.
*/ */
index: number; index: number;
/** /**
* Animated node representing whether the card is closing. * Animated node representing whether the card is closing (1 - closing, 0 - not closing).
*/ */
closing: Animated.Node<0 | 1>; closing: Animated.AnimatedInterpolation;
/** /**
* Animated node representing whether the card is being swiped. * Animated node representing whether the card is being swiped (1 - swiping, 0 - not swiping).
*/ */
swiping: Animated.Node<0 | 1>; swiping: Animated.AnimatedInterpolation;
/** /**
* Animated node representing multiplier when direction is inverted. * Animated node representing multiplier when direction is inverted (-1 - inverted, 1 - normal).
*/ */
inverted: Animated.Node<-1 | 1>; inverted: Animated.AnimatedInterpolation;
/** /**
* Layout measurements for various items we use for animation. * Layout measurements for various items we use for animation.
*/ */
@@ -527,7 +530,7 @@ export type StackHeaderInterpolationProps = {
/** /**
* Animated node representing the progress value of the current screen. * Animated node representing the progress value of the current screen.
*/ */
progress: Animated.Node<number>; progress: Animated.AnimatedInterpolation;
}; };
/** /**
* Values for the current screen the screen after this one in the stack. * Values for the current screen the screen after this one in the stack.
@@ -537,7 +540,7 @@ export type StackHeaderInterpolationProps = {
/** /**
* Animated node representing the progress value of the next screen. * Animated node representing the progress value of the next screen.
*/ */
progress: Animated.Node<number>; progress: Animated.AnimatedInterpolation;
}; };
/** /**
* Layout measurements for various items we use for animation. * Layout measurements for various items we use for animation.

View File

@@ -0,0 +1,33 @@
import { Animated } from 'react-native';
const { add, multiply } = Animated;
/**
* Use an Animated Node based on a condition. Similar to Reanimated's `cond`.
*
* @param condition Animated Node representing the condition, must be 0 or 1, 1 means `true`, 0 means `false`
* @param main Animated Node to use if the condition is `true`
* @param fallback Animated Node to use if the condition is `false`
*/
export default function conditional(
condition: Animated.AnimatedInterpolation,
main: Animated.AnimatedInterpolation,
fallback: Animated.AnimatedInterpolation
) {
// To implement this behavior, we multiply the main node with the condition.
// So if condition is 0, result will be 0, and if condition is 1, result will be main node.
// Then we multiple reverse of the condition (0 if condition is 1) with the fallback.
// So if condition is 0, result will be fallback node, and if condition is 1, result will be 0,
// This way, one of them will always be 0, and other one will be the value we need.
// In the end we add them both together, 0 + value we need = value we need
return add(
multiply(condition, main),
multiply(
condition.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
fallback
)
);
}

View File

@@ -0,0 +1,18 @@
import getInvertedMultiplier from './getInvertedMultiplier';
import { GestureDirection, Layout } from '../types';
export default function getDistanceForDirection(
layout: Layout,
gestureDirection: GestureDirection
): number {
const multiplier = getInvertedMultiplier(gestureDirection);
switch (gestureDirection) {
case 'vertical':
case 'vertical-inverted':
return layout.height * multiplier;
case 'horizontal':
case 'horizontal-inverted':
return layout.width * multiplier;
}
}

View File

@@ -0,0 +1,17 @@
import { I18nManager } from 'react-native';
import { GestureDirection } from '../types';
export default function getInvertedMultiplier(
gestureDirection: GestureDirection
): 1 | -1 {
switch (gestureDirection) {
case 'vertical':
return 1;
case 'vertical-inverted':
return -1;
case 'horizontal':
return I18nManager.isRTL ? -1 : 1;
case 'horizontal-inverted':
return I18nManager.isRTL ? 1 : -1;
}
}

View File

@@ -1,4 +1,4 @@
export default function memoize<Result, Deps extends ReadonlyArray<any>>( export default function memoize<Result, Deps extends readonly any[]>(
callback: (...deps: Deps) => Result callback: (...deps: Deps) => Result
) { ) {
let previous: Deps | undefined; let previous: Deps | undefined;

View File

@@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { import {
Animated,
I18nManager, I18nManager,
Image, Image,
View, View,
@@ -7,46 +8,56 @@ import {
StyleSheet, StyleSheet,
LayoutChangeEvent, LayoutChangeEvent,
} from 'react-native'; } from 'react-native';
import Animated from 'react-native-reanimated'; import { useTheme } from '@react-navigation/native';
import MaskedView from '../MaskedView'; import MaskedView from '../MaskedView';
import TouchableItem from '../TouchableItem'; import TouchableItem from '../TouchableItem';
import { StackHeaderLeftButtonProps } from '../../types'; import { StackHeaderLeftButtonProps } from '../../types';
type Props = StackHeaderLeftButtonProps & { type Props = StackHeaderLeftButtonProps;
tintColor: string;
};
type State = { export default function HeaderBackButton({
fullLabelWidth?: number; disabled,
}; allowFontScaling,
backImage,
label,
labelStyle,
labelVisible = Platform.OS === 'ios',
onLabelLayout,
onPress,
pressColorAndroid: customPressColorAndroid,
screenLayout,
tintColor: customTintColor,
titleLayout,
truncatedLabel = 'Back',
}: Props) {
const { dark, colors } = useTheme();
class HeaderBackButton extends React.Component<Props, State> { const [initialLabelWidth, setInitialLabelWidth] = React.useState<
static defaultProps = { undefined | number
pressColorAndroid: 'rgba(0, 0, 0, .32)', >(undefined);
tintColor: Platform.select({
ios: '#037aff', const tintColor =
web: '#5f6368', customTintColor !== undefined
}), ? customTintColor
labelVisible: Platform.OS === 'ios', : Platform.select({
truncatedLabel: 'Back', ios: colors.primary,
default: colors.text,
});
const pressColorAndroid =
customPressColorAndroid !== undefined
? customPressColorAndroid
: dark
? 'rgba(255, 255, 255, .32)'
: 'rgba(0, 0, 0, .32)';
const handleLabelLayout = (e: LayoutChangeEvent) => {
onLabelLayout?.(e);
setInitialLabelWidth(e.nativeEvent.layout.x + e.nativeEvent.layout.width);
}; };
state: State = {}; const shouldTruncateLabel = () => {
private handleLabelLayout = (e: LayoutChangeEvent) => {
const { onLabelLayout } = this.props;
onLabelLayout && onLabelLayout(e);
this.setState({
fullLabelWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
});
};
private shouldTruncateLabel = () => {
const { titleLayout, screenLayout, label } = this.props;
const { fullLabelWidth: initialLabelWidth } = this.state;
return ( return (
!label || !label ||
(initialLabelWidth && (initialLabelWidth &&
@@ -56,9 +67,7 @@ class HeaderBackButton extends React.Component<Props, State> {
); );
}; };
private renderBackImage = () => { const renderBackImage = () => {
const { backImage, labelVisible, tintColor } = this.props;
if (backImage) { if (backImage) {
return backImage({ tintColor }); return backImage({ tintColor });
} else { } else {
@@ -76,19 +85,8 @@ class HeaderBackButton extends React.Component<Props, State> {
} }
}; };
private renderLabel() { const renderLabel = () => {
const { const leftLabelText = shouldTruncateLabel() ? truncatedLabel : label;
label,
truncatedLabel,
allowFontScaling,
labelVisible,
backImage,
labelStyle,
tintColor,
screenLayout,
} = this.props;
const leftLabelText = this.shouldTruncateLabel() ? truncatedLabel : label;
if (!labelVisible || leftLabelText === undefined) { if (!labelVisible || leftLabelText === undefined) {
return null; return null;
@@ -109,7 +107,7 @@ class HeaderBackButton extends React.Component<Props, State> {
onLayout={ onLayout={
// This measurement is used to determine if we should truncate the label when it doesn't fit // This measurement is used to determine if we should truncate the label when it doesn't fit
// Only measure it when label is not truncated because we want the measurement of full label // Only measure it when label is not truncated because we want the measurement of full label
leftLabelText === label ? this.handleLabelLayout : undefined leftLabelText === label ? handleLabelLayout : undefined
} }
style={[ style={[
styles.label, styles.label,
@@ -145,42 +143,37 @@ class HeaderBackButton extends React.Component<Props, State> {
{labelElement} {labelElement}
</MaskedView> </MaskedView>
); );
} };
private handlePress = () => const handlePress = () => onPress && requestAnimationFrame(onPress);
this.props.onPress && requestAnimationFrame(this.props.onPress);
render() { return (
const { pressColorAndroid, label, disabled } = this.props; <TouchableItem
disabled={disabled}
return ( accessible
<TouchableItem accessibilityRole="button"
disabled={disabled} accessibilityComponentType="button"
accessible accessibilityLabel={
accessibilityRole="button" label && label !== 'Back' ? `${label}, back` : 'Go back'
accessibilityComponentType="button" }
accessibilityLabel={ accessibilityTraits="button"
label && label !== 'Back' ? `${label}, back` : 'Go back' testID="header-back"
} delayPressIn={0}
accessibilityTraits="button" onPress={disabled ? undefined : handlePress}
testID="header-back" pressColor={pressColorAndroid}
delayPressIn={0} style={[styles.container, disabled && styles.disabled]}
onPress={disabled ? undefined : this.handlePress} hitSlop={Platform.select({
pressColor={pressColorAndroid} ios: undefined,
style={[styles.container, disabled && styles.disabled]} default: { top: 16, right: 16, bottom: 16, left: 16 },
hitSlop={Platform.select({ })}
ios: undefined, borderless
default: { top: 16, right: 16, bottom: 16, left: 16 }, >
})} <React.Fragment>
borderless {renderBackImage()}
> {renderLabel()}
<React.Fragment> </React.Fragment>
{this.renderBackImage()} </TouchableItem>
{this.renderLabel()} );
</React.Fragment>
</TouchableItem>
);
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -253,5 +246,3 @@ const styles = StyleSheet.create({
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }], transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
}, },
}); });
export default HeaderBackButton;

Some files were not shown because too many files have changed in this diff Show More