mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 20:35:19 +08:00
Move react-native code from react-navigation repo
This commit is contained in:
3
packages/native/.babelrc
Normal file
3
packages/native/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["react-native"]
|
||||
}
|
||||
62
packages/native/.circleci/config.yml
Normal file
62
packages/native/.circleci/config.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
version: 2
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: circleci/node:7.10
|
||||
working_directory: ~/project
|
||||
|
||||
jobs:
|
||||
install-dependencies:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "package.json" }}
|
||||
- v1-dependencies-
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-example-{{ checksum "example/package.json" }}
|
||||
- v1-dependencies-example-
|
||||
- run: |
|
||||
yarn install
|
||||
yarn install --cwd example
|
||||
- save_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
paths: node_modules
|
||||
- save_cache:
|
||||
key: v1-dependencies-example-{{ checksum "example/package.json" }}
|
||||
paths: example/node_modules
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
lint:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run: |
|
||||
yarn run lint
|
||||
unit-tests:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/project
|
||||
- run: yarn test -- --coverage
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
destination: coverage
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-and-test:
|
||||
jobs:
|
||||
- install-dependencies
|
||||
- lint:
|
||||
requires:
|
||||
- install-dependencies
|
||||
- unit-tests:
|
||||
requires:
|
||||
- install-dependencies
|
||||
21
packages/native/.editorconfig
Normal file
21
packages/native/.editorconfig
Normal file
@@ -0,0 +1,21 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
3
packages/native/.eslintignore
Normal file
3
packages/native/.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
jest-setup.js
|
||||
16
packages/native/.eslintrc
Normal file
16
packages/native/.eslintrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "eslint-config-satya164",
|
||||
|
||||
"plugins": ["react-native-globals"],
|
||||
|
||||
"env": {
|
||||
"es6": true,
|
||||
"react-native-globals/all": true,
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"import/no-unresolved": "off",
|
||||
"react/sort-comp": "off",
|
||||
"jest/no-disabled-tests": "off",
|
||||
}
|
||||
}
|
||||
53
packages/native/.gitignore
vendored
Normal file
53
packages/native/.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# XDE
|
||||
.expo/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
tsconfig.json
|
||||
jsconfig.json
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
# Android/IJ
|
||||
#
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
android/keystores/debug.keystore
|
||||
|
||||
# Build
|
||||
dist/
|
||||
21
packages/native/LICENSE.md
Normal file
21
packages/native/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 React Native Community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
packages/native/README.md
Normal file
9
packages/native/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# React Navigation Native
|
||||
|
||||
[](https://circleci.com/gh/react-navigation/react-navigation-native/tree/master)
|
||||
|
||||
React Native support for React Navigation
|
||||
|
||||
## Docs
|
||||
|
||||
Documentation can be found on the [React Navigation website](https://reactnavigation.org/docs/).
|
||||
13
packages/native/example/.babelrc
Normal file
13
packages/native/example/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": ["expo"],
|
||||
"plugins": [
|
||||
[
|
||||
"module-resolver",
|
||||
{
|
||||
"alias": {
|
||||
"@react-navigation/native": "../src"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
6
packages/native/example/.buckconfig
Normal file
6
packages/native/example/.buckconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
[android]
|
||||
target = Google Inc.:Google APIs:23
|
||||
|
||||
[maven_repositories]
|
||||
central = https://repo1.maven.org/maven2
|
||||
11
packages/native/example/.eslintrc
Normal file
11
packages/native/example/.eslintrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../.eslintrc",
|
||||
|
||||
"settings": {
|
||||
"import/core-modules": [ "expo", "react-navigation-stack" ]
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"react/prop-types": "off"
|
||||
}
|
||||
}
|
||||
1
packages/native/example/.watchmanconfig
Normal file
1
packages/native/example/.watchmanconfig
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
28
packages/native/example/App.js
Normal file
28
packages/native/example/App.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Expo from 'expo';
|
||||
import { View, Text } from 'react-native';
|
||||
import { createNavigationContainer } from '@react-navigation/native';
|
||||
import { createSwitchNavigator } from '@react-navigation/core';
|
||||
|
||||
class Home extends React.Component {
|
||||
static navigationOptions = {
|
||||
title: 'Examples',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text>Examples</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AppNavigator = createSwitchNavigator({
|
||||
Home,
|
||||
Profile: Home,
|
||||
});
|
||||
|
||||
const App = createNavigationContainer(AppNavigator);
|
||||
|
||||
Expo.registerRootComponent(App);
|
||||
8
packages/native/example/README.md
Normal file
8
packages/native/example/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Run the example
|
||||
|
||||
- [View it with Expo](https://expo.io/@satya164/react-navigation-tabs-demos)
|
||||
- Run the example locally
|
||||
+ Clone the repository and `cd` to this directory
|
||||
+ Run `yarn` to install the dependencies
|
||||
+ Run `yarn start` to start the packager
|
||||
+ Scan the QR Code with the Expo app
|
||||
17
packages/native/example/app.json
Normal file
17
packages/native/example/app.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "React Navigation Stack Example",
|
||||
"description": "Demonstrates the various capabilities of react-navigation-stack",
|
||||
"slug": "react-navigation-stack-demo",
|
||||
"sdkVersion": "30.0.0",
|
||||
"version": "1.0.0",
|
||||
"primaryColor": "#2196f3",
|
||||
"packagerOpts": {
|
||||
"assetExts": [
|
||||
"ttf"
|
||||
],
|
||||
"config": "./rn-cli.config.js",
|
||||
"projectRoots": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
34
packages/native/example/package.json
Normal file
34
packages/native/example/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "native-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"postinstall": "rm -rf node_modules/expo-react-native-adapter/node_modules/react && rm -rf node_modules/expo-gl/node_modules/react"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^3.0.0-alpha.4",
|
||||
"expo": "~30.0.0",
|
||||
"hoist-non-react-statics": "^2.5.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "16.3.1",
|
||||
"react-native": "~0.55.4",
|
||||
"react-native-paper": "2.0.0-alpha.4",
|
||||
"react-native-screens": "^1.0.0-alpha.9",
|
||||
"react-navigation": "^2.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^3.0.0",
|
||||
"babel-preset-expo": "^4.0.0",
|
||||
"glob-to-regexp": "^0.3.0"
|
||||
},
|
||||
"main": "App.js",
|
||||
"resolutions": {
|
||||
"**/react": "16.3.1",
|
||||
"**/prop-types": "15.6.0",
|
||||
"**/react-lifecycles-compat": "3.0.4",
|
||||
"**/hoist-non-react-statics": "2.5.0"
|
||||
}
|
||||
}
|
||||
23
packages/native/example/rn-cli.config.js
Normal file
23
packages/native/example/rn-cli.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
|
||||
const path = require('path');
|
||||
const glob = require('glob-to-regexp');
|
||||
const blacklist = require('metro/src/blacklist');
|
||||
const pak = require('../package.json');
|
||||
const pak2 = require('./package.json');
|
||||
|
||||
const dependencies = Object.keys(pak.dependencies);
|
||||
const localDependencies = Object.keys(pak2.dependencies);
|
||||
const peerDependencies = Object.keys(pak.peerDependencies);
|
||||
|
||||
module.exports = {
|
||||
getProjectRoots() {
|
||||
return [__dirname, path.resolve(__dirname, '..')];
|
||||
},
|
||||
getProvidesModuleNodeModules() {
|
||||
return [...dependencies, ...localDependencies, ...peerDependencies];
|
||||
},
|
||||
getBlacklistRE() {
|
||||
return blacklist([glob(`${path.resolve(__dirname, '..')}/node_modules/*`)]);
|
||||
},
|
||||
};
|
||||
5532
packages/native/example/yarn.lock
Normal file
5532
packages/native/example/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
45
packages/native/jest-setup.js
Normal file
45
packages/native/jest-setup.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @flow
|
||||
* eslint-env jest
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
// See https://github.com/facebook/jest/issues/2208
|
||||
jest.mock('Linking', () => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
openURL: jest.fn(),
|
||||
canOpenURL: jest.fn(),
|
||||
getInitialURL: jest
|
||||
.fn()
|
||||
.mockImplementation((value: string) => Promise.resolve(value)),
|
||||
}));
|
||||
|
||||
// See https://github.com/facebook/react-native/issues/11659
|
||||
jest.mock('ScrollView', () => {
|
||||
// $FlowExpectedError
|
||||
const RealComponent = require.requireActual('ScrollView');
|
||||
class ScrollView extends RealComponent {
|
||||
scrollTo = () => {};
|
||||
}
|
||||
return ScrollView;
|
||||
});
|
||||
|
||||
// Mock setState so it waits using setImmediate before actually being called,
|
||||
// so we can use jest's mock timers to control it.
|
||||
// setState in the test renderer is sync instead of async like react and react-native.
|
||||
// This doesn't work with our NavigationContainer tests which test react-navigation's
|
||||
// behaviour against the async nature of setState.
|
||||
const setState = React.Component.prototype.setState;
|
||||
// $FlowExpectedError
|
||||
Object.defineProperty(React.Component.prototype, 'setState', {
|
||||
value: function() {
|
||||
setImmediate(() => {
|
||||
setState.apply(this, arguments);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// $FlowExpectedError
|
||||
Date.now = jest.fn(() => 0);
|
||||
90
packages/native/package.json
Normal file
90
packages/native/package.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "@react-navigation/native",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"description": "React Native support for React Navigation",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist/",
|
||||
"src/",
|
||||
"LICENSE.md",
|
||||
"README.md"
|
||||
],
|
||||
"react-native": "src/index.js",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix",
|
||||
"build": "babel --no-babelrc --plugins=syntax-jsx,syntax-class-properties,syntax-object-rest-spread,transform-flow-strip-types src --copy-files --out-dir dist --ignore '**/__tests__/**'",
|
||||
"prepare": "yarn build"
|
||||
},
|
||||
"keywords": [
|
||||
"react-native",
|
||||
"react-navigation",
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/react-navigation/react-navigation-native.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/react-navigation/react-navigation-native/issues"
|
||||
},
|
||||
"homepage": "https://github.com/react-navigation/react-navigation-native#readme",
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^3.0.0-alpha.4",
|
||||
"hoist-non-react-statics": "^3.0.1",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/vector-icons": "^6.2.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-plugin-syntax-class-properties": "^6.13.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||
"babel-preset-react-native": "^4.0.0",
|
||||
"eslint": "^4.12.1",
|
||||
"eslint-config-satya164": "^1.0.1",
|
||||
"eslint-plugin-react-native-globals": "^0.1.0",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "^22.1.3",
|
||||
"prettier": "^1.8.2",
|
||||
"react": "16.3.1",
|
||||
"react-dom": "16.3.1",
|
||||
"react-native": "~0.55.4",
|
||||
"react-navigation": "^2.11.2",
|
||||
"react-test-renderer": "16.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": "^1.0.0",
|
||||
"react-native-reanimated": "^1.0.0 || ^1.0.0-alpha",
|
||||
"react-native-screens": "^1.0.0 || ^1.0.0-alpha",
|
||||
"react-navigation": ">=2.0 || ^2.0.0-beta"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native",
|
||||
"testRegex": "/__tests__/[^/]+-test\\.js$",
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"jest-setup.js"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/example/"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|react-navigation|@react-navigation/core)"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
79
packages/native/src/ResourceSavingSceneView.js
Normal file
79
packages/native/src/ResourceSavingSceneView.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import { SceneView } from '@react-navigation/core';
|
||||
|
||||
const FAR_FAR_AWAY = 3000; // this should be big enough to move the whole view out of its container
|
||||
|
||||
class ResourceSavingSceneView extends React.PureComponent {
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.isFocused && !prevState.awake) {
|
||||
return { awake: true };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
awake: props.lazy ? props.isFocused : true,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { awake } = this.state;
|
||||
const {
|
||||
isFocused,
|
||||
childNavigation,
|
||||
navigation,
|
||||
removeClippedSubviews,
|
||||
lazy,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
Platform.OS === 'android'
|
||||
? removeClippedSubviews
|
||||
: !isFocused && removeClippedSubviews
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
this._mustAlwaysBeVisible() || isFocused
|
||||
? styles.innerAttached
|
||||
: styles.innerDetached
|
||||
}
|
||||
>
|
||||
{awake ? <SceneView {...rest} navigation={childNavigation} /> : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_mustAlwaysBeVisible = () => {
|
||||
return this.props.animationEnabled || this.props.swipeEnabled;
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
innerAttached: {
|
||||
flex: 1,
|
||||
},
|
||||
innerDetached: {
|
||||
flex: 1,
|
||||
top: FAR_FAR_AWAY,
|
||||
},
|
||||
});
|
||||
|
||||
export default polyfill(ResourceSavingSceneView);
|
||||
277
packages/native/src/__tests__/NavigationContainer-test.js
Normal file
277
packages/native/src/__tests__/NavigationContainer-test.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import createNavigationContainer, {
|
||||
_TESTING_ONLY_reset_container_count,
|
||||
} from '../createNavigationContainer';
|
||||
|
||||
import {
|
||||
NavigationActions,
|
||||
createNavigator,
|
||||
StackRouter,
|
||||
SwitchView,
|
||||
} from '@react-navigation/core';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
const router = StackRouter(routeConfigMap, stackConfig);
|
||||
return createNavigationContainer(
|
||||
createNavigator(SwitchView, router, stackConfig)
|
||||
);
|
||||
}
|
||||
|
||||
describe('NavigationContainer', () => {
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
const FooScreen = () => <div />;
|
||||
const BarScreen = () => <div />;
|
||||
const BazScreen = () => <div />;
|
||||
const CarScreen = () => <div />;
|
||||
const DogScreen = () => <div />;
|
||||
const ElkScreen = () => <div />;
|
||||
const Stack = createStackNavigator(
|
||||
{
|
||||
foo: {
|
||||
screen: FooScreen,
|
||||
},
|
||||
bar: {
|
||||
screen: BarScreen,
|
||||
},
|
||||
baz: {
|
||||
screen: BazScreen,
|
||||
},
|
||||
car: {
|
||||
screen: CarScreen,
|
||||
},
|
||||
dog: {
|
||||
screen: DogScreen,
|
||||
},
|
||||
elk: {
|
||||
screen: ElkScreen,
|
||||
},
|
||||
},
|
||||
{
|
||||
initialRouteName: 'foo',
|
||||
}
|
||||
);
|
||||
const NavigationContainer = createNavigationContainer(Stack);
|
||||
|
||||
describe('state.nav', () => {
|
||||
it("should be preloaded with the router's initial state", () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
expect(navigationContainer.state.nav).toMatchObject({ index: 0 });
|
||||
expect(navigationContainer.state.nav.routes).toBeInstanceOf(Array);
|
||||
expect(navigationContainer.state.nav.routes.length).toBe(1);
|
||||
expect(navigationContainer.state.nav.routes[0]).toMatchObject({
|
||||
routeName: 'foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatch', () => {
|
||||
it('returns true when given a valid action', () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'bar' })
|
||||
)
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when given an invalid action', () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(navigationContainer.dispatch(NavigationActions.back())).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('updates state.nav with an action by the next tick', () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'bar' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Fake the passing of a tick
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(navigationContainer.state.nav).toMatchObject({
|
||||
index: 1,
|
||||
routes: [{ routeName: 'foo' }, { routeName: 'bar' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not discard actions when called twice in one tick', () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
const initialState = JSON.parse(
|
||||
JSON.stringify(navigationContainer.state.nav)
|
||||
);
|
||||
|
||||
// First dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'bar' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Make sure that the test runner has NOT synchronously applied setState before the tick
|
||||
expect(navigationContainer.state.nav).toMatchObject(initialState);
|
||||
|
||||
// Second dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'baz' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Fake the passing of a tick
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(navigationContainer.state.nav).toMatchObject({
|
||||
index: 2,
|
||||
routes: [
|
||||
{ routeName: 'foo' },
|
||||
{ routeName: 'bar' },
|
||||
{ routeName: 'baz' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not discard actions when called more than 2 times in one tick', () => {
|
||||
const navigationContainer = renderer
|
||||
.create(<NavigationContainer />)
|
||||
.getInstance();
|
||||
const initialState = JSON.parse(
|
||||
JSON.stringify(navigationContainer.state.nav)
|
||||
);
|
||||
|
||||
// First dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'bar' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Make sure that the test runner has NOT synchronously applied setState before the tick
|
||||
expect(navigationContainer.state.nav).toMatchObject(initialState);
|
||||
|
||||
// Second dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'baz' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Third dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'car' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Fourth dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'dog' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Fifth dispatch
|
||||
expect(
|
||||
navigationContainer.dispatch(
|
||||
NavigationActions.navigate({ routeName: 'elk' })
|
||||
)
|
||||
).toEqual(true);
|
||||
|
||||
// Fake the passing of a tick
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(navigationContainer.state.nav).toMatchObject({
|
||||
index: 5,
|
||||
routes: [
|
||||
{ routeName: 'foo' },
|
||||
{ routeName: 'bar' },
|
||||
{ routeName: 'baz' },
|
||||
{ routeName: 'car' },
|
||||
{ routeName: 'dog' },
|
||||
{ routeName: 'elk' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('warnings', () => {
|
||||
function spyConsole() {
|
||||
let spy = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spy.console = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
spy.console.mockRestore();
|
||||
});
|
||||
|
||||
return spy;
|
||||
}
|
||||
|
||||
describe('detached navigators', () => {
|
||||
beforeEach(() => {
|
||||
_TESTING_ONLY_reset_container_count();
|
||||
});
|
||||
|
||||
let spy = spyConsole();
|
||||
|
||||
it('warns when you render more than one container explicitly', () => {
|
||||
class BlankScreen extends React.Component {
|
||||
render() {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
class RootScreen extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<ChildNavigator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ChildNavigator = createNavigationContainer(
|
||||
createStackNavigator({
|
||||
Child: BlankScreen,
|
||||
})
|
||||
);
|
||||
|
||||
const RootStack = createNavigationContainer(
|
||||
createStackNavigator({
|
||||
Root: RootScreen,
|
||||
})
|
||||
);
|
||||
|
||||
renderer.create(<RootStack />).toJSON();
|
||||
expect(spy).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NavigationContainer warnings detached navigators warns when you render more than one container explicitly 1`] = `
|
||||
Object {
|
||||
"console": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
"You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: https://reactnavigation.org/docs/common-mistakes.html#explicitly-rendering-more-than-one-navigator",
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`it adds isLandscape to props 1`] = `
|
||||
<View
|
||||
isLandscape={false}
|
||||
/>
|
||||
`;
|
||||
15
packages/native/src/__tests__/withOrientation-test.js
Normal file
15
packages/native/src/__tests__/withOrientation-test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import renderer from 'react-test-renderer';
|
||||
import withOrientation, { isOrientationLandscape } from '../withOrientation';
|
||||
|
||||
test('it adds isLandscape to props', () => {
|
||||
const WrappedComponent = withOrientation(View);
|
||||
const rendered = renderer.create(<WrappedComponent />).toJSON();
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('calculates orientation correctly', () => {
|
||||
const isLandscape = isOrientationLandscape({ width: 10, height: 1 });
|
||||
expect(isLandscape).toBeTruthy();
|
||||
});
|
||||
57
packages/native/src/createKeyboardAwareNavigator.js
Normal file
57
packages/native/src/createKeyboardAwareNavigator.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
export default (Navigator, navigatorConfig) =>
|
||||
class KeyboardAwareNavigator extends React.Component {
|
||||
static router = Navigator.router;
|
||||
_previouslyFocusedTextInput = null;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Navigator
|
||||
{...this.props}
|
||||
onGestureBegin={this._handleGestureBegin}
|
||||
onGestureCanceled={this._handleGestureCanceled}
|
||||
onGestureFinish={this._handleGestureFinish}
|
||||
onTransitionStart={this._handleTransitionStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_handleGestureBegin = () => {
|
||||
this._previouslyFocusedTextInput = TextInput.State.currentlyFocusedField();
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.blurTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
};
|
||||
|
||||
_handleGestureCanceled = () => {
|
||||
if (this._previouslyFocusedTextInput) {
|
||||
TextInput.State.focusTextInput(this._previouslyFocusedTextInput);
|
||||
}
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
};
|
||||
|
||||
_handleGestureFinish = () => {
|
||||
this._previouslyFocusedTextInput = null;
|
||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||
};
|
||||
|
||||
_handleTransitionStart = (transitionProps, prevTransitionProps) => {
|
||||
// TODO: We should not even have received the transition start event
|
||||
// in the case where the index did not change, I believe. We
|
||||
// should revisit this after 2.0 release.
|
||||
if (transitionProps.index !== prevTransitionProps.index) {
|
||||
const currentField = TextInput.State.currentlyFocusedField();
|
||||
if (currentField) {
|
||||
TextInput.State.blurTextInput(currentField);
|
||||
}
|
||||
}
|
||||
|
||||
const onTransitionStart =
|
||||
this.props.onTransitionStart || navigatorConfig.onTransitionStart;
|
||||
onTransitionStart &&
|
||||
onTransitionStart(transitionProps, prevTransitionProps);
|
||||
};
|
||||
};
|
||||
385
packages/native/src/createNavigationContainer.js
Normal file
385
packages/native/src/createNavigationContainer.js
Normal file
@@ -0,0 +1,385 @@
|
||||
import React from 'react';
|
||||
import { AsyncStorage, Linking, Platform, BackHandler } from 'react-native';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import {
|
||||
NavigationActions,
|
||||
pathUtils,
|
||||
getNavigation,
|
||||
} from '@react-navigation/core';
|
||||
import invariant from './utils/invariant';
|
||||
import docsUrl from './utils/docsUrl';
|
||||
|
||||
const { urlToPathAndParams } = pathUtils;
|
||||
|
||||
function isStateful(props) {
|
||||
return !props.navigation;
|
||||
}
|
||||
|
||||
function validateProps(props) {
|
||||
if (isStateful(props)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { navigation, screenProps, ...containerProps } = props;
|
||||
|
||||
const keys = Object.keys(containerProps);
|
||||
|
||||
if (keys.length !== 0) {
|
||||
throw new Error(
|
||||
'This navigator has both navigation and container props, so it is ' +
|
||||
`unclear if it should own its own state. Remove props: "${keys.join(
|
||||
', '
|
||||
)}" ` +
|
||||
'if the navigator should get its state from the navigation prop. If the ' +
|
||||
'navigator should maintain its own state, do not pass a navigation prop.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Track the number of stateful container instances. Warn if >0 and not using the
|
||||
// detached prop to explicitly acknowledge the behavior. We should deprecated implicit
|
||||
// stateful navigation containers in a future release and require a provider style pattern
|
||||
// instead in order to eliminate confusion entirely.
|
||||
let _statefulContainerCount = 0;
|
||||
export function _TESTING_ONLY_reset_container_count() {
|
||||
_statefulContainerCount = 0;
|
||||
}
|
||||
|
||||
// We keep a global flag to catch errors during the state persistence hydrating scenario.
|
||||
// The innermost navigator who catches the error will dispatch a new init action.
|
||||
let _reactNavigationIsHydratingState = false;
|
||||
// Unfortunate to use global state here, but it seems necessesary for the time
|
||||
// being. There seems to be some problems with cascading componentDidCatch
|
||||
// handlers. Ideally the inner non-stateful navigator catches the error and
|
||||
// re-throws it, to be caught by the top-level stateful navigator.
|
||||
|
||||
/**
|
||||
* Create an HOC that injects the navigation and manages the navigation state
|
||||
* in case it's not passed from above.
|
||||
* This allows to use e.g. the StackNavigator and TabNavigator as root-level
|
||||
* components.
|
||||
*/
|
||||
export default function createNavigationContainer(Component) {
|
||||
class NavigationContainer extends React.Component {
|
||||
subs = null;
|
||||
|
||||
static router = Component.router;
|
||||
static navigationOptions = null;
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
validateProps(nextProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
_actionEventSubscribers = new Set();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
validateProps(props);
|
||||
|
||||
this._initialAction = NavigationActions.init();
|
||||
|
||||
if (this._isStateful()) {
|
||||
this.subs = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
if (!this._isMounted) {
|
||||
this.subs && this.subs.remove();
|
||||
} else {
|
||||
// dispatch returns true if the action results in a state change,
|
||||
// and false otherwise. This maps well to what BackHandler expects
|
||||
// from a callback -- true if handled, false if not handled
|
||||
return this.dispatch(NavigationActions.back());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
nav:
|
||||
this._isStateful() && !props.persistenceKey
|
||||
? Component.router.getStateForAction(this._initialAction)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
_renderLoading() {
|
||||
return this.props.renderLoadingExperimental
|
||||
? this.props.renderLoadingExperimental()
|
||||
: null;
|
||||
}
|
||||
|
||||
_isStateful() {
|
||||
return isStateful(this.props);
|
||||
}
|
||||
|
||||
_validateProps(props) {
|
||||
if (this._isStateful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { navigation, screenProps, ...containerProps } = props;
|
||||
|
||||
const keys = Object.keys(containerProps);
|
||||
|
||||
if (keys.length !== 0) {
|
||||
throw new Error(
|
||||
'This navigator has both navigation and container props, so it is ' +
|
||||
`unclear if it should own its own state. Remove props: "${keys.join(
|
||||
', '
|
||||
)}" ` +
|
||||
'if the navigator should get its state from the navigation prop. If the ' +
|
||||
'navigator should maintain its own state, do not pass a navigation prop.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_handleOpenURL = ({ url }) => {
|
||||
const { enableURLHandling, uriPrefix } = this.props;
|
||||
if (enableURLHandling === false) {
|
||||
return;
|
||||
}
|
||||
const parsedUrl = urlToPathAndParams(url, uriPrefix);
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const action = Component.router.getActionForPathAndParams(path, params);
|
||||
if (action) {
|
||||
this.dispatch(action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onNavigationStateChange(prevNav, nav, action) {
|
||||
if (
|
||||
typeof this.props.onNavigationStateChange === 'undefined' &&
|
||||
this._isStateful() &&
|
||||
!!process.env.REACT_NAV_LOGGING
|
||||
) {
|
||||
/* eslint-disable no-console */
|
||||
if (console.group) {
|
||||
console.group('Navigation Dispatch: ');
|
||||
console.log('Action: ', action);
|
||||
console.log('New State: ', nav);
|
||||
console.log('Last State: ', prevNav);
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.log('Navigation Dispatch: ', {
|
||||
action,
|
||||
newState: nav,
|
||||
lastState: prevNav,
|
||||
});
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.props.onNavigationStateChange === 'function') {
|
||||
this.props.onNavigationStateChange(prevNav, nav, action);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Clear cached _navState every tick
|
||||
if (this._navState === this.state.nav) {
|
||||
this._navState = null;
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (!this._isStateful()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (__DEV__ && !this.props.detached) {
|
||||
if (_statefulContainerCount > 0) {
|
||||
// Temporarily only show this on iOS due to this issue:
|
||||
// https://github.com/react-navigation/react-navigation/issues/4196#issuecomment-390827829
|
||||
if (Platform.OS === 'ios') {
|
||||
console.warn(
|
||||
`You should only render one navigator explicitly in your app, and other navigators should by rendered by including them in that navigator. Full details at: ${docsUrl(
|
||||
'common-mistakes.html#explicitly-rendering-more-than-one-navigator'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_statefulContainerCount++;
|
||||
Linking.addEventListener('url', this._handleOpenURL);
|
||||
|
||||
// Pull out anything that can impact state
|
||||
const { persistenceKey, uriPrefix, enableURLHandling } = this.props;
|
||||
let parsedUrl = null;
|
||||
let startupStateJSON = null;
|
||||
if (enableURLHandling !== false) {
|
||||
startupStateJSON =
|
||||
persistenceKey && (await AsyncStorage.getItem(persistenceKey));
|
||||
const url = await Linking.getInitialURL();
|
||||
parsedUrl = url && urlToPathAndParams(url, uriPrefix);
|
||||
}
|
||||
|
||||
// Initialize state. This must be done *after* any async code
|
||||
// so we don't end up with a different value for this.state.nav
|
||||
// due to changes while async function was resolving
|
||||
let action = this._initialAction;
|
||||
let startupState = this.state.nav;
|
||||
if (!startupState) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Init new Navigation State');
|
||||
startupState = Component.router.getStateForAction(action);
|
||||
}
|
||||
|
||||
// Pull persisted state from AsyncStorage
|
||||
if (startupStateJSON) {
|
||||
try {
|
||||
startupState = JSON.parse(startupStateJSON);
|
||||
_reactNavigationIsHydratingState = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Pull state out of URL
|
||||
if (parsedUrl) {
|
||||
const { path, params } = parsedUrl;
|
||||
const urlAction = Component.router.getActionForPathAndParams(
|
||||
path,
|
||||
params
|
||||
);
|
||||
if (urlAction) {
|
||||
!!process.env.REACT_NAV_LOGGING &&
|
||||
console.log('Applying Navigation Action for Initial URL:', url);
|
||||
action = urlAction;
|
||||
startupState = Component.router.getStateForAction(
|
||||
urlAction,
|
||||
startupState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchActions = () =>
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: this.state.nav,
|
||||
lastState: null,
|
||||
})
|
||||
);
|
||||
|
||||
if (startupState === this.state.nav) {
|
||||
dispatchActions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ nav: startupState }, () => {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
dispatchActions();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidCatch(e, errorInfo) {
|
||||
if (_reactNavigationIsHydratingState) {
|
||||
_reactNavigationIsHydratingState = false;
|
||||
console.warn(
|
||||
'Uncaught exception while starting app from persisted navigation state! Trying to render again with a fresh navigation state..'
|
||||
);
|
||||
this.dispatch(NavigationActions.init());
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
_persistNavigationState = async nav => {
|
||||
const { persistenceKey } = this.props;
|
||||
if (!persistenceKey) {
|
||||
return;
|
||||
}
|
||||
await AsyncStorage.setItem(persistenceKey, JSON.stringify(nav));
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
Linking.removeEventListener('url', this._handleOpenURL);
|
||||
this.subs && this.subs.remove();
|
||||
|
||||
if (this._isStateful()) {
|
||||
_statefulContainerCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-tick temporary storage for state.nav
|
||||
|
||||
dispatch = action => {
|
||||
if (this.props.navigation) {
|
||||
return this.props.navigation.dispatch(action);
|
||||
}
|
||||
|
||||
// navState will have the most up-to-date value, because setState sometimes behaves asyncronously
|
||||
this._navState = this._navState || this.state.nav;
|
||||
const lastNavState = this._navState;
|
||||
invariant(lastNavState, 'should be set in constructor if stateful');
|
||||
const reducedState = Component.router.getStateForAction(
|
||||
action,
|
||||
lastNavState
|
||||
);
|
||||
const navState = reducedState === null ? lastNavState : reducedState;
|
||||
|
||||
const dispatchActionEvents = () => {
|
||||
this._actionEventSubscribers.forEach(subscriber =>
|
||||
subscriber({
|
||||
type: 'action',
|
||||
action,
|
||||
state: navState,
|
||||
lastState: lastNavState,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (reducedState === null) {
|
||||
// The router will return null when action has been handled and the state hasn't changed.
|
||||
// dispatch returns true when something has been handled.
|
||||
dispatchActionEvents();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (navState !== lastNavState) {
|
||||
// Cache updates to state.nav during the tick to ensure that subsequent calls will not discard this change
|
||||
this._navState = navState;
|
||||
this.setState({ nav: navState }, () => {
|
||||
this._onNavigationStateChange(lastNavState, navState, action);
|
||||
dispatchActionEvents();
|
||||
this._persistNavigationState(navState);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatchActionEvents();
|
||||
return false;
|
||||
};
|
||||
|
||||
_getScreenProps = () => this.props.screenProps;
|
||||
|
||||
render() {
|
||||
let navigation = this.props.navigation;
|
||||
if (this._isStateful()) {
|
||||
const navState = this.state.nav;
|
||||
if (!navState) {
|
||||
return this._renderLoading();
|
||||
}
|
||||
if (!this._navigation || this._navigation.state !== navState) {
|
||||
this._navigation = getNavigation(
|
||||
Component.router,
|
||||
navState,
|
||||
this.dispatch,
|
||||
this._actionEventSubscribers,
|
||||
this._getScreenProps,
|
||||
() => this._navigation
|
||||
);
|
||||
}
|
||||
navigation = this._navigation;
|
||||
}
|
||||
invariant(navigation, 'failed to get navigation');
|
||||
return <Component {...this.props} navigation={navigation} />;
|
||||
}
|
||||
}
|
||||
|
||||
return polyfill(NavigationContainer);
|
||||
}
|
||||
18
packages/native/src/index.js
Normal file
18
packages/native/src/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint global-require: 0 */
|
||||
|
||||
module.exports = {
|
||||
get createNavigationContainer() {
|
||||
return require('./createNavigationContainer').default;
|
||||
},
|
||||
get createKeyboardAwareNavigator() {
|
||||
return require('./createKeyboardAwareNavigator').default;
|
||||
},
|
||||
|
||||
get ResourceSavingSceneView() {
|
||||
return require('./ResourceSavingSceneView').default;
|
||||
},
|
||||
|
||||
get withOrientation() {
|
||||
return require('./withOrientation').default;
|
||||
},
|
||||
};
|
||||
3
packages/native/src/utils/docsUrl.js
Normal file
3
packages/native/src/utils/docsUrl.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function docsUrl(path) {
|
||||
return `https://reactnavigation.org/docs/${path}`;
|
||||
}
|
||||
47
packages/native/src/utils/invariant.js
Normal file
47
packages/native/src/utils/invariant.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Use invariant() to assert state which your program assumes to be true.
|
||||
*
|
||||
* Provide sprintf-style format (only %s is supported) and arguments
|
||||
* to provide information about what broke and what you were
|
||||
* expecting.
|
||||
*
|
||||
* The invariant message will be stripped in production, but the invariant
|
||||
* will remain to ensure logic does not differ in production.
|
||||
*/
|
||||
|
||||
var validateFormat = function(format) {};
|
||||
|
||||
if (__DEV__) {
|
||||
validateFormat = function(format) {
|
||||
if (format === undefined) {
|
||||
throw new Error('invariant requires an error message argument');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function invariant(condition, format, a, b, c, d, e, f) {
|
||||
validateFormat(format);
|
||||
|
||||
if (!condition) {
|
||||
var error;
|
||||
if (format === undefined) {
|
||||
error = new Error(
|
||||
'Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.'
|
||||
);
|
||||
} else {
|
||||
var args = [a, b, c, d, e, f];
|
||||
var argIndex = 0;
|
||||
error = new Error(
|
||||
format.replace(/%s/g, function() {
|
||||
return args[argIndex++];
|
||||
})
|
||||
);
|
||||
error.name = 'Invariant Violation';
|
||||
}
|
||||
|
||||
error.framesToPop = 1; // we don't care about invariant's own frame
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = invariant;
|
||||
35
packages/native/src/withOrientation.js
Normal file
35
packages/native/src/withOrientation.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Dimensions } from 'react-native';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
|
||||
export const isOrientationLandscape = ({ width, height }) => width > height;
|
||||
|
||||
export default function(WrappedComponent) {
|
||||
class withOrientation extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const isLandscape = isOrientationLandscape(Dimensions.get('window'));
|
||||
this.state = { isLandscape };
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this.handleOrientationChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.handleOrientationChange);
|
||||
}
|
||||
|
||||
handleOrientationChange = ({ window }) => {
|
||||
const isLandscape = isOrientationLandscape(window);
|
||||
this.setState({ isLandscape });
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} {...this.state} />;
|
||||
}
|
||||
}
|
||||
|
||||
return hoistNonReactStatic(withOrientation, WrappedComponent);
|
||||
}
|
||||
7444
packages/native/yarn.lock
Normal file
7444
packages/native/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user