Compare commits
1 Commits
@react-nav
...
@satya164/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
515e652b37 |
@@ -8,6 +8,7 @@
|
||||
"@react-navigation/routers",
|
||||
"@react-navigation/compat",
|
||||
"@react-navigation/stack",
|
||||
"@react-navigation/web-stack",
|
||||
"@react-navigation/drawer",
|
||||
"@react-navigation/bottom-tabs",
|
||||
"@react-navigation/material-top-tabs",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { AsyncStorage } from 'react-native';
|
||||
|
||||
export default AsyncStorage;
|
||||
@@ -1,14 +0,0 @@
|
||||
export default {
|
||||
getItem(key: string) {
|
||||
return Promise.resolve(localStorage.getItem(key));
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
return Promise.resolve(localStorage.setItem(key, value));
|
||||
},
|
||||
removeItem(key: string) {
|
||||
return Promise.resolve(localStorage.removeItem(key));
|
||||
},
|
||||
clear() {
|
||||
return Promise.resolve(localStorage.clear());
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
|
||||
import { View, ScrollView, StyleSheet } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import {
|
||||
createCompatNavigatorFactory,
|
||||
@@ -23,8 +23,6 @@ type NestedStackParams = {
|
||||
Article: { author: string };
|
||||
};
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||
CompatStackParams
|
||||
>> = ({ navigation }) => {
|
||||
@@ -46,7 +44,7 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -72,7 +70,7 @@ const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -102,7 +100,7 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: navigation.getParam('author') }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import {
|
||||
Link,
|
||||
@@ -22,24 +22,13 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const LinkButton = ({
|
||||
to,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof Button> & { to: string }) => {
|
||||
const { onPress, ...props } = useLinkProps({ to });
|
||||
const props = useLinkProps({ to });
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
{...rest}
|
||||
{...Platform.select({
|
||||
web: { onClick: onPress } as any,
|
||||
default: { onPress },
|
||||
})}
|
||||
/>
|
||||
);
|
||||
return <Button {...props} {...rest} />;
|
||||
};
|
||||
|
||||
const ArticleScreen = ({
|
||||
@@ -80,10 +69,7 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -117,7 +103,7 @@ const AlbumsScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -17,8 +17,6 @@ type ModalStackParams = {
|
||||
|
||||
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -44,10 +42,7 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -71,7 +66,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -18,8 +18,6 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -45,10 +43,7 @@ const ArticleScreen = ({
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -76,7 +71,7 @@ const NewsFeedScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={scrollEnabled} />
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -104,7 +99,7 @@ const AlbumsScreen = ({
|
||||
Pop by 2
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,8 +20,6 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -47,10 +45,7 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
@@ -80,7 +75,7 @@ const AlbumsScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={scrollEnabled} />
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button, Paragraph } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -15,8 +15,6 @@ type SimpleStackParams = {
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const scrollEnabled = Platform.select({ web: true, default: false });
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
@@ -42,10 +40,7 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article
|
||||
author={{ name: route.params.author }}
|
||||
scrollEnabled={scrollEnabled}
|
||||
/>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
3
example/src/Screens/WebStack.native.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function WebStack() {
|
||||
return null;
|
||||
}
|
||||
142
example/src/Screens/WebStack.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createWebStackNavigator,
|
||||
WebStackNavigationProp,
|
||||
} from '@react-navigation/web-stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
type WebStackParams = {
|
||||
Article: { author: string };
|
||||
NewsFeed: undefined;
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
type WebStackNavigation = WebStackNavigationProp<WebStackParams>;
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: WebStackNavigation;
|
||||
route: RouteProp<WebStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.replace('NewsFeed')}
|
||||
style={styles.button}
|
||||
>
|
||||
Replace with feed
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop()}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop screen
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const NewsFeedScreen = ({ navigation }: { navigation: WebStackNavigation }) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.navigate('Album')}
|
||||
style={styles.button}
|
||||
>
|
||||
Navigate to album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: { navigation: WebStackNavigation }) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
||||
style={styles.button}
|
||||
>
|
||||
Push article
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.pop(2)}
|
||||
style={styles.button}
|
||||
>
|
||||
Pop by 2
|
||||
</Button>
|
||||
</View>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const WebStack = createWebStackNavigator<WebStackParams>();
|
||||
|
||||
type Props = Partial<React.ComponentProps<typeof WebStack.Navigator>> & {
|
||||
navigation: WebStackNavigationProp<ParamListBase>;
|
||||
};
|
||||
|
||||
export default function WebStackScreen({ navigation, ...rest }: Props) {
|
||||
navigation.setOptions({
|
||||
headerShown: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<WebStack.Navigator {...rest}>
|
||||
<WebStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
options={({ route }) => ({
|
||||
title: `Article by ${route.params.author}`,
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<WebStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<WebStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{ title: 'Album' }}
|
||||
/>
|
||||
</WebStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
AsyncStorage,
|
||||
YellowBox,
|
||||
Platform,
|
||||
StatusBar,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from 'react-native-paper';
|
||||
import {
|
||||
InitialState,
|
||||
NavigationContainerRef,
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
@@ -40,9 +42,9 @@ import {
|
||||
HeaderStyleInterpolators,
|
||||
} from '@react-navigation/stack';
|
||||
|
||||
import AsyncStorage from './AsyncStorage';
|
||||
import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import WebStack from './Screens/WebStack';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||
import StackTransparent from './Screens/StackTransparent';
|
||||
@@ -75,6 +77,11 @@ type RootStackParamList = {
|
||||
};
|
||||
|
||||
const SCREENS = {
|
||||
...(Platform.OS === 'web'
|
||||
? {
|
||||
WebStack: { title: 'Web Stack', component: WebStack },
|
||||
}
|
||||
: null),
|
||||
SimpleStack: { title: 'Simple Stack', component: SimpleStack },
|
||||
ModalPresentationStack: {
|
||||
title: 'Modal Presentation Stack',
|
||||
@@ -128,6 +135,8 @@ const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
|
||||
Asset.loadAsync(StackAssets);
|
||||
|
||||
export default function App() {
|
||||
const containerRef = React.useRef<NavigationContainerRef>(null);
|
||||
|
||||
const [theme, setTheme] = React.useState(DefaultTheme);
|
||||
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
@@ -205,6 +214,7 @@ export default function App() {
|
||||
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
|
||||
)}
|
||||
<NavigationContainer
|
||||
ref={containerRef}
|
||||
initialState={initialState}
|
||||
onStateChange={(state) =>
|
||||
AsyncStorage.setItem(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* /index.html 200
|
||||
@@ -3,42 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.2...@react-navigation/bottom-tabs@5.3.3) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.1...@react-navigation/bottom-tabs@5.3.2) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.0...@react-navigation/bottom-tabs@5.3.1) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.8...@react-navigation/bottom-tabs@5.3.0) (2020-04-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/bottom-tabs",
|
||||
"description": "Bottom tab navigator following iOS design guidelines",
|
||||
"version": "5.3.3",
|
||||
"version": "5.2.8",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.13...@react-navigation/compat@5.1.14) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.12...@react-navigation/compat@5.1.13) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.11...@react-navigation/compat@5.1.12) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.10...@react-navigation/compat@5.1.11) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.1.14",
|
||||
"version": "5.1.10",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||
"bugs": {
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/react": "^16.9.23",
|
||||
"react": "~16.9.0",
|
||||
"typescript": "^3.8.3"
|
||||
|
||||
@@ -3,23 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.5...@react-navigation/core@5.4.0) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* handle empty paths when parsing ([c3fa83e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3fa83efe0d73db76365f8be3d6a8ca1d1289b71))
|
||||
* parsing url ([bd35b4f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/bd35b4fc202c3868fb75c3675b62de67557089e1))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/core",
|
||||
"description": "Core utilities for building navigators",
|
||||
"version": "5.4.0",
|
||||
"version": "5.3.5",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
@@ -29,7 +29,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.4.2",
|
||||
"@react-navigation/routers": "^5.4.1",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"nanoid": "^3.0.2",
|
||||
"query-string": "^6.12.0",
|
||||
|
||||
@@ -219,8 +219,6 @@ const BaseNavigationContainer = React.forwardRef(
|
||||
dispatch,
|
||||
canGoBack,
|
||||
getRootState,
|
||||
dangerouslyGetState: () => state,
|
||||
dangerouslyGetParent: () => undefined,
|
||||
}));
|
||||
|
||||
const builderContext = React.useMemo(
|
||||
|
||||
@@ -426,49 +426,6 @@ it('ignores empty string paths', () => {
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('keeps query params if path is empty', () => {
|
||||
const path = '/?foo=42';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux', params: { foo: 42 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toEqual(
|
||||
path
|
||||
);
|
||||
});
|
||||
|
||||
it('cuts nested configs too', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
@@ -538,8 +495,6 @@ it('handles empty path at the end', () => {
|
||||
});
|
||||
|
||||
it('returns "/" for empty path', () => {
|
||||
const path = '/';
|
||||
|
||||
const config = {
|
||||
Foo: {
|
||||
path: '',
|
||||
@@ -564,8 +519,7 @@ it('returns "/" for empty path', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
expect(getPathFromState(state, config)).toBe('/');
|
||||
});
|
||||
|
||||
it('parses no path specified', () => {
|
||||
|
||||
@@ -677,7 +677,7 @@ it('accepts initialRouteName without config for it', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if path is empty and no matching screen is present', () => {
|
||||
it('returns undefined if path is empty', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
@@ -695,292 +695,3 @@ it('returns undefined if path is empty and no matching screen is present', () =>
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns matching screen if path is empty', () => {
|
||||
const path = '';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: '',
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('returns matching screen with params if path is empty', () => {
|
||||
const path = '?foo=42';
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Qux', params: { foo: 42 } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't match nested screen if path is empty", () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Qux: {
|
||||
path: '',
|
||||
parse: { foo: Number },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('chooses more exhaustive pattern', () => {
|
||||
const path = '/foo/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings', () => {
|
||||
const path = '/foos';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles same paths beginnings with params', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles not taking path with too many segments', () => {
|
||||
const path = '/foos/5';
|
||||
|
||||
const config = {
|
||||
Foe: {
|
||||
path: '/',
|
||||
initialRouteName: 'Foo',
|
||||
screens: {
|
||||
Foo: 'foo',
|
||||
Bis: {
|
||||
path: 'foos/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
},
|
||||
Bas: {
|
||||
path: 'foos/:id/:nip',
|
||||
parse: {
|
||||
id: Number,
|
||||
pwd: Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
},
|
||||
{
|
||||
name: 'Bis',
|
||||
params: { id: 5 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
@@ -115,6 +115,8 @@ export default function getPathFromState(
|
||||
pattern = nestedRouteNames.substring(1);
|
||||
}
|
||||
|
||||
// we don't add empty path strings to path
|
||||
if (pattern !== '') {
|
||||
const config =
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
@@ -168,13 +170,15 @@ export default function getPathFromState(
|
||||
path += `?${query}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
}
|
||||
|
||||
// Remove multiple as well as trailing slashes
|
||||
path = path.replace(/\/+/, '/');
|
||||
path = path.length > 1 ? path.replace(/\/$/, '') : path;
|
||||
path =
|
||||
path !== '/' && path.slice(path.length - 1) === '/'
|
||||
? path.slice(0, -1)
|
||||
: path;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import escape from 'escape-string-regexp';
|
||||
import queryString from 'query-string';
|
||||
import {
|
||||
NavigationState,
|
||||
@@ -19,8 +20,7 @@ type Options = {
|
||||
};
|
||||
|
||||
type RouteConfig = {
|
||||
screen: string;
|
||||
match: string | null;
|
||||
match: RegExp;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: ParseConfig | undefined;
|
||||
@@ -58,8 +58,10 @@ export default function getStateFromPath(
|
||||
path: string,
|
||||
options: Options = {}
|
||||
): ResultState | undefined {
|
||||
if (path === '') {
|
||||
return undefined;
|
||||
}
|
||||
let initialRoutes: InitialRouteConfig[] = [];
|
||||
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = ([] as RouteConfig[]).concat(
|
||||
...Object.keys(options).map((key) =>
|
||||
@@ -67,74 +69,24 @@ export default function getStateFromPath(
|
||||
)
|
||||
);
|
||||
|
||||
// sort configs so the most exhaustive is always first to be chosen
|
||||
configs.sort(
|
||||
(config1, config2) =>
|
||||
config2.pattern.split('/').length - config1.pattern.split('/').length
|
||||
);
|
||||
let result: PartialState<NavigationState> | undefined;
|
||||
let current: PartialState<NavigationState> | undefined;
|
||||
|
||||
let remaining = path
|
||||
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/^\//, '') // Remove extra leading slash
|
||||
.replace(/\?.*/, ''); // Remove query params which we will handle later
|
||||
|
||||
if (remaining === '') {
|
||||
// We need to add special handling of empty path so navigation to empty path also works
|
||||
// When handling empty path, we should only look at the root level config
|
||||
const match = configs.find(
|
||||
(config) =>
|
||||
config.pattern === '' &&
|
||||
config.routeNames.every(
|
||||
// make sure that none of the parent configs have a non-empty path defined
|
||||
(name) => !configs.find((c) => c.screen === name)?.pattern
|
||||
)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
return createNestedStateObject(
|
||||
match.routeNames,
|
||||
initialRoutes,
|
||||
parseQueryParams(path, match.parse)
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let result: PartialState<NavigationState> | undefined;
|
||||
let current: PartialState<NavigationState> | undefined;
|
||||
|
||||
while (remaining) {
|
||||
let routeNames: string[] | undefined;
|
||||
let params: Record<string, any> | undefined;
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
for (const config of configs) {
|
||||
if (!config.match) {
|
||||
continue;
|
||||
}
|
||||
const match = remaining.match(config.match);
|
||||
|
||||
let didMatch = true;
|
||||
const matchParts = config.match.split('/');
|
||||
const remainingParts = remaining.split('/');
|
||||
|
||||
// we check if remaining path has enough segments to be handled with this pattern
|
||||
if (config.pattern.split('/').length > remainingParts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we keep info about the index of segment on which the params start
|
||||
let paramsIndex = 0;
|
||||
// the beginning of the remaining path should be the same as the part of config before params
|
||||
for (paramsIndex; paramsIndex < matchParts.length; paramsIndex++) {
|
||||
if (matchParts[paramsIndex] !== remainingParts[paramsIndex]) {
|
||||
didMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the first part of the path matches, we need to extract params from the path
|
||||
if (didMatch) {
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeNames = [...config.routeNames];
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
@@ -144,7 +96,8 @@ export default function getStateFromPath(
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
|
||||
const key = p.replace(/^:/, '');
|
||||
const value = remainingParts[i + paramsIndex]; // The param segments start from the end of matched part
|
||||
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
|
||||
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
@@ -154,16 +107,8 @@ export default function getStateFromPath(
|
||||
}, {});
|
||||
}
|
||||
|
||||
// if pattern and remaining path have same amount of segments, there should be nothing left
|
||||
if (config.pattern.split('/').length === remainingParts.length) {
|
||||
remaining = '';
|
||||
} else {
|
||||
// For each segment of the pattern, remove one segment from remaining path
|
||||
let i = config.pattern.split('/').length;
|
||||
while (i--) {
|
||||
remaining = remaining.substr(remaining.indexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
// Remove the matched segment from the remaining path
|
||||
remaining = remaining.replace(match[0], '');
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -178,7 +123,34 @@ export default function getStateFromPath(
|
||||
remaining = segments.join('/');
|
||||
}
|
||||
|
||||
const state = createNestedStateObject(routeNames, initialRoutes, params);
|
||||
let state: InitialState;
|
||||
let routeName = routeNames.shift() as string;
|
||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
|
||||
state = createNestedState(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
|
||||
if (routeNames.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((routeName = routeNames.shift() as string)) {
|
||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
nestedState.routes[nestedState.index || 0].state = createNestedState(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
if (routeNames.length > 0) {
|
||||
nestedState = nestedState.routes[nestedState.index || 0]
|
||||
.state as InitialState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
// The state should be nested inside the deepest route we parsed before
|
||||
@@ -200,13 +172,29 @@ export default function getStateFromPath(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const route = findFocusedRoute(current);
|
||||
const params = parseQueryParams(
|
||||
path,
|
||||
findParseConfigForRoute(route.name, configs)
|
||||
);
|
||||
const query = path.split('?')[1];
|
||||
|
||||
if (query) {
|
||||
while (current?.routes[current.index || 0].state) {
|
||||
// The query params apply to the deepest route
|
||||
current = current.routes[current.index || 0].state;
|
||||
}
|
||||
|
||||
const route = (current as PartialState<NavigationState>).routes[
|
||||
current?.index || 0
|
||||
];
|
||||
|
||||
const params = queryString.parse(query);
|
||||
const parseFunction = findParseConfigForRoute(route.name, configs);
|
||||
|
||||
if (parseFunction) {
|
||||
Object.keys(params).forEach((name) => {
|
||||
if (parseFunction[name] && typeof params[name] === 'string') {
|
||||
params[name] = parseFunction[name](params[name] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (params) {
|
||||
route.params = { ...route.params, ...params };
|
||||
}
|
||||
|
||||
@@ -227,15 +215,16 @@ function createNormalizedConfigs(
|
||||
|
||||
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(key, routeNames, value));
|
||||
if (value !== '') {
|
||||
configs.push(createConfigItem(routeNames, value));
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||
// it can have `path` property and
|
||||
// it could have `screens` prop which has nested configs
|
||||
if (typeof value.path === 'string') {
|
||||
configs.push(createConfigItem(key, routeNames, value.path, value.parse));
|
||||
if (value.path && value.path !== '') {
|
||||
configs.push(createConfigItem(routeNames, value.path, value.parse));
|
||||
}
|
||||
|
||||
if (value.screens) {
|
||||
// property `initialRouteName` without `screens` has no purpose
|
||||
if (value.initialRouteName) {
|
||||
@@ -262,16 +251,15 @@ function createNormalizedConfigs(
|
||||
}
|
||||
|
||||
function createConfigItem(
|
||||
screen: string,
|
||||
routeNames: string[],
|
||||
pattern: string,
|
||||
parse?: ParseConfig
|
||||
): RouteConfig {
|
||||
// part being matched ends on the first param
|
||||
const match = pattern !== '' ? pattern.split('/:')[0] : null;
|
||||
const match = new RegExp(
|
||||
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
|
||||
);
|
||||
|
||||
return {
|
||||
screen,
|
||||
match,
|
||||
pattern,
|
||||
// The routeNames array is mutated, so copy it to keep the current state
|
||||
@@ -307,9 +295,9 @@ function findInitialRoute(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// returns state object with values depending on whether
|
||||
// returns nested state object with values depending on whether
|
||||
// it is the end of state and if there is initialRoute for this level
|
||||
function createStateObject(
|
||||
function createNestedState(
|
||||
initialRoute: string | undefined,
|
||||
routeName: string,
|
||||
isEmpty: boolean,
|
||||
@@ -343,73 +331,3 @@ function createStateObject(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNestedStateObject(
|
||||
routeNames: string[],
|
||||
initialRoutes: InitialRouteConfig[],
|
||||
params: object | undefined
|
||||
) {
|
||||
let state: InitialState;
|
||||
let routeName = routeNames.shift() as string;
|
||||
let initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
|
||||
state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
|
||||
if (routeNames.length > 0) {
|
||||
let nestedState = state;
|
||||
|
||||
while ((routeName = routeNames.shift() as string)) {
|
||||
initialRoute = findInitialRoute(routeName, initialRoutes);
|
||||
nestedState.routes[nestedState.index || 0].state = createStateObject(
|
||||
initialRoute,
|
||||
routeName,
|
||||
routeNames.length === 0,
|
||||
params
|
||||
);
|
||||
if (routeNames.length > 0) {
|
||||
nestedState = nestedState.routes[nestedState.index || 0]
|
||||
.state as InitialState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function findFocusedRoute(state: InitialState) {
|
||||
let current: InitialState | undefined = state;
|
||||
|
||||
while (current?.routes[current.index || 0].state) {
|
||||
// The query params apply to the deepest route
|
||||
current = current.routes[current.index || 0].state;
|
||||
}
|
||||
|
||||
const route = (current as PartialState<NavigationState>).routes[
|
||||
current?.index || 0
|
||||
];
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
function parseQueryParams(
|
||||
path: string,
|
||||
parseConfig?: Record<string, (value: string) => any>
|
||||
) {
|
||||
const query = path.split('?')[1];
|
||||
const params = queryString.parse(query);
|
||||
|
||||
if (parseConfig) {
|
||||
Object.keys(params).forEach((name) => {
|
||||
if (parseConfig[name] && typeof params[name] === 'string') {
|
||||
params[name] = parseConfig[name](params[name] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(params).length ? params : undefined;
|
||||
}
|
||||
|
||||
@@ -3,48 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.6.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.2...@react-navigation/drawer@5.6.3) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.6.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.1...@react-navigation/drawer@5.6.2) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.0...@react-navigation/drawer@5.6.1) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.1...@react-navigation/drawer@5.6.0) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix closing drawer on web with tap on overlay ([70be3f6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/70be3f6d863c56211e2f90bdf743bd8526338248))
|
||||
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/drawer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/drawer",
|
||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||
"version": "5.6.3",
|
||||
"version": "5.5.1",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DrawerNavigationState,
|
||||
DrawerActionHelpers,
|
||||
} from '@react-navigation/native';
|
||||
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
||||
|
||||
export type Scene = {
|
||||
route: Route<string>;
|
||||
@@ -32,7 +32,6 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
||||
drawerType?: 'front' | 'back' | 'slide' | 'permanent';
|
||||
/**
|
||||
* How far from the edge of the screen the swipe gesture should activate.
|
||||
* Not supported on Web.
|
||||
*/
|
||||
edgeWidth?: number;
|
||||
/**
|
||||
@@ -59,9 +58,8 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
|
||||
statusBarAnimation?: 'slide' | 'none' | 'fade';
|
||||
/**
|
||||
* Props to pass to the underlying pan gesture handler.
|
||||
* Not supported on Web.
|
||||
*/
|
||||
gestureHandlerProps?: PanGestureHandlerProperties;
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
/**
|
||||
* 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.
|
||||
@@ -115,15 +113,13 @@ export type DrawerNavigationOptions = {
|
||||
* Whether you can use gestures to open or close the drawer.
|
||||
* Setting this to `false` disables swipe gestures as well as tap on overlay to close.
|
||||
* See `swipeEnabled` to disable only the swipe gesture.
|
||||
* Defaults to `true`.
|
||||
* Not supported on Web.
|
||||
* Defaults to `true`
|
||||
*/
|
||||
gestureEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* Whether you can use swipe gestures to open or close the drawer.
|
||||
* Defaults to `true`.
|
||||
* Not supported on Web.
|
||||
* Defaults to `true`
|
||||
*/
|
||||
swipeEnabled?: boolean;
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
StyleProp,
|
||||
View,
|
||||
InteractionManager,
|
||||
TouchableWithoutFeedback,
|
||||
} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
TapGestureHandler,
|
||||
GestureState,
|
||||
} from './GestureHandler';
|
||||
State as GestureState,
|
||||
TapGestureHandlerStateChangeEvent,
|
||||
} from 'react-native-gesture-handler';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import Overlay from './Overlay';
|
||||
|
||||
const {
|
||||
@@ -79,6 +79,7 @@ type Props = {
|
||||
open: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
onGestureRef?: (ref: PanGestureHandler | null) => void;
|
||||
gestureEnabled: boolean;
|
||||
swipeEnabled: boolean;
|
||||
drawerPosition: 'left' | 'right';
|
||||
@@ -510,7 +511,18 @@ export default class DrawerView extends React.Component<Props> {
|
||||
},
|
||||
]);
|
||||
|
||||
private handleTapStateChange = event([
|
||||
private handleTapStateChange =
|
||||
Platform.OS === 'web'
|
||||
? // FIXME: Drawer doesn't close on Web with the same code that we use for native
|
||||
({ nativeEvent }: TapGestureHandlerStateChangeEvent) => {
|
||||
if (
|
||||
nativeEvent.state === GestureState.END &&
|
||||
nativeEvent.oldState === GestureState.ACTIVE
|
||||
) {
|
||||
this.toggleDrawer(false);
|
||||
}
|
||||
}
|
||||
: event([
|
||||
{
|
||||
nativeEvent: {
|
||||
oldState: (s: Animated.Value<number>) =>
|
||||
@@ -567,6 +579,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
sceneContainerStyle,
|
||||
drawerStyle,
|
||||
overlayStyle,
|
||||
onGestureRef,
|
||||
renderDrawerContent,
|
||||
renderSceneContent,
|
||||
gestureHandlerProps,
|
||||
@@ -611,6 +624,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<PanGestureHandler
|
||||
ref={onGestureRef}
|
||||
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
|
||||
onGestureEvent={this.handleGestureEvent}
|
||||
@@ -649,15 +663,7 @@ export default class DrawerView extends React.Component<Props> {
|
||||
</View>
|
||||
{
|
||||
// Disable overlay if sidebar is permanent
|
||||
drawerType === 'permanent' ? null : Platform.OS === 'web' ? (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={
|
||||
gestureEnabled ? () => this.toggleDrawer(false) : undefined
|
||||
}
|
||||
>
|
||||
<Overlay progress={progress} style={overlayStyle} />
|
||||
</TouchableWithoutFeedback>
|
||||
) : (
|
||||
drawerType === 'permanent' ? null : (
|
||||
<TapGestureHandler
|
||||
enabled={gestureEnabled}
|
||||
onHandlerStateChange={this.handleTapStateChange}
|
||||
@@ -731,11 +737,6 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
main: {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
// FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
|
||||
// But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
|
||||
web: null,
|
||||
default: { overflow: 'hidden' },
|
||||
}),
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,10 @@ import {
|
||||
} from 'react-native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
GestureHandlerRootView,
|
||||
} from 'react-native-gesture-handler';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
DrawerNavigationState,
|
||||
@@ -18,7 +22,7 @@ import {
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
import { GestureHandlerRootView } from './GestureHandler';
|
||||
import DrawerGestureContext from '../utils/DrawerGestureContext';
|
||||
import SafeAreaProviderCompat from './SafeAreaProviderCompat';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
import DrawerContent from './DrawerContent';
|
||||
@@ -90,6 +94,8 @@ export default function DrawerView({
|
||||
Dimensions.get('window')
|
||||
);
|
||||
|
||||
const drawerGestureRef = React.useRef<PanGestureHandler>(null);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
const isDrawerOpen = state.history.some((it) => it.type === 'drawer');
|
||||
@@ -199,6 +205,7 @@ export default function DrawerView({
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
<GestureHandlerWrapper style={styles.content}>
|
||||
<SafeAreaProviderCompat>
|
||||
<DrawerGestureContext.Provider value={drawerGestureRef}>
|
||||
<DrawerOpenContext.Provider value={isDrawerOpen}>
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
@@ -206,6 +213,10 @@ export default function DrawerView({
|
||||
swipeEnabled={swipeEnabled}
|
||||
onOpen={handleDrawerOpen}
|
||||
onClose={handleDrawerClose}
|
||||
onGestureRef={(ref) => {
|
||||
// @ts-ignore
|
||||
drawerGestureRef.current = ref;
|
||||
}}
|
||||
gestureHandlerProps={gestureHandlerProps}
|
||||
drawerType={drawerType}
|
||||
drawerPosition={drawerPosition}
|
||||
@@ -242,6 +253,7 @@ export default function DrawerView({
|
||||
dimensions={dimensions}
|
||||
/>
|
||||
</DrawerOpenContext.Provider>
|
||||
</DrawerGestureContext.Provider>
|
||||
</SafeAreaProviderCompat>
|
||||
</GestureHandlerWrapper>
|
||||
</NavigationHelpersContext.Provider>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
PanGestureHandler as PanGestureHandlerNative,
|
||||
PanGestureHandlerProperties,
|
||||
} from 'react-native-gesture-handler';
|
||||
import DrawerGestureContext from '../utils/DrawerGestureContext';
|
||||
|
||||
export function PanGestureHandler(props: PanGestureHandlerProperties) {
|
||||
const gestureRef = React.useRef<PanGestureHandlerNative>(null);
|
||||
|
||||
return (
|
||||
<DrawerGestureContext.Provider value={gestureRef}>
|
||||
<PanGestureHandlerNative {...props} />
|
||||
</DrawerGestureContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
GestureHandlerRootView,
|
||||
TapGestureHandler,
|
||||
State as GestureState,
|
||||
PanGestureHandlerGestureEvent,
|
||||
} from 'react-native-gesture-handler';
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import type {
|
||||
PanGestureHandlerProperties,
|
||||
TapGestureHandlerProperties,
|
||||
} from 'react-native-gesture-handler';
|
||||
|
||||
const Dummy: any = ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
export const PanGestureHandler = Dummy as React.ComponentType<
|
||||
PanGestureHandlerProperties
|
||||
>;
|
||||
|
||||
export const TapGestureHandler = Dummy as React.ComponentType<
|
||||
TapGestureHandlerProperties
|
||||
>;
|
||||
|
||||
export const GestureHandlerRootView = View;
|
||||
|
||||
export const GestureState = {
|
||||
UNDETERMINED: 0,
|
||||
FAILED: 1,
|
||||
BEGAN: 2,
|
||||
CANCELLED: 3,
|
||||
ACTIVE: 4,
|
||||
END: 5,
|
||||
};
|
||||
|
||||
export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
||||
@@ -29,7 +29,7 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
||||
styles.container,
|
||||
Platform.OS === 'web'
|
||||
? { display: isVisible ? 'flex' : 'none' }
|
||||
: { overflow: 'hidden' },
|
||||
: null,
|
||||
style,
|
||||
]}
|
||||
collapsable={false}
|
||||
@@ -52,6 +52,7 @@ export default class ResourceSavingScene extends React.Component<Props> {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
attached: {
|
||||
flex: 1,
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.13...@react-navigation/material-bottom-tabs@5.1.14) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.12...@react-navigation/material-bottom-tabs@5.1.13) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.11...@react-navigation/material-bottom-tabs@5.1.12) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.10...@react-navigation/material-bottom-tabs@5.1.11) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.9...@react-navigation/material-bottom-tabs@5.1.10) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-bottom-tabs",
|
||||
"description": "Integration for bottom navigation component from react-native-paper",
|
||||
"version": "5.1.14",
|
||||
"version": "5.1.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
"@types/react-native-vector-icons": "^6.4.5",
|
||||
|
||||
@@ -3,38 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.13...@react-navigation/material-top-tabs@5.1.14) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.12...@react-navigation/material-top-tabs@5.1.13) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.11...@react-navigation/material-top-tabs@5.1.12) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.10...@react-navigation/material-top-tabs@5.1.11) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.9...@react-navigation/material-top-tabs@5.1.10) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/material-top-tabs",
|
||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||
"version": "5.1.14",
|
||||
"version": "5.1.10",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -3,59 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.2...@react-navigation/native@5.2.3) (2020-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default linking enabled to true ([c7b8e2e](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/c7b8e2e9666733143eef156b27f3e4995c36b856))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.1...@react-navigation/native@5.2.2) (2020-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't throw when using 'useLinking'. fixes [#8171](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8171) ([10eca8b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/10eca8b92edbce6dbef8abaf189e4b59a29b3748))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.0...@react-navigation/native@5.2.1) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* render fallback only if linking is enabled. closes [#8161](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8161) ([1c075ff](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/1c075ffb169d233ed0515efea264a5a69b4de52e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.7...@react-navigation/native@5.2.0) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add catch to thenable returned by getInitialState ([d6fa279](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/d6fa279d9371c7a6403d10d209a2a64147891c63))
|
||||
* return onPress instead of onClick for useLinkProps ([ae5442e](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/ae5442ebe812b91fa1f12164f27d1aeed918ab0e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
|
||||
* add a useLinkProps hook ([f2291d1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/f2291d110faa2aa8e10c9133c1c0c28d54af7917))
|
||||
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/942d2be2c72720469475ce12ec8df23825994dbf))
|
||||
* add Link component as useLinkTo hook for navigating to links ([2573b5b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/2573b5beaac1240434e52f3f57bb29da2f541c88))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.6...@react-navigation/native@5.1.7) (2020-04-27)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/native
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"description": "React Native integration for React Navigation",
|
||||
"version": "5.2.3",
|
||||
"version": "5.1.7",
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
@@ -31,7 +31,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^5.4.0"
|
||||
"@react-navigation/core": "^5.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Text, TextProps, GestureResponderEvent, Platform } from 'react-native';
|
||||
import { Text, TextProps, GestureResponderEvent } from 'react-native';
|
||||
import { NavigationAction } from '@react-navigation/core';
|
||||
import useLinkProps from './useLinkProps';
|
||||
|
||||
@@ -28,15 +28,16 @@ export default function Link({ to, action, ...rest }: Props) {
|
||||
rest.onPress?.(e);
|
||||
}
|
||||
|
||||
if (props.onClick) {
|
||||
props.onClick(e);
|
||||
} else {
|
||||
props.onPress(e);
|
||||
}
|
||||
};
|
||||
|
||||
return React.createElement(Text, {
|
||||
...props,
|
||||
...rest,
|
||||
...Platform.select({
|
||||
web: { onClick: onPress } as any,
|
||||
default: { onPress },
|
||||
}),
|
||||
...(props.onClick ? { onClick: onPress } : { onPress }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||
|
||||
const linkingContext = React.useMemo(() => ({ options: linking }), [linking]);
|
||||
|
||||
if (isLinkingEnabled && !isReady) {
|
||||
if (!isReady) {
|
||||
// This is temporary until we have Suspense for data-fetching
|
||||
// Then the fallback will be handled by a parent `Suspense` component
|
||||
return fallback as React.ReactElement;
|
||||
|
||||
@@ -17,7 +17,7 @@ it('throws if multiple instances of useLinking are used', () => {
|
||||
let element;
|
||||
|
||||
expect(() => (element = render(<Sample />))).toThrowError(
|
||||
'Looks like you have configured linking in multiple places.'
|
||||
"Looks like you are using 'useLinking' in multiple components."
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
@@ -41,7 +41,9 @@ it('throws if multiple instances of useLinking are used', () => {
|
||||
<B />
|
||||
</>
|
||||
))
|
||||
).toThrowError('Looks like you have configured linking in multiple places.');
|
||||
).toThrowError(
|
||||
"Looks like you are using 'useLinking' in multiple components."
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
element?.unmount();
|
||||
@@ -55,19 +57,5 @@ it('throws if multiple instances of useLinking are used', () => {
|
||||
|
||||
render(wrapper2).unmount();
|
||||
|
||||
expect(() => (element = render(wrapper2))).not.toThrow();
|
||||
|
||||
// @ts-ignore
|
||||
element?.unmount();
|
||||
|
||||
function Sample3() {
|
||||
useLinking(ref, options);
|
||||
useLinking(ref, { ...options, enabled: false });
|
||||
return null;
|
||||
}
|
||||
|
||||
expect(() => (element = render(<Sample3 />))).not.toThrowError();
|
||||
|
||||
// @ts-ignore
|
||||
element?.unmount();
|
||||
expect(() => render(wrapper2)).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -57,6 +57,9 @@ export default function useLinkProps({ to, action }: Props) {
|
||||
return {
|
||||
href: to,
|
||||
accessibilityRole: 'link' as const,
|
||||
onPress,
|
||||
...Platform.select({
|
||||
web: { onClick: onPress } as any,
|
||||
default: { onPress },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,28 +12,22 @@ let isUsingLinking = false;
|
||||
export default function useLinking(
|
||||
ref: React.RefObject<NavigationContainerRef>,
|
||||
{
|
||||
enabled = true,
|
||||
enabled,
|
||||
prefixes,
|
||||
config,
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
}: LinkingOptions
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (enabled !== false && isUsingLinking) {
|
||||
if (isUsingLinking) {
|
||||
throw new Error(
|
||||
[
|
||||
'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:',
|
||||
"- You are not using both 'linking' prop and 'useLinking'",
|
||||
"- You don't have 'useLinking' in multiple components",
|
||||
Platform.OS === 'android'
|
||||
? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
|
||||
: '',
|
||||
]
|
||||
.join('\n')
|
||||
.trim()
|
||||
"Looks like you are using 'useLinking' in multiple components. This is likely an error since deep links should only be handled in one place to avoid conflicts." +
|
||||
(Platform.OS === 'android'
|
||||
? "\n\nIf you're not using it in multiple components, ensure that you have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple activities which run multiple instances of the root component."
|
||||
: '')
|
||||
);
|
||||
} else {
|
||||
isUsingLinking = enabled !== false;
|
||||
isUsingLinking = true;
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -10,15 +10,6 @@ import { LinkingOptions } from './types';
|
||||
|
||||
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||
|
||||
type HistoryState = { index: number };
|
||||
|
||||
declare const history: {
|
||||
state?: HistoryState;
|
||||
go(delta: number): void;
|
||||
pushState(state: HistoryState, title: string, url: string): void;
|
||||
replaceState(state: HistoryState, title: string, url: string): void;
|
||||
};
|
||||
|
||||
const getStateLength = (state: NavigationState) => {
|
||||
let length = 0;
|
||||
|
||||
@@ -43,25 +34,19 @@ let isUsingLinking = false;
|
||||
export default function useLinking(
|
||||
ref: React.RefObject<NavigationContainerRef>,
|
||||
{
|
||||
enabled = true,
|
||||
enabled,
|
||||
config,
|
||||
getStateFromPath = getStateFromPathDefault,
|
||||
getPathFromState = getPathFromStateDefault,
|
||||
}: LinkingOptions
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (enabled !== false && isUsingLinking) {
|
||||
if (isUsingLinking) {
|
||||
throw new Error(
|
||||
[
|
||||
'Looks like you have configured linking in multiple places. This is likely an error since URL integration should only be handled in one place to avoid conflicts. Make sure that:',
|
||||
"- You are not using both 'linking' prop and 'useLinking'",
|
||||
"- You don't have 'useLinking' in multiple components",
|
||||
]
|
||||
.join('\n')
|
||||
.trim()
|
||||
"Looks like you are using 'useLinking' in multiple components. This is likely an error since URL integration should only be handled in one place to avoid conflicts. Also ensure that you set your android activity launchMode to single task in your AndroiManifest.xml file."
|
||||
);
|
||||
} else {
|
||||
isUsingLinking = enabled !== false;
|
||||
isUsingLinking = true;
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -95,18 +80,11 @@ export default function useLinking(
|
||||
}
|
||||
}
|
||||
|
||||
const then = (callback: (state: ResultState | undefined) => void) =>
|
||||
Promise.resolve(callback(value));
|
||||
|
||||
// Make it a thenable to keep consistent with the native impl
|
||||
const thenable = {
|
||||
then,
|
||||
catch() {
|
||||
return thenable;
|
||||
},
|
||||
return {
|
||||
then: (callback: (state: ResultState | undefined) => void) =>
|
||||
callback(value),
|
||||
};
|
||||
|
||||
return thenable;
|
||||
}, []);
|
||||
|
||||
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
type Thenable<T> = { then(cb: (result: T) => void): void };
|
||||
|
||||
export default function useThenable<T>(create: () => Thenable<T>) {
|
||||
export default function useThenable<T>(
|
||||
create: () => {
|
||||
then(success: (result: T) => void, error?: (error: any) => void): void;
|
||||
}
|
||||
) {
|
||||
const [promise] = React.useState(create);
|
||||
|
||||
// Check if our thenable is synchronous
|
||||
@@ -22,20 +24,20 @@ export default function useThenable<T>(create: () => Thenable<T>) {
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const resolve = async () => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await promise;
|
||||
} finally {
|
||||
if (!resolved) {
|
||||
promise.then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
setState([true, result]);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
console.error(error);
|
||||
setState([true, undefined]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!resolved) {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -3,17 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.1...@react-navigation/routers@5.4.2) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix backBehavior with initialRoute ([#8110](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/issues/8110)) ([420f692](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/commit/420f6926e111d32c2388c44ff0bee2b8ea238a57))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.0...@react-navigation/routers@5.4.1) (2020-04-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/routers",
|
||||
"description": "Routers to help build custom navigators",
|
||||
"version": "5.4.2",
|
||||
"version": "5.4.1",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
|
||||
@@ -59,31 +59,21 @@ export const TabActions = {
|
||||
const getRouteHistory = (
|
||||
routes: Route<string>[],
|
||||
index: number,
|
||||
backBehavior: BackBehavior,
|
||||
initialRouteName: string | undefined
|
||||
backBehavior: BackBehavior
|
||||
) => {
|
||||
const history = [{ type: TYPE_ROUTE, key: routes[index].key }];
|
||||
let initialRouteIndex;
|
||||
|
||||
switch (backBehavior) {
|
||||
case 'initialRoute':
|
||||
if (index !== 0) {
|
||||
history.unshift({ type: TYPE_ROUTE, key: routes[0].key });
|
||||
}
|
||||
break;
|
||||
case 'order':
|
||||
for (let i = index; i > 0; i--) {
|
||||
history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key });
|
||||
}
|
||||
break;
|
||||
case 'initialRoute':
|
||||
initialRouteIndex = routes.findIndex(
|
||||
(route) => route.name === initialRouteName
|
||||
);
|
||||
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
|
||||
|
||||
if (initialRouteIndex !== index) {
|
||||
history.unshift({
|
||||
type: TYPE_ROUTE,
|
||||
key: routes[initialRouteIndex].key,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'history':
|
||||
// The history will fill up on navigation
|
||||
break;
|
||||
@@ -95,8 +85,7 @@ const getRouteHistory = (
|
||||
const changeIndex = (
|
||||
state: TabNavigationState,
|
||||
index: number,
|
||||
backBehavior: BackBehavior,
|
||||
initialRouteName: string | undefined
|
||||
backBehavior: BackBehavior
|
||||
) => {
|
||||
let history;
|
||||
|
||||
@@ -107,12 +96,7 @@ const changeIndex = (
|
||||
.filter((it) => (it.type === 'route' ? it.key !== currentKey : false))
|
||||
.concat({ type: TYPE_ROUTE, key: currentKey });
|
||||
} else {
|
||||
history = getRouteHistory(
|
||||
state.routes,
|
||||
index,
|
||||
backBehavior,
|
||||
initialRouteName
|
||||
);
|
||||
history = getRouteHistory(state.routes, index, backBehavior);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -146,12 +130,7 @@ export default function TabRouter({
|
||||
params: routeParamList[name],
|
||||
}));
|
||||
|
||||
const history = getRouteHistory(
|
||||
routes,
|
||||
index,
|
||||
backBehavior,
|
||||
initialRouteName
|
||||
);
|
||||
const history = getRouteHistory(routes, index, backBehavior);
|
||||
|
||||
return {
|
||||
stale: false,
|
||||
@@ -210,12 +189,7 @@ export default function TabRouter({
|
||||
);
|
||||
|
||||
if (!history?.length) {
|
||||
history = getRouteHistory(
|
||||
routes,
|
||||
index,
|
||||
backBehavior,
|
||||
initialRouteName
|
||||
);
|
||||
history = getRouteHistory(routes, index, backBehavior);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -249,12 +223,7 @@ export default function TabRouter({
|
||||
);
|
||||
|
||||
if (!history.length) {
|
||||
history = getRouteHistory(
|
||||
routes,
|
||||
index,
|
||||
backBehavior,
|
||||
initialRouteName
|
||||
);
|
||||
history = getRouteHistory(routes, index, backBehavior);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -273,7 +242,7 @@ export default function TabRouter({
|
||||
return state;
|
||||
}
|
||||
|
||||
return changeIndex(state, index, backBehavior, initialRouteName);
|
||||
return changeIndex(state, index, backBehavior);
|
||||
},
|
||||
|
||||
getStateForAction(state, action) {
|
||||
@@ -315,8 +284,7 @@ export default function TabRouter({
|
||||
: state.routes,
|
||||
},
|
||||
index,
|
||||
backBehavior,
|
||||
initialRouteName
|
||||
backBehavior
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -882,78 +882,6 @@ it('handles back action with backBehavior: initialRoute', () => {
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('handles back action with backBehavior: initialRoute and initialRouteName', () => {
|
||||
const router = TabRouter({
|
||||
backBehavior: 'initialRoute',
|
||||
initialRouteName: 'baz',
|
||||
});
|
||||
|
||||
const options = {
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routeParamList: {},
|
||||
};
|
||||
|
||||
let state = router.getInitialState(options);
|
||||
|
||||
expect(
|
||||
router.getStateForAction(state, CommonActions.goBack(), options)
|
||||
).toEqual(null);
|
||||
|
||||
state = router.getStateForAction(
|
||||
state,
|
||||
TabActions.jumpTo('qux'),
|
||||
options
|
||||
) as TabNavigationState;
|
||||
|
||||
expect(
|
||||
router.getStateForAction(state, CommonActions.goBack(), options)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'tab-test',
|
||||
index: 1,
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz' },
|
||||
{ key: 'qux-test', name: 'qux' },
|
||||
],
|
||||
history: [{ type: 'route', key: 'baz-test' }],
|
||||
});
|
||||
|
||||
state = router.getStateForAction(
|
||||
state,
|
||||
TabActions.jumpTo('bar'),
|
||||
options
|
||||
) as TabNavigationState;
|
||||
|
||||
expect(
|
||||
router.getStateForAction(state, CommonActions.goBack(), options)
|
||||
).toEqual({
|
||||
stale: false,
|
||||
type: 'tab',
|
||||
key: 'tab-test',
|
||||
index: 1,
|
||||
routeNames: ['bar', 'baz', 'qux'],
|
||||
routes: [
|
||||
{ key: 'bar-test', name: 'bar' },
|
||||
{ key: 'baz-test', name: 'baz' },
|
||||
{ key: 'qux-test', name: 'qux' },
|
||||
],
|
||||
history: [{ type: 'route', key: 'baz-test' }],
|
||||
});
|
||||
|
||||
state = router.getStateForAction(
|
||||
state,
|
||||
TabActions.jumpTo('baz'),
|
||||
options
|
||||
) as TabNavigationState;
|
||||
|
||||
expect(
|
||||
router.getStateForAction(state, CommonActions.goBack(), options)
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('handles back action with backBehavior: none', () => {
|
||||
const router = TabRouter({ backBehavior: 'none' });
|
||||
const options = {
|
||||
|
||||
@@ -3,41 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [5.2.18](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.17...@react-navigation/stack@5.2.18) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.17](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.16...@react-navigation/stack@5.2.17) (2020-05-01)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.16](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.15...@react-navigation/stack@5.2.16) (2020-04-30)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/stack
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.15](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.14...@react-navigation/stack@5.2.15) (2020-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [5.2.14](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.13...@react-navigation/stack@5.2.14) (2020-04-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/stack",
|
||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||
"version": "5.2.18",
|
||||
"version": "5.2.14",
|
||||
"keywords": [
|
||||
"react-native-component",
|
||||
"react-component",
|
||||
@@ -40,7 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-native-community/masked-view": "^0.1.7",
|
||||
"@react-navigation/native": "^5.2.3",
|
||||
"@react-navigation/native": "^5.1.7",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-native": "^0.61.22",
|
||||
|
||||
@@ -276,8 +276,7 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
cardStyle?: StyleProp<ViewStyle>;
|
||||
/**
|
||||
* Whether transition animation should be enabled the screen.
|
||||
* If you set it to `false`, the screen won't animate when pushing or popping.
|
||||
* Defaults to `true` on Android and iOS, `false` on Web.
|
||||
* If you set it to `false`, the screen won't animate when pushing or popping. Defaults to `true`.
|
||||
*/
|
||||
animationEnabled?: boolean;
|
||||
/**
|
||||
@@ -287,12 +286,10 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
animationTypeForReplace?: 'push' | 'pop';
|
||||
/**
|
||||
* Whether you can use gestures to dismiss this screen. Defaults to `true` on iOS, `false` on Android.
|
||||
* Not supported on Web.
|
||||
*/
|
||||
gestureEnabled?: boolean;
|
||||
/**
|
||||
* Object to override the distance of touch start from the edge of the screen to recognize gestures.
|
||||
* Not supported on Web.
|
||||
*/
|
||||
gestureResponseDistance?: {
|
||||
/**
|
||||
@@ -305,8 +302,8 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
horizontal?: number;
|
||||
};
|
||||
/**
|
||||
* Number which determines the relevance of velocity for the gesture. Defaults to 0.3.
|
||||
* Not supported on Web.
|
||||
* Number which determines the relevance of velocity for the gesture.
|
||||
* Defaults to 0.3.
|
||||
*/
|
||||
gestureVelocityImpact?: number;
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
||||
|
||||
export default React.createContext<React.Ref<
|
||||
import('react-native-gesture-handler').PanGestureHandler
|
||||
> | null>(null);
|
||||
export default React.createContext<React.Ref<PanGestureHandler> | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
PanGestureHandler as PanGestureHandlerNative,
|
||||
PanGestureHandlerProperties,
|
||||
} from 'react-native-gesture-handler';
|
||||
import GestureHandlerRefContext from '../utils/GestureHandlerRefContext';
|
||||
|
||||
export function PanGestureHandler(props: PanGestureHandlerProperties) {
|
||||
const gestureRef = React.useRef<PanGestureHandlerNative>(null);
|
||||
|
||||
return (
|
||||
<GestureHandlerRefContext.Provider value={gestureRef}>
|
||||
<PanGestureHandlerNative {...props} />
|
||||
</GestureHandlerRefContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
GestureHandlerRootView,
|
||||
State as GestureState,
|
||||
PanGestureHandlerGestureEvent,
|
||||
} from 'react-native-gesture-handler';
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
|
||||
|
||||
const Dummy: any = ({ children }: { children: React.ReactNode }) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
export const PanGestureHandler = Dummy as React.ComponentType<
|
||||
PanGestureHandlerProperties
|
||||
>;
|
||||
|
||||
export const GestureHandlerRootView = View;
|
||||
|
||||
export const GestureState = {
|
||||
UNDETERMINED: 0,
|
||||
FAILED: 1,
|
||||
BEGAN: 2,
|
||||
CANCELLED: 3,
|
||||
ACTIVE: 4,
|
||||
END: 5,
|
||||
};
|
||||
|
||||
export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
import MaskedView from '../MaskedView';
|
||||
import { TouchableItem } from '../TouchableItem';
|
||||
import TouchableItem from '../TouchableItem';
|
||||
import { StackHeaderLeftButtonProps } from '../../types';
|
||||
|
||||
type Props = StackHeaderLeftButtonProps;
|
||||
|
||||
@@ -9,15 +9,14 @@ import {
|
||||
Platform,
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
import { EdgeInsets } from 'react-native-safe-area-context';
|
||||
import Color from 'color';
|
||||
|
||||
import CardSheet from './CardSheet';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
GestureState,
|
||||
State as GestureState,
|
||||
PanGestureHandlerGestureEvent,
|
||||
} from '../GestureHandler';
|
||||
} from 'react-native-gesture-handler';
|
||||
import { EdgeInsets } from 'react-native-safe-area-context';
|
||||
import Color from 'color';
|
||||
import StackGestureRefContext from '../../utils/GestureHandlerRefContext';
|
||||
import CardAnimationContext from '../../utils/CardAnimationContext';
|
||||
import getDistanceForDirection from '../../utils/getDistanceForDirection';
|
||||
import getInvertedMultiplier from '../../utils/getInvertedMultiplier';
|
||||
@@ -37,7 +36,6 @@ type Props = ViewProps & {
|
||||
gesture: Animated.Value;
|
||||
layout: Layout;
|
||||
insets: EdgeInsets;
|
||||
pageOverflowEnabled: boolean;
|
||||
gestureDirection: GestureDirection;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
@@ -414,6 +412,8 @@ export default class Card extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
private gestureRef = React.createRef<PanGestureHandler>();
|
||||
|
||||
private contentRef = React.createRef<View>();
|
||||
|
||||
render() {
|
||||
@@ -430,7 +430,6 @@ export default class Card extends React.Component<Props> {
|
||||
shadowEnabled,
|
||||
gestureEnabled,
|
||||
gestureDirection,
|
||||
pageOverflowEnabled,
|
||||
children,
|
||||
containerStyle: customContainerStyle,
|
||||
contentStyle,
|
||||
@@ -500,6 +499,7 @@ export default class Card extends React.Component<Props> {
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<PanGestureHandler
|
||||
ref={this.gestureRef}
|
||||
enabled={layout.width !== 0 && gestureEnabled}
|
||||
onGestureEvent={handleGestureEvent}
|
||||
onHandlerStateChange={this.handleGestureStateChange}
|
||||
@@ -523,14 +523,14 @@ export default class Card extends React.Component<Props> {
|
||||
pointerEvents="none"
|
||||
/>
|
||||
) : null}
|
||||
<CardSheet
|
||||
<View
|
||||
ref={this.contentRef}
|
||||
enabled={pageOverflowEnabled}
|
||||
layout={layout}
|
||||
style={contentStyle}
|
||||
style={[styles.content, contentStyle]}
|
||||
>
|
||||
<StackGestureRefContext.Provider value={this.gestureRef}>
|
||||
{children}
|
||||
</CardSheet>
|
||||
</StackGestureRefContext.Provider>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
@@ -544,6 +544,10 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
|
||||
@@ -4,13 +4,7 @@ import { Route, useTheme } from '@react-navigation/native';
|
||||
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||
import Card from './Card';
|
||||
import HeaderHeightContext from '../../utils/HeaderHeightContext';
|
||||
import {
|
||||
Scene,
|
||||
Layout,
|
||||
StackHeaderMode,
|
||||
StackCardMode,
|
||||
TransitionPreset,
|
||||
} from '../../types';
|
||||
import { Scene, Layout, StackHeaderMode, TransitionPreset } from '../../types';
|
||||
|
||||
type Props = TransitionPreset & {
|
||||
index: number;
|
||||
@@ -51,7 +45,6 @@ type Props = TransitionPreset & {
|
||||
horizontal?: number;
|
||||
};
|
||||
gestureVelocityImpact?: number;
|
||||
mode: StackCardMode;
|
||||
headerMode: StackHeaderMode;
|
||||
headerShown?: boolean;
|
||||
headerTransparent?: boolean;
|
||||
@@ -80,7 +73,6 @@ function CardContainer({
|
||||
gestureVelocityImpact,
|
||||
getPreviousRoute,
|
||||
getFocusedRoute,
|
||||
mode,
|
||||
headerMode,
|
||||
headerShown,
|
||||
headerStyleInterpolator,
|
||||
@@ -186,7 +178,6 @@ function CardContainer({
|
||||
accessibilityElementsHidden={!focused}
|
||||
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
|
||||
pointerEvents={active ? 'box-none' : pointerEvents}
|
||||
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
|
||||
containerStyle={
|
||||
headerMode === 'float' && !headerTransparent && headerShown !== false
|
||||
? { marginTop: headerHeight }
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { View, ViewProps, StyleSheet } from 'react-native';
|
||||
|
||||
type Props = ViewProps & {
|
||||
enabled: boolean;
|
||||
layout: { width: number; height: number };
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
// This component will render a page which overflows the screen
|
||||
// if the container fills the body by comparing the size
|
||||
// This lets the document.body handle scrolling of the content
|
||||
// It's necessary for mobile browsers to be able to hide address bar on scroll
|
||||
export default React.forwardRef<View, Props>(function CardSheet(
|
||||
{ enabled, layout, style, ...rest },
|
||||
ref
|
||||
) {
|
||||
const [fill, setFill] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof document === 'undefined' || !document.body) {
|
||||
// Only run when DOM is available
|
||||
return;
|
||||
}
|
||||
|
||||
const width = document.body.clientWidth;
|
||||
const height = document.body.clientHeight;
|
||||
|
||||
setFill(width === layout.width && height === layout.height);
|
||||
}, [layout.height, layout.width]);
|
||||
|
||||
return (
|
||||
<View
|
||||
{...rest}
|
||||
ref={ref}
|
||||
style={[enabled && fill ? styles.page : styles.card, style]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
minHeight: '100%',
|
||||
},
|
||||
card: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
@@ -519,7 +519,6 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
onHeaderHeightChange={this.handleHeaderLayout}
|
||||
getPreviousRoute={getPreviousRoute}
|
||||
getFocusedRoute={this.getFocusedRoute}
|
||||
mode={mode}
|
||||
headerMode={headerMode}
|
||||
headerShown={headerShown}
|
||||
headerTransparent={headerTransparent}
|
||||
@@ -565,6 +564,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
floating: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { View, Platform, StyleSheet } from 'react-native';
|
||||
import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import {
|
||||
NavigationHelpersContext,
|
||||
StackActions,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
Route,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
import { GestureHandlerRootView } from '../GestureHandler';
|
||||
import CardStack from './CardStack';
|
||||
import KeyboardManager from '../KeyboardManager';
|
||||
import HeaderContainer, {
|
||||
@@ -420,7 +420,7 @@ export default class StackView extends React.Component<Props, State> {
|
||||
} = this.state;
|
||||
|
||||
const headerMode =
|
||||
mode === 'card' && Platform.OS === 'ios' ? 'float' : 'screen';
|
||||
mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen';
|
||||
|
||||
return (
|
||||
<NavigationHelpersContext.Provider value={navigation}>
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* TouchableItem renders a touchable that looks native on both iOS and Android.
|
||||
*
|
||||
* It provides an abstraction on top of TouchableNativeFeedback and
|
||||
* TouchableOpacity.
|
||||
*
|
||||
* On iOS you can pass the props of TouchableOpacity, on Android pass the props
|
||||
* of TouchableNativeFeedback.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Platform,
|
||||
TouchableNativeFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
import BorderlessButton from './BorderlessButton';
|
||||
|
||||
export type Props = ViewProps & {
|
||||
pressColor: string;
|
||||
disabled?: boolean;
|
||||
borderless?: boolean;
|
||||
delayPressIn?: number;
|
||||
onPress?: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ANDROID_VERSION_LOLLIPOP = 21;
|
||||
|
||||
export class TouchableItem extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
borderless: false,
|
||||
pressColor: 'rgba(0, 0, 0, .32)',
|
||||
};
|
||||
|
||||
render() {
|
||||
/*
|
||||
* TouchableNativeFeedback.Ripple causes a crash on old Android versions,
|
||||
* therefore only enable it on Android Lollipop and above.
|
||||
*
|
||||
* All touchables on Android should have the ripple effect according to
|
||||
* platform design guidelines.
|
||||
* We need to pass the background prop to specify a borderless ripple effect.
|
||||
*/
|
||||
if (
|
||||
Platform.OS === 'android' &&
|
||||
Platform.Version >= ANDROID_VERSION_LOLLIPOP
|
||||
) {
|
||||
const { style, pressColor, borderless, children, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
{...rest}
|
||||
useForeground={TouchableNativeFeedback.canUseNativeForeground()}
|
||||
background={TouchableNativeFeedback.Ripple(pressColor, borderless)}
|
||||
>
|
||||
<View style={style}>{React.Children.only(children)}</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
} else if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<BorderlessButton
|
||||
hitSlop={{ top: 10, bottom: 10, right: 10, left: 10 }}
|
||||
disallowInterruption
|
||||
enabled={!this.props.disabled}
|
||||
{...this.props}
|
||||
>
|
||||
{this.props.children}
|
||||
</BorderlessButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableOpacity {...this.props}>
|
||||
{this.props.children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,80 @@
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
/**
|
||||
* TouchableItem renders a touchable that looks native on both iOS and Android.
|
||||
*
|
||||
* It provides an abstraction on top of TouchableNativeFeedback and
|
||||
* TouchableOpacity.
|
||||
*
|
||||
* On iOS you can pass the props of TouchableOpacity, on Android pass the props
|
||||
* of TouchableNativeFeedback.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Platform,
|
||||
TouchableNativeFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
export const TouchableItem = (TouchableOpacity as any) as typeof import('./TouchableItem.native').TouchableItem;
|
||||
import BorderlessButton from './BorderlessButton';
|
||||
|
||||
type Props = ViewProps & {
|
||||
pressColor: string;
|
||||
disabled?: boolean;
|
||||
borderless?: boolean;
|
||||
delayPressIn?: number;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
const ANDROID_VERSION_LOLLIPOP = 21;
|
||||
|
||||
export default class TouchableItem extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
borderless: false,
|
||||
pressColor: 'rgba(0, 0, 0, .32)',
|
||||
};
|
||||
|
||||
render() {
|
||||
/*
|
||||
* TouchableNativeFeedback.Ripple causes a crash on old Android versions,
|
||||
* therefore only enable it on Android Lollipop and above.
|
||||
*
|
||||
* All touchables on Android should have the ripple effect according to
|
||||
* platform design guidelines.
|
||||
* We need to pass the background prop to specify a borderless ripple effect.
|
||||
*/
|
||||
if (
|
||||
Platform.OS === 'android' &&
|
||||
Platform.Version >= ANDROID_VERSION_LOLLIPOP
|
||||
) {
|
||||
const { style, pressColor, borderless, children, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
{...rest}
|
||||
useForeground={TouchableNativeFeedback.canUseNativeForeground()}
|
||||
background={TouchableNativeFeedback.Ripple(pressColor, borderless)}
|
||||
>
|
||||
<View style={style}>{React.Children.only(children)}</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
} else if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<BorderlessButton
|
||||
hitSlop={{ top: 10, bottom: 10, right: 10, left: 10 }}
|
||||
disallowInterruption
|
||||
enabled={!this.props.disabled}
|
||||
{...this.props}
|
||||
>
|
||||
{this.props.children}
|
||||
</BorderlessButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableOpacity {...this.props}>
|
||||
{this.props.children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
packages/web-stack/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 React Navigation Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
5
packages/web-stack/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# `@react-navigation/web-stack`
|
||||
|
||||
Stack navigator for React Navigation on Web.
|
||||
|
||||
Installation instructions and documentation can be found on the [React Navigation website](https://reactnavigation.org/docs/web-stack-navigator.html).
|
||||
62
packages/web-stack/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@react-navigation/webstack",
|
||||
"description": "Stack navigator component for Web",
|
||||
"version": "5.0.0",
|
||||
"keywords": [
|
||||
"react",
|
||||
"react-native",
|
||||
"react-component",
|
||||
"react-navigation",
|
||||
"web",
|
||||
"stack"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/web-stack",
|
||||
"bugs": {
|
||||
"url": "https://github.com/react-navigation/react-navigation/issues"
|
||||
},
|
||||
"homepage": "https://reactnavigation.org/docs/web-stack-navigator.html",
|
||||
"main": "lib/commonjs/index.js",
|
||||
"react-native": "src/index.tsx",
|
||||
"module": "lib/module/index.js",
|
||||
"types": "lib/typescript/src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"clean": "del lib"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/bob": "^0.10.0",
|
||||
"@react-navigation/native": "^5.1.6",
|
||||
"@types/react": "^16.9.23",
|
||||
"del-cli": "^3.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-dom": "~16.9.0",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/native": "^5.0.5",
|
||||
"react": "*"
|
||||
},
|
||||
"@react-native-community/bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
23
packages/web-stack/src/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Navigators
|
||||
*/
|
||||
export { default as createWebStackNavigator } from './navigators/createWebStackNavigator';
|
||||
|
||||
/**
|
||||
* Views
|
||||
*/
|
||||
export { default as WebStackView } from './views/Stack/WebStackView';
|
||||
export { default as Header } from './views/Header/Header';
|
||||
export { default as HeaderTitle } from './views/Header/HeaderTitle';
|
||||
export { default as HeaderBackButton } from './views/Header/HeaderBackButton';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export type {
|
||||
WebStackNavigationOptions,
|
||||
WebStackNavigationProp,
|
||||
WebStackHeaderProps,
|
||||
WebStackHeaderLeftButtonProps,
|
||||
WebStackHeaderTitleProps,
|
||||
} from './types';
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useNavigationBuilder,
|
||||
createNavigatorFactory,
|
||||
DefaultNavigatorOptions,
|
||||
EventArg,
|
||||
StackRouter,
|
||||
StackRouterOptions,
|
||||
StackNavigationState,
|
||||
StackActions,
|
||||
} from '@react-navigation/native';
|
||||
import WebStackView from '../views/Stack/WebStackView';
|
||||
import {
|
||||
WebStackNavigationConfig,
|
||||
WebStackNavigationOptions,
|
||||
WebStackNavigationEventMap,
|
||||
} from '../types';
|
||||
|
||||
type Props = DefaultNavigatorOptions<WebStackNavigationOptions> &
|
||||
StackRouterOptions &
|
||||
WebStackNavigationConfig;
|
||||
|
||||
function StackNavigator({
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
StackNavigationState,
|
||||
StackRouterOptions,
|
||||
WebStackNavigationOptions,
|
||||
WebStackNavigationEventMap
|
||||
>(StackRouter, {
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('tabPress', (e) => {
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
// Run the operation in the next frame so we're sure all listeners have been run
|
||||
// This is necessary to know if preventDefault() has been called
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
state.index > 0 &&
|
||||
isFocused &&
|
||||
!(e as EventArg<'tabPress', true>).defaultPrevented
|
||||
) {
|
||||
// When user taps on already focused tab and we're inside the tab,
|
||||
// reset the stack to replicate native behaviour
|
||||
navigation.dispatch({
|
||||
...StackActions.popToTop(),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
});
|
||||
}),
|
||||
[navigation, state.index, state.key]
|
||||
);
|
||||
|
||||
return (
|
||||
<WebStackView
|
||||
{...rest}
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigatorFactory<
|
||||
StackNavigationState,
|
||||
WebStackNavigationOptions,
|
||||
WebStackNavigationEventMap,
|
||||
typeof StackNavigator
|
||||
>(StackNavigator);
|
||||
166
packages/web-stack/src/types.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
Descriptor,
|
||||
RouteProp,
|
||||
NavigationHelpers,
|
||||
StackNavigationState,
|
||||
StackActionHelpers,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
export type WebStackNavigationEventMap = {};
|
||||
|
||||
export type WebStackNavigationHelpers = NavigationHelpers<
|
||||
ParamListBase,
|
||||
WebStackNavigationEventMap
|
||||
>;
|
||||
|
||||
export type WebStackNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
StackNavigationState,
|
||||
WebStackNavigationOptions,
|
||||
WebStackNavigationEventMap
|
||||
> &
|
||||
StackActionHelpers<ParamList>;
|
||||
|
||||
export type WebStackHeaderOptions = {
|
||||
/**
|
||||
* String or a function that returns a React Element to be used by the header.
|
||||
* Defaults to scene `title`.
|
||||
* It receives `allowFontScaling`, `onLayout`, `style` and `children` in the options object as an argument.
|
||||
* The title string is passed in `children`.
|
||||
*/
|
||||
headerTitle?: string | ((props: WebStackHeaderTitleProps) => React.ReactNode);
|
||||
/**
|
||||
* How to align the the header title.
|
||||
* Defaults to `center` on iOS and `left` on Android.
|
||||
*/
|
||||
headerTitleAlign?: 'left' | 'center';
|
||||
/**
|
||||
* Style object for the title component.
|
||||
*/
|
||||
headerTitleStyle?: React.CSSProperties;
|
||||
/**
|
||||
* Tint color for the header.
|
||||
*/
|
||||
headerTintColor?: string;
|
||||
/**
|
||||
* Function which returns a React Element to display on the left side of the header.
|
||||
* It receives a number of arguments when rendered (`onPress`, `label`, `labelStyle` and more.
|
||||
*/
|
||||
headerLeft?: (props: WebStackHeaderLeftButtonProps) => React.ReactNode;
|
||||
/**
|
||||
* Function which returns a React Element to display on the right side of the header.
|
||||
*/
|
||||
headerRight?: (props: { tintColor?: string }) => React.ReactNode;
|
||||
/**
|
||||
* Style object for the header. You can specify a custom background color here, for example.
|
||||
*/
|
||||
headerStyle?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export type WebStackHeaderProps = {
|
||||
/**
|
||||
* Navigation prop for the header.
|
||||
*/
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
/**
|
||||
* Navigation prop for the header.
|
||||
*/
|
||||
navigation: WebStackNavigationProp<ParamListBase>;
|
||||
/**
|
||||
* Descriptors for the header.
|
||||
*/
|
||||
descriptor: WebStackDescriptor;
|
||||
/**
|
||||
* If header should display back button
|
||||
*/
|
||||
canGoBack: boolean;
|
||||
};
|
||||
|
||||
export type WebStackDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
StackNavigationState,
|
||||
WebStackNavigationOptions
|
||||
>;
|
||||
|
||||
export type WebStackDescriptorMap = {
|
||||
[key: string]: WebStackDescriptor;
|
||||
};
|
||||
|
||||
export type WebStackNavigationOptions = WebStackHeaderOptions & {
|
||||
/**
|
||||
* String that can be displayed in the header as a fallback for `headerTitle`.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* Function that given `HeaderProps` returns a React Element to display as a header.
|
||||
*/
|
||||
header?: (props: WebStackHeaderProps) => React.ReactNode;
|
||||
/**
|
||||
* Whether to show the header. The header is shown by default unless `headerMode` was set to `none`.
|
||||
* Setting this to `false` hides the header.
|
||||
*/
|
||||
headerShown?: boolean;
|
||||
/**
|
||||
* Style object for the card in stack.
|
||||
* 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..
|
||||
*/
|
||||
cardStyle?: React.CSSProperties;
|
||||
/**
|
||||
* Whether transition animation should be enabled the screen.
|
||||
* If you set it to `false`, the screen won't animate when pushing or popping. Defaults to `true`.
|
||||
*/
|
||||
animationEnabled?: boolean;
|
||||
/**
|
||||
* The type of animation to use when this screen replaces another screen. Defaults to `push`.
|
||||
* When `pop` is used, the `pop` animation is applied to the screen being replaced.
|
||||
*/
|
||||
animationTypeForReplace?: 'push' | 'pop';
|
||||
};
|
||||
|
||||
export type WebStackNavigationConfig = {};
|
||||
|
||||
export type WebStackHeaderLeftButtonProps = {
|
||||
/**
|
||||
* Whether the button is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Callback to call when the button is clicked.
|
||||
* By default, this triggers `goBack`.
|
||||
*/
|
||||
onClick?: () => void;
|
||||
/**
|
||||
* Style object for the button
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* Whether it's possible to navigate back in stack.
|
||||
*/
|
||||
canGoBack?: boolean;
|
||||
};
|
||||
|
||||
export type WebStackHeaderTitleProps = {
|
||||
/**
|
||||
* Tint color for the header.
|
||||
*/
|
||||
tintColor?: string;
|
||||
/**
|
||||
* Content of the title element. Usually the title string.
|
||||
*/
|
||||
children?: string;
|
||||
/**
|
||||
* Style object for the title element.
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
17
packages/web-stack/src/utils/debounce.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
duration: number
|
||||
): T {
|
||||
let timeout: NodeJS.Timeout | number | undefined;
|
||||
|
||||
return function (this: any, ...args) {
|
||||
if (!timeout) {
|
||||
// eslint-disable-next-line babel/no-invalid-this
|
||||
func.apply(this, args);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
timeout = undefined;
|
||||
}, duration);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
49
packages/web-stack/src/views/Header/Header.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { StackActions } from '@react-navigation/native';
|
||||
|
||||
import HeaderSegment from './HeaderSegment';
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { WebStackHeaderProps, WebStackHeaderTitleProps } from '../../types';
|
||||
|
||||
export default React.memo(function Header({
|
||||
navigation,
|
||||
route,
|
||||
descriptor,
|
||||
}: WebStackHeaderProps) {
|
||||
const { options } = descriptor;
|
||||
const title =
|
||||
typeof options.headerTitle !== 'function' &&
|
||||
options.headerTitle !== undefined
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: route.name;
|
||||
|
||||
const goBack = React.useCallback(
|
||||
debounce(() => {
|
||||
if (navigation.isFocused() && navigation.canGoBack()) {
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
});
|
||||
}
|
||||
}, 50),
|
||||
[navigation, route.key]
|
||||
);
|
||||
|
||||
return (
|
||||
<HeaderSegment
|
||||
{...options}
|
||||
route={route}
|
||||
descriptor={descriptor}
|
||||
title={title}
|
||||
headerTitle={
|
||||
typeof options.headerTitle !== 'function'
|
||||
? (props: WebStackHeaderTitleProps) => <HeaderTitle {...props} />
|
||||
: options.headerTitle
|
||||
}
|
||||
onGoBack={goBack}
|
||||
/>
|
||||
);
|
||||
});
|
||||
30
packages/web-stack/src/views/Header/HeaderBackButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
import { WebStackHeaderLeftButtonProps } from '../../types';
|
||||
|
||||
type Props = WebStackHeaderLeftButtonProps;
|
||||
|
||||
export default function HeaderBackButton({ disabled, onClick, style }: Props) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
/* TODO: styling */
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
style={{
|
||||
color: style?.color !== undefined ? style.color : colors.text,
|
||||
...styles.button,
|
||||
...style,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
Back
|
||||
{/* TODO: SVG image for back button */}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
button: {},
|
||||
};
|
||||
74
packages/web-stack/src/views/Header/HeaderContainer.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NavigationContext,
|
||||
NavigationRouteContext,
|
||||
Route,
|
||||
ParamListBase,
|
||||
RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
import Header from './Header';
|
||||
import { WebStackNavigationProp, WebStackDescriptor } from '../../types';
|
||||
|
||||
export type Props = {
|
||||
scenes: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
descriptor: WebStackDescriptor;
|
||||
}[];
|
||||
getPreviousRoute: (props: {
|
||||
route: Route<string>;
|
||||
}) => Route<string> | undefined;
|
||||
getFocusedRoute: () => Route<string>;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export default function HeaderContainer({
|
||||
scenes,
|
||||
getFocusedRoute,
|
||||
getPreviousRoute,
|
||||
style,
|
||||
}: Props) {
|
||||
const focusedRoute = getFocusedRoute();
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
{scenes.slice(-3).map((scene, i, self) => {
|
||||
if (i !== self.length - 1 || !scene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { options } = scene.descriptor;
|
||||
const isFocused = focusedRoute.key === scene.route.key;
|
||||
const previousRoute = getPreviousRoute({ route: scene.route });
|
||||
|
||||
const props = {
|
||||
canGoBack: !previousRoute,
|
||||
route: scene.route,
|
||||
descriptor: scene.descriptor,
|
||||
navigation: scene.descriptor.navigation as WebStackNavigationProp<
|
||||
ParamListBase
|
||||
>,
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider
|
||||
key={scene.route.key}
|
||||
value={scene.descriptor.navigation}
|
||||
>
|
||||
<NavigationRouteContext.Provider value={scene.route}>
|
||||
<div aria-hidden={isFocused ? true : false}>
|
||||
{options.headerShown !== false ? (
|
||||
options.header !== undefined ? (
|
||||
options.header(props)
|
||||
) : (
|
||||
<Header {...props} />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
packages/web-stack/src/views/Header/HeaderSegment.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import {
|
||||
WebStackHeaderLeftButtonProps,
|
||||
WebStackHeaderTitleProps,
|
||||
WebStackHeaderOptions,
|
||||
WebStackDescriptor,
|
||||
} from '../../types';
|
||||
|
||||
type Props = WebStackHeaderOptions & {
|
||||
headerTitle: (props: WebStackHeaderTitleProps) => React.ReactNode;
|
||||
onGoBack?: () => void;
|
||||
title?: string;
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
descriptor: WebStackDescriptor;
|
||||
};
|
||||
|
||||
export default function HeaderSegment(props: Props) {
|
||||
const {
|
||||
title: currentTitle,
|
||||
onGoBack,
|
||||
headerTitle,
|
||||
headerLeft: left = onGoBack
|
||||
? (props: WebStackHeaderLeftButtonProps) => (
|
||||
<HeaderBackButton {...props} />
|
||||
)
|
||||
: undefined,
|
||||
headerTintColor,
|
||||
headerRight: right,
|
||||
headerTitleStyle,
|
||||
headerStyle,
|
||||
} = props;
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
const leftButton = left
|
||||
? left({
|
||||
onClick: onGoBack,
|
||||
canGoBack: Boolean(onGoBack),
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colors.card,
|
||||
borderBottomColor: colors.border,
|
||||
...styles.header,
|
||||
...headerStyle,
|
||||
}}
|
||||
>
|
||||
{leftButton}
|
||||
{headerTitle({
|
||||
children: currentTitle,
|
||||
style: { marginLeft: 18, marginRight: 16, ...headerTitleStyle },
|
||||
})}
|
||||
{right ? right({ tintColor: headerTintColor }) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
header: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
};
|
||||
26
packages/web-stack/src/views/Header/HeaderTitle.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
|
||||
type Props = JSX.IntrinsicElements['h1'];
|
||||
|
||||
export default function HeaderTitle({ style, ...rest }: Props) {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<h1
|
||||
{...rest}
|
||||
style={{
|
||||
color: style?.color === undefined ? colors.text : style?.color,
|
||||
...styles.title,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = {
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 500,
|
||||
},
|
||||
};
|
||||
91
packages/web-stack/src/views/Stack/Card.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NavigationContext,
|
||||
NavigationRouteContext,
|
||||
Route,
|
||||
RouteProp,
|
||||
ParamListBase,
|
||||
useTheme,
|
||||
} from '@react-navigation/native';
|
||||
import Header from '../Header/Header';
|
||||
import { WebStackDescriptor, WebStackNavigationProp } from '../../types';
|
||||
|
||||
type Props = {
|
||||
active: boolean;
|
||||
focused: boolean;
|
||||
closing: boolean;
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
descriptor: WebStackDescriptor;
|
||||
canGoBack: boolean;
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
onOpenRoute: (props: { route: Route<string> }) => void;
|
||||
onCloseRoute: (props: { route: Route<string> }) => void;
|
||||
};
|
||||
|
||||
function Card({
|
||||
active,
|
||||
closing,
|
||||
focused,
|
||||
canGoBack,
|
||||
onCloseRoute,
|
||||
onOpenRoute,
|
||||
renderScene,
|
||||
route,
|
||||
descriptor,
|
||||
}: Props) {
|
||||
const { colors } = useTheme();
|
||||
const { options, navigation } = descriptor;
|
||||
|
||||
const headerProps = {
|
||||
canGoBack,
|
||||
route,
|
||||
descriptor,
|
||||
navigation: navigation as WebStackNavigationProp<ParamListBase>,
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider key={route.key} value={descriptor.navigation}>
|
||||
<NavigationRouteContext.Provider value={route}>
|
||||
<div
|
||||
aria-hidden={focused ? true : false}
|
||||
style={{
|
||||
opacity: active ? 1 : 0,
|
||||
pointerEvents: focused ? 'auto' : 'none',
|
||||
backgroundColor: colors.background,
|
||||
...styles.container,
|
||||
...options.cardStyle,
|
||||
}}
|
||||
onTransitionEnd={() => {
|
||||
if (closing) {
|
||||
onCloseRoute({ route });
|
||||
} else {
|
||||
onOpenRoute({ route });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options.headerShown !== false ? (
|
||||
options.header !== undefined ? (
|
||||
options.header(headerProps)
|
||||
) : (
|
||||
<Header {...headerProps} />
|
||||
)
|
||||
) : null}
|
||||
{renderScene({ route })}
|
||||
</div>
|
||||
</NavigationRouteContext.Provider>
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Card);
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'absolute' as const,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
transitionDuration: '200ms',
|
||||
},
|
||||
};
|
||||
121
packages/web-stack/src/views/Stack/CardStack.tsx
Executable file
@@ -0,0 +1,121 @@
|
||||
import * as React from 'react';
|
||||
import { Route, StackNavigationState } from '@react-navigation/native';
|
||||
import Card from './Card';
|
||||
import { WebStackDescriptorMap, WebStackDescriptor } from '../../types';
|
||||
|
||||
type Props = {
|
||||
state: StackNavigationState;
|
||||
descriptors: WebStackDescriptorMap;
|
||||
routes: Route<string>[];
|
||||
openingRouteKeys: string[];
|
||||
closingRouteKeys: string[];
|
||||
onOpenRoute: (props: { route: Route<string> }) => void;
|
||||
onCloseRoute: (props: { route: Route<string> }) => void;
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
};
|
||||
|
||||
type State = {
|
||||
routes: Route<string>[];
|
||||
descriptors: WebStackDescriptorMap;
|
||||
scenes: { route: Route<string>; descriptor: WebStackDescriptor }[];
|
||||
};
|
||||
|
||||
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
|
||||
|
||||
export default class CardStack extends React.Component<Props, State> {
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (
|
||||
props.routes === state.routes &&
|
||||
props.descriptors === state.descriptors
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
routes: props.routes,
|
||||
scenes: props.routes.map((route, index, self) => {
|
||||
const previousRoute = self[index - 1];
|
||||
const nextRoute = self[index + 1];
|
||||
|
||||
const oldScene = state.scenes[index];
|
||||
|
||||
const descriptor =
|
||||
props.descriptors[route.key] ||
|
||||
state.descriptors[route.key] ||
|
||||
(oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
|
||||
|
||||
const nextDescriptor =
|
||||
props.descriptors[nextRoute?.key] ||
|
||||
state.descriptors[nextRoute?.key];
|
||||
|
||||
const previousDescriptor =
|
||||
props.descriptors[previousRoute?.key] ||
|
||||
state.descriptors[previousRoute?.key];
|
||||
|
||||
const scene = {
|
||||
route,
|
||||
descriptor,
|
||||
__memo: [route, descriptor, nextDescriptor, previousDescriptor],
|
||||
};
|
||||
|
||||
if (
|
||||
oldScene &&
|
||||
scene.__memo.every((it, i) => {
|
||||
// @ts-ignore
|
||||
return oldScene.__memo[i] === it;
|
||||
})
|
||||
) {
|
||||
return oldScene;
|
||||
}
|
||||
|
||||
return scene;
|
||||
}),
|
||||
descriptors: props.descriptors,
|
||||
};
|
||||
}
|
||||
|
||||
state: State = {
|
||||
routes: [],
|
||||
scenes: [],
|
||||
descriptors: this.props.descriptors,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
state,
|
||||
routes,
|
||||
closingRouteKeys,
|
||||
onOpenRoute,
|
||||
onCloseRoute,
|
||||
renderScene,
|
||||
} = this.props;
|
||||
|
||||
const { scenes } = this.state;
|
||||
|
||||
const focusedRoute = state.routes[state.index];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{routes.map((route, index, self) => {
|
||||
const focused = focusedRoute.key === route.key;
|
||||
const scene = scenes[index];
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={route.key}
|
||||
active={index === self.length - 1}
|
||||
focused={focused}
|
||||
closing={closingRouteKeys.includes(route.key)}
|
||||
route={route as any}
|
||||
descriptor={scene.descriptor}
|
||||
canGoBack={scenes.length > index}
|
||||
renderScene={renderScene}
|
||||
onOpenRoute={onOpenRoute}
|
||||
onCloseRoute={onCloseRoute}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
371
packages/web-stack/src/views/Stack/WebStackView.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
StackActions,
|
||||
StackNavigationState,
|
||||
Route,
|
||||
} from '@react-navigation/native';
|
||||
|
||||
import CardStack from './CardStack';
|
||||
import {
|
||||
WebStackNavigationHelpers,
|
||||
WebStackNavigationConfig,
|
||||
WebStackDescriptorMap,
|
||||
} from '../../types';
|
||||
|
||||
type Props = WebStackNavigationConfig & {
|
||||
state: StackNavigationState;
|
||||
navigation: WebStackNavigationHelpers;
|
||||
descriptors: WebStackDescriptorMap;
|
||||
};
|
||||
|
||||
type State = {
|
||||
// Local copy of the routes which are actually rendered
|
||||
routes: Route<string>[];
|
||||
// Previous routes, to compare whether routes have changed or not
|
||||
previousRoutes: Route<string>[];
|
||||
// Previous descriptors, to compare whether descriptors have changed or not
|
||||
previousDescriptors: WebStackDescriptorMap;
|
||||
// List of routes being opened, we need to animate pushing of these new routes
|
||||
openingRouteKeys: string[];
|
||||
// List of routes being closed, we need to animate popping of these routes
|
||||
closingRouteKeys: string[];
|
||||
// List of routes being replaced, we need to keep a copy until the new route animates in
|
||||
replacingRouteKeys: string[];
|
||||
// Since the local routes can vary from the routes from props, we need to keep the descriptors for old routes
|
||||
// Otherwise we won't be able to access the options for routes that were removed
|
||||
descriptors: WebStackDescriptorMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two arrays with primitive values as the content.
|
||||
* We need to make sure that both values and order match.
|
||||
*/
|
||||
const isArrayEqual = (a: any[], b: any[]) =>
|
||||
a.length === b.length && a.every((it, index) => it === b[index]);
|
||||
|
||||
export default class StackView extends React.Component<Props, State> {
|
||||
static getDerivedStateFromProps(
|
||||
props: Readonly<Props>,
|
||||
state: Readonly<State>
|
||||
) {
|
||||
// If there was no change in routes, we don't need to compute anything
|
||||
if (
|
||||
(props.state.routes === state.previousRoutes ||
|
||||
isArrayEqual(
|
||||
props.state.routes.map((r) => r.key),
|
||||
state.previousRoutes.map((r) => r.key)
|
||||
)) &&
|
||||
state.routes.length
|
||||
) {
|
||||
let routes = state.routes;
|
||||
let previousRoutes = state.previousRoutes;
|
||||
let descriptors = props.descriptors;
|
||||
let previousDescriptors = state.previousDescriptors;
|
||||
|
||||
if (props.descriptors !== state.previousDescriptors) {
|
||||
descriptors = state.routes.reduce<WebStackDescriptorMap>(
|
||||
(acc, route) => {
|
||||
acc[route.key] =
|
||||
props.descriptors[route.key] || state.descriptors[route.key];
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
previousDescriptors = props.descriptors;
|
||||
}
|
||||
|
||||
if (props.state.routes !== state.previousRoutes) {
|
||||
// if any route objects have changed, we should update them
|
||||
const map = props.state.routes.reduce<Record<string, Route<string>>>(
|
||||
(acc, route) => {
|
||||
acc[route.key] = route;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
routes = state.routes.map((route) => map[route.key] || route);
|
||||
previousRoutes = props.state.routes;
|
||||
}
|
||||
|
||||
return {
|
||||
routes,
|
||||
previousRoutes,
|
||||
descriptors,
|
||||
previousDescriptors,
|
||||
};
|
||||
}
|
||||
|
||||
// Here we determine which routes were added or removed to animate them
|
||||
// We keep a copy of the route being removed in local state to be able to animate it
|
||||
|
||||
let routes =
|
||||
props.state.index < props.state.routes.length - 1
|
||||
? // Remove any extra routes from the state
|
||||
// The last visible route should be the focused route, i.e. at current index
|
||||
props.state.routes.slice(0, props.state.index + 1)
|
||||
: props.state.routes;
|
||||
|
||||
// Now we need to determine which routes were added and removed
|
||||
let {
|
||||
openingRouteKeys,
|
||||
closingRouteKeys,
|
||||
replacingRouteKeys,
|
||||
previousRoutes,
|
||||
} = state;
|
||||
|
||||
const previousFocusedRoute = previousRoutes[previousRoutes.length - 1] as
|
||||
| Route<string>
|
||||
| undefined;
|
||||
const nextFocusedRoute = routes[routes.length - 1];
|
||||
|
||||
const isAnimationEnabled = (key: string) => {
|
||||
const descriptor = props.descriptors[key] || state.descriptors[key];
|
||||
|
||||
return descriptor ? descriptor.options.animationEnabled !== false : true;
|
||||
};
|
||||
|
||||
const getAnimationTypeForReplace = (key: string) => {
|
||||
const descriptor = props.descriptors[key] || state.descriptors[key];
|
||||
|
||||
return descriptor.options.animationTypeForReplace ?? 'push';
|
||||
};
|
||||
|
||||
if (
|
||||
previousFocusedRoute &&
|
||||
previousFocusedRoute.key !== nextFocusedRoute.key
|
||||
) {
|
||||
// We only need to animate routes if the focused route changed
|
||||
// Animating previous routes won't be visible coz the focused route is on top of everything
|
||||
|
||||
if (!previousRoutes.some((r) => r.key === nextFocusedRoute.key)) {
|
||||
// A new route has come to the focus, we treat this as a push
|
||||
// A replace can also trigger this, the animation should look like push
|
||||
|
||||
if (
|
||||
isAnimationEnabled(nextFocusedRoute.key) &&
|
||||
!openingRouteKeys.includes(nextFocusedRoute.key)
|
||||
) {
|
||||
// In this case, we need to animate pushing the focused route
|
||||
// We don't care about animating any other added routes because they won't be visible
|
||||
openingRouteKeys = [...openingRouteKeys, nextFocusedRoute.key];
|
||||
|
||||
closingRouteKeys = closingRouteKeys.filter(
|
||||
(key) => key !== nextFocusedRoute.key
|
||||
);
|
||||
replacingRouteKeys = replacingRouteKeys.filter(
|
||||
(key) => key !== nextFocusedRoute.key
|
||||
);
|
||||
|
||||
if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
|
||||
// The previous focused route isn't present in state, we treat this as a replace
|
||||
|
||||
openingRouteKeys = openingRouteKeys.filter(
|
||||
(key) => key !== previousFocusedRoute.key
|
||||
);
|
||||
|
||||
if (getAnimationTypeForReplace(nextFocusedRoute.key) === 'pop') {
|
||||
closingRouteKeys = [
|
||||
...closingRouteKeys,
|
||||
previousFocusedRoute.key,
|
||||
];
|
||||
|
||||
// By default, new routes have a push animation, so we add it to `openingRouteKeys` before
|
||||
// But since user configured it to animate the old screen like a pop, we need to add this without animation
|
||||
// So remove it from `openingRouteKeys` which will remove the animation
|
||||
openingRouteKeys = openingRouteKeys.filter(
|
||||
(key) => key !== nextFocusedRoute.key
|
||||
);
|
||||
|
||||
// Keep the route being removed at the end to animate it out
|
||||
routes = [...routes, previousFocusedRoute];
|
||||
} else {
|
||||
replacingRouteKeys = [
|
||||
...replacingRouteKeys,
|
||||
previousFocusedRoute.key,
|
||||
];
|
||||
|
||||
closingRouteKeys = closingRouteKeys.filter(
|
||||
(key) => key !== previousFocusedRoute.key
|
||||
);
|
||||
|
||||
// Keep the old route in the state because it's visible under the new route, and removing it will feel abrupt
|
||||
// We need to insert it just before the focused one (the route being pushed)
|
||||
// After the push animation is completed, routes being replaced will be removed completely
|
||||
routes = routes.slice();
|
||||
routes.splice(routes.length - 1, 0, previousFocusedRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
|
||||
// The previously focused route was removed, we treat this as a pop
|
||||
|
||||
if (
|
||||
isAnimationEnabled(previousFocusedRoute.key) &&
|
||||
!closingRouteKeys.includes(previousFocusedRoute.key)
|
||||
) {
|
||||
closingRouteKeys = [...closingRouteKeys, previousFocusedRoute.key];
|
||||
|
||||
// Sometimes a route can be closed before the opening animation finishes
|
||||
// So we also need to remove it from the opening list
|
||||
openingRouteKeys = openingRouteKeys.filter(
|
||||
(key) => key !== previousFocusedRoute.key
|
||||
);
|
||||
replacingRouteKeys = replacingRouteKeys.filter(
|
||||
(key) => key !== previousFocusedRoute.key
|
||||
);
|
||||
|
||||
// Keep a copy of route being removed in the state to be able to animate it
|
||||
routes = [...routes, previousFocusedRoute];
|
||||
}
|
||||
} else {
|
||||
// Looks like some routes were re-arranged and no focused routes were added/removed
|
||||
// i.e. the currently focused route already existed and the previously focused route still exists
|
||||
// We don't know how to animate this
|
||||
}
|
||||
} else if (replacingRouteKeys.length || closingRouteKeys.length) {
|
||||
// Keep the routes we are closing or replacing if animation is enabled for them
|
||||
routes = routes.slice();
|
||||
routes.splice(
|
||||
routes.length - 1,
|
||||
0,
|
||||
...state.routes.filter(({ key }) =>
|
||||
isAnimationEnabled(key)
|
||||
? replacingRouteKeys.includes(key) || closingRouteKeys.includes(key)
|
||||
: false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!routes.length) {
|
||||
throw new Error(
|
||||
'There should always be at least one route in the navigation state.'
|
||||
);
|
||||
}
|
||||
|
||||
const descriptors = routes.reduce<WebStackDescriptorMap>((acc, route) => {
|
||||
acc[route.key] =
|
||||
props.descriptors[route.key] || state.descriptors[route.key];
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
routes,
|
||||
previousRoutes: props.state.routes,
|
||||
previousDescriptors: props.descriptors,
|
||||
openingRouteKeys,
|
||||
closingRouteKeys,
|
||||
replacingRouteKeys,
|
||||
descriptors,
|
||||
};
|
||||
}
|
||||
|
||||
state: State = {
|
||||
routes: [],
|
||||
previousRoutes: [],
|
||||
previousDescriptors: {},
|
||||
openingRouteKeys: [],
|
||||
closingRouteKeys: [],
|
||||
replacingRouteKeys: [],
|
||||
descriptors: {},
|
||||
};
|
||||
|
||||
private renderScene = ({ route }: { route: Route<string> }) => {
|
||||
const descriptor =
|
||||
this.state.descriptors[route.key] || this.props.descriptors[route.key];
|
||||
|
||||
if (!descriptor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return descriptor.render();
|
||||
};
|
||||
|
||||
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
if (
|
||||
this.state.replacingRouteKeys.every((key) => key !== route.key) &&
|
||||
state.routeNames.includes(route.name) &&
|
||||
!state.routes.some((r) => r.key === route.key)
|
||||
) {
|
||||
// If route isn't present in current state, assume that a close animation was cancelled
|
||||
// So we need to add this route back to the state
|
||||
navigation.navigate(route);
|
||||
} else {
|
||||
this.setState((state) => ({
|
||||
routes: state.replacingRouteKeys.length
|
||||
? state.routes.filter(
|
||||
(r) => !state.replacingRouteKeys.includes(r.key)
|
||||
)
|
||||
: state.routes,
|
||||
openingRouteKeys: state.openingRouteKeys.filter(
|
||||
(key) => key !== route.key
|
||||
),
|
||||
closingRouteKeys: state.closingRouteKeys.filter(
|
||||
(key) => key !== route.key
|
||||
),
|
||||
replacingRouteKeys: [],
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
private handleCloseRoute = ({ route }: { route: Route<string> }) => {
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
if (state.routes.some((r) => r.key === route.key)) {
|
||||
// If a route exists in state, trigger a pop
|
||||
// This will happen in when the route was closed from the card component
|
||||
// e.g. When the close animation triggered from a gesture ends
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
target: state.key,
|
||||
});
|
||||
} else {
|
||||
// We need to clean up any state tracking the route and pop it immediately
|
||||
this.setState((state) => ({
|
||||
routes: state.routes.filter((r) => r.key !== route.key),
|
||||
openingRouteKeys: state.openingRouteKeys.filter(
|
||||
(key) => key !== route.key
|
||||
),
|
||||
closingRouteKeys: state.closingRouteKeys.filter(
|
||||
(key) => key !== route.key
|
||||
),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
state,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
navigation,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
routes,
|
||||
descriptors,
|
||||
openingRouteKeys,
|
||||
closingRouteKeys,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<CardStack
|
||||
routes={routes}
|
||||
openingRouteKeys={openingRouteKeys}
|
||||
closingRouteKeys={closingRouteKeys}
|
||||
onOpenRoute={this.handleOpenRoute}
|
||||
onCloseRoute={this.handleCloseRoute}
|
||||
renderScene={this.renderScene}
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
packages/web-stack/src/views/assets/back-icon-mask.png
Normal file
|
After Width: | Height: | Size: 913 B |
BIN
packages/web-stack/src/views/assets/back-icon.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
packages/web-stack/src/views/assets/back-icon@1.5x.android.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
packages/web-stack/src/views/assets/back-icon@1.5x.ios.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
packages/web-stack/src/views/assets/back-icon@1x.android.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
packages/web-stack/src/views/assets/back-icon@1x.ios.png
Normal file
|
After Width: | Height: | Size: 290 B |
BIN
packages/web-stack/src/views/assets/back-icon@2x.android.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
packages/web-stack/src/views/assets/back-icon@2x.ios.png
Normal file
|
After Width: | Height: | Size: 405 B |
BIN
packages/web-stack/src/views/assets/back-icon@3x.android.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
packages/web-stack/src/views/assets/back-icon@3x.ios.png
Normal file
|
After Width: | Height: | Size: 761 B |
BIN
packages/web-stack/src/views/assets/back-icon@4x.android.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
packages/web-stack/src/views/assets/back-icon@4x.ios.png
Normal file
|
After Width: | Height: | Size: 812 B |
6
packages/web-stack/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
11
packages/web-stack/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../routers" },
|
||||
{ "path": "../native" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
}
|
||||
}
|
||||