Move react-native code from react-navigation repo

This commit is contained in:
Eric Vicenti
2018-10-11 16:52:38 -07:00
parent 73e2e0b5b0
commit 704c20b135
32 changed files with 14376 additions and 0 deletions

3
packages/native/.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
"presets": ["react-native"]
}

View 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

View 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

View File

@@ -0,0 +1,3 @@
node_modules/
dist/
jest-setup.js

16
packages/native/.eslintrc Normal file
View 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
View 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/

View 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.

View File

@@ -0,0 +1,9 @@
# React Navigation Native
[![CircleCI badge](https://circleci.com/gh/react-navigation/react-navigation-native/tree/master.svg?style=shield)](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/).

View File

@@ -0,0 +1,13 @@
{
"presets": ["expo"],
"plugins": [
[
"module-resolver",
{
"alias": {
"@react-navigation/native": "../src"
}
}
]
]
}

View File

@@ -0,0 +1,6 @@
[android]
target = Google Inc.:Google APIs:23
[maven_repositories]
central = https://repo1.maven.org/maven2

View File

@@ -0,0 +1,11 @@
{
"extends": "../.eslintrc",
"settings": {
"import/core-modules": [ "expo", "react-navigation-stack" ]
},
"rules": {
"react/prop-types": "off"
}
}

View File

@@ -0,0 +1 @@
{}

View 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);

View 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

View 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": ""
}
}
}

View 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"
}
}

View 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/*`)]);
},
};

File diff suppressed because it is too large Load Diff

View 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);

View 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
}
}

View 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);

View 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();
});
});
});
});

View File

@@ -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",
],
],
},
}
`;

View File

@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it adds isLandscape to props 1`] = `
<View
isLandscape={false}
/>
`;

View 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();
});

View 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);
};
};

View 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);
}

View 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;
},
};

View File

@@ -0,0 +1,3 @@
export default function docsUrl(path) {
return `https://reactnavigation.org/docs/${path}`;
}

View 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;

View 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

File diff suppressed because it is too large Load Diff