mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-19 18:38:16 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed9ee4d5e5 | ||
|
|
36f9788e85 | ||
|
|
712ec9bab1 | ||
|
|
786456b645 | ||
|
|
3d7a62490c | ||
|
|
445e4d95b8 | ||
|
|
9c12052199 | ||
|
|
994c2c0828 | ||
|
|
c27a197ddb | ||
|
|
07afa55265 | ||
|
|
3ac5f412b7 | ||
|
|
70a2c3b97c | ||
|
|
4bd6f17b46 | ||
|
|
9824e90b9f | ||
|
|
eae992467b | ||
|
|
6b4d92ca4d | ||
|
|
41d3c97cea | ||
|
|
ab3e053338 | ||
|
|
b14262c2ef | ||
|
|
03d9133a7d | ||
|
|
d0835351bd | ||
|
|
f892526e7b | ||
|
|
1afdb799fc | ||
|
|
83d36dcf7c | ||
|
|
aa94038190 | ||
|
|
0b698ae5d6 | ||
|
|
dd3ce66120 | ||
|
|
82754d41d9 | ||
|
|
9d54ec68dd | ||
|
|
460754fde1 | ||
|
|
ffd1865485 | ||
|
|
50320bf0d9 | ||
|
|
74a04c3ce5 | ||
|
|
54d0d5180d | ||
|
|
14eb5a1e75 | ||
|
|
222c77a360 | ||
|
|
39316fc339 | ||
|
|
27eb73cc14 | ||
|
|
f01b4896e6 | ||
|
|
556c31626e | ||
|
|
b6bca3ed2e | ||
|
|
0c56b21b46 | ||
|
|
912c7ca076 | ||
|
|
73c76f1e4b | ||
|
|
d746a587b0 | ||
|
|
dee03c839a | ||
|
|
2104bf1a04 | ||
|
|
4e2a409dca | ||
|
|
51bfe8dd19 | ||
|
|
04a4512c1b |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -25,7 +25,7 @@ Bugs with react-navigation must be reproducible *without any external libraries
|
||||
|
||||
### How to reproduce
|
||||
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repoistory as that is outside of the scope of React Navigation.
|
||||
- You must provide a way to reproduce the problem. If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation.
|
||||
- Either re-create the bug on [Snack](https://snack.expo.io) or link to a GitHub repository with code that reproduces the bug.
|
||||
- Explain how to run the example app and any steps that we need to take to reproduce the issue from the example app.
|
||||
|
||||
|
||||
145
CHANGELOG.md
145
CHANGELOG.md
@@ -7,6 +7,132 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [2.18.3] - [2018-11-26](https://github.com/react-navigation/react-navigation/releases/tag/2.18.3)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support React.forwardRef on createStackNavigator
|
||||
|
||||
## [2.18.2] - [2018-10-26](https://github.com/react-navigation/react-navigation/releases/tag/2.18.2)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert "Backport fix for child navigation object caching" due to edge case with transitioner
|
||||
|
||||
## [2.18.1] - [2018-10-23](https://github.com/react-navigation/react-navigation/releases/tag/2.18.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Backport fix for child navigation object caching
|
||||
|
||||
## [2.18.0] - [2018-10-11](https://github.com/react-navigation/react-navigation/releases/tag/2.18.0)
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce getActiveChildNavigationOptions (#5080)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-tabs to 0.8.4 to fix issue with Snack
|
||||
- Flow changes:
|
||||
- Update StackViewConfig to match recent changes (#5067)
|
||||
- Mark key in StackActions.replace as optional (#5073)
|
||||
- Remove drawer actions from react-navigation-web
|
||||
- Add disableRouteNamePaths option to router configs (#4824)
|
||||
|
||||
## [2.17.0] - [2018-09-25](https://github.com/react-navigation/react-navigation/releases/tag/2.17.0)
|
||||
|
||||
### Changed
|
||||
|
||||
- Add `dangerouslyGetParent()` to flow typings
|
||||
- Update react-navigation-stack to 0.7.0
|
||||
- Add transparentCard option to fix cards with a transparent bg when using rn-screens
|
||||
- Add window dimensions for iPhone XS Max and iPhone XR
|
||||
- Vendor clamp
|
||||
- Add overflow hidden to stack container
|
||||
- Completion on mount: StackView is responsible for calling the navigation completion action when `state.isTransitioning` is set. This fix handles that case when the stack is first mounting.
|
||||
|
||||
## [2.16.0] - [2018-09-19](https://github.com/react-navigation/react-navigation/releases/tag/2.16.0)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-stack to 0.6.0 to make react-native-screens a peerDependency.
|
||||
- Updated react-navigation-tabs to 0.8.2 to make react-native-screens a peerDependency and add support for it in bottom tab navigator.
|
||||
- Make react-native-screens a direct dependency of react-navigation.
|
||||
|
||||
## [2.15.0] - [2018-09-19](https://github.com/react-navigation/react-navigation/releases/tag/2.15.0)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-safe-area-view to 0.11.0 to support iPhoneXS Max and iPhoneXR.
|
||||
|
||||
## [2.14.2] - [2018-09-14](https://github.com/react-navigation/react-navigation/releases/tag/2.14.2)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-stack to 0.5.1 to clamp interpolated values in animations.
|
||||
|
||||
## [2.14.1] - [2018-09-14](https://github.com/react-navigation/react-navigation/releases/tag/2.14.1)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-stack to 0.5.0 to solve black screen on back and unpressable header area with hidden header when using react-native-screens.
|
||||
|
||||
## [2.14.0] - [2018-09-12](https://github.com/react-navigation/react-navigation/releases/tag/2.14.0)
|
||||
|
||||
### Added
|
||||
|
||||
- Updated react-navigation-stack to add experimental support for react-native-screens. See https://github.com/kmagiera/react-native-screens for information about how to enable it.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-native-safe-area-view to 0.10.0 to solve circular dependency issue (fixes https://github.com/react-navigation/react-navigation/issues/4973)
|
||||
|
||||
## [2.13.0] - [2018-09-06](https://github.com/react-navigation/react-navigation/releases/tag/2.13.0)
|
||||
|
||||
### Added
|
||||
|
||||
- When `tabBarIcon` is a function it is now provided with a `horizontal` option that indicates whether horizontal tabs are being rendered (label to the right of the icon) or not.
|
||||
- Add some missing flow types ([1](https://github.com/react-navigation/react-navigation/pull/4836), [2](https://github.com/react-navigation/react-navigation/pull/4917)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated react-navigation-stack to 0.3.0.
|
||||
- Updated react-navigation-tabs to 0.7.0.
|
||||
- Pinned `create-react-context` dependency to `0.2.2` (https://github.com/react-navigation/react-navigation/issues/4934)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixes tab label font sizes in landscape and portrait.
|
||||
- Default tab bar background color and header background color are white on iOS.
|
||||
|
||||
## [2.12.1] - [2018-08-23](https://github.com/react-navigation/react-navigation/releases/tag/2.12.1)
|
||||
|
||||
### Fixed
|
||||
- Fix crash on react-native@>=0.56 described in https://github.com/react-navigation/react-navigation/issues/4886
|
||||
|
||||
## [2.12.0] - [2018-08-22](https://github.com/react-navigation/react-navigation/releases/tag/2.12.0)
|
||||
|
||||
### Changed
|
||||
- Move stack specific view code to react-navigation-stack
|
||||
- Add accessibility props for inactive screens in stack (https://github.com/react-navigation/react-navigation-stack/commit/4e04428e26df9076413b57b3346a7ce357de1a77)
|
||||
- Updated header title to match iOS 11/12 style correctly (https://github.com/react-navigation/react-navigation-stack/pull/1)
|
||||
- Add support for animating the header background on screen transitions and add interpolator to animate it along with the rest of the screen, but this is still opt-in behavior (https://github.com/react-navigation/react-navigation-stack/pull/3)
|
||||
- Updated react-native-safe-area-view to 0.9.0
|
||||
|
||||
## [2.11.2] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.2)
|
||||
### Changed
|
||||
- Revert rename of pathUtils
|
||||
|
||||
## [2.11.1] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.1)
|
||||
### Changed
|
||||
- Fix some exports related to the 2.11.0 changes to move stack navigator out of core
|
||||
|
||||
## [2.11.0] - [2018-08-03](https://github.com/react-navigation/react-navigation/releases/tag/2.11.0)
|
||||
### Added
|
||||
- Export some modules that are useful for moving stack navigator outside of core
|
||||
|
||||
## [2.10.0] - [2018-08-02](https://github.com/react-navigation/react-navigation/releases/tag/2.10.0)
|
||||
### Added
|
||||
- `lazy` and `optimizationsEnabled` options to `createMaterialTopTabNavigator` (react-navigation-tabs@0.6.0)
|
||||
@@ -24,7 +150,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
- Export `StackViewTransitionConfigs` to allow you to extend default config in custom transition configs. [#4761](https://github.com/react-navigation/react-navigation/pull/4761)
|
||||
|
||||
### Fixed
|
||||
- Error when building with haul: ref to pathToRegexp.compile(#4658).
|
||||
- Error when building with haul: ref to pathToRegexp.compile. [#4658](https://github.com/react-navigation/react-navigation/pull/4658).
|
||||
|
||||
## [2.9.1] - [2018-07-24](https://github.com/react-navigation/react-navigation/releases/tag/2.9.1)
|
||||
@@ -127,7 +252,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
### Changed
|
||||
- Improved examples
|
||||
|
||||
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.10.0...HEAD
|
||||
[Unreleased]: https://github.com/react-navigation/react-navigation/compare/2.18.3...HEAD
|
||||
[2.18.3]: https://github.com/react-navigation/react-navigation/compare/2.18.2...2.18.3
|
||||
[2.18.2]: https://github.com/react-navigation/react-navigation/compare/2.18.1...2.18.2
|
||||
[2.18.1]: https://github.com/react-navigation/react-navigation/compare/2.18.0...2.18.1
|
||||
[2.18.0]: https://github.com/react-navigation/react-navigation/compare/2.17.0...2.18.0
|
||||
[2.17.0]: https://github.com/react-navigation/react-navigation/compare/2.16.0...2.17.0
|
||||
[2.16.0]: https://github.com/react-navigation/react-navigation/compare/2.15.0...2.16.0
|
||||
[2.15.0]: https://github.com/react-navigation/react-navigation/compare/2.14.2...2.15.0
|
||||
[2.14.2]: https://github.com/react-navigation/react-navigation/compare/2.14.1...2.14.2
|
||||
[2.14.1]: https://github.com/react-navigation/react-navigation/compare/2.14.0...2.14.1
|
||||
[2.14.0]: https://github.com/react-navigation/react-navigation/compare/2.13.1...2.14.0
|
||||
[2.13.0]: https://github.com/react-navigation/react-navigation/compare/2.12.1...2.13.0
|
||||
[2.12.1]: https://github.com/react-navigation/react-navigation/compare/2.12.0...2.12.1
|
||||
[2.12.0]: https://github.com/react-navigation/react-navigation/compare/2.11.2...2.12.0
|
||||
[2.11.2]: https://github.com/react-navigation/react-navigation/compare/2.11.1...2.11.2
|
||||
[2.11.1]: https://github.com/react-navigation/react-navigation/compare/2.11.0...2.11.1
|
||||
[2.11.0]: https://github.com/react-navigation/react-navigation/compare/2.10.0...2.11.0
|
||||
[2.10.0]: https://github.com/react-navigation/react-navigation/compare/2.9.3...2.10.0
|
||||
[2.9.3]: https://github.com/react-navigation/react-navigation/compare/2.9.2...2.9.3
|
||||
[2.9.2]: https://github.com/react-navigation/react-navigation/compare/2.9.1...2.9.2
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
import { useScreens } from 'react-native-screens';
|
||||
|
||||
// Uncomment this to use react-native-screens
|
||||
// useScreens();
|
||||
|
||||
import App from './js/App';
|
||||
export default App;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"splash": {
|
||||
"image": "./assets/icons/splash.png"
|
||||
},
|
||||
"sdkVersion": "28.0.0",
|
||||
"sdkVersion": "30.0.0",
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
|
||||
@@ -34,6 +34,7 @@ import SimpleStack from './SimpleStack';
|
||||
import StackWithHeaderPreset from './StackWithHeaderPreset';
|
||||
import StackWithTranslucentHeader from './StackWithTranslucentHeader';
|
||||
import SimpleTabs from './SimpleTabs';
|
||||
import CustomTabUI from './CustomTabUI';
|
||||
import SwitchWithStacks from './SwitchWithStacks';
|
||||
import TabsWithNavigationFocus from './TabsWithNavigationFocus';
|
||||
import TabsWithNavigationEvents from './TabsWithNavigationEvents';
|
||||
@@ -137,6 +138,10 @@ const ExampleInfo = {
|
||||
description:
|
||||
'Demo automatic handling of keyboard showing/hiding inside StackNavigator',
|
||||
},
|
||||
CustomTabUI: {
|
||||
name: 'Custom Tabs UI',
|
||||
description: 'Render additional views around a Tab navigator',
|
||||
},
|
||||
};
|
||||
|
||||
const ExampleRoutes = {
|
||||
@@ -161,6 +166,7 @@ const ExampleRoutes = {
|
||||
ModalStack: ModalStack,
|
||||
StacksWithKeys: StacksWithKeys,
|
||||
StacksInTabs: StacksInTabs,
|
||||
CustomTabUI: CustomTabUI,
|
||||
StacksOverTabs: StacksOverTabs,
|
||||
StacksOverTopTabs: StacksOverTopTabs,
|
||||
LinkStack: {
|
||||
|
||||
133
examples/NavigationPlayground/js/CustomTabUI.js
Normal file
133
examples/NavigationPlayground/js/CustomTabUI.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
LayoutAnimation,
|
||||
View,
|
||||
StyleSheet,
|
||||
StatusBar,
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import {
|
||||
SafeAreaView,
|
||||
createMaterialTopTabNavigator,
|
||||
createNavigationContainer,
|
||||
} from 'react-navigation';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import { Button } from './commonComponents/ButtonWithMargin';
|
||||
|
||||
class MyHomeScreen extends React.Component {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
<Text>Home Screen</Text>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Home')}
|
||||
title="Go to home tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReccomendedScreen extends React.Component {
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Reccomended',
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-people' : 'ios-people-outline'}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
};
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
<Text>Reccomended Screen</Text>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Home')}
|
||||
title="Go to home tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeaturedScreen extends React.Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
tabBarLabel: 'Featured',
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-star' : 'ios-star-outline'}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
});
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
return (
|
||||
<SafeAreaView forceInset={{ horizontal: 'always', top: 'always' }}>
|
||||
<Text>Featured Screen</Text>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Home')}
|
||||
title="Go to home tab"
|
||||
/>
|
||||
<Button onPress={() => navigation.goBack(null)} title="Go back" />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SimpleTabs = createMaterialTopTabNavigator({
|
||||
Home: MyHomeScreen,
|
||||
Reccomended: ReccomendedScreen,
|
||||
Featured: FeaturedScreen,
|
||||
});
|
||||
|
||||
class TabNavigator extends React.Component {
|
||||
static router = SimpleTabs.router;
|
||||
componentWillUpdate() {
|
||||
LayoutAnimation.easeInEaseOut();
|
||||
}
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const { routes, index } = navigation.state;
|
||||
const activeRoute = routes[index];
|
||||
let bottom = null;
|
||||
if (activeRoute.routeName !== 'Home') {
|
||||
bottom = (
|
||||
<View style={{ height: 50, borderTopWidth: StyleSheet.hairlineWidth }}>
|
||||
<Button title="Check out" onPress={() => {}} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StatusBar barStyle="default" />
|
||||
<SafeAreaView
|
||||
style={{ flex: 1 }}
|
||||
forceInset={{ horizontal: 'always', top: 'always' }}
|
||||
>
|
||||
<SimpleTabs navigation={navigation} />
|
||||
</SafeAreaView>
|
||||
{bottom}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TabNavigator;
|
||||
@@ -40,10 +40,10 @@ MyHomeScreen.navigationOptions = {
|
||||
accessibilityLabel: 'TEST_ID_HOME_ACLBL',
|
||||
},
|
||||
tabBarLabel: 'Home',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-home' : 'ios-home-outline'}
|
||||
size={26}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
@@ -60,10 +60,10 @@ class MyPeopleScreen extends React.Component<MyPeopleScreenProps> {
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'People',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-people' : 'ios-people-outline'}
|
||||
size={26}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
@@ -100,10 +100,10 @@ class MyChatScreen extends React.Component<MyChatScreenProps> {
|
||||
|
||||
static navigationOptions = {
|
||||
tabBarLabel: 'Chat',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-chatboxes' : 'ios-chatboxes-outline'}
|
||||
size={26}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
@@ -135,10 +135,10 @@ const MySettingsScreen = ({ navigation }) => (
|
||||
|
||||
MySettingsScreen.navigationOptions = {
|
||||
tabBarLabel: 'Settings',
|
||||
tabBarIcon: ({ tintColor, focused }) => (
|
||||
tabBarIcon: ({ tintColor, focused, horizontal }) => (
|
||||
<Ionicons
|
||||
name={focused ? 'ios-settings' : 'ios-settings-outline'}
|
||||
size={26}
|
||||
size={horizontal ? 20 : 26}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { Header, createStackNavigator } from 'react-navigation';
|
||||
import {
|
||||
Header,
|
||||
HeaderStyleInterpolator,
|
||||
createStackNavigator,
|
||||
} from 'react-navigation';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import SampleText from './SampleText';
|
||||
@@ -230,6 +234,14 @@ const StackWithTranslucentHeader = createStackNavigator(
|
||||
},
|
||||
{
|
||||
headerTransitionPreset: 'uikit',
|
||||
|
||||
// You can leave this out if you don't want the card shadow to
|
||||
// be visible through the header
|
||||
transitionConfig: () => ({
|
||||
headerBackgroundInterpolator:
|
||||
HeaderStyleInterpolator.forBackgroundWithTranslation,
|
||||
}),
|
||||
|
||||
navigationOptions: {
|
||||
headerTransparent: true,
|
||||
headerStyle: {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SafeAreaView,
|
||||
createStackNavigator,
|
||||
createBottomTabNavigator,
|
||||
getActiveChildNavigationOptions,
|
||||
} from 'react-navigation';
|
||||
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
@@ -94,16 +95,10 @@ const TabNav = createBottomTabNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
TabNav.navigationOptions = ({ navigation }) => {
|
||||
let { routeName } = navigation.state.routes[navigation.state.index];
|
||||
let title;
|
||||
if (routeName === 'SettingsTab') {
|
||||
title = 'Settings';
|
||||
} else if (routeName === 'MainTab') {
|
||||
title = 'Home';
|
||||
}
|
||||
TabNav.navigationOptions = ({ navigation, screenProps }) => {
|
||||
const childOptions = getActiveChildNavigationOptions(navigation, screenProps);
|
||||
return {
|
||||
title,
|
||||
title: childOptions.title,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -13,31 +13,19 @@ const TabsInDrawer = createDrawerNavigator({
|
||||
SimpleTabs: {
|
||||
screen: SimpleTabs,
|
||||
navigationOptions: {
|
||||
drawer: () => ({
|
||||
label: 'Simple Tabs',
|
||||
icon: ({ tintColor }) => (
|
||||
<MaterialIcons
|
||||
name="filter-1"
|
||||
size={24}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
drawerLabel: 'Simple tabs',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons name="filter-1" size={24} style={{ color: tintColor }} />
|
||||
),
|
||||
},
|
||||
},
|
||||
StacksOverTabs: {
|
||||
screen: StacksOverTabs,
|
||||
navigationOptions: {
|
||||
drawer: () => ({
|
||||
label: 'Stacks Over Tabs',
|
||||
icon: ({ tintColor }) => (
|
||||
<MaterialIcons
|
||||
name="filter-2"
|
||||
size={24}
|
||||
style={{ color: tintColor }}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
drawerLabel: 'Stacks Over Tabs',
|
||||
drawerIcon: ({ tintColor }) => (
|
||||
<MaterialIcons name="filter-2" size={24} style={{ color: tintColor }} />
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "node_modules/expo/AppEntry.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"test": "flow"
|
||||
"test": "flow",
|
||||
"postinstall": "rm -rf node_modules/react-native-screens"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^28.0.0",
|
||||
"expo": "^30.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"react": "16.3.1",
|
||||
"react-native": "^0.55.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
27
flow/react-navigation.js
vendored
27
flow/react-navigation.js
vendored
@@ -143,6 +143,14 @@ declare module 'react-navigation' {
|
||||
+type: 'Navigation/TOGGLE_DRAWER',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationDrawerOpenedAction = {|
|
||||
+type: 'Navigation/DRAWER_OPENED',
|
||||
+key?: string,
|
||||
|};
|
||||
declare export type NavigationDrawerClosedAction = {|
|
||||
+type: 'Navigation/DRAWER_CLOSED',
|
||||
+key?: string,
|
||||
|};
|
||||
|
||||
declare export type NavigationAction =
|
||||
| NavigationBackAction
|
||||
@@ -157,7 +165,9 @@ declare module 'react-navigation' {
|
||||
| NavigationCompleteTransitionAction
|
||||
| NavigationOpenDrawerAction
|
||||
| NavigationCloseDrawerAction
|
||||
| NavigationToggleDrawerAction;
|
||||
| NavigationToggleDrawerAction
|
||||
| NavigationDrawerOpenedAction
|
||||
| NavigationDrawerClosedAction;
|
||||
|
||||
/**
|
||||
* NavigationState is a tree of routes for a single navigator, where each
|
||||
@@ -394,10 +404,18 @@ declare module 'react-navigation' {
|
||||
mode?: 'card' | 'modal',
|
||||
headerMode?: HeaderMode,
|
||||
headerTransitionPreset?: 'fade-in-place' | 'uikit',
|
||||
headerLayoutPreset?: 'left' | 'center',
|
||||
headerBackTitleVisible?: boolean,
|
||||
cardStyle?: ViewStyleProp,
|
||||
transitionConfig?: () => TransitionConfig,
|
||||
transitionConfig?: (
|
||||
transitionProps: NavigationTransitionProps,
|
||||
prevTransitionProps: ?NavigationTransitionProps,
|
||||
isModal: boolean
|
||||
) => TransitionConfig,
|
||||
onTransitionStart?: () => void,
|
||||
onTransitionEnd?: () => void,
|
||||
transparentCard?: boolean,
|
||||
disableKeyboardHandling?: boolean,
|
||||
|};
|
||||
|
||||
declare export type StackNavigatorConfig = {|
|
||||
@@ -516,6 +534,7 @@ declare module 'react-navigation' {
|
||||
callback: NavigationEventCallback
|
||||
) => NavigationEventSubscription,
|
||||
getParam: (paramName: string, fallback?: any) => any,
|
||||
dangerouslyGetParent: () => NavigationScreenProp<*>,
|
||||
isFocused: () => boolean,
|
||||
// Shared action creators that exist for all routers
|
||||
goBack: (routeKey?: ?string) => boolean,
|
||||
@@ -799,7 +818,7 @@ declare module 'react-navigation' {
|
||||
actions: Array<NavigationNavigateAction>,
|
||||
}) => NavigationResetAction,
|
||||
replace: (payload: {
|
||||
key: string,
|
||||
key?: string,
|
||||
routeName: string,
|
||||
params?: NavigationParams,
|
||||
action?: NavigationNavigateAction,
|
||||
@@ -813,6 +832,8 @@ declare module 'react-navigation' {
|
||||
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
|
||||
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
|
||||
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
|
||||
DRAWER_OPENED: 'Navigation/DRAWER_OPENED',
|
||||
DRAWER_CLOSED: 'Navigation/DRAWER_CLOSED',
|
||||
|
||||
openDrawer: (payload: {
|
||||
key?: string,
|
||||
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-navigation",
|
||||
"version": "2.10.0",
|
||||
"version": "2.18.3",
|
||||
"description": "Routing and navigation for your React Native apps",
|
||||
"main": "src/react-navigation.js",
|
||||
"repository": {
|
||||
@@ -31,15 +31,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"clamp": "^1.0.1",
|
||||
"create-react-context": "^0.2.1",
|
||||
"create-react-context": "0.2.2",
|
||||
"hoist-non-react-statics": "^2.2.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"query-string": "^6.1.0",
|
||||
"react-is": "^16.5.2",
|
||||
"react-lifecycles-compat": "^3",
|
||||
"react-native-safe-area-view": "^0.8.0",
|
||||
"react-native-safe-area-view": "0.11.0",
|
||||
"react-native-screens": "^1.0.0-alpha.11",
|
||||
"react-navigation-deprecated-tab-navigator": "1.3.0",
|
||||
"react-navigation-drawer": "0.5.0",
|
||||
"react-navigation-tabs": "0.6.0"
|
||||
"react-navigation-stack": "0.7.0",
|
||||
"react-navigation-tabs": "0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
@@ -60,7 +63,7 @@
|
||||
"lint-staged": "^4.2.1",
|
||||
"prettier": "^1.12.1",
|
||||
"prettier-eslint": "^8.8.1",
|
||||
"react": "16.2.0",
|
||||
"react": "16.5.2",
|
||||
"react-native": "^0.52.0",
|
||||
"react-native-vector-icons": "^4.2.0",
|
||||
"react-test-renderer": "^16.0.0"
|
||||
@@ -90,7 +93,7 @@
|
||||
"<rootDir>/examples/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator)"
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|react-navigation-stack)"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import NavigationActions from '../NavigationActions';
|
||||
import createStackNavigator from '../navigators/createStackNavigator';
|
||||
|
||||
// TODO: we should create a dummy navigator here
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
import createNavigationContainer, {
|
||||
_TESTING_ONLY_reset_container_count,
|
||||
} from '../createNavigationContainer';
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import StackNavigator from '../createContainedStackNavigator';
|
||||
|
||||
const SubNavigator = StackNavigator({
|
||||
Home: {
|
||||
screen: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedDirect = StackNavigator({
|
||||
Sub: {
|
||||
screen: SubNavigator,
|
||||
},
|
||||
});
|
||||
|
||||
const NavNestedIndirect = StackNavigator({
|
||||
Sub: {
|
||||
// eslint-disable-next-line react/display-name
|
||||
screen: props => <SubNavigator {...props} />,
|
||||
},
|
||||
});
|
||||
|
||||
/* Prevent React error boundaries from swallowing the errors */
|
||||
NavNestedIndirect.prototype.componentDidCatch = null;
|
||||
SubNavigator.prototype.componentDidCatch = null;
|
||||
|
||||
describe('Nested navigators', () => {
|
||||
it('renders succesfully as direct child', () => {
|
||||
const rendered = renderer.create(<NavNestedDirect />).toJSON();
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('throw when trying to pass navigation prop', () => {
|
||||
const tryRender = () => {
|
||||
renderer.create(<NavNestedIndirect />);
|
||||
};
|
||||
expect(tryRender).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import StackNavigator from '../createContainedStackNavigator';
|
||||
import withNavigation from '../../views/withNavigation';
|
||||
import { _TESTING_ONLY_reset_container_count } from '../../createNavigationContainer';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
class HomeScreen extends Component {
|
||||
static navigationOptions = ({ navigation }) => ({
|
||||
title: `Welcome ${
|
||||
navigation.state.params ? navigation.state.params.user : 'anonymous'
|
||||
}`,
|
||||
gesturesEnabled: true,
|
||||
headerStyle: [{ backgroundColor: 'red' }, styles.header],
|
||||
});
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const routeConfig = {
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
},
|
||||
};
|
||||
|
||||
describe('StackNavigator', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
it('renders successfully', () => {
|
||||
const MyStackNavigator = StackNavigator(routeConfig);
|
||||
const rendered = renderer.create(<MyStackNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('applies correct values when headerRight is present', () => {
|
||||
const MyStackNavigator = StackNavigator({
|
||||
Home: {
|
||||
screen: HomeScreen,
|
||||
navigationOptions: {
|
||||
headerRight: <View />,
|
||||
},
|
||||
},
|
||||
});
|
||||
const rendered = renderer.create(<MyStackNavigator />).toJSON();
|
||||
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('passes navigation to headerRight when wrapped in withNavigation', () => {
|
||||
const spy = jest.fn();
|
||||
|
||||
class TestComponent extends React.Component {
|
||||
render() {
|
||||
return <View>{this.props.onPress(this.props.navigation)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
const TestComponentWithNavigation = withNavigation(TestComponent);
|
||||
|
||||
class A extends React.Component {
|
||||
static navigationOptions = {
|
||||
headerRight: <TestComponentWithNavigation onPress={spy} />,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const Nav = StackNavigator({ A: { screen: A } });
|
||||
|
||||
renderer.create(<Nav />);
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
navigate: expect.any(Function),
|
||||
addListener: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Nested navigators renders succesfully as direct child 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#F7F7F7",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`Nested navigators throw when trying to pass navigation prop 1`] = `"No \\"routes\\" found in navigation state. Did you try to pass the navigation prop of a React component to a Navigator child? See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop"`;
|
||||
@@ -1,391 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StackNavigator applies correct values when headerRight is present 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 70,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 70,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`StackNavigator renders successfully 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
onMoveShouldSetResponder={[Function]}
|
||||
onMoveShouldSetResponderCapture={[Function]}
|
||||
onResponderEnd={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderReject={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderStart={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
onStartShouldSetResponderCapture={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "column-reverse",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#000",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="auto"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#E9E9EF",
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"marginTop": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"shadowColor": "black",
|
||||
"shadowOffset": Object {
|
||||
"height": 0,
|
||||
"width": 0,
|
||||
},
|
||||
"shadowOpacity": 0.2,
|
||||
"shadowRadius": 5,
|
||||
"top": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
Object {
|
||||
"translateY": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateX": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
onLayout={[Function]}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "red",
|
||||
"borderBottomColor": "#A7A7AA",
|
||||
"borderBottomWidth": 0.5,
|
||||
"height": 64,
|
||||
"opacity": 0.5,
|
||||
"paddingBottom": 0,
|
||||
"paddingLeft": 0,
|
||||
"paddingRight": 0,
|
||||
"paddingTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
collapsable={undefined}
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"bottom": 0,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"left": 0,
|
||||
"opacity": 1,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityTraits="header"
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
collapsable={undefined}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(0, 0, 0, .9)",
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"marginHorizontal": 16,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Welcome anonymous
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createStackNavigator from './createStackNavigator';
|
||||
|
||||
const StackNavigator = (routeConfigs, config = {}) => {
|
||||
const navigator = createStackNavigator(routeConfigs, config);
|
||||
return createNavigationContainer(navigator);
|
||||
};
|
||||
|
||||
export default StackNavigator;
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import getChildEventSubscriber from '../getChildEventSubscriber';
|
||||
|
||||
function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
class Navigator extends React.Component {
|
||||
static router = router;
|
||||
@@ -16,7 +14,7 @@ function createNavigator(NavigatorView, router, navigationConfig) {
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const prevDescriptors = prevState.descriptors;
|
||||
const { navigation, screenProps } = nextProps;
|
||||
const { dispatch, state, addListener } = navigation;
|
||||
const { state } = navigation;
|
||||
const { routes } = state;
|
||||
if (typeof routes === 'undefined') {
|
||||
throw new TypeError(
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import createKeyboardAwareNavigator from './createKeyboardAwareNavigator';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
const {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
disableKeyboardHandling,
|
||||
getCustomActionCreators,
|
||||
} = stackConfig;
|
||||
|
||||
const stackRouterConfig = {
|
||||
initialRouteKey,
|
||||
initialRouteName,
|
||||
initialRouteParams,
|
||||
paths,
|
||||
navigationOptions,
|
||||
getCustomActionCreators,
|
||||
};
|
||||
|
||||
const router = StackRouter(routeConfigMap, stackRouterConfig);
|
||||
|
||||
// Create a navigator with StackView as the view
|
||||
let Navigator = createNavigator(StackView, router, stackConfig);
|
||||
if (!disableKeyboardHandling) {
|
||||
Navigator = createKeyboardAwareNavigator(Navigator, stackConfig);
|
||||
}
|
||||
|
||||
return Navigator;
|
||||
}
|
||||
|
||||
export default createStackNavigator;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import createNavigator from '../navigators/createNavigator';
|
||||
import SwitchRouter from '../routers/SwitchRouter';
|
||||
import SwitchView from '../views/SwitchView/SwitchView';
|
||||
|
||||
47
src/react-navigation.js
vendored
47
src/react-navigation.js
vendored
@@ -16,14 +16,23 @@ module.exports = {
|
||||
get createNavigator() {
|
||||
return require('./navigators/createNavigator').default;
|
||||
},
|
||||
get createKeyboardAwareNavigator() {
|
||||
return require('./navigators/createKeyboardAwareNavigator').default;
|
||||
},
|
||||
get NavigationProvider() {
|
||||
return require('./views/NavigationContext').default.NavigationProvider;
|
||||
},
|
||||
get NavigationConsumer() {
|
||||
return require('./views/NavigationContext').default.NavigationConsumer;
|
||||
},
|
||||
get createStackNavigator() {
|
||||
return require('./navigators/createContainedStackNavigator').default;
|
||||
return require('react-navigation-stack').createStackNavigator;
|
||||
},
|
||||
get StackNavigator() {
|
||||
console.warn(
|
||||
'The StackNavigator function name is deprecated, please use createStackNavigator instead'
|
||||
);
|
||||
return require('./navigators/createContainedStackNavigator').default;
|
||||
return require('react-navigation-stack').createStackNavigator;
|
||||
},
|
||||
get createSwitchNavigator() {
|
||||
return require('./navigators/createContainedSwitchNavigator').default;
|
||||
@@ -88,19 +97,36 @@ module.exports = {
|
||||
get SwitchRouter() {
|
||||
return require('./routers/SwitchRouter').default;
|
||||
},
|
||||
get createConfigGetter() {
|
||||
return require('./routers/createConfigGetter').default;
|
||||
},
|
||||
get getScreenForRouteName() {
|
||||
return require('./routers/getScreenForRouteName').default;
|
||||
},
|
||||
get validateRouteConfigMap() {
|
||||
return require('./routers/validateRouteConfigMap').default;
|
||||
},
|
||||
|
||||
// Utils
|
||||
get getActiveChildNavigationOptions() {
|
||||
return require('./utils/getActiveChildNavigationOptions').default;
|
||||
},
|
||||
get pathUtils() {
|
||||
return require('./routers/pathUtils').default;
|
||||
},
|
||||
|
||||
// Views
|
||||
get Transitioner() {
|
||||
return require('./views/Transitioner').default;
|
||||
return require('react-navigation-stack').Transitioner;
|
||||
},
|
||||
get StackView() {
|
||||
return require('./views/StackView/StackView').default;
|
||||
return require('react-navigation-stack').StackView;
|
||||
},
|
||||
get StackViewCard() {
|
||||
return require('./views/StackView/StackViewCard').default;
|
||||
return require('react-navigation-stack').StackViewCard;
|
||||
},
|
||||
get StackViewTransitionConfigs() {
|
||||
return require('./views/StackView/StackViewTransitionConfigs').default;
|
||||
return require('react-navigation-stack').StackViewTransitionConfigs;
|
||||
},
|
||||
get SafeAreaView() {
|
||||
return require('react-native-safe-area-view').default;
|
||||
@@ -114,13 +140,16 @@ module.exports = {
|
||||
|
||||
// Header
|
||||
get Header() {
|
||||
return require('./views/Header/Header').default;
|
||||
return require('react-navigation-stack').Header;
|
||||
},
|
||||
get HeaderTitle() {
|
||||
return require('./views/Header/HeaderTitle').default;
|
||||
return require('react-navigation-stack').HeaderTitle;
|
||||
},
|
||||
get HeaderBackButton() {
|
||||
return require('./views/Header/HeaderBackButton').default;
|
||||
return require('react-navigation-stack').HeaderBackButton;
|
||||
},
|
||||
get HeaderStyleInterpolator() {
|
||||
return require('react-navigation-stack').HeaderStyleInterpolator;
|
||||
},
|
||||
|
||||
// DrawerView
|
||||
|
||||
@@ -27,9 +27,6 @@ module.exports = {
|
||||
get StackActions() {
|
||||
return require('./routers/StackActions').default;
|
||||
},
|
||||
get DrawerActions() {
|
||||
return require('./routers/DrawerActions').default;
|
||||
},
|
||||
|
||||
// Routers
|
||||
get StackRouter() {
|
||||
|
||||
@@ -107,7 +107,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
const {
|
||||
getPathAndParamsForRoute,
|
||||
getActionForPathAndParams,
|
||||
} = createPathParser(childRouters, routeConfigs, stackConfig.paths);
|
||||
} = createPathParser(childRouters, routeConfigs, stackConfig);
|
||||
|
||||
return {
|
||||
childRouters,
|
||||
@@ -543,8 +543,10 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
state,
|
||||
childRoute.key,
|
||||
route,
|
||||
// the following tells replaceAt to NOT change the index to this route for the setParam action, because people don't expect param-setting actions to switch the active route
|
||||
action.type === NavigationActions.SET_PARAMS
|
||||
// the following tells replaceAt to NOT change the index to this route for the setParam action or complete transition action,
|
||||
// because people don't expect these actions to switch the active route
|
||||
action.type === NavigationActions.SET_PARAMS ||
|
||||
action.type === StackActions.COMPLETE_TRANSITION
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export default (routeConfigs, config = {}) => {
|
||||
const {
|
||||
getPathAndParamsForRoute,
|
||||
getActionForPathAndParams,
|
||||
} = createPathParser(childRouters, routeConfigs, config.paths);
|
||||
} = createPathParser(childRouters, routeConfigs, config);
|
||||
|
||||
if (initialRouteIndex === -1) {
|
||||
throw new Error(
|
||||
|
||||
@@ -4,9 +4,9 @@ import React from 'react';
|
||||
|
||||
import SwitchRouter from '../SwitchRouter';
|
||||
import StackRouter from '../StackRouter';
|
||||
import TabRouter from '../TabRouter';
|
||||
import StackActions from '../StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import { urlToPathAndParams } from '../pathUtils';
|
||||
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -442,16 +442,6 @@ const performRouterTest = createTestRouter => {
|
||||
});
|
||||
|
||||
test('URI encoded path param gets parsed and correctly printed', () => {
|
||||
const router = createTestRouter({
|
||||
main: {
|
||||
screen: () => <div />,
|
||||
},
|
||||
person: {
|
||||
path: 'people/:name',
|
||||
screen: () => <div />,
|
||||
},
|
||||
});
|
||||
|
||||
const action = testRouter.getActionForPathAndParams('people/Henry%20L');
|
||||
expect(action).toEqual({
|
||||
routeName: 'person',
|
||||
@@ -594,3 +584,39 @@ test('Handles nested switch routers', () => {
|
||||
expect(action.action.type).toEqual(NavigationActions.NAVIGATE);
|
||||
expect(action.action.routeName).toEqual('B');
|
||||
});
|
||||
|
||||
const performRouteNameAsPathDisabledTest = createTestRouter => {
|
||||
const BScreen = () => <div />;
|
||||
const NestedNavigator = () => <div />;
|
||||
NestedNavigator.router = createTestRouter({
|
||||
B: {
|
||||
screen: BScreen,
|
||||
path: 'baz',
|
||||
},
|
||||
});
|
||||
const router = createTestRouter(
|
||||
{
|
||||
A: NestedNavigator,
|
||||
},
|
||||
{ disableRouteNamePaths: true }
|
||||
);
|
||||
|
||||
test('disableRouteNamePaths option on router prevent the default path to be the routeName', () => {
|
||||
const action = router.getActionForPathAndParams('baz', {});
|
||||
|
||||
expect(action.routeName).toBe('A');
|
||||
expect(action.action.routeName).toBe('B');
|
||||
});
|
||||
};
|
||||
|
||||
describe('Stack router handles disableRouteNamePaths', () => {
|
||||
performRouteNameAsPathDisabledTest(StackRouter);
|
||||
});
|
||||
|
||||
describe('Switch router handles disableRouteNamePaths', () => {
|
||||
performRouteNameAsPathDisabledTest(SwitchRouter);
|
||||
});
|
||||
|
||||
describe('Tab router handles disableRouteNamePaths', () => {
|
||||
performRouteNameAsPathDisabledTest(TabRouter);
|
||||
});
|
||||
|
||||
@@ -492,7 +492,7 @@ describe('StackRouter', () => {
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.POP_TO_TOP,
|
||||
@@ -520,7 +520,7 @@ describe('StackRouter', () => {
|
||||
},
|
||||
state
|
||||
);
|
||||
const barKey = state2.routes[1].routes[0].key;
|
||||
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: StackActions.POP_TO_TOP,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React from 'react';
|
||||
import TabRouter from '../TabRouter';
|
||||
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
|
||||
const INIT_ACTION = { type: NavigationActions.INIT };
|
||||
|
||||
@@ -12,6 +12,10 @@ ProfileNavigator.router = StackRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const ScreenWithForwardRef = React.forwardRef((props, ref) => (
|
||||
<div ref={ref} />
|
||||
));
|
||||
|
||||
describe('validateRouteConfigMap', () => {
|
||||
test('Fails on empty bare screen', () => {
|
||||
const invalidMap = {
|
||||
@@ -57,4 +61,10 @@ describe('validateRouteConfigMap', () => {
|
||||
};
|
||||
validateRouteConfigMap(validMap);
|
||||
});
|
||||
test('Succeeds on React.forwardRef', () => {
|
||||
const validMap = {
|
||||
Chat: ScreenWithForwardRef,
|
||||
};
|
||||
validateRouteConfigMap(validMap);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export default (routeConfigs, navigatorScreenConfig) => (
|
||||
navigation,
|
||||
screenProps
|
||||
) => {
|
||||
const { state, dispatch } = navigation;
|
||||
const { state } = navigation;
|
||||
const route = state;
|
||||
|
||||
invariant(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isValidElementType } from 'react-is';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ export default function getScreenForRouteName(routeConfigs, routeName) {
|
||||
if (typeof routeConfig.getScreen === 'function') {
|
||||
const screen = routeConfig.getScreen();
|
||||
invariant(
|
||||
typeof screen === 'function',
|
||||
isValidElementType(screen),
|
||||
`The getScreen defined for route '${routeName} didn't return a valid ` +
|
||||
'screen or navigator.\n\n' +
|
||||
'Please pass it like this:\n' +
|
||||
|
||||
@@ -67,7 +67,7 @@ export const urlToPathAndParams = (url, uriPrefix) => {
|
||||
export const createPathParser = (
|
||||
childRouters,
|
||||
routeConfigs,
|
||||
pathConfigs = {}
|
||||
{ paths: pathConfigs = {}, disableRouteNamePaths }
|
||||
) => {
|
||||
const pathsByRouteNames = {};
|
||||
let paths = [];
|
||||
@@ -84,8 +84,8 @@ export const createPathParser = (
|
||||
}
|
||||
|
||||
if (pathPattern === undefined) {
|
||||
// If the user hasn't specified a path at all, then we assume the routeName is an appropriate path
|
||||
pathPattern = routeName;
|
||||
// If the user hasn't specified a path at all nor disableRouteNamePaths, then we assume the routeName is an appropriate path
|
||||
pathPattern = disableRouteNamePaths ? null : routeName;
|
||||
}
|
||||
|
||||
invariant(
|
||||
@@ -216,3 +216,8 @@ export const createPathParser = (
|
||||
};
|
||||
return { getActionForPathAndParams, getPathAndParamsForRoute };
|
||||
};
|
||||
|
||||
export default {
|
||||
getParamsFromPath,
|
||||
createPathParser,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isValidElementType } from 'react-is';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
/**
|
||||
@@ -17,9 +18,7 @@ function validateRouteConfigMap(routeConfigs) {
|
||||
|
||||
if (
|
||||
!screenComponent ||
|
||||
(typeof screenComponent !== 'function' &&
|
||||
typeof screenComponent !== 'string' &&
|
||||
!routeConfig.getScreen)
|
||||
(!isValidElementType(screenComponent) && !routeConfig.getScreen)
|
||||
) {
|
||||
throw new Error(`The component for route '${routeName}' must be a React component. For example:
|
||||
|
||||
|
||||
9
src/utils/getActiveChildNavigationOptions.js
Normal file
9
src/utils/getActiveChildNavigationOptions.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const getActiveChildNavigationOptions = (navigation, screenProps) => {
|
||||
const { state, router, getChildNavigation } = navigation;
|
||||
const activeRoute = state.routes[state.index];
|
||||
const activeNavigation = getChildNavigation(activeRoute.key);
|
||||
const options = router.getScreenOptions(activeNavigation, screenProps);
|
||||
return options;
|
||||
};
|
||||
|
||||
export default getActiveChildNavigationOptions;
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Animated } from 'react-native';
|
||||
|
||||
export default class AnimatedValueSubscription {
|
||||
constructor(value, callback) {
|
||||
this._value = value;
|
||||
|
||||
@@ -1,705 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Image,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
I18nManager,
|
||||
ViewPropTypes,
|
||||
MaskedViewIOS,
|
||||
} from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import ModularHeaderBackButton from './ModularHeaderBackButton';
|
||||
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
|
||||
import withOrientation from '../withOrientation';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
|
||||
// These can be adjusted by using headerTitleContainerStyle on navigationOptions
|
||||
const TITLE_OFFSET_CENTER_ALIGN = Platform.OS === 'ios' ? 70 : 56;
|
||||
const TITLE_OFFSET_LEFT_ALIGN = Platform.OS === 'ios' ? 20 : 56;
|
||||
|
||||
const getTitleOffsets = (
|
||||
layoutPreset,
|
||||
forceBackTitle,
|
||||
hasLeftComponent,
|
||||
hasRightComponent
|
||||
) => {
|
||||
if (layoutPreset === 'left') {
|
||||
// Maybe at some point we should do something different if the back title is
|
||||
// explicitly enabled, for now people can control it manually
|
||||
|
||||
let style = {
|
||||
left: TITLE_OFFSET_LEFT_ALIGN,
|
||||
right: TITLE_OFFSET_LEFT_ALIGN,
|
||||
};
|
||||
|
||||
if (!hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return style;
|
||||
} else if (layoutPreset === 'center') {
|
||||
let style = {
|
||||
left: TITLE_OFFSET_CENTER_ALIGN,
|
||||
right: TITLE_OFFSET_CENTER_ALIGN,
|
||||
};
|
||||
if (!hasLeftComponent && !hasRightComponent) {
|
||||
style.left = 0;
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
};
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
? isLandscape && !Platform.isPad
|
||||
? 32
|
||||
: 44
|
||||
: 56;
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
layoutInterpolator: HeaderStyleInterpolator.forLayout,
|
||||
leftInterpolator: HeaderStyleInterpolator.forLeft,
|
||||
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
|
||||
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
|
||||
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
|
||||
titleInterpolator: HeaderStyleInterpolator.forCenter,
|
||||
rightInterpolator: HeaderStyleInterpolator.forRight,
|
||||
};
|
||||
|
||||
static get HEIGHT() {
|
||||
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
}
|
||||
|
||||
state = {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_getHeaderTitleString(scene) {
|
||||
const options = scene.descriptor.options;
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
}
|
||||
|
||||
if (options.title && typeof options.title !== 'string' && __DEV__) {
|
||||
throw new Error(
|
||||
`Invalid title for route "${
|
||||
scene.route.routeName
|
||||
}" - title must be string or null, instead it was of type ${typeof options.title}`
|
||||
);
|
||||
}
|
||||
|
||||
return options.title;
|
||||
}
|
||||
|
||||
_getLastScene(scene) {
|
||||
return this.props.scenes.find(s => s.index === scene.index - 1);
|
||||
}
|
||||
|
||||
_getBackButtonTitleString(scene) {
|
||||
const lastScene = this._getLastScene(scene);
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
const { headerBackTitle } = lastScene.descriptor.options;
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
return this._getHeaderTitleString(lastScene);
|
||||
}
|
||||
|
||||
_getTruncatedBackButtonTitle(scene) {
|
||||
const lastScene = this._getLastScene(scene);
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
return lastScene.descriptor.options.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const { layoutPreset } = this.props;
|
||||
const { options } = props.scene.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.scene);
|
||||
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
|
||||
// When title is centered, the width of left/right components depends on the
|
||||
// calculated size of the title.
|
||||
const onLayout =
|
||||
layoutPreset === 'center'
|
||||
? e => {
|
||||
this.setState({
|
||||
widths: {
|
||||
...this.state.widths,
|
||||
[props.scene.key]: e.nativeEvent.layout.width,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const HeaderTitleComponent =
|
||||
headerTitle && typeof headerTitle !== 'string'
|
||||
? headerTitle
|
||||
: HeaderTitle;
|
||||
return (
|
||||
<HeaderTitleComponent
|
||||
onLayout={onLayout}
|
||||
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
|
||||
style={[
|
||||
color ? { color } : null,
|
||||
layoutPreset === 'center'
|
||||
? { textAlign: 'center' }
|
||||
: { textAlign: 'left' },
|
||||
titleStyle,
|
||||
]}
|
||||
>
|
||||
{titleString}
|
||||
</HeaderTitleComponent>
|
||||
);
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return options.headerLeft;
|
||||
}
|
||||
|
||||
if (!options.headerLeft && props.scene.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
);
|
||||
const width = this.state.widths[props.scene.key]
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
props.scene.descriptor.navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<RenderedLeftComponent
|
||||
onPress={goBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
backTitleVisible={this.props.backTitleVisible}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
layoutPreset={this.props.layoutPreset}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderModularLeftComponent = (
|
||||
props,
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options, navigation } = props.scene.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
);
|
||||
const width = this.state.widths[props.scene.key]
|
||||
? (this.props.layout.initWidth - this.state.widths[props.scene.key]) / 2
|
||||
: undefined;
|
||||
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
navigation.goBack(props.scene.descriptor.key);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModularHeaderBackButton
|
||||
onPress={goBack}
|
||||
ButtonContainerComponent={ButtonContainerComponent}
|
||||
LabelContainerComponent={LabelContainerComponent}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
backImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
const { headerRight } = props.scene.descriptor.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
let { style } = props;
|
||||
if (options.headerLeftContainerStyle) {
|
||||
style = [style, options.headerLeftContainerStyle];
|
||||
}
|
||||
|
||||
// On Android, or if we have a custom header left, or if we have a custom back image, we
|
||||
// do not use the modular header (which is the one that imitates UINavigationController)
|
||||
if (
|
||||
transitionPreset !== 'uikit' ||
|
||||
options.headerBackImage ||
|
||||
options.headerLeft ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderLeftComponent,
|
||||
this.props.leftInterpolator
|
||||
);
|
||||
} else {
|
||||
return this._renderModularSubView(
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderModularLeftComponent,
|
||||
this.props.leftLabelInterpolator,
|
||||
this.props.leftButtonInterpolator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_renderTitle(props, options) {
|
||||
const { layoutPreset, transitionPreset } = this.props;
|
||||
let style = [
|
||||
{ justifyContent: layoutPreset === 'center' ? 'center' : 'flex-start' },
|
||||
getTitleOffsets(
|
||||
layoutPreset,
|
||||
false,
|
||||
options.hasLeftComponent,
|
||||
options.hasRightComponent
|
||||
),
|
||||
options.headerTitleContainerStyle,
|
||||
];
|
||||
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
'title',
|
||||
this._renderTitleComponent,
|
||||
transitionPreset === 'uikit'
|
||||
? this.props.titleFromLeftInterpolator
|
||||
: this.props.titleInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderRight(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
let { style } = props;
|
||||
if (options.headerRightContainerStyle) {
|
||||
style = [style, options.headerRightContainerStyle];
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
'right',
|
||||
this._renderRightComponent,
|
||||
this.props.rightInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderModularSubView(
|
||||
props,
|
||||
name,
|
||||
renderer,
|
||||
labelStyleInterpolator,
|
||||
buttonStyleInterpolator
|
||||
) {
|
||||
const { scene } = props;
|
||||
const { index, isStale, key } = scene;
|
||||
|
||||
// Never render a modular back button on the first screen in a stack.
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = this.props.navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const ButtonContainer = ({ children }) => (
|
||||
<Animated.View
|
||||
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const LabelContainer = ({ children }) => (
|
||||
<Animated.View
|
||||
style={[labelStyleInterpolator({ ...this.props, ...props })]}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const subView = renderer(props, ButtonContainer, LabelContainer);
|
||||
|
||||
if (subView === null) {
|
||||
return subView;
|
||||
}
|
||||
|
||||
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${name}_${key}`}
|
||||
pointerEvents={pointerEvents}
|
||||
style={[styles.item, styles[name], props.style]}
|
||||
>
|
||||
{subView}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSubView(props, name, renderer, styleInterpolator) {
|
||||
const { scene } = props;
|
||||
const { index, isStale, key } = scene;
|
||||
|
||||
const offset = this.props.navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const subView = renderer(props);
|
||||
|
||||
if (subView == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pointerEvents = offset !== 0 || isStale ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
key={`${name}_${key}`}
|
||||
style={[
|
||||
styles.item,
|
||||
styles[name],
|
||||
props.style,
|
||||
styleInterpolator({
|
||||
...this.props,
|
||||
...props,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{subView}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderHeader(props) {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (options.header === null) {
|
||||
return null;
|
||||
}
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
hasLeftComponent: !!left,
|
||||
hasRightComponent: !!right,
|
||||
headerTitleContainerStyle: options.headerTitleContainerStyle,
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
key: `scene_${props.scene.key}`,
|
||||
};
|
||||
|
||||
if (
|
||||
options.headerLeft ||
|
||||
options.headerBackImage ||
|
||||
Platform.OS !== 'ios' ||
|
||||
transitionPreset !== 'uikit'
|
||||
) {
|
||||
return (
|
||||
<View {...wrapperProps}>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MaskedViewIOS
|
||||
{...wrapperProps}
|
||||
maskElement={
|
||||
<View style={styles.iconMaskContainer}>
|
||||
<Image
|
||||
source={require('../assets/back-icon-mask.png')}
|
||||
style={styles.iconMask}
|
||||
/>
|
||||
<View style={styles.iconMaskFillerRect} />
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</MaskedViewIOS>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let appBar;
|
||||
const { mode, scene, isLandscape } = this.props;
|
||||
|
||||
if (mode === 'float') {
|
||||
const scenesByIndex = {};
|
||||
this.props.scenes.forEach(scene => {
|
||||
scenesByIndex[scene.index] = scene;
|
||||
});
|
||||
const scenesProps = Object.values(scenesByIndex).map(scene => ({
|
||||
position: this.props.position,
|
||||
progress: this.props.progress,
|
||||
scene,
|
||||
}));
|
||||
appBar = scenesProps.map(this._renderHeader, this);
|
||||
} else {
|
||||
appBar = this._renderHeader({
|
||||
position: new Animated.Value(this.props.scene.index),
|
||||
progress: new Animated.Value(0),
|
||||
scene: this.props.scene,
|
||||
});
|
||||
}
|
||||
|
||||
const { options } = scene.descriptor;
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
|
||||
const {
|
||||
alignItems,
|
||||
justifyContent,
|
||||
flex,
|
||||
flexDirection,
|
||||
flexGrow,
|
||||
flexShrink,
|
||||
flexBasis,
|
||||
flexWrap,
|
||||
position,
|
||||
padding,
|
||||
paddingHorizontal,
|
||||
paddingRight,
|
||||
paddingLeft,
|
||||
// paddingVertical,
|
||||
// paddingTop,
|
||||
// paddingBottom,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
...safeHeaderStyle
|
||||
} = headerStyleObj;
|
||||
|
||||
if (__DEV__) {
|
||||
warnIfHeaderStyleDefined(alignItems, 'alignItems');
|
||||
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
|
||||
warnIfHeaderStyleDefined(flex, 'flex');
|
||||
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
|
||||
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
|
||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
||||
warnIfHeaderStyleDefined(padding, 'padding');
|
||||
warnIfHeaderStyleDefined(position, 'position');
|
||||
warnIfHeaderStyleDefined(paddingHorizontal, 'paddingHorizontal');
|
||||
warnIfHeaderStyleDefined(paddingRight, 'paddingRight');
|
||||
warnIfHeaderStyleDefined(paddingLeft, 'paddingLeft');
|
||||
// warnIfHeaderStyleDefined(paddingVertical, 'paddingVertical');
|
||||
// warnIfHeaderStyleDefined(paddingTop, 'paddingTop');
|
||||
// warnIfHeaderStyleDefined(paddingBottom, 'paddingBottom');
|
||||
warnIfHeaderStyleDefined(top, 'top');
|
||||
warnIfHeaderStyleDefined(right, 'right');
|
||||
warnIfHeaderStyleDefined(bottom, 'bottom');
|
||||
warnIfHeaderStyleDefined(left, 'left');
|
||||
}
|
||||
|
||||
// TODO: warn if any unsafe styles are provided
|
||||
const containerStyles = [
|
||||
options.headerTransparent
|
||||
? styles.transparentContainer
|
||||
: styles.container,
|
||||
{ height: appBarHeight },
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
this.props.layoutInterpolator(this.props),
|
||||
Platform.OS === 'ios' && !options.headerTransparent
|
||||
? {
|
||||
backgroundColor:
|
||||
safeHeaderStyle.backgroundColor || DEFAULT_BACKGROUND_COLOR,
|
||||
}
|
||||
: null,
|
||||
]}
|
||||
>
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
{options.headerBackground}
|
||||
</View>
|
||||
<View style={styles.flexOne}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
||||
if (styleProp === 'position' && value === 'absolute') {
|
||||
console.warn(
|
||||
"position: 'absolute' is not supported on headerStyle. If you would like to render content under the header, use the headerTransparent navigationOption."
|
||||
);
|
||||
} else if (value !== undefined) {
|
||||
console.warn(
|
||||
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let platformContainerStyles;
|
||||
if (Platform.OS === 'ios') {
|
||||
platformContainerStyles = {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#A7A7AA',
|
||||
};
|
||||
} else {
|
||||
platformContainerStyles = {
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
shadowOffset: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
elevation: 4,
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
||||
...platformContainerStyles,
|
||||
},
|
||||
transparentContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...platformContainerStyles,
|
||||
borderBottomWidth: 0,
|
||||
borderBottomColor: 'transparent',
|
||||
elevation: 0,
|
||||
},
|
||||
header: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
item: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
iconMaskContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconMaskFillerRect: {
|
||||
flex: 1,
|
||||
backgroundColor: '#d8d8d8',
|
||||
marginLeft: -3,
|
||||
},
|
||||
iconMask: {
|
||||
// These are mostly the same as the icon in ModularHeaderBackButton
|
||||
height: 21,
|
||||
width: 12,
|
||||
marginLeft: 9,
|
||||
marginTop: -0.5, // resizes down to 20.5
|
||||
alignSelf: 'center',
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
left: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
right: {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexOne: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(Header);
|
||||
@@ -1,175 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
I18nManager,
|
||||
Image,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
const defaultBackImage = require('../assets/back-icon.png');
|
||||
|
||||
class HeaderBackButton extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
pressColorAndroid: 'rgba(0, 0, 0, .32)',
|
||||
tintColor: Platform.select({
|
||||
ios: '#037aff',
|
||||
}),
|
||||
truncatedTitle: 'Back',
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
_onTextLayout = e => {
|
||||
if (this.state.initialTextWidth) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
const { backImage, title, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
|
||||
return <BackImage {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onPress, pressColorAndroid, layoutPreset, title } = this.props;
|
||||
|
||||
let button = (
|
||||
<TouchableItem
|
||||
accessibilityComponentType="button"
|
||||
accessibilityLabel={title}
|
||||
accessibilityTraits="button"
|
||||
testID="header-back"
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
pressColor={pressColorAndroid}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{this._renderBackImage()}
|
||||
{this._maybeRenderTitle()}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return <View style={styles.androidButtonWrapper}>{button}</View>;
|
||||
} else {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
_maybeRenderTitle() {
|
||||
const {
|
||||
layoutPreset,
|
||||
backTitleVisible,
|
||||
width,
|
||||
title,
|
||||
titleStyle,
|
||||
tintColor,
|
||||
truncatedTitle,
|
||||
} = this.props;
|
||||
|
||||
const renderTruncated =
|
||||
this.state.initialTextWidth && width
|
||||
? this.state.initialTextWidth > width
|
||||
: false;
|
||||
|
||||
const backButtonTitle = renderTruncated ? truncatedTitle : title;
|
||||
|
||||
// If the left preset is used and we aren't on Android, then we
|
||||
// default to disabling the label
|
||||
const titleDefaultsToDisabled =
|
||||
layoutPreset === 'left' ||
|
||||
Platform.OS === 'android' ||
|
||||
typeof backButtonTitle !== 'string';
|
||||
|
||||
// If the title is explicitly enabled then we respect that
|
||||
if (titleDefaultsToDisabled && !backTitleVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
onLayout={this._onTextLayout}
|
||||
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{backButtonTitle}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
androidButtonWrapper: {
|
||||
margin: 13,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
title: {
|
||||
fontSize: 17,
|
||||
paddingRight: 10,
|
||||
},
|
||||
icon:
|
||||
Platform.OS === 'ios'
|
||||
? {
|
||||
height: 21,
|
||||
width: 13,
|
||||
marginLeft: 9,
|
||||
marginRight: 22,
|
||||
marginVertical: 12,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
}
|
||||
: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
margin: 3,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
iconWithTitle:
|
||||
Platform.OS === 'ios'
|
||||
? {
|
||||
marginRight: 6,
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
export default HeaderBackButton;
|
||||
@@ -1,336 +0,0 @@
|
||||
import { Dimensions, I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
|
||||
function hasHeader(scene) {
|
||||
if (!scene) {
|
||||
return true;
|
||||
}
|
||||
const { descriptor } = scene;
|
||||
return descriptor.options.header !== null;
|
||||
}
|
||||
|
||||
const crossFadeInterpolation = (scenes, first, index, last) => ({
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.9,
|
||||
index - 0.2,
|
||||
index,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0.3 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Utilities that build the style for the navigation header.
|
||||
*
|
||||
* +-------------+-------------+-------------+
|
||||
* | | | |
|
||||
* | Left | Title | Right |
|
||||
* | Component | Component | Component |
|
||||
* | | | |
|
||||
* +-------------+-------------+-------------+
|
||||
*/
|
||||
|
||||
function isGoingBack(scenes) {
|
||||
const lastSceneIndexInScenes = scenes.length - 1;
|
||||
return !scenes[lastSceneIndexInScenes].isActive;
|
||||
}
|
||||
|
||||
function forLayout(props) {
|
||||
const { layout, position, scene, scenes, mode } = props;
|
||||
if (mode !== 'float') {
|
||||
return {};
|
||||
}
|
||||
const isBack = isGoingBack(scenes);
|
||||
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
if (!interpolate) return {};
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
// We really shouldn't render the scene at all until we know the width of the
|
||||
// stack. That said, in every case that I have ever seen, this has just been
|
||||
// the full width of the window. This won't continue to be true if we support
|
||||
// layouts like iPad master-detail. For now, in order to solve
|
||||
// https://github.com/react-navigation/react-navigation/issues/4264, I have
|
||||
// opted for the heuristic that we will use the window width until we have
|
||||
// measured (and they will usually be the same).
|
||||
const width = layout.initWidth || Dimensions.get('window').width;
|
||||
|
||||
// Make sure the header stays hidden when transitioning between 2 screens
|
||||
// with no header.
|
||||
if (
|
||||
(isBack && !hasHeader(scenes[index]) && !hasHeader(scenes[last])) ||
|
||||
(!isBack && !hasHeader(scenes[first]) && !hasHeader(scenes[index]))
|
||||
) {
|
||||
return {
|
||||
transform: [{ translateX: width }],
|
||||
};
|
||||
}
|
||||
|
||||
const rtlMult = I18nManager.isRTL ? -1 : 1;
|
||||
const translateX = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [
|
||||
rtlMult * (hasHeader(scenes[first]) ? 0 : width),
|
||||
rtlMult * (hasHeader(scenes[index]) ? 0 : isBack ? width : -width),
|
||||
rtlMult * (hasHeader(scenes[last]) ? 0 : -width),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
transform: [{ translateX }],
|
||||
};
|
||||
}
|
||||
|
||||
function forLeft(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function forCenter(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function forRight(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(
|
||||
crossFadeInterpolation(scenes, first, index, last)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS UINavigationController style interpolators
|
||||
*/
|
||||
|
||||
function forLeftButton(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
// The gist of what we're doing here is animating the left button _normally_ (fast fade)
|
||||
// when both scenes in transition have headers. When the current, next, or previous scene _don't_
|
||||
// have a header, we don't fade the button, and only set it's opacity to 0 at the last moment
|
||||
// of the transition.
|
||||
const inputRange = [
|
||||
first,
|
||||
first + 0.001,
|
||||
first + Math.abs(index - first) / 2,
|
||||
index,
|
||||
last - Math.abs(last - index) / 2,
|
||||
last - 0.001,
|
||||
last,
|
||||
];
|
||||
const outputRange = [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0.1 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0.1 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
];
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange,
|
||||
outputRange,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: this offset calculation is an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. See the comment on title for more information.
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
|
||||
function forLeftLabel(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
const offset = LEFT_LABEL_OFFSET;
|
||||
|
||||
// Similarly to the animation of the left label, when animating to or from a scene without
|
||||
// a header, we keep the label at full opacity and in the same position for as long as possible.
|
||||
return {
|
||||
// For now we fade out the label before fading in the title, so the
|
||||
// differences between the label and title position can be hopefully not so
|
||||
// noticable to the user
|
||||
opacity: position.interpolate({
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.35,
|
||||
index,
|
||||
index + 0.5,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0.5 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, first + 0.001, index, last - 0.001, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [
|
||||
-offset * 1.5,
|
||||
hasHeader(scenes[first]) ? -offset * 1.5 : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? offset : 0,
|
||||
offset,
|
||||
]
|
||||
: [
|
||||
offset,
|
||||
hasHeader(scenes[first]) ? offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? -offset * 1.5 : 0,
|
||||
-offset * 1.5,
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: this offset calculation is a an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. We want the back button label to transition
|
||||
* smoothly into the title text and to do this we need to understand
|
||||
* where the title is positioned within the title container (since it is
|
||||
* centered).
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
|
||||
function forCenterFromLeft(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index - 0.5, index, index + 0.5, last];
|
||||
const offset = TITLE_OFFSET_IOS;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
index - 0.5,
|
||||
index,
|
||||
index + 0.7,
|
||||
last - 0.001,
|
||||
last,
|
||||
],
|
||||
outputRange: [
|
||||
0,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[first]) ? 0 : 1,
|
||||
hasHeader(scenes[index]) ? 1 : 0,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
hasHeader(scenes[last]) ? 0 : 1,
|
||||
0,
|
||||
],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, first + 0.001, index, last - 0.001, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [
|
||||
-offset,
|
||||
hasHeader(scenes[first]) ? -offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? offset : 0,
|
||||
offset,
|
||||
]
|
||||
: [
|
||||
offset,
|
||||
hasHeader(scenes[first]) ? offset : 0,
|
||||
0,
|
||||
hasHeader(scenes[last]) ? -offset : 0,
|
||||
-offset,
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
forLayout,
|
||||
forLeft,
|
||||
forLeftButton,
|
||||
forLeftLabel,
|
||||
forCenterFromLeft,
|
||||
forCenter,
|
||||
forRight,
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text, View, Platform, StyleSheet, Animated } from 'react-native';
|
||||
|
||||
const AnimatedText = Animated.Text;
|
||||
|
||||
const HeaderTitle = ({ style, ...rest }) => (
|
||||
<AnimatedText
|
||||
numberOfLines={1}
|
||||
{...rest}
|
||||
style={[styles.title, style]}
|
||||
accessibilityTraits="header"
|
||||
/>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
fontSize: Platform.OS === 'ios' ? 17 : 20,
|
||||
fontWeight: Platform.OS === 'ios' ? '700' : '500',
|
||||
color: 'rgba(0, 0, 0, .9)',
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default HeaderTitle;
|
||||
@@ -1,139 +0,0 @@
|
||||
import React from 'react';
|
||||
import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
const defaultBackImage = require('../assets/back-icon.png');
|
||||
|
||||
class ModularHeaderBackButton extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
tintColor: '#037aff',
|
||||
truncatedTitle: 'Back',
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
_onTextLayout = e => {
|
||||
if (this.state.initialTextWidth) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
initialTextWidth: e.nativeEvent.layout.x + e.nativeEvent.layout.width,
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
const { backImage, title, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!title && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
|
||||
return <BackImage {...props} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
width,
|
||||
title,
|
||||
titleStyle,
|
||||
tintColor,
|
||||
truncatedTitle,
|
||||
} = this.props;
|
||||
|
||||
const renderTruncated =
|
||||
this.state.initialTextWidth && width
|
||||
? this.state.initialTextWidth > width
|
||||
: false;
|
||||
|
||||
let backButtonTitle = renderTruncated ? truncatedTitle : title;
|
||||
|
||||
// TODO: When we've sorted out measuring in the header, let's revisit this.
|
||||
// This is clearly a bad workaround.
|
||||
if (backButtonTitle && backButtonTitle.length > 8) {
|
||||
backButtonTitle = truncatedTitle;
|
||||
}
|
||||
|
||||
const { ButtonContainerComponent, LabelContainerComponent } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableItem
|
||||
accessibilityComponentType="button"
|
||||
accessibilityLabel={backButtonTitle}
|
||||
accessibilityTraits="button"
|
||||
testID="header-back"
|
||||
delayPressIn={0}
|
||||
onPress={onPress}
|
||||
style={styles.container}
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<ButtonContainerComponent>
|
||||
{this._renderBackImage()}
|
||||
</ButtonContainerComponent>
|
||||
{typeof backButtonTitle === 'string' && (
|
||||
<LabelContainerComponent>
|
||||
<Text
|
||||
onLayout={this._onTextLayout}
|
||||
style={[
|
||||
styles.title,
|
||||
!!tintColor && { color: tintColor },
|
||||
titleStyle,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{backButtonTitle}
|
||||
</Text>
|
||||
</LabelContainerComponent>
|
||||
)}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
title: {
|
||||
fontSize: 17,
|
||||
paddingRight: 10,
|
||||
},
|
||||
icon: {
|
||||
height: 21,
|
||||
width: 12,
|
||||
marginLeft: 9,
|
||||
marginRight: 22,
|
||||
marginVertical: 12,
|
||||
resizeMode: 'contain',
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
iconWithTitle: {
|
||||
marginRight: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export default ModularHeaderBackButton;
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import createReactContext from 'create-react-context';
|
||||
|
||||
const NavigationContext = createReactContext();
|
||||
|
||||
export const NavigationProvider = NavigationContext.Provider;
|
||||
export const NavigationConsumer = NavigationContext.Consumer;
|
||||
|
||||
export default {
|
||||
NavigationProvider,
|
||||
NavigationConsumer,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import SceneView from './SceneView';
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
|
||||
const SCENE_KEY_PREFIX = 'scene_';
|
||||
|
||||
/**
|
||||
* Helper function to compare route keys (e.g. "9", "11").
|
||||
*/
|
||||
function compareKey(one, two) {
|
||||
const delta = one.length - two.length;
|
||||
if (delta > 0) {
|
||||
return 1;
|
||||
}
|
||||
if (delta < 0) {
|
||||
return -1;
|
||||
}
|
||||
return one > two ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to sort scenes based on their index and view key.
|
||||
*/
|
||||
function compareScenes(one, two) {
|
||||
if (one.index > two.index) {
|
||||
return 1;
|
||||
}
|
||||
if (one.index < two.index) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return compareKey(one.key, two.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether two routes are the same.
|
||||
*/
|
||||
function areScenesShallowEqual(one, two) {
|
||||
return (
|
||||
one.key === two.key &&
|
||||
one.index === two.index &&
|
||||
one.isStale === two.isStale &&
|
||||
one.isActive === two.isActive &&
|
||||
areRoutesShallowEqual(one.route, two.route)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether two routes are the same.
|
||||
*/
|
||||
function areRoutesShallowEqual(one, two) {
|
||||
if (!one || !two) {
|
||||
return one === two;
|
||||
}
|
||||
|
||||
if (one.key !== two.key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shallowEqual(one, two);
|
||||
}
|
||||
|
||||
export default function ScenesReducer(
|
||||
scenes,
|
||||
nextState,
|
||||
prevState,
|
||||
descriptors
|
||||
) {
|
||||
// Always update the descriptors
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
// It will be resolved in a better way when we re-write Transitioner
|
||||
scenes.forEach(scene => {
|
||||
const { route } = scene;
|
||||
if (descriptors && descriptors[route.key]) {
|
||||
scene.descriptor = descriptors[route.key];
|
||||
}
|
||||
});
|
||||
|
||||
// Bail out early if we didn't update the state
|
||||
if (prevState === nextState) {
|
||||
return scenes;
|
||||
}
|
||||
|
||||
const prevScenes = new Map();
|
||||
const freshScenes = new Map();
|
||||
const staleScenes = new Map();
|
||||
|
||||
// Populate stale scenes from previous scenes marked as stale.
|
||||
scenes.forEach(scene => {
|
||||
const { key } = scene;
|
||||
if (scene.isStale) {
|
||||
staleScenes.set(key, scene);
|
||||
}
|
||||
prevScenes.set(key, scene);
|
||||
});
|
||||
|
||||
const nextKeys = new Set();
|
||||
nextState.routes.forEach((route, index) => {
|
||||
const key = SCENE_KEY_PREFIX + route.key;
|
||||
|
||||
let descriptor = descriptors && descriptors[route.key];
|
||||
|
||||
const scene = {
|
||||
index,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
key,
|
||||
route,
|
||||
descriptor,
|
||||
};
|
||||
invariant(
|
||||
!nextKeys.has(key),
|
||||
`navigation.state.routes[${index}].key "${key}" conflicts with ` +
|
||||
'another route!'
|
||||
);
|
||||
nextKeys.add(key);
|
||||
|
||||
if (staleScenes.has(key)) {
|
||||
// A previously `stale` scene is now part of the nextState, so we
|
||||
// revive it by removing it from the stale scene map.
|
||||
staleScenes.delete(key);
|
||||
}
|
||||
freshScenes.set(key, scene);
|
||||
});
|
||||
|
||||
if (prevState) {
|
||||
// Look at the previous routes and classify any removed scenes as `stale`.
|
||||
prevState.routes.forEach((route, index) => {
|
||||
const key = SCENE_KEY_PREFIX + route.key;
|
||||
if (freshScenes.has(key)) {
|
||||
return;
|
||||
}
|
||||
const lastScene = scenes.find(scene => scene.route.key === route.key);
|
||||
|
||||
// We can get into a weird place where we have a queued transition and then clobber
|
||||
// that transition without ever actually rendering the scene, in which case
|
||||
// there is no lastScene. If the descriptor is not available on the lastScene
|
||||
// or the descriptors prop then we just skip adding it to stale scenes and it's
|
||||
// not ever rendered.
|
||||
const descriptor = lastScene
|
||||
? lastScene.descriptor
|
||||
: descriptors[route.key];
|
||||
|
||||
if (descriptor) {
|
||||
staleScenes.set(key, {
|
||||
index,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
key,
|
||||
route,
|
||||
descriptor,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const nextScenes = [];
|
||||
|
||||
const mergeScene = nextScene => {
|
||||
const { key } = nextScene;
|
||||
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
|
||||
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
|
||||
// Reuse `prevScene` as `scene` so view can avoid unnecessary re-render.
|
||||
// This assumes that the scene's navigation state is immutable.
|
||||
nextScenes.push(prevScene);
|
||||
} else {
|
||||
nextScenes.push(nextScene);
|
||||
}
|
||||
};
|
||||
|
||||
staleScenes.forEach(mergeScene);
|
||||
freshScenes.forEach(mergeScene);
|
||||
|
||||
nextScenes.sort(compareScenes);
|
||||
|
||||
let activeScenesCount = 0;
|
||||
nextScenes.forEach((scene, ii) => {
|
||||
const isActive = !scene.isStale && scene.index === nextState.index;
|
||||
if (isActive !== scene.isActive) {
|
||||
nextScenes[ii] = {
|
||||
...scene,
|
||||
isActive,
|
||||
};
|
||||
}
|
||||
if (isActive) {
|
||||
activeScenesCount++;
|
||||
}
|
||||
});
|
||||
|
||||
invariant(
|
||||
activeScenesCount === 1,
|
||||
'there should always be only one scene active, not %s.',
|
||||
activeScenesCount
|
||||
);
|
||||
|
||||
if (nextScenes.length !== scenes.length) {
|
||||
return nextScenes;
|
||||
}
|
||||
|
||||
if (
|
||||
nextScenes.some(
|
||||
(scene, index) => !areScenesShallowEqual(scenes[index], scene)
|
||||
)
|
||||
) {
|
||||
return nextScenes;
|
||||
}
|
||||
|
||||
// scenes haven't changed.
|
||||
return scenes;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import StackViewLayout from './StackViewLayout';
|
||||
import Transitioner from '../Transitioner';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
|
||||
const NativeAnimatedModule =
|
||||
NativeModules && NativeModules.NativeAnimatedModule;
|
||||
|
||||
class StackView extends React.Component {
|
||||
static defaultProps = {
|
||||
navigationConfig: {
|
||||
mode: 'card',
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
onTransitionStart={
|
||||
this.props.onTransitionStart ||
|
||||
this.props.navigationConfig.onTransitionStart
|
||||
}
|
||||
onTransitionEnd={(transition, lastTransition) => {
|
||||
const { navigationConfig, navigation } = this.props;
|
||||
const onTransitionEnd =
|
||||
this.props.onTransitionEnd || navigationConfig.onTransitionEnd;
|
||||
if (transition.navigation.state.isTransitioning) {
|
||||
navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
key: navigation.state.key,
|
||||
})
|
||||
);
|
||||
}
|
||||
onTransitionEnd && onTransitionEnd(transition, lastTransition);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_configureTransition = (transitionProps, prevTransitionProps) => {
|
||||
return {
|
||||
...TransitionConfigs.getTransitionConfig(
|
||||
this.props.navigationConfig.transitionConfig,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
this.props.navigationConfig.mode === 'modal'
|
||||
).transitionSpec,
|
||||
useNativeDriver: !!NativeAnimatedModule,
|
||||
};
|
||||
};
|
||||
|
||||
_render = (transitionProps, lastTransitionProps) => {
|
||||
const { screenProps, navigationConfig } = this.props;
|
||||
return (
|
||||
<StackViewLayout
|
||||
{...navigationConfig}
|
||||
onGestureBegin={this.props.onGestureBegin}
|
||||
onGestureCanceled={this.props.onGestureCanceled}
|
||||
onGestureEnd={this.props.onGestureEnd}
|
||||
screenProps={screenProps}
|
||||
descriptors={this.props.descriptors}
|
||||
transitionProps={transitionProps}
|
||||
lastTransitionProps={lastTransitionProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default StackView;
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Animated, StyleSheet } from 'react-native';
|
||||
import createPointerEventsContainer from './createPointerEventsContainer';
|
||||
|
||||
/**
|
||||
* Component that renders the scene as card for the <StackView />.
|
||||
*/
|
||||
class Card extends React.Component {
|
||||
render() {
|
||||
const { children, pointerEvents, style } = this.props;
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
ref={this.props.onComponentRef}
|
||||
style={[styles.main, style]}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: '#E9E9EF',
|
||||
shadowColor: 'black',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 5,
|
||||
},
|
||||
});
|
||||
|
||||
Card = createPointerEventsContainer(Card);
|
||||
|
||||
export default Card;
|
||||
@@ -1,638 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
PanResponder,
|
||||
Platform,
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
import Header from '../Header/Header';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import StackActions from '../../routers/StackActions';
|
||||
import SceneView from '../SceneView';
|
||||
import withOrientation from '../withOrientation';
|
||||
import { NavigationProvider } from '../NavigationContext';
|
||||
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
|
||||
const IS_IPHONE_X =
|
||||
Platform.OS === 'ios' &&
|
||||
!Platform.isPad &&
|
||||
!Platform.isTVOS &&
|
||||
(WINDOW_HEIGHT === 812 || WINDOW_WIDTH === 812);
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
* Enumerate possible values for validation
|
||||
*/
|
||||
const HEADER_LAYOUT_PRESET_VALUES = ['center', 'left'];
|
||||
const HEADER_TRANSITION_PRESET_VALUES = ['uikit', 'fade-in-place'];
|
||||
|
||||
/**
|
||||
* The max duration of the card animation in milliseconds after released gesture.
|
||||
* The actual duration should be always less then that because the rest distance
|
||||
* is always less then the full distance of the layout.
|
||||
*/
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
/**
|
||||
* The gesture distance threshold to trigger the back behavior. For instance,
|
||||
* `1/2` means that moving greater than 1/2 of the width of the screen will
|
||||
* trigger a back action
|
||||
*/
|
||||
const POSITION_THRESHOLD = 1 / 2;
|
||||
|
||||
/**
|
||||
* The threshold (in pixels) to start the gesture action.
|
||||
*/
|
||||
const RESPOND_THRESHOLD = 20;
|
||||
|
||||
/**
|
||||
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
||||
*/
|
||||
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
|
||||
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||
|
||||
const animatedSubscribeValue = animatedValue => {
|
||||
if (!animatedValue.__isNative) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(animatedValue._listeners).length === 0) {
|
||||
animatedValue.addListener(emptyFunction);
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultHeaderHeight = isLandscape => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
return 32;
|
||||
} else if (IS_IPHONE_X) {
|
||||
return 88;
|
||||
} else {
|
||||
return 64;
|
||||
}
|
||||
} else {
|
||||
return 56;
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
* be updated according to its relative position. This means that a card can effectively be
|
||||
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
|
||||
* corresponding location for the touch.
|
||||
*/
|
||||
_gestureStartValue = 0;
|
||||
|
||||
// tracks if a touch is currently happening
|
||||
_isResponding = false;
|
||||
|
||||
/**
|
||||
* immediateIndex is used to represent the expected index that we will be on after a
|
||||
* transition. To achieve a smooth animation when swiping back, the action to go back
|
||||
* doesn't actually fire until the transition completes. The immediateIndex is used during
|
||||
* the transition so that gestures can be handled correctly. This is a work-around for
|
||||
* cases when the user quickly swipes back several times.
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// Used when card's header is null and mode is float to make transition
|
||||
// between screens with headers and those without headers smooth.
|
||||
// This is not a great heuristic here. We don't know synchronously
|
||||
// on mount what the header height is so we have just used the most
|
||||
// common cases here.
|
||||
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
|
||||
};
|
||||
}
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
|
||||
if (__DEV__ && typeof header === 'string') {
|
||||
throw new Error(
|
||||
`Invalid header value: "${header}". The header option must be a valid React component or null, not a string.`
|
||||
);
|
||||
}
|
||||
|
||||
if (header === null && headerMode === 'screen') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if it's a react element
|
||||
if (React.isValidElement(header)) {
|
||||
return header;
|
||||
}
|
||||
|
||||
// Handle the case where the header option is a function, and provide the default
|
||||
const renderHeader = header || (props => <Header {...props} />);
|
||||
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
headerRightInterpolator,
|
||||
} = this._getTransitionConfig();
|
||||
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
lastTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<NavigationProvider value={scene.descriptor.navigation}>
|
||||
{renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
layoutPreset: this._getHeaderLayoutPreset(),
|
||||
backTitleVisible: this._getheaderBackTitleVisible(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
})}
|
||||
</NavigationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_animatedSubscribe(props) {
|
||||
// Hack to make this work with native driven animations. We add a single listener
|
||||
// so the JS value of the following animated values gets updated. We rely on
|
||||
// some Animated private APIs and not doing so would require using a bunch of
|
||||
// value listeners but we'd have to remove them to not leak and I'm not sure
|
||||
// when we'd do that with the current structure we have. `stopAnimation` callback
|
||||
// is also broken with native animated values that have no listeners so if we
|
||||
// want to remove this we have to fix this too.
|
||||
animatedSubscribeValue(props.transitionProps.layout.width);
|
||||
animatedSubscribeValue(props.transitionProps.layout.height);
|
||||
animatedSubscribeValue(props.transitionProps.position);
|
||||
}
|
||||
|
||||
_reset(resetToIndex, duration) {
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
Animated.spring(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
stiffness: 5000,
|
||||
damping: 600,
|
||||
mass: 3,
|
||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
}).start();
|
||||
} else {
|
||||
Animated.timing(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
duration,
|
||||
easing: EaseInOut,
|
||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
_goBack(backFromIndex, duration) {
|
||||
const { navigation, position, scenes } = this.props.transitionProps;
|
||||
const toValue = Math.max(backFromIndex - 1, 0);
|
||||
|
||||
// set temporary index for gesture handler to respect until the action is
|
||||
// dispatched at the end of the transition.
|
||||
this._immediateIndex = toValue;
|
||||
|
||||
const onCompleteAnimation = () => {
|
||||
this._immediateIndex = null;
|
||||
const backFromScene = scenes.find(s => s.index === toValue + 1);
|
||||
if (!this._isResponding && backFromScene) {
|
||||
navigation.dispatch(
|
||||
NavigationActions.back({
|
||||
key: backFromScene.route.key,
|
||||
immediate: true,
|
||||
})
|
||||
);
|
||||
navigation.dispatch(StackActions.completeTransition());
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
Animated.spring(position, {
|
||||
toValue,
|
||||
stiffness: 5000,
|
||||
damping: 600,
|
||||
mass: 3,
|
||||
useNativeDriver: position.__isNative,
|
||||
}).start(onCompleteAnimation);
|
||||
} else {
|
||||
Animated.timing(position, {
|
||||
toValue,
|
||||
duration,
|
||||
easing: EaseInOut,
|
||||
useNativeDriver: position.__isNative,
|
||||
}).start(onCompleteAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
_panResponder = PanResponder.create({
|
||||
onPanResponderTerminate: () => {
|
||||
const { navigation } = this.props.transitionProps;
|
||||
const { index } = navigation.state;
|
||||
this._isResponding = false;
|
||||
this._reset(index, 0);
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
},
|
||||
onPanResponderGrant: () => {
|
||||
const {
|
||||
transitionProps: { navigation, position, scene },
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
|
||||
if (index !== scene.index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position.stopAnimation(value => {
|
||||
this._isResponding = true;
|
||||
this._gestureStartValue = value;
|
||||
});
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
},
|
||||
onMoveShouldSetPanResponder: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
if (index !== scene.index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
|
||||
const currentDragPosition =
|
||||
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
|
||||
const axisLength = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const axisHasBeenMeasured = !!axisLength;
|
||||
|
||||
// Measure the distance from the touch to the edge of the screen
|
||||
const screenEdgeDistance = gestureDirectionInverted
|
||||
? axisLength - (currentDragPosition - currentDragDistance)
|
||||
: currentDragPosition - currentDragDistance;
|
||||
// Compare to the gesture distance relavant to card or modal
|
||||
|
||||
const {
|
||||
gestureResponseDistance: userGestureResponseDistance = {},
|
||||
} = options;
|
||||
const gestureResponseDistance = isVertical
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
: userGestureResponseDistance.horizontal ||
|
||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
|
||||
if (screenEdgeDistance > gestureResponseDistance) {
|
||||
// Reject touches that started in the middle of the screen
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDraggedEnough =
|
||||
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
|
||||
|
||||
const isOnFirstCard = immediateIndex === 0;
|
||||
const shouldSetResponder =
|
||||
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
|
||||
return shouldSetResponder;
|
||||
},
|
||||
onPanResponderMove: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
// Handle the moving touches for our granted responder
|
||||
const startValue = this._gestureStartValue;
|
||||
const axis = isVertical ? 'dy' : 'dx';
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const currentValue =
|
||||
axis === 'dx' && gestureDirectionInverted
|
||||
? startValue + gesture[axis] / axisDistance
|
||||
: startValue - gesture[axis] / axisDistance;
|
||||
const value = clamp(index - 1, currentValue, index);
|
||||
position.setValue(value);
|
||||
},
|
||||
onPanResponderTerminationRequest: () =>
|
||||
// Returning false will prevent other views from becoming responder while
|
||||
// the navigation view is the responder (mid-gesture)
|
||||
false,
|
||||
onPanResponderRelease: (event, gesture) => {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout, scene },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = scene.descriptor;
|
||||
const gestureDirection = options.gestureDirection;
|
||||
|
||||
const gestureDirectionInverted =
|
||||
typeof gestureDirection === 'string'
|
||||
? gestureDirection === 'inverted'
|
||||
: I18nManager.isRTL;
|
||||
|
||||
if (!this._isResponding) {
|
||||
return;
|
||||
}
|
||||
this._isResponding = false;
|
||||
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
|
||||
// Calculate animate duration according to gesture speed and moved distance
|
||||
const axisDistance = isVertical
|
||||
? layout.height.__getValue()
|
||||
: layout.width.__getValue();
|
||||
const movementDirection = gestureDirectionInverted ? -1 : 1;
|
||||
const movedDistance =
|
||||
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
|
||||
const gestureVelocity =
|
||||
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
|
||||
const defaultVelocity = axisDistance / ANIMATION_DURATION;
|
||||
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
||||
const resetDuration = gestureDirectionInverted
|
||||
? (axisDistance - movedDistance) / velocity
|
||||
: movedDistance / velocity;
|
||||
const goBackDuration = gestureDirectionInverted
|
||||
? movedDistance / velocity
|
||||
: (axisDistance - movedDistance) / velocity;
|
||||
|
||||
// To asyncronously get the current animated value, we need to run stopAnimation:
|
||||
position.stopAnimation(value => {
|
||||
// If the speed of the gesture release is significant, use that as the indication
|
||||
// of intent
|
||||
if (gestureVelocity < -0.5) {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
return;
|
||||
}
|
||||
if (gestureVelocity > 0.5) {
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
||||
// and the back will happen.
|
||||
if (value <= index - POSITION_THRESHOLD) {
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
} else {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
_onFloatingHeaderLayout = e => {
|
||||
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
|
||||
};
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
|
||||
if (headerMode === 'float') {
|
||||
const { scene } = this.props.transitionProps;
|
||||
floatingHeader = (
|
||||
<View pointerEvents="box-none" onLayout={this._onFloatingHeaderLayout}>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const {
|
||||
transitionProps: { scene, scenes },
|
||||
mode,
|
||||
} = this.props;
|
||||
const { options } = scene.descriptor;
|
||||
|
||||
const gesturesEnabled =
|
||||
typeof options.gesturesEnabled === 'boolean'
|
||||
? options.gesturesEnabled
|
||||
: Platform.OS === 'ios';
|
||||
|
||||
const responder = !gesturesEnabled ? null : this._panResponder;
|
||||
|
||||
const handlers = gesturesEnabled ? responder.panHandlers : {};
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
this._getTransitionConfig().containerStyle,
|
||||
];
|
||||
|
||||
return (
|
||||
<View {...handlers} style={containerStyle}>
|
||||
<View style={styles.scenes}>
|
||||
{scenes.map(s => this._renderCard(s))}
|
||||
</View>
|
||||
{floatingHeader}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_getHeaderMode() {
|
||||
if (this.props.headerMode) {
|
||||
return this.props.headerMode;
|
||||
}
|
||||
if (Platform.OS === 'android' || this.props.mode === 'modal') {
|
||||
return 'screen';
|
||||
}
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderLayoutPreset() {
|
||||
const { headerLayoutPreset } = this.props;
|
||||
if (headerLayoutPreset) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
this._getHeaderTransitionPreset() === 'uikit' &&
|
||||
headerLayoutPreset === 'left' &&
|
||||
Platform.OS === 'ios'
|
||||
) {
|
||||
console.warn(
|
||||
`headerTransitionPreset with the value 'ui-kit' is incompatible with headerLayoutPreset 'left'`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (HEADER_LAYOUT_PRESET_VALUES.includes(headerLayoutPreset)) {
|
||||
return headerLayoutPreset;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`Invalid configuration applied for headerLayoutPreset - expected one of ${HEADER_LAYOUT_PRESET_VALUES.join(
|
||||
', '
|
||||
)} but received ${JSON.stringify(headerLayoutPreset)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return 'left';
|
||||
} else {
|
||||
return 'center';
|
||||
}
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
const { headerTransitionPreset } = this.props;
|
||||
if (headerTransitionPreset) {
|
||||
if (HEADER_TRANSITION_PRESET_VALUES.includes(headerTransitionPreset)) {
|
||||
return headerTransitionPreset;
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.error(
|
||||
`Invalid configuration applied for headerTransitionPreset - expected one of ${HEADER_TRANSITION_PRESET_VALUES.join(
|
||||
', '
|
||||
)} but received ${JSON.stringify(headerTransitionPreset)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
_getheaderBackTitleVisible() {
|
||||
const { headerBackTitleVisible } = this.props;
|
||||
|
||||
return headerBackTitleVisible;
|
||||
}
|
||||
|
||||
_renderInnerScene(scene) {
|
||||
const { options, navigation, getComponent } = scene.descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.scenes}>
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
</View>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getTransitionConfig = () => {
|
||||
const isModal = this.props.mode === 'modal';
|
||||
|
||||
return TransitionConfigs.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
this.props.transitionProps,
|
||||
this.props.lastTransitionProps,
|
||||
isModal
|
||||
);
|
||||
};
|
||||
|
||||
_renderCard = scene => {
|
||||
const { screenInterpolator } = this._getTransitionConfig();
|
||||
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props.transitionProps, scene });
|
||||
|
||||
// If this screen has "header" set to `null` in it's navigation options, but
|
||||
// it exists in a stack with headerMode float, add a negative margin to
|
||||
// compensate for the hidden header
|
||||
const { options } = scene.descriptor;
|
||||
const hasHeader = options.header !== null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
let marginTop = 0;
|
||||
if (!hasHeader && headerMode === 'float') {
|
||||
marginTop = -this.state.floatingHeaderHeight;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...this.props.transitionProps}
|
||||
key={`card_${scene.key}`}
|
||||
style={[style, { marginTop }, this.props.cardStyle]}
|
||||
scene={scene}
|
||||
>
|
||||
{this._renderInnerScene(scene)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
// Header is physically rendered after scenes so that Header won't be
|
||||
// covered by the shadows of the scenes.
|
||||
// That said, we'd have use `flexDirection: 'column-reverse'` to move
|
||||
// Header above the scenes.
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
scenes: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(StackViewLayout);
|
||||
@@ -1,167 +0,0 @@
|
||||
import { I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the card in the cards stack.
|
||||
*
|
||||
* +------------+
|
||||
* +-+ |
|
||||
* +-+ | |
|
||||
* | | | |
|
||||
* | | | Focused |
|
||||
* | | | Card |
|
||||
* | | | |
|
||||
* +-+ | |
|
||||
* +-+ |
|
||||
* +------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render the initial style when the initial layout isn't measured yet.
|
||||
*/
|
||||
function forInitial(props) {
|
||||
const { navigation, scene } = props;
|
||||
|
||||
const focused = navigation.state.index === scene.index;
|
||||
const opacity = focused ? 1 : 0;
|
||||
// If not focused, move the scene far away.
|
||||
const translate = focused ? 0 : 1000000;
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX: translate }, { translateY: translate }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the right.
|
||||
*/
|
||||
function forHorizontal(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = position.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
});
|
||||
|
||||
const width = layout.initWidth;
|
||||
const translateX = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-width, 0, width * 0.3]
|
||||
: [width, 0, width * -0.3],
|
||||
});
|
||||
const translateY = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the bottom (used for modals).
|
||||
*/
|
||||
function forVertical(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = position.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
});
|
||||
|
||||
const height = layout.initHeight;
|
||||
const translateY = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [height, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Android-style fade in from the bottom.
|
||||
*/
|
||||
function forFadeFromBottomAndroid(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index, last - 0.01, last];
|
||||
|
||||
const opacity = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [0, 1, 1, 0],
|
||||
});
|
||||
|
||||
const translateY = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [50, 0, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fadeIn and fadeOut
|
||||
*/
|
||||
function forFade(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [0, 1, 1],
|
||||
});
|
||||
|
||||
return {
|
||||
opacity,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
forHorizontal,
|
||||
forVertical,
|
||||
forFadeFromBottomAndroid,
|
||||
forFade,
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import StyleInterpolator from './StackViewStyleInterpolator';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
|
||||
let IOSTransitionSpec;
|
||||
if (supportsImprovedSpringAnimation()) {
|
||||
// These are the exact values from UINavigationController's animation configuration
|
||||
IOSTransitionSpec = {
|
||||
timing: Animated.spring,
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
};
|
||||
} else {
|
||||
// This is an approximation of the IOS spring animation using a derived bezier curve
|
||||
IOSTransitionSpec = {
|
||||
duration: 500,
|
||||
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
}
|
||||
|
||||
// Standard iOS navigation transition
|
||||
const SlideFromRightIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forHorizontal,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard iOS navigation transition for modals
|
||||
const ModalSlideFromBottomIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forVertical,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when opening an Activity
|
||||
const FadeInFromBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
|
||||
transitionSpec: {
|
||||
duration: 350,
|
||||
easing: Easing.out(Easing.poly(5)), // decelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when closing an Activity
|
||||
const FadeOutToBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
|
||||
transitionSpec: {
|
||||
duration: 230,
|
||||
easing: Easing.in(Easing.poly(4)), // accelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
if (Platform.OS === 'android') {
|
||||
// Use the default Android animation no matter if the screen is a modal.
|
||||
// Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
if (
|
||||
prevTransitionProps &&
|
||||
transitionProps.index < prevTransitionProps.index
|
||||
) {
|
||||
// Navigating back to the previous screen
|
||||
return FadeOutToBottomAndroid;
|
||||
}
|
||||
return FadeInFromBottomAndroid;
|
||||
}
|
||||
// iOS and other platforms
|
||||
if (isModal) {
|
||||
return ModalSlideFromBottomIOS;
|
||||
}
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(
|
||||
transitionConfigurer,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
const defaultConfig = defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
export default {
|
||||
defaultTransitionConfig,
|
||||
getTransitionConfig,
|
||||
SlideFromRightIOS,
|
||||
ModalSlideFromBottomIOS,
|
||||
FadeInFromBottomAndroid,
|
||||
FadeOutToBottomAndroid,
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import invariant from '../../utils/invariant';
|
||||
import AnimatedValueSubscription from '../AnimatedValueSubscription';
|
||||
|
||||
const MIN_POSITION_OFFSET = 0.01;
|
||||
|
||||
/**
|
||||
* Create a higher-order component that automatically computes the
|
||||
* `pointerEvents` property for a component whenever navigation position
|
||||
* changes.
|
||||
*/
|
||||
export default function createPointerEventsContainer(Component) {
|
||||
class Container extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
this._bindPosition();
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
pointerEvents={this._pointerEvents}
|
||||
onComponentRef={this._onComponentRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onComponentRef = component => {
|
||||
this._component = component;
|
||||
if (component) {
|
||||
invariant(
|
||||
typeof component.setNativeProps === 'function',
|
||||
'component must implement method `setNativeProps`'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_bindPosition() {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
this._positionListener = new AnimatedValueSubscription(
|
||||
this.props.position,
|
||||
this._onPositionChange
|
||||
);
|
||||
}
|
||||
|
||||
_onPositionChange = () => {
|
||||
if (this._component) {
|
||||
const pointerEvents = this._computePointerEvents();
|
||||
if (this._pointerEvents !== pointerEvents) {
|
||||
this._pointerEvents = pointerEvents;
|
||||
this._component.setNativeProps({ pointerEvents });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_computePointerEvents() {
|
||||
const { navigation, position, scene } = this.props;
|
||||
|
||||
if (scene.isStale || navigation.state.index !== scene.index) {
|
||||
// The scene isn't focused.
|
||||
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
||||
}
|
||||
|
||||
const offset = position.__getAnimatedValue() - navigation.state.index;
|
||||
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
||||
// The positon is still away from scene's index.
|
||||
// Scene's children should not receive touches until the position
|
||||
// is close enough to scene's index.
|
||||
return 'box-only';
|
||||
}
|
||||
|
||||
return 'auto';
|
||||
}
|
||||
}
|
||||
return Container;
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native';
|
||||
import invariant from '../utils/invariant';
|
||||
|
||||
import NavigationScenesReducer from './ScenesReducer';
|
||||
|
||||
// Used for all animations unless overriden
|
||||
const DefaultTransitionSpec = {
|
||||
duration: 250,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
|
||||
class Transitioner extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// The initial layout isn't measured. Measured layout will be only available
|
||||
// when the component is mounted.
|
||||
const layout = {
|
||||
height: new Animated.Value(0),
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
isMeasured: false,
|
||||
width: new Animated.Value(0),
|
||||
};
|
||||
|
||||
this.state = {
|
||||
layout,
|
||||
position: new Animated.Value(this.props.navigation.state.index),
|
||||
progress: new Animated.Value(1),
|
||||
scenes: NavigationScenesReducer(
|
||||
[],
|
||||
this.props.navigation.state,
|
||||
null,
|
||||
this.props.descriptors
|
||||
),
|
||||
};
|
||||
|
||||
this._prevTransitionProps = null;
|
||||
this._transitionProps = buildTransitionProps(props, this.state);
|
||||
this._isMounted = false;
|
||||
this._isTransitionRunning = false;
|
||||
this._queuedTransition = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
let nextScenes = NavigationScenesReducer(
|
||||
this.state.scenes,
|
||||
nextProps.navigation.state,
|
||||
this.props.navigation.state,
|
||||
nextProps.descriptors
|
||||
);
|
||||
if (!nextProps.navigation.state.isTransitioning) {
|
||||
nextScenes = filterStale(nextScenes);
|
||||
}
|
||||
|
||||
// Update nextScenes when we change screenProps
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
if (nextProps.screenProps !== this.props.screenProps) {
|
||||
this.setState({ nextScenes });
|
||||
}
|
||||
|
||||
if (nextScenes === this.state.scenes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexHasChanged =
|
||||
nextProps.navigation.state.index !== this.props.navigation.state.index;
|
||||
if (this._isTransitionRunning) {
|
||||
this._queuedTransition = { nextProps, nextScenes, indexHasChanged };
|
||||
return;
|
||||
}
|
||||
|
||||
this._startTransition(nextProps, nextScenes, indexHasChanged);
|
||||
}
|
||||
|
||||
_startTransition(nextProps, nextScenes, indexHasChanged) {
|
||||
const nextState = {
|
||||
...this.state,
|
||||
scenes: nextScenes,
|
||||
};
|
||||
|
||||
const { position, progress } = nextState;
|
||||
|
||||
progress.setValue(0);
|
||||
|
||||
this._prevTransitionProps = this._transitionProps;
|
||||
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
||||
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
|
||||
if (!this._transitionProps.navigation.state.isTransitioning) {
|
||||
this.setState(nextState, async () => {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
progress.setValue(1);
|
||||
position.setValue(toValue);
|
||||
this._onTransitionEnd();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// get the transition spec.
|
||||
const transitionUserSpec = nextProps.configureTransition
|
||||
? nextProps.configureTransition(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
)
|
||||
: null;
|
||||
|
||||
const transitionSpec = {
|
||||
...DefaultTransitionSpec,
|
||||
...transitionUserSpec,
|
||||
};
|
||||
|
||||
const { timing } = transitionSpec;
|
||||
delete transitionSpec.timing;
|
||||
|
||||
const positionHasChanged = position.__getValue() !== toValue;
|
||||
|
||||
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
||||
const animations =
|
||||
indexHasChanged && positionHasChanged
|
||||
? [
|
||||
timing(progress, {
|
||||
...transitionSpec,
|
||||
toValue: 1,
|
||||
}),
|
||||
timing(position, {
|
||||
...transitionSpec,
|
||||
toValue: nextProps.navigation.state.index,
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
// update scenes and play the transition
|
||||
this._isTransitionRunning = true;
|
||||
this.setState(nextState, async () => {
|
||||
if (nextProps.onTransitionStart) {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
}
|
||||
Animated.parallel(animations).start(this._onTransitionEnd);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View onLayout={this._onLayout} style={styles.main}>
|
||||
{this.props.render(this._transitionProps, this._prevTransitionProps)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onLayout = event => {
|
||||
const { height, width } = event.nativeEvent.layout;
|
||||
if (
|
||||
this.state.layout.initWidth === width &&
|
||||
this.state.layout.initHeight === height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const layout = {
|
||||
...this.state.layout,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
isMeasured: true,
|
||||
};
|
||||
|
||||
layout.height.setValue(height);
|
||||
layout.width.setValue(width);
|
||||
|
||||
const nextState = {
|
||||
...this.state,
|
||||
layout,
|
||||
};
|
||||
|
||||
this._transitionProps = buildTransitionProps(this.props, nextState);
|
||||
this.setState(nextState);
|
||||
};
|
||||
|
||||
_onTransitionEnd = () => {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
const prevTransitionProps = this._prevTransitionProps;
|
||||
this._prevTransitionProps = null;
|
||||
|
||||
const scenes = filterStale(this.state.scenes);
|
||||
|
||||
const nextState = {
|
||||
...this.state,
|
||||
scenes,
|
||||
};
|
||||
|
||||
this._transitionProps = buildTransitionProps(this.props, nextState);
|
||||
|
||||
this.setState(nextState, async () => {
|
||||
if (this.props.onTransitionEnd) {
|
||||
const result = this.props.onTransitionEnd(
|
||||
this._transitionProps,
|
||||
prevTransitionProps
|
||||
);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._queuedTransition) {
|
||||
this._startTransition(
|
||||
this._queuedTransition.nextProps,
|
||||
this._queuedTransition.nextScenes,
|
||||
this._queuedTransition.indexHasChanged
|
||||
);
|
||||
this._queuedTransition = null;
|
||||
} else {
|
||||
this._isTransitionRunning = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function buildTransitionProps(props, state) {
|
||||
const { navigation } = props;
|
||||
|
||||
const { layout, position, progress, scenes } = state;
|
||||
|
||||
const scene = scenes.find(isSceneActive);
|
||||
|
||||
invariant(scene, 'Could not find active scene');
|
||||
|
||||
return {
|
||||
layout,
|
||||
navigation,
|
||||
position,
|
||||
progress,
|
||||
scenes,
|
||||
scene,
|
||||
index: scene.index,
|
||||
};
|
||||
}
|
||||
|
||||
function isSceneNotStale(scene) {
|
||||
return !scene.isStale;
|
||||
}
|
||||
|
||||
function filterStale(scenes) {
|
||||
const filtered = scenes.filter(isSceneNotStale);
|
||||
if (filtered.length === scenes.length) {
|
||||
return scenes;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function isSceneActive(scene) {
|
||||
return scene.isActive;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Transitioner;
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
import NavigationEvents from '../NavigationEvents';
|
||||
import { NavigationProvider } from '../NavigationContext';
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
import ScenesReducer from '../ScenesReducer';
|
||||
|
||||
const MOCK_DESCRIPTOR = {};
|
||||
|
||||
/**
|
||||
* Simulate scenes transtion with changes of navigation states.
|
||||
*/
|
||||
function testTransition(states) {
|
||||
let descriptors = states
|
||||
.reduce((acc, state) => acc.concat(state), [])
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = MOCK_DESCRIPTOR;
|
||||
return acc;
|
||||
}, {});
|
||||
const routes = states.map(keys => ({
|
||||
index: 0,
|
||||
routes: keys.map(key => ({ key, routeName: '' })),
|
||||
isTransitioning: false,
|
||||
}));
|
||||
|
||||
let scenes = [];
|
||||
let prevState = null;
|
||||
routes.forEach(nextState => {
|
||||
scenes = ScenesReducer(scenes, nextState, prevState, descriptors);
|
||||
prevState = nextState;
|
||||
});
|
||||
|
||||
return scenes;
|
||||
}
|
||||
|
||||
describe('ScenesReducer', () => {
|
||||
it('gets initial scenes', () => {
|
||||
const scenes = testTransition([['1', '2']]);
|
||||
|
||||
expect(scenes).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('pushes new scenes', () => {
|
||||
// Transition from ['1', '2'] to ['1', '2', '3'].
|
||||
const scenes = testTransition([['1', '2'], ['1', '2', '3']]);
|
||||
|
||||
expect(scenes).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('gets active scene when index changes', () => {
|
||||
const state1 = {
|
||||
index: 0,
|
||||
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
index: 1,
|
||||
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1);
|
||||
const route = scenes2.find(scene => scene.isActive).route;
|
||||
expect(route).toEqual({ key: '2', routeName: '' });
|
||||
});
|
||||
|
||||
it('gets same scenes', () => {
|
||||
const state1 = {
|
||||
index: 0,
|
||||
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
index: 0,
|
||||
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1);
|
||||
expect(scenes1).toBe(scenes2);
|
||||
});
|
||||
|
||||
it('gets different scenes when keys are different', () => {
|
||||
const state1 = {
|
||||
index: 0,
|
||||
routes: [{ key: '1', routeName: '' }, { key: '2', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
index: 0,
|
||||
routes: [{ key: '2', routeName: '' }, { key: '1', routeName: '' }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const descriptors = { 1: jest.mock(), 2: jest.mock() };
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
it('gets different scenes when routes are different', () => {
|
||||
const state1 = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', x: 1, routeName: '' },
|
||||
{ key: '2', x: 2, routeName: '' },
|
||||
],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', x: 3, routeName: '' },
|
||||
{ key: '2', x: 4, routeName: '' },
|
||||
],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
|
||||
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
it('gets different scenes when state index changes', () => {
|
||||
const state1 = {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', x: 1, routeName: '' },
|
||||
{ key: '2', x: 2, routeName: '' },
|
||||
],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const state2 = {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: '1', x: 1, routeName: '' },
|
||||
{ key: '2', x: 2, routeName: '' },
|
||||
],
|
||||
isTransitioning: false,
|
||||
};
|
||||
|
||||
const descriptors = { 1: MOCK_DESCRIPTOR, 2: MOCK_DESCRIPTOR };
|
||||
const scenes1 = ScenesReducer([], state1, null, descriptors);
|
||||
const scenes2 = ScenesReducer(scenes1, state2, state1, descriptors);
|
||||
expect(scenes1).not.toBe(scenes2);
|
||||
});
|
||||
|
||||
it('pops scenes', () => {
|
||||
// Transition from ['1', '2', '3'] to ['1', '2'].
|
||||
const scenes = testTransition([['1', '2', '3'], ['1', '2']]);
|
||||
|
||||
expect(scenes).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('replaces scenes', () => {
|
||||
const scenes = testTransition([['1', '2'], ['3']]);
|
||||
|
||||
expect(scenes).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('revives scenes', () => {
|
||||
const scenes = testTransition([['1', '2'], ['3'], ['2']]);
|
||||
|
||||
expect(scenes).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_1',
|
||||
route: {
|
||||
key: '1',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
isActive: true,
|
||||
isStale: false,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_2',
|
||||
route: {
|
||||
key: '2',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
index: 0,
|
||||
isActive: false,
|
||||
isStale: true,
|
||||
descriptor: MOCK_DESCRIPTOR,
|
||||
key: 'scene_3',
|
||||
route: {
|
||||
key: '3',
|
||||
routeName: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
/* eslint react/display-name:0 */
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Transitioner from '../Transitioner';
|
||||
|
||||
describe('Transitioner', () => {
|
||||
it('should not trigger onTransitionStart and onTransitionEnd when route params are changed', () => {
|
||||
const onTransitionStartCallback = jest.fn();
|
||||
const onTransitionEndCallback = jest.fn();
|
||||
|
||||
const transitionerProps = {
|
||||
configureTransition: (transitionProps, prevTransitionProps) => ({}),
|
||||
navigation: {
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', routeName: 'Foo' },
|
||||
{ key: '2', routeName: 'Bar' },
|
||||
],
|
||||
},
|
||||
goBack: () => false,
|
||||
dispatch: () => false,
|
||||
setParams: () => false,
|
||||
navigate: () => false,
|
||||
},
|
||||
render: () => <div />,
|
||||
onTransitionStart: onTransitionStartCallback,
|
||||
onTransitionEnd: onTransitionEndCallback,
|
||||
};
|
||||
|
||||
const nextTransitionerProps = {
|
||||
...transitionerProps,
|
||||
navigation: {
|
||||
...transitionerProps.navigation,
|
||||
state: {
|
||||
index: 0,
|
||||
routes: [
|
||||
{ key: '1', routeName: 'Foo', params: { name: 'Zoom' } },
|
||||
{ key: '2', routeName: 'Bar' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const component = renderer.create(<Transitioner {...transitionerProps} />);
|
||||
component.update(<Transitioner {...nextTransitionerProps} />);
|
||||
expect(onTransitionStartCallback).not.toBeCalled();
|
||||
expect(onTransitionEndCallback).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import hoistStatics from 'hoist-non-react-statics';
|
||||
import invariant from '../utils/invariant';
|
||||
import { NavigationConsumer } from './NavigationContext';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import hoistStatics from 'hoist-non-react-statics';
|
||||
import invariant from '../utils/invariant';
|
||||
import withNavigation from './withNavigation';
|
||||
|
||||
Reference in New Issue
Block a user