├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENCE.md ├── Makefile ├── README.md ├── docs ├── BOTTOM_NAVIGATION.md ├── NAVIGATION.md ├── PERFORMANCE.md ├── REACT_NATIVE_WEB.md ├── SAMPLE_CODE.png ├── TABS.md ├── bottom-navigation.gif ├── demo.gif ├── navigation.gif └── tabs.gif ├── examples ├── minimal-native-app │ ├── .babelrc │ ├── .expo │ │ ├── packager-info.json │ │ └── settings.json │ ├── .watchmanconfig │ ├── README.md │ ├── app.json │ ├── index.js │ ├── package.json │ ├── rn-cli.config.js │ ├── scripts │ │ └── link-workspaces.js │ └── src │ │ └── index.js ├── polaris-web-app │ ├── .gitignore │ ├── README.md │ ├── jest-puppeteer.config.js │ ├── jest-setup.js │ ├── jest-teardown.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── __tests__ │ │ └── index.spec.js │ │ ├── index.js │ │ └── registerServiceWorker.js └── real-world-native-app │ ├── .babelrc │ ├── .expo │ ├── packager-info.json │ └── settings.json │ ├── .watchmanconfig │ ├── README.md │ ├── app.json │ ├── index.js │ ├── package.json │ ├── rn-cli.config.js │ ├── scripts │ └── link-workspaces.js │ └── src │ ├── App.js │ ├── Article.js │ ├── Feed.js │ ├── List.js │ ├── Profile.js │ ├── assets │ ├── feed.png │ └── profile.png │ ├── index.js │ └── theme.js ├── flow-typed └── npm │ └── react-router_v4.x.x.js ├── package.json ├── packages ├── react-router-navigation-core │ ├── package.json │ └── src │ │ ├── CardStack.js │ │ ├── HistoryUtils.js │ │ ├── PropTypes.js │ │ ├── RouteUtils.js │ │ ├── SceneView.js │ │ ├── StackUtils.js │ │ ├── StateUtils.js │ │ ├── TabStack.js │ │ ├── TypeDefinitions.js │ │ ├── __tests__ │ │ ├── CardStack.spec.js │ │ ├── HistoryUtils.spec.js │ │ ├── RouteUtils.spec.js │ │ ├── SceneView.spec.js │ │ ├── StackUtils.spec.js │ │ ├── StateUtils.spec.js │ │ ├── TabStack.spec.js │ │ ├── __mocks__ │ │ │ └── index.js │ │ ├── __snapshots__ │ │ │ ├── CardStack.spec.js.snap │ │ │ ├── SceneView.spec.js.snap │ │ │ └── TabStack.spec.js.snap │ │ └── utils.js │ │ └── index.js ├── react-router-navigation │ ├── README.md │ ├── package.json │ └── src │ │ ├── BottomNavigation.js │ │ ├── Card.js │ │ ├── DefaultNavigationRenderer.js │ │ ├── DefaultTabsRenderer.js │ │ ├── NavBar.js │ │ ├── Navigation.js │ │ ├── PropTypes.js │ │ ├── Tab.js │ │ ├── TabBarBottom.js │ │ ├── Tabs.js │ │ └── index.js └── react-router-polaris │ ├── package.json │ └── src │ ├── DefaultTabsRenderer.js │ ├── Tab.js │ ├── Tabs.js │ ├── TypeDefinitions.js │ └── index.js ├── scripts └── tests.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | flow-typed 2 | node_modules 3 | __tests__ 4 | coverage 5 | lib 6 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | .*/node_modules/.*/node_modules/warning/.* 11 | .*/node_modules/.*/node_modules/react-native-tab-view/.* 12 | .*/packages/.*/node_modules/fbjs/.* 13 | 14 | ; Ignore misc packages 15 | /node_modules/react-native-tab-view/.* 16 | .*/fbemitter/.* 17 | .*/expo/.* 18 | 19 | ; Ignore duplicate module providers 20 | ; For RN Apps installed via npm, "Libraries" folder is inside 21 | ; "node_modules/react-native" but in the source repo it is in the root 22 | .*/Libraries/react-native/React.js 23 | 24 | ; Ignore polyfills 25 | .*/Libraries/polyfills/.* 26 | 27 | ; Ignore metro 28 | .*/node_modules/metro/.* 29 | 30 | ; Ignore test files 31 | .*/tests/.* 32 | .*/__tests__/.* 33 | 34 | ; Ignore xdl 35 | .*/node_modules/xdl/.* 36 | 37 | [include] 38 | 39 | [libs] 40 | node_modules/react-native/Libraries/react-native/react-native-interface.js 41 | node_modules/react-native/flow/ 42 | node_modules/react-native/flow-github/ 43 | 44 | [options] 45 | emoji=true 46 | 47 | module.system=haste 48 | 49 | munge_underscores=true 50 | 51 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 52 | 53 | module.file_ext=.js 54 | module.file_ext=.jsx 55 | module.file_ext=.json 56 | module.file_ext=.native.js 57 | 58 | suppress_type=$FlowIssue 59 | suppress_type=$FlowFixMe 60 | suppress_type=$FlowFixMeProps 61 | suppress_type=$FlowFixMeState 62 | 63 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 64 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 65 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 66 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 67 | 68 | [version] 69 | ^0.67.1 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Do you want to request a _feature_ or report a _bug_?** 2 | ... 3 | 4 | **What is the current behavior?** 5 | _If the current behavior is a bug, please provide necessary steps for reproduction of this issue, or better the reduced test case (without any external dependencies, if possible)._ 6 | 7 | 1. ... 8 | 9 | 2. ... 10 | 11 | **What is the expected behavior?** 12 | ... 13 | 14 | **Environment (include versions). Did this work in previous versions?** 15 | 16 | * Device: `...` 17 | * OS: `...` 18 | * React-Native (version): `x.x.x` 19 | * React-Router (version): `x.x.x` 20 | * React-Router-Navigation (version): `x.x.x` 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # VSCODE 6 | # 7 | jsconfig.json 8 | 9 | # Xcode 10 | # 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # Android/IJ 30 | # 31 | *.iml 32 | .idea 33 | .gradle 34 | local.properties 35 | 36 | # node.js 37 | # 38 | node_modules/ 39 | npm-debug.log 40 | 41 | # BUCK 42 | buck-out/ 43 | \.buckd/ 44 | android/app/libs 45 | android/keystores/debug.keystore 46 | 47 | # Yarn 48 | yarn-error.log 49 | 50 | # fastlane 51 | # 52 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 53 | # screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 56 | 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | 61 | react-native-packager*/ 62 | haste-map-react-native-packager* 63 | 64 | # Debug 65 | npm-debug.* 66 | 67 | # NPM 68 | package-lock.json 69 | lib 70 | 71 | # Jest 72 | coverage 73 | 74 | # Netlify 75 | .netlify -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | coverage 4 | flow-typed 5 | .babelrc 6 | .git 7 | .watchmanconfig 8 | yarn.lock 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "parser": "flow" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 9 5 | 6 | dist: trusty 7 | 8 | sudo: required 9 | 10 | cache: 11 | yarn: true 12 | directories: 13 | - node_modules 14 | 15 | install: 16 | - yarn 17 | 18 | script: 19 | - yarn test 20 | 21 | before_install: 22 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.9.4 23 | - export PATH="$HOME/.yarn/bin:$PATH" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `leo.lebrasf@gmail.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wino Technologies SAS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS = -j1 2 | 3 | format: 4 | node_modules/.bin/prettier --write "**/*.js" 5 | 6 | test: 7 | node_modules/.bin/eslint . 8 | node_modules/.bin/flow --show-all-errors 9 | node scripts/tests.js 10 | 11 | clean: 12 | trash yarn.lock 13 | trash packages/*/yarn.lock 14 | trash examples/*/yarn.lock 15 | trash packages/*/node_modules 16 | trash examples/*/node_modules 17 | trash node_modules 18 | 19 | precommit: 20 | pretty-quick --staged -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-router-navigation 2 | 3 | [![Build Status](https://travis-ci.org/winoteam/react-router-navigation.svg?branch=master)](https://travis-ci.org/winoteam/react-router-navigation) 4 | [![npm version](https://badge.fury.io/js/react-router-navigation.svg)](https://badge.fury.io/js/react-router-navigation) 5 | [![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 6 | 7 | > ❌ This project is not maintained anymore ... go see [`react-navigation`](https://github.com/react-navigation/react-navigation) 8 | 9 | **`react-router-navigation` provides tools to navigate between multiple screens with navigators or tab views. This library is based on `react-router`, `react-navigation`, and `react-native-tab-view`.** 10 | 11 | 12 | 13 | ## 🔥 Highlights 14 | 15 | * **Just an add-on to [`react-router`](https://github.com/ReactTraining/react-router)** 16 | * Declarative composability 17 | * Allow you to call transitions anywhere in your code with simple components 18 | * Dynamic Routing 19 | * URL Driven Development 20 | * Easy-to-use navigation solution using `react-navigation` 21 | * Tab Bar Support using `react-native-tab-view` 22 | * Cross-platform (iOS, Android and Web) 23 | * First class deep linking support 24 | * Nested Navigators 25 | * [Fully-tested](https://facebook.github.io/jest/) & [strictly-typed](https://flow.org/) 26 | * [TypeScript support](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/23114) 27 | 28 | ## 📟 Demos 29 | * [Minimal native app](examples/minimal-native-app) 30 | * [Real world native app](examples/real-world-native-app) 31 | * [Polaris web app](examples/polaris-web-app) ➡️ [See it !](https://focused-blackwell-61841d.netlify.com/) 32 | 33 | ## 💻 How to use 34 | 35 | Install as project dependency: 36 | 37 | ```shell 38 | $ yarn add react-router react-router-native react-router-navigation 39 | ``` 40 | 41 | Now you can use React Router Navigation to navigate between your screens: 42 | 43 | 44 | 45 | ## 💡 Guide 46 | 47 | To learn how the library work, head to this introduction written by [@CharlesMangwa](https://twitter.com/Charles_Mangwa): [Thousand ways to navigate in React-Native](https://medium.com/the-react-native-log/thousand-ways-to-navigate-in-react-native-f7a1e311a0e8) 48 | 49 | ## 📖 Docs 50 | 51 | * [``](docs/NAVIGATION.md) handles the transition between different scenes in your app. 52 | * [``](docs/TABS.md) make it easy to explore and switch between different views. 53 | * [``](docs/BOTTOM_NAVIGATION.md) make it easy to explore and switch between top-level views in a single tap. 54 | * Works great with [React Native web](https://github.com/necolas/react-native-web). [Getting started](docs/REACT_NATIVE_WEB.md) 55 | * And some [performance tips](docs/PERFORMANCE.md) 56 | 57 | ## 🕺 Contribute 58 | 59 | **Want to hack on `react-router-navigation`? Awesome! We welcome contributions from anyone and everyone. :rocket:** 60 | 61 | 1. Fork this repository to your own GitHub account and then clone it to your local device 62 | 2. Install dependencies using Yarn: `yarn` 63 | 3. Ensure that the tests are passing using `yarn test` 64 | 4. Send a pull request 🙌 65 | 66 | Remember to add tests for your change if possible. 67 | ️ 68 | ## 👋 Questions 69 | 70 | If you have any questions, feel free to get in touch on Twitter [@Leo_LeBras](https://twitter.com/Leo_LeBras) or open an issue. 71 | 72 | ## 😍 Thanks 73 | 74 | `react-router-navigation` is based on [React Router](https://github.com/reactjs/react-router). Thanks to Ryan Florence [@ryanflorence](https://twitter.com/ryanflorence), Michael Jackson [@mjackson](https://twitter.com/mjackson) and all the contributors for their work on [`react-router`](https://github.com/reactjs/react-router) and [`history`](https://github.com/mjackson/history). 75 | 76 | Special thanks to [@ericvicenti](https://twitter.com/ericvicenti), [@skevy](https://twitter.com/skevy), [@satya164](https://twitter.com/satya164) and [@grabbou](https://twitter.com/grabbou) for their work on [`react-navigation`](https://github.com/react-community/react-navigation/) and [@satya164](https://twitter.com/satya164) for his work on [`react-native-tab-view`](https://github.com/react-native-community/react-native-tab-view). 77 | -------------------------------------------------------------------------------- /docs/BOTTOM_NAVIGATION.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | Bottom navigation bars make it easy to explore and switch between top-level views in a single tap. 4 | 5 | ## Example 6 | 7 | 8 | 9 | ```js 10 | import * as React from 'react' 11 | import { BottomNavigation, Tab } from 'react-router-navigation' 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | 18 | ) 19 | ``` 20 | 21 | ## Options 22 | 23 | ### TabBar props 24 | 25 | * **hideTabBar** `?boolean` whether to display tab bar 26 | * **tabBarStyle** `?StyleSheet` override style for the tab bar 27 | * **renderTabBar** `?Function` callback which renders a tab bar 28 | * **tabStyle** `?StyleSheet` override style for the tab item 29 | * **tabTintColor** `?string` label and icon color of the tab 30 | * **tabActiveTintColor** `?string` label and icon color of the active tab 31 | * **label** `?string` text that appears on each item 32 | * **labelStyle** `?(StyleSheet | Function)` styling text item 33 | * **renderLabel** `?Function` callback which renders a label 34 | * **renderTabIcon** `?Function` optional callback which receives the current scene and returns a React Element to be used as an icon 35 | 36 | #### `` props 37 | 38 | * [`... TabBar props`](https://github.com/winoteam/react-router-navigation/blob/master/docs/BOTTOM_NAVIGATION.md#tabbar-props) 39 | * **style** `?StyleSheet` override or extend the default style for `` container 40 | * **initialLayout** optional object containing the initial `height` and `width`, can be passed to prevent the one frame delay in rendering 41 | * **lazy** `?boolean` whether to load tabs lazily when you start switching 42 | 43 | #### `` props 44 | 45 | * [`... ` props](https://reacttraining.com/react-router/native/api/Route) 46 | * [`... TabBar props`](https://github.com/winoteam/react-router-navigation/blob/master/docs/BOTTOM_NAVIGATION.md#tabbar-props) 47 | * **routePath** `?string` any valid URL path 48 | * **initialPath** `?string` any valid URL path 49 | * **onReset** `?Function` callback which resets the current tab 50 | -------------------------------------------------------------------------------- /docs/NAVIGATION.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | Navigation make it easy to explore and switch between different views. 4 | 5 | ## Example 6 | 7 | 8 | 9 | ```js 10 | import * as React from 'react' 11 | import { Navigation, Card } from 'react-router-navigation' 12 | 13 | const App = () => ( 14 | 15 | 16 | 21 | 22 | ) 23 | ``` 24 | 25 | ## Options 26 | 27 | #### NavBar props 28 | 29 | * **hideNavBar** `?boolean` whether to display nav bar 30 | * **renderNavBar** `?string` callback which renders a custom navigation bar 31 | * **navBarStyle** `?StyleSheet` override style for the navigation bar 32 | * **hideBackButton** `?boolean` whether to display default back button 33 | * **backButtonTintColor** `?string` sets the color of the back button 34 | * **backButtonTitle** `?string` text that appears on back button 35 | * **renderLeftButton** `?Function` callback which renders a custom left button 36 | * **title** `?string` string to be displayed in the navigation bar 37 | * **titleStyle** `?StyleSheet` style override for the title element 38 | * **renderTitle** `?Function` callback which renders a custom title component 39 | * **renderRightButton** `?Function` callback which renders a right button 40 | 41 | #### `` props 42 | 43 | * [... `` props](https://github.com/winoteam/react-router-navigation/blob/master/docs/NAVIGATION.md#navbar-props) 44 | * **onTransitionStart** `?Function` function invoked when the card transition animation is about to start 45 | * **onTransitionEnd** `?Function` function invoked once the card transition animation completes 46 | * **cardStyle** `?StyleSheet` override style for the card component 47 | * **configureTransition** `?Function` function which customize animation 48 | * **mode** `?string` sets the mode configuration. Can be either `card` (default) or `modal` 49 | * **gesturesEnabled** `?boolean` whether you can use gestures to dismiss this screen. 50 | 51 | #### `` props 52 | 53 | * [`... ` props](https://reacttraining.com/react-router/native/api/Route) 54 | * [`... ` props](https://github.com/winoteam/react-router-navigation/blob/master/docs/NAVIGATION.md#navigation--props) 55 | * **routePath** `?string` any valid URL path 56 | -------------------------------------------------------------------------------- /docs/PERFORMANCE.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | ## Avoid unnecessary re-renders 4 | 5 | ### Pure Render Anti-Pattern 6 | 7 | > [React/Redux Performance Tuning Tips](https://medium.com/@arikmaor/react-redux-performance-tuning-tips-cef1a6c50759) 8 | > When using a pure component, pay special attention to functions. 9 | 10 | It is important to note that `react-router-navigation` uses [`shallowCompare`](https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/shallowEqual.js) internally which simply uses `===` to check each instance. Keep that in mind ! 🖐 11 | 12 | ❌ **NEVER do this:** 13 | 14 | ```js 15 | render() { 16 | return ( 17 | 18 | My title} 20 | render={() => } 21 | /> 22 | 23 | ) 24 | } 25 | ``` 26 | 27 | ✅ **Instead do this:** 28 | 29 | ```js 30 | renderTitle() { 31 | return My title 32 | } 33 | 34 | renderCard() { 35 | return 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 45 | 46 | ) 47 | } 48 | ``` 49 | 50 | ### Use `shouldComponentUpdate` 51 | 52 | ``, `` and `` are updated every time the parent receives new props. If your view is expensive, it's good idea to move each route inside a separate stateful component and use the shouldComponentUpdate lifecycle hook to prevent unnecessary re-renders. 53 | 54 | ⚠️ **Instead of:** 55 | 56 | ```js 57 | const App = () => ( 58 | 59 | } /> 60 | } /> 61 | 62 | ) 63 | ``` 64 | 65 | ✅ **Prefer the following:** 66 | 67 | ```js 68 | class App extends React.Component { 69 | shouldComponentUpdate() { 70 | return false 71 | } 72 | render() { 73 | return ( 74 | 75 | 76 | 77 | 78 | ) 79 | } 80 | } 81 | 82 | class MyExpensiveComponentA extends React.Component { 83 | shouldComponentUpdate() { 84 | return false 85 | } 86 | render() { 87 | return 88 | } 89 | } 90 | 91 | class MyExpensiveComponentB extends React.Component { 92 | shouldComponentUpdate() { 93 | return false 94 | } 95 | render() { 96 | return 97 | } 98 | } 99 | ``` 100 | 101 | ## Avoid one frame delay for tabs 102 | 103 | > [`react-native-tab-view`](https://github.com/react-native-community/react-native-tab-view#user-content-avoid-one-frame-delay) 104 | > We need to measure the width of the container and hence need to wait before rendering some elements on the screen. If you know the initial width upfront, you can pass it in and we won't need to wait for measuring it. Most of the time, it's just the window width. 105 | 106 | `` and `` can take an `initialLayout` prop in order to prevent the same 1-frame delay in rendering the tabs correctly 107 | -------------------------------------------------------------------------------- /docs/REACT_NATIVE_WEB.md: -------------------------------------------------------------------------------- 1 | # React Native Web 2 | 3 | ## Supports [React Native Web](https://github.com/necolas/react-native-web) 4 | 5 | ### Getting Started with RNW 6 | 7 | The quickest way to get started with React Native Web (assuming you already have a project setup) is to create a folder called routing (or something similar) and then create two files in there. `Routing.web.js` and `Routing.native.js` 8 | 9 | Then you can use the following code: 10 | 11 | #### `Routing.web.js:` 12 | ```js 13 | export { 14 | BrowserRouter as Router, 15 | Link, 16 | Switch, 17 | Route, 18 | Redirect, 19 | } from 'react-router-dom'; 20 | ``` 21 | 22 | #### `Routing.native.js` 23 | 24 | ```js 25 | export { NativeRouter as Router, Link, Redirect } from 'react-router-native'; 26 | 27 | export { Navigation as Switch, Card as Route } from 'react-router-navigation'; 28 | ``` 29 | 30 | 31 | And then in your `App.js` 32 | 33 | ```js 34 | import { Router, Switch, Route } from './routing/Routing'; 35 | 36 | class MyApp extends Component { 37 | ... 38 | 39 | render(){ 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | ``` 52 | 53 | And that would enable you to use routing as you would normally use it in any **REACT** app with [React-Router-Dom](https://reacttraining.com/react-router/) 54 | 55 | 56 | #### Bonus 57 | 58 | Here is a starter repo for React Native web that includes the routing out of the box: 59 | 60 | https://github.com/joefazz/react-native-web-starter/tree/navigation-react-router 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/SAMPLE_CODE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/docs/SAMPLE_CODE.png -------------------------------------------------------------------------------- /docs/TABS.md: -------------------------------------------------------------------------------- 1 | # `` 2 | 3 | Tabs make it easy to explore and switch between different views. 4 | 5 | ## Example 6 | 7 | 8 | 9 | ```js 10 | import * as React from 'react' 11 | import { Tabs, Tab } from 'react-router-navigation' 12 | 13 | const App = () => ( 14 | 19 | 20 | 21 | 22 | 23 | ) 24 | ``` 25 | 26 | ## Options 27 | 28 | ### TabBar props 29 | 30 | * **hideTabBar** `?boolean` whether to display tab bar 31 | * **tabBarStyle** `?StyleSheet` style override for the tab bar 32 | * **renderTabBar** `?Function` callback which renders a bottom tab bar 33 | * **tabBarPosition** `?('top' | 'bottom')` sets the position of the tab bar 34 | * **tabStyle** `?StyleSheet` style override for the tab bar 35 | * **tabBarIndicatorStyle** `?StyleSheet` style object for the tab indicator 36 | * **tabTintColor** `?string` label and icon color of the tab 37 | * **tabActiveTintColor** `?string` label and icon color of the active tab 38 | * **label** `?string` text that appears on each item 39 | * **labelStyle** `?(StyleSheet | Function)` styling text item 40 | * **renderLabel** `?Function` callback which renders a label 41 | 42 | #### `` props 43 | 44 | * [`... TabBar props` props](https://github.com/winoteam/react-router-navigation/blob/master/docs/BOTTOM_NAVIGATION.md#tabbar-props) 45 | * **style** `?StyleSheet` override or extend the default style for `` container 46 | * **initialLayout** optional object containing the initial `height` and `width`, can be passed to prevent the one frame delay in rendering 47 | * **lazy** `?boolean` whether to load tabs lazily when you start switching 48 | 49 | #### `` props 50 | 51 | * [`... ` props](https://reacttraining.com/react-router/native/api/Route) 52 | * [`... TabBar props` props](https://github.com/winoteam/react-router-navigation/blob/master/docs/BOTTOM_NAVIGATION.md#tabbar-props) 53 | * **routePath** `?string` any valid URL path 54 | * **initialPath** `?string` any valid URL path 55 | * **onReset** `?Function` callback which resets the current tab -------------------------------------------------------------------------------- /docs/bottom-navigation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/docs/bottom-navigation.gif -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/docs/demo.gif -------------------------------------------------------------------------------- /docs/navigation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/docs/navigation.gif -------------------------------------------------------------------------------- /docs/tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/docs/tabs.gif -------------------------------------------------------------------------------- /examples/minimal-native-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/minimal-native-app/.expo/packager-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "expoServerPort": null, 3 | "packagerPort": null, 4 | "packagerPid": null 5 | } -------------------------------------------------------------------------------- /examples/minimal-native-app/.expo/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostType": "tunnel", 3 | "lanType": "ip", 4 | "dev": true, 5 | "strict": false, 6 | "minify": false, 7 | "urlType": "exp", 8 | "urlRandomness": null 9 | } 10 | -------------------------------------------------------------------------------- /examples/minimal-native-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/minimal-native-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app). 2 | 3 | Below you'll find information about performing common tasks. The most recent version of this guide is available [here](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/README.md). 4 | 5 | ## Updating to New Releases 6 | 7 | You should only need to update the global installation of `create-react-native-app` very rarely, ideally never. 8 | 9 | Updating the `react-native-scripts` dependency of your app should be as simple as bumping the version number in `package.json` and reinstalling your project's dependencies. 10 | 11 | Upgrading to a new version of React Native requires updating the `react-native`, `react`, and `expo` package versions, and setting the correct `sdkVersion` in `app.json`. See the [versioning guide](https://github.com/react-community/create-react-native-app/blob/master/VERSIONS.md) for up-to-date information about package version compatibility. 12 | 13 | ## Available Scripts 14 | 15 | If Yarn was installed when the project was initialized, then dependencies will have been installed via Yarn, and you should probably use it to run these commands as well. Unlike dependency installation, command running syntax is identical for Yarn and NPM at the time of this writing. 16 | 17 | ### `npm start` 18 | 19 | Runs your app in development mode. 20 | 21 | Open it in the [Expo app](https://expo.io) on your phone to view it. It will reload if you save edits to your files, and you will see build errors and logs in the terminal. 22 | 23 | Sometimes you may need to reset or clear the React Native packager's cache. To do so, you can pass the `--reset-cache` flag to the start script: 24 | 25 | ``` 26 | npm start -- --reset-cache 27 | # or 28 | yarn start -- --reset-cache 29 | ``` 30 | 31 | #### `npm test` 32 | 33 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests. 34 | 35 | #### `npm run ios` 36 | 37 | Like `npm start`, but also attempts to open your app in the iOS Simulator if you're on a Mac and have it installed. 38 | 39 | #### `npm run android` 40 | 41 | Like `npm start`, but also attempts to open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). We also recommend installing Genymotion as your Android emulator. Once you've finished setting up the native build environment, there are two options for making the right copy of `adb` available to Create React Native App: 42 | 43 | ##### Using Android Studio's `adb` 44 | 45 | 1. Make sure that you can run adb from your terminal. 46 | 2. Open Genymotion and navigate to `Settings -> ADB`. Select “Use custom Android SDK tools” and update with your [Android SDK directory](https://stackoverflow.com/questions/25176594/android-sdk-location). 47 | 48 | ##### Using Genymotion's `adb` 49 | 50 | 1. Find Genymotion’s copy of adb. On macOS for example, this is normally `/Applications/Genymotion.app/Contents/MacOS/tools/`. 51 | 2. Add the Genymotion tools directory to your path (instructions for [Mac](http://osxdaily.com/2014/08/14/add-new-path-to-path-command-line/), [Linux](http://www.computerhope.com/issues/ch001647.htm), and [Windows](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)). 52 | 3. Make sure that you can run adb from your terminal. 53 | 54 | #### `npm run eject` 55 | 56 | This will start the process of "ejecting" from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project. 57 | 58 | **Warning:** Running eject is a permanent action (aside from whatever version control system you use). An ejected app will require you to have an [Xcode and/or Android Studio environment](https://facebook.github.io/react-native/docs/getting-started.html) set up. 59 | -------------------------------------------------------------------------------- /examples/minimal-native-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "sdkVersion": "28.0.0", 4 | "ignoreNodeModulesValidation": true, 5 | "packagerOpts": { 6 | "config": "rn-cli.config.js", 7 | "projectRoots": "" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /examples/minimal-native-app/index.js: -------------------------------------------------------------------------------- 1 | import App from './src/' 2 | import Expo from 'expo' 3 | import React from 'react' 4 | 5 | const AwakeInDevApp = props => [ 6 | , 7 | process.env.NODE_ENV === 'development' ? ( 8 | 9 | ) : null, 10 | ] 11 | 12 | Expo.registerRootComponent(AwakeInDevApp) 13 | -------------------------------------------------------------------------------- /examples/minimal-native-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-native-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "node scripts/link-workspaces.js", 8 | "start": "react-native-scripts start", 9 | "eject": "react-native-scripts eject", 10 | "android": "react-native-scripts android", 11 | "ios": "react-native-scripts ios" 12 | }, 13 | "dependencies": { 14 | "expo": "28.0.0", 15 | "react": "16.3.1", 16 | "react-native": "0.55.4", 17 | "react-router": "4.2.0", 18 | "react-router-native": "4.2.0", 19 | "react-router-navigation": "2.0.0-alpha.7" 20 | }, 21 | "devDependencies": { 22 | "crna-make-symlinks-for-yarn-workspaces": "^1.0.1", 23 | "metro-bundler-config-yarn-workspaces": "^1.0.2", 24 | "react-native-scripts": "1.14.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/minimal-native-app/rn-cli.config.js: -------------------------------------------------------------------------------- 1 | const getConfig = require('metro-bundler-config-yarn-workspaces') 2 | const options = { nodeModules: require('path').resolve(__dirname, '..', '..') } 3 | 4 | module.exports = getConfig(__dirname, options) 5 | -------------------------------------------------------------------------------- /examples/minimal-native-app/scripts/link-workspaces.js: -------------------------------------------------------------------------------- 1 | require('crna-make-symlinks-for-yarn-workspaces')(`${__dirname}/../`) 2 | -------------------------------------------------------------------------------- /examples/minimal-native-app/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { 3 | StatusBar, 4 | StyleSheet, 5 | View, 6 | TouchableOpacity, 7 | Text, 8 | } from 'react-native' 9 | import { Switch, Route, Redirect } from 'react-router' 10 | import { NativeRouter, Link } from 'react-router-native' 11 | import { Navigation, Card, Tabs, Tab } from 'react-router-navigation' 12 | 13 | const PRIMARY_COLOR = 'rgb(226, 68, 68)' 14 | const SECONDARY_COLOR = 'rgb(226, 144, 68)' 15 | 16 | const styles = StyleSheet.create({ 17 | scene: { 18 | flex: 1, 19 | padding: 18, 20 | }, 21 | tabs: { 22 | backgroundColor: PRIMARY_COLOR, 23 | }, 24 | tab: { 25 | paddingTop: 10, 26 | opacity: 10, 27 | }, 28 | indicator: { 29 | backgroundColor: 'white', 30 | }, 31 | button: { 32 | alignSelf: 'flex-start', 33 | marginTop: 10, 34 | marginLeft: -8, 35 | paddingVertical: 10, 36 | paddingHorizontal: 18, 37 | borderWidth: 1, 38 | borderColor: 'grey', 39 | borderRadius: 3, 40 | }, 41 | strong: { 42 | fontWeight: '700', 43 | marginBottom: 10, 44 | }, 45 | }) 46 | 47 | export default class App extends React.Component { 48 | state = { 49 | navigation: {}, 50 | card: {}, 51 | } 52 | 53 | renderFistCard = () => { 54 | return ( 55 | 56 | 57 | Push a new scene 58 | 59 | 62 | this.setState({ 63 | navigation: { 64 | navBarStyle: { 65 | backgroundColor: PRIMARY_COLOR, 66 | borderBottomWidth: 0, 67 | }, 68 | titleStyle: { color: 'white' }, 69 | barStyle: 'light-content', 70 | backButtonTintColor: 'white', 71 | }, 72 | }) 73 | } 74 | > 75 | Change navbar style 76 | 77 | 78 | ) 79 | } 80 | 81 | renderSecondCard = () => { 82 | return ( 83 | 84 | 85 | Push tabs 86 | 87 | { 90 | this.setState(prevState => ({ 91 | navigation: { 92 | ...prevState.navigation, 93 | barStyle: 'light-content', 94 | }, 95 | card: { 96 | ...prevState.card, 97 | navBarStyle: { 98 | backgroundColor: SECONDARY_COLOR, 99 | borderBottomWidth: 0, 100 | }, 101 | titleStyle: { color: 'white' }, 102 | backButtonTintColor: 'white', 103 | }, 104 | })) 105 | }} 106 | > 107 | Change navbar style 108 | 109 | { 112 | this.setState(prevState => ({ 113 | card: { 114 | ...prevState.card, 115 | title: 'New title !', 116 | }, 117 | })) 118 | }} 119 | > 120 | Change title 121 | 122 | 123 | ) 124 | } 125 | 126 | renderThirdCard = contextRouter => { 127 | const { location, match } = contextRouter 128 | return ( 129 | 130 | } 134 | /> 135 | ( 137 | 142 | 148 | 154 | 160 | 161 | )} 162 | /> 163 | 164 | ) 165 | } 166 | 167 | renderFirstTab = contextRouter => { 168 | const { match } = contextRouter 169 | const basePath = match && match.url.slice(0, match.url.lastIndexOf('/')) 170 | return ( 171 | 172 | One 173 | 179 | Go to "two" 180 | 181 | 187 | Go to "three" 188 | 189 | 190 | ) 191 | } 192 | 193 | renderSecondTab = contextRouter => { 194 | const { match } = contextRouter 195 | const basePath = match && match.url.slice(0, match.url.lastIndexOf('/')) 196 | return ( 197 | 198 | Two 199 | 205 | Go to "one" 206 | 207 | 213 | Go to "three" 214 | 215 | 216 | ) 217 | } 218 | 219 | renderThirdTab = contextRouter => { 220 | const { match } = contextRouter 221 | const basePath = match && match.url.slice(0, match.url.lastIndexOf('/')) 222 | return ( 223 | 224 | Three 225 | 231 | Go to "one" 232 | 233 | 239 | Go to "two" 240 | 241 | 242 | ) 243 | } 244 | 245 | render() { 246 | const { navigation, card } = this.state 247 | return ( 248 | 249 | 250 | 251 | 256 | 257 | 267 | 268 | 269 | 270 | 271 | ) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /examples/polaris-web-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/polaris-web-app/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | command: 'yarn start', 4 | port: 3000, 5 | launchTimeout: 25000, 6 | options: { 7 | cwd: __dirname, 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /examples/polaris-web-app/jest-setup.js: -------------------------------------------------------------------------------- 1 | const { setup } = require('jest-environment-puppeteer') 2 | 3 | module.exports = async function globalSetup() { 4 | const path = require('path') 5 | const configPath = path.resolve(__dirname, './jest-puppeteer.config.js') 6 | process.env.JEST_PUPPETEER_CONFIG = configPath 7 | await setup() 8 | } 9 | -------------------------------------------------------------------------------- /examples/polaris-web-app/jest-teardown.js: -------------------------------------------------------------------------------- 1 | const { teardown } = require('jest-environment-puppeteer') 2 | 3 | module.exports = async function globalTeardown() { 4 | await teardown() 5 | } 6 | -------------------------------------------------------------------------------- /examples/polaris-web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polaris-web-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@shopify/polaris": "^2.5.0", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-router": "4.2.0", 10 | "react-router-dom": "4.2.0", 11 | "react-router-polaris": "0.1.3", 12 | "react-scripts": "1.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "deploy": "netlify deploy", 18 | "eject": "react-scripts eject" 19 | }, 20 | "devDependencies": { 21 | "jest": "^23.6.0", 22 | "jest-puppeteer": "^3.4.0", 23 | "puppeteer": "^1.9.0" 24 | }, 25 | "jest": { 26 | "preset": "jest-puppeteer", 27 | "globalSetup": "./jest-setup.js", 28 | "globalTeardown": "./jest-teardown.js" 29 | } 30 | } -------------------------------------------------------------------------------- /examples/polaris-web-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/examples/polaris-web-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/polaris-web-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Polaris Web App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/polaris-web-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/polaris-web-app/src/App.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 32px; 3 | } -------------------------------------------------------------------------------- /examples/polaris-web-app/src/App.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { type ContextRouter } from 'react-router' 5 | import { Card } from '@shopify/polaris' 6 | import { Tabs, Tab } from 'react-router-polaris' 7 | import './App.css' 8 | 9 | type Props = {} 10 | 11 | class App extends React.Component { 12 | renderAllCustomersTab = (contextRouter: ContextRouter) => { 13 | const { pathname } = contextRouter.location 14 | return ( 15 | 16 |

First tab « {pathname} »

17 |
18 | ) 19 | } 20 | 21 | renderAcceptsMarketingTab = (contextRouter: ContextRouter) => { 22 | const { pathname } = contextRouter.location 23 | return ( 24 | 25 |

Second tab « {pathname} »

26 |
27 | ) 28 | } 29 | 30 | renderRepeatCustomersTab = (contextRouter: ContextRouter) => { 31 | const { pathname } = contextRouter.location 32 | return ( 33 | 34 |

Third tab « {pathname} »

35 |
36 | ) 37 | } 38 | 39 | renderProspectsTab = (contextRouter: ContextRouter) => { 40 | const { pathname } = contextRouter.location 41 | return ( 42 | 43 |

Fourth tab « {pathname} »

44 |
45 | ) 46 | } 47 | 48 | render() { 49 | return ( 50 |
51 | 52 | 58 | 63 | 68 | 73 | 74 |
75 | ) 76 | } 77 | } 78 | 79 | export default App 80 | -------------------------------------------------------------------------------- /examples/polaris-web-app/src/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('Polaris web app', () => { 2 | beforeAll(async () => { 3 | jest.setTimeout(25000) 4 | }) 5 | 6 | it('should load correctly', async () => { 7 | await page.goto('http://localhost:3000/accepts-marketing') 8 | await page.waitForSelector('.container') 9 | await expect(page).toMatch('Second tab') 10 | await expect(page).not.toMatch('First tab') 11 | }) 12 | 13 | it('should re-render tabs on url change', async () => { 14 | await page.goto('http://localhost:3000/') 15 | await page.waitForSelector('.container') 16 | await page.goto('http://localhost:3000/accepts-marketing') 17 | await expect(page).toMatch('Second tab') 18 | await expect(page).not.toMatch('First tab') 19 | }) 20 | 21 | it('should re-render tabs on click', async () => { 22 | await page.goto('http://localhost:3000/') 23 | await page.waitForSelector('.container') 24 | await page.click('button[tabIndex="-1"]') 25 | await expect(page).toMatch('Second tab') 26 | await expect(page.url()).toMatch('http://localhost:3000/accepts-marketing') 27 | await expect(page).not.toMatch('First tab') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/polaris-web-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import { AppProvider, Layout } from '@shopify/polaris' 5 | import App from './App' 6 | import registerServiceWorker from './registerServiceWorker' 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ) 18 | registerServiceWorker() 19 | -------------------------------------------------------------------------------- /examples/polaris-web-app/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/real-world-native-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/real-world-native-app/.expo/packager-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "expoServerPort": 19000, 3 | "packagerPort": 19001, 4 | "packagerPid": 43084 5 | } -------------------------------------------------------------------------------- /examples/real-world-native-app/.expo/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostType": "tunnel", 3 | "lanType": "ip", 4 | "dev": true, 5 | "strict": false, 6 | "minify": false, 7 | "urlType": "exp", 8 | "urlRandomness": null 9 | } 10 | -------------------------------------------------------------------------------- /examples/real-world-native-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/real-world-native-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app). 2 | 3 | Below you'll find information about performing common tasks. The most recent version of this guide is available [here](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/README.md). 4 | 5 | ## Updating to New Releases 6 | 7 | You should only need to update the global installation of `create-react-native-app` very rarely, ideally never. 8 | 9 | Updating the `react-native-scripts` dependency of your app should be as simple as bumping the version number in `package.json` and reinstalling your project's dependencies. 10 | 11 | Upgrading to a new version of React Native requires updating the `react-native`, `react`, and `expo` package versions, and setting the correct `sdkVersion` in `app.json`. See the [versioning guide](https://github.com/react-community/create-react-native-app/blob/master/VERSIONS.md) for up-to-date information about package version compatibility. 12 | 13 | ## Available Scripts 14 | 15 | If Yarn was installed when the project was initialized, then dependencies will have been installed via Yarn, and you should probably use it to run these commands as well. Unlike dependency installation, command running syntax is identical for Yarn and NPM at the time of this writing. 16 | 17 | ### `npm start` 18 | 19 | Runs your app in development mode. 20 | 21 | Open it in the [Expo app](https://expo.io) on your phone to view it. It will reload if you save edits to your files, and you will see build errors and logs in the terminal. 22 | 23 | Sometimes you may need to reset or clear the React Native packager's cache. To do so, you can pass the `--reset-cache` flag to the start script: 24 | 25 | ``` 26 | npm start -- --reset-cache 27 | # or 28 | yarn start -- --reset-cache 29 | ``` 30 | 31 | #### `npm test` 32 | 33 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests. 34 | 35 | #### `npm run ios` 36 | 37 | Like `npm start`, but also attempts to open your app in the iOS Simulator if you're on a Mac and have it installed. 38 | 39 | #### `npm run android` 40 | 41 | Like `npm start`, but also attempts to open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). We also recommend installing Genymotion as your Android emulator. Once you've finished setting up the native build environment, there are two options for making the right copy of `adb` available to Create React Native App: 42 | 43 | ##### Using Android Studio's `adb` 44 | 45 | 1. Make sure that you can run adb from your terminal. 46 | 2. Open Genymotion and navigate to `Settings -> ADB`. Select “Use custom Android SDK tools” and update with your [Android SDK directory](https://stackoverflow.com/questions/25176594/android-sdk-location). 47 | 48 | ##### Using Genymotion's `adb` 49 | 50 | 1. Find Genymotion’s copy of adb. On macOS for example, this is normally `/Applications/Genymotion.app/Contents/MacOS/tools/`. 51 | 2. Add the Genymotion tools directory to your path (instructions for [Mac](http://osxdaily.com/2014/08/14/add-new-path-to-path-command-line/), [Linux](http://www.computerhope.com/issues/ch001647.htm), and [Windows](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)). 52 | 3. Make sure that you can run adb from your terminal. 53 | 54 | #### `npm run eject` 55 | 56 | This will start the process of "ejecting" from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project. 57 | 58 | **Warning:** Running eject is a permanent action (aside from whatever version control system you use). An ejected app will require you to have an [Xcode and/or Android Studio environment](https://facebook.github.io/react-native/docs/getting-started.html) set up. 59 | -------------------------------------------------------------------------------- /examples/real-world-native-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "sdkVersion": "28.0.0", 4 | "ignoreNodeModulesValidation": true, 5 | "packagerOpts": { 6 | "config": "rn-cli.config.js", 7 | "projectRoots": "" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /examples/real-world-native-app/index.js: -------------------------------------------------------------------------------- 1 | import App from './src/' 2 | import Expo from 'expo' 3 | import React from 'react' 4 | 5 | const AwakeInDevApp = props => [ 6 | , 7 | process.env.NODE_ENV === 'development' ? ( 8 | 9 | ) : null, 10 | ] 11 | 12 | Expo.registerRootComponent(AwakeInDevApp) 13 | -------------------------------------------------------------------------------- /examples/real-world-native-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-world-native-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "node scripts/link-workspaces.js", 8 | "start": "react-native-scripts start", 9 | "eject": "react-native-scripts eject", 10 | "android": "react-native-scripts android", 11 | "ios": "react-native-scripts ios" 12 | }, 13 | "dependencies": { 14 | "expo": "28.0.0", 15 | "history": "4.7.2", 16 | "path-to-regexp": "2.2.1", 17 | "react-native-tab-view": "1.2.0", 18 | "react-navigation": "1.5.8", 19 | "react-redux": "5.0.6", 20 | "react-router-redux": "5.0.0-alpha.9", 21 | "react-native": "0.55.4", 22 | "react-router": "4.2.0", 23 | "react-router-native": "4.2.0", 24 | "react-router-navigation": "2.0.0-alpha.7", 25 | "redux": "3.7.2" 26 | }, 27 | "devDependencies": { 28 | "crna-make-symlinks-for-yarn-workspaces": "^1.0.1", 29 | "metro-bundler-config-yarn-workspaces": "^1.0.2", 30 | "react-native-scripts": "1.14.0" 31 | } 32 | } -------------------------------------------------------------------------------- /examples/real-world-native-app/rn-cli.config.js: -------------------------------------------------------------------------------- 1 | const getConfig = require('metro-bundler-config-yarn-workspaces') 2 | const options = { nodeModules: require('path').resolve(__dirname, '..', '..') } 3 | 4 | module.exports = getConfig(__dirname, options) 5 | -------------------------------------------------------------------------------- /examples/real-world-native-app/scripts/link-workspaces.js: -------------------------------------------------------------------------------- 1 | require('crna-make-symlinks-for-yarn-workspaces')(`${__dirname}/../`) 2 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/App.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { Platform, Image } from 'react-native' 5 | import { BottomNavigation, Tab } from 'react-router-navigation' 6 | import type { RouterHistory, ContextRouter } from 'react-router' 7 | import Feed from './Feed' 8 | import Profile from './Profile' 9 | import { NEUTRAL_COLOR_50, BRAND_COLOR_60 } from './theme' 10 | 11 | type Props = { 12 | history: RouterHistory, 13 | } 14 | 15 | export default class App extends React.Component { 16 | feed: ?Feed = null 17 | 18 | onReset = () => { 19 | if (this.feed && this.feed.listView) { 20 | this.feed.listView.scrollTo({ y: 0 }) 21 | } 22 | } 23 | 24 | renderFeed = (contextRouter: ContextRouter) => { 25 | return ( 26 | (this.feed = c)} 28 | history={contextRouter.history} 29 | location={contextRouter.location} 30 | match={contextRouter.match} 31 | /> 32 | ) 33 | } 34 | 35 | renderTabIcon = (tabIconProps: { 36 | tabActiveTintColor: string, 37 | tabTintColor: string, 38 | focused: boolean, 39 | route: { name: string }, 40 | }) => { 41 | const { route, focused, tabActiveTintColor, tabTintColor } = tabIconProps 42 | switch (route.name) { 43 | case '/feed': 44 | return ( 45 | 54 | ) 55 | case '/profile/(likes|bookmarks|settings)': 56 | return ( 57 | 66 | ) 67 | default: 68 | return null 69 | } 70 | } 71 | 72 | shouldComponentUpdate() { 73 | return false 74 | } 75 | 76 | render() { 77 | return ( 78 | 82 | 89 | 96 | 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/Article.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { StyleSheet, View, Text, TouchableOpacity } from 'react-native' 5 | import type { ContextRouter } from 'react-router' 6 | import { Link } from 'react-router-native' 7 | import { BRAND_COLOR_50, BRAND_COLOR_60 } from './theme' 8 | 9 | const styles = StyleSheet.create({ 10 | scene: { 11 | flex: 1, 12 | alignItems: 'flex-start', 13 | padding: 20, 14 | }, 15 | link: { 16 | marginTop: 16, 17 | marginLeft: -8, 18 | paddingVertical: 10, 19 | paddingHorizontal: 18, 20 | borderWidth: 1, 21 | borderColor: BRAND_COLOR_50, 22 | borderRadius: 3, 23 | }, 24 | span: { 25 | color: BRAND_COLOR_60, 26 | }, 27 | strong: { 28 | marginTop: 5, 29 | fontWeight: '700', 30 | }, 31 | }) 32 | 33 | type Props = ContextRouter 34 | 35 | type State = {| 36 | time: 0, 37 | |} 38 | 39 | export default class Article extends React.Component { 40 | timer: ?IntervalID = null 41 | 42 | state = { time: 0 } 43 | 44 | componentDidMount() { 45 | this.timer = setInterval(() => { 46 | if ( 47 | this.props.match && 48 | this.props.match.url === this.props.location.pathname 49 | ) { 50 | this.setState(prevState => ({ 51 | // $FlowFixMe 52 | time: prevState.time + 250, 53 | })) 54 | } 55 | }, 250) 56 | } 57 | 58 | componentWillUnmount() { 59 | if (this.timer) clearInterval(this.timer) 60 | } 61 | 62 | shouldComponentUpdate(nextProps: Props, nextState: State) { 63 | return this.state.time !== nextState.time 64 | } 65 | 66 | render() { 67 | const { history, match } = this.props 68 | const params = match && match.params 69 | if (!params || !params.id) return null 70 | return ( 71 | 72 | 73 | {params.method === 'update' ? 'Updating' : 'Reading'} time:{' '} 74 | {this.state.time / 1000}s 75 | 76 | 81 | 82 | Push to article {parseInt(params.id, 10) + 1} (n + 1) 83 | 84 | 85 | 91 | 92 | Replace to article {parseInt(params.id, 10) + 1} (n + 1) 93 | 94 | 95 | history.goBack()}> 96 | Go back (n-1) 97 | 98 | {history.entries.slice(0, history.index + 1).length > 2 && ( 99 | history.go(-2)}> 100 | Pop (n-2) 101 | 102 | )} 103 | 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/Feed.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { StyleSheet, Text, TouchableOpacity } from 'react-native' 5 | import { Navigation, Card } from 'react-router-navigation' 6 | import { type ContextRouter, type Match } from 'react-router' 7 | import { Link } from 'react-router-native' 8 | import { HeaderTitle } from 'react-navigation' 9 | import pathToRegexp from 'path-to-regexp' 10 | import List from './List' 11 | import Article from './Article' 12 | import { NEUTRAL_COLOR_00, BRAND_COLOR_50 } from './theme' 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | }, 18 | navBar: { 19 | backgroundColor: BRAND_COLOR_50, 20 | }, 21 | title: { 22 | color: NEUTRAL_COLOR_00, 23 | }, 24 | link: { 25 | marginHorizontal: 8, 26 | paddingVertical: 6, 27 | paddingHorizontal: 12, 28 | borderWidth: 1, 29 | borderColor: NEUTRAL_COLOR_00, 30 | borderRadius: 3, 31 | }, 32 | span: { 33 | color: NEUTRAL_COLOR_00, 34 | }, 35 | }) 36 | 37 | type Props = ContextRouter 38 | 39 | export default class Feed extends React.Component { 40 | listView: ?List = null 41 | 42 | renderArticleTitle = (titleProps: { match: Match }) => { 43 | const { match } = titleProps 44 | return ( 45 | 46 | Item {match && match.params.id} 47 | 48 | ) 49 | } 50 | 51 | renderArticleRightComponent = (rightComponentProps: { match: Match }) => { 52 | const { match } = rightComponentProps 53 | const toPath = pathToRegexp.compile(match.path) 54 | if (!match || !match.params) return null 55 | const newPath = toPath({ 56 | ...match.params, 57 | method: match.params.method === 'update' ? 'read' : 'update', 58 | }) 59 | return ( 60 | 66 | 67 | {match.params.method === 'update' ? 'Done' : 'Edit'} 68 | 69 | 70 | ) 71 | } 72 | 73 | renderList = (contextRouter: ContextRouter) => { 74 | return ( 75 | (this.listView = c)} 77 | history={contextRouter.history} 78 | location={contextRouter.location} 79 | match={contextRouter.match} 80 | /> 81 | ) 82 | } 83 | 84 | shouldComponentUpdate() { 85 | return false 86 | } 87 | 88 | render() { 89 | const { match } = this.props 90 | if (!match) return null 91 | return ( 92 | 97 | 103 | 111 | 112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/List.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { StyleSheet, PixelRatio, ListView, View, Text } from 'react-native' 5 | import { Link } from 'react-router-native' 6 | import type { ContextRouter } from 'react-router' 7 | 8 | const styles = StyleSheet.create({ 9 | container: { 10 | flex: 1, 11 | }, 12 | separator: { 13 | borderBottomWidth: 1 / PixelRatio.get(), 14 | borderBottomColor: '#cdcdcd', 15 | }, 16 | row: { 17 | padding: 15, 18 | backgroundColor: 'white', 19 | fontSize: 16, 20 | fontWeight: '500', 21 | }, 22 | }) 23 | 24 | type Props = ContextRouter 25 | 26 | type State = {| 27 | dataSource: Object, 28 | |} 29 | 30 | export default class List extends React.Component { 31 | listView: ?List = null 32 | 33 | constructor(props: Props) { 34 | super(props) 35 | const ds = new ListView.DataSource({ 36 | rowHasChanged: () => false, 37 | }) 38 | this.state = { 39 | dataSource: ds.cloneWithRows( 40 | Array.from({ length: 100 }).map((a, i) => `Article #${i + 1}`), 41 | ), 42 | } 43 | } 44 | 45 | scrollTo = (options: { x?: number, y?: number, animated?: boolean }) => { 46 | if (this.listView) this.listView.scrollTo(options) 47 | } 48 | 49 | renderRow = (rowData: string) => { 50 | const { match } = this.props 51 | if (!match || !match.params) return null 52 | return ( 53 | 54 | {rowData} 55 | 56 | ) 57 | } 58 | 59 | renderSeparator = (sectionIndex: number, rowIndex: number) => { 60 | return 61 | } 62 | 63 | render() { 64 | return ( 65 | (this.listView = c)} 67 | style={styles.container} 68 | dataSource={this.state.dataSource} 69 | renderRow={this.renderRow} 70 | renderSeparator={this.renderSeparator} 71 | /> 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/Profile.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { 5 | StyleSheet, 6 | Platform, 7 | View, 8 | TouchableOpacity, 9 | Text, 10 | Dimensions, 11 | PixelRatio, 12 | } from 'react-native' 13 | import { Link } from 'react-router-native' 14 | import { Tabs, Tab } from 'react-router-navigation' 15 | import { TabBar, type SceneRendererProps } from 'react-native-tab-view' 16 | import { SafeAreaView } from 'react-navigation' 17 | import { NEUTRAL_COLOR_50, BRAND_COLOR_50 } from './theme' 18 | 19 | const styles = StyleSheet.create({ 20 | tabBarSafeView: { 21 | backgroundColor: BRAND_COLOR_50, 22 | }, 23 | tabBar: { 24 | paddingTop: 25 | Platform.OS === 'ios' && Dimensions.get('window').height !== 812 ? 10 : 0, 26 | backgroundColor: BRAND_COLOR_50, 27 | }, 28 | indicatorStyle: { 29 | backgroundColor: 'white', 30 | }, 31 | scene: { 32 | flex: 1, 33 | justifyContent: 'center', 34 | alignItems: 'center', 35 | }, 36 | actions: { 37 | justifyContent: 'center', 38 | alignItems: 'center', 39 | marginTop: 12, 40 | }, 41 | link: { 42 | marginTop: 12, 43 | paddingVertical: 10, 44 | paddingHorizontal: 18, 45 | borderWidth: 1, 46 | borderColor: BRAND_COLOR_50, 47 | borderRadius: 3, 48 | }, 49 | span: { 50 | color: BRAND_COLOR_50, 51 | }, 52 | strong: { 53 | fontWeight: '700', 54 | }, 55 | separator: { 56 | backgroundColor: NEUTRAL_COLOR_50, 57 | marginVertical: 20, 58 | width: '75%', 59 | height: 1 / PixelRatio.get(), 60 | }, 61 | }) 62 | 63 | type Props = {} 64 | 65 | type State = {| 66 | tabsLength: number, 67 | |} 68 | 69 | export default class Profile extends React.Component { 70 | state = { tabsLength: 2 } 71 | 72 | renderTabBar = (tabBarProps: SceneRendererProps) => { 73 | return ( 74 | 78 | 79 | 80 | ) 81 | } 82 | 83 | renderTabLikes = () => { 84 | const { tabsLength } = this.state 85 | return ( 86 | 87 | 88 | Current: likes 89 | 90 | 96 | Go to bookmarks 97 | 98 | 104 | Go to the article #4 (replace) 105 | 106 | 111 | Go to the article #4 (push) 112 | 113 | {tabsLength === 2 && ( 114 | 118 | Add settings tab 119 | 120 | )} 121 | {tabsLength === 3 && ( 122 | 128 | Go to settings 129 | 130 | )} 131 | 132 | ) 133 | } 134 | 135 | renderTabBookmarks = () => { 136 | const { tabsLength } = this.state 137 | return ( 138 | 139 | 140 | Current: bookmarks 141 | 142 | 148 | Go to likes 149 | 150 | {tabsLength === 2 && ( 151 | 155 | Add settings tab 156 | 157 | )} 158 | {tabsLength === 3 && ( 159 | 165 | Go to settings 166 | 167 | )} 168 | 169 | ) 170 | } 171 | 172 | renderTabSettings = () => { 173 | return ( 174 | 175 | 176 | Current: settings 177 | 178 | 184 | Go to likes 185 | 186 | 192 | Go to bookmarks 193 | 194 | 195 | ) 196 | } 197 | 198 | handleToggleSettingsTab = () => { 199 | this.setState(prevState => ({ 200 | tabsLength: prevState.tabsLength === 2 ? 3 : 2, 201 | })) 202 | } 203 | 204 | shouldComponentUpdate(nextProps: Props, nextState: State) { 205 | return this.state.tabsLength !== nextState.tabsLength 206 | } 207 | 208 | render() { 209 | const { tabsLength } = this.state 210 | return ( 211 | 216 | 217 | 222 | {tabsLength === 3 && ( 223 | 228 | )} 229 | 230 | ) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/assets/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/examples/real-world-native-app/src/assets/feed.png -------------------------------------------------------------------------------- /examples/real-world-native-app/src/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winoteam/react-router-navigation/82dc8da81fa18eb283cd1e0c8c6148077ffcc16b/examples/real-world-native-app/src/assets/profile.png -------------------------------------------------------------------------------- /examples/real-world-native-app/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { StyleSheet, StatusBar, View } from 'react-native' 5 | import { Switch, Route, Redirect } from 'react-router' 6 | import { 7 | ConnectedRouter, 8 | routerReducer, 9 | routerMiddleware, 10 | } from 'react-router-redux' 11 | import createHistory from 'history/createMemoryHistory' 12 | import { Provider } from 'react-redux' 13 | import { createStore, combineReducers, applyMiddleware } from 'redux' 14 | import App from './App' 15 | 16 | const styles = StyleSheet.create({ 17 | tabs: { 18 | flex: 1, 19 | }, 20 | }) 21 | 22 | const history = createHistory() 23 | const historyMiddleware = routerMiddleware(history) 24 | const loggerMiddleware = () => next => action => { 25 | if (action && action.type === '@@router/LOCATION_CHANGE') { 26 | console.log(history.entries.map(({ pathname }) => pathname)) 27 | } 28 | if (action) next(action) 29 | } 30 | 31 | const store = createStore( 32 | combineReducers({ router: routerReducer }), 33 | applyMiddleware(historyMiddleware, loggerMiddleware), 34 | ) 35 | 36 | const Root = () => { 37 | return ( 38 | 39 | 40 | 41 | } /> 42 | ( 45 | 46 | 47 | 48 | 49 | )} 50 | /> 51 | 52 | 53 | 54 | ) 55 | } 56 | 57 | export default Root 58 | -------------------------------------------------------------------------------- /examples/real-world-native-app/src/theme.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export const NEUTRAL_COLOR_00 = '#efefef' 4 | export const NEUTRAL_COLOR_50 = '#a5aaB2' 5 | export const BRAND_COLOR_50 = '#1b95e0' 6 | export const BRAND_COLOR_60 = '#1581c4' 7 | -------------------------------------------------------------------------------- /flow-typed/npm/react-router_v4.x.x.js: -------------------------------------------------------------------------------- 1 | declare module 'react-router' { 2 | // NOTE: many of these are re-exported by react-router-dom and 3 | // react-router-native, so when making changes, please be sure to update those 4 | // as well. 5 | declare export type Location = { 6 | pathname: string, 7 | search: string, 8 | hash: string, 9 | state?: *, 10 | key?: string, 11 | } 12 | 13 | declare export type LocationShape = { 14 | pathname?: string, 15 | search?: string, 16 | hash?: string, 17 | state?: *, 18 | } 19 | 20 | declare export type HistoryAction = 'PUSH' | 'REPLACE' | 'POP' 21 | 22 | declare export type RouterHistory = { 23 | length: number, 24 | location: Location, 25 | action: HistoryAction, 26 | listen(callback: (location: Location, action: HistoryAction) => void): () => void, 27 | push(path: string | LocationShape, state?: *): void, 28 | replace(path: string | LocationShape, state?: *): void, 29 | go(n: number): void, 30 | goBack(): void, 31 | goForward(): void, 32 | canGo?: (n: number) => boolean, 33 | block(callback: (location: Location, action: HistoryAction) => boolean): void, 34 | // createMemoryHistory 35 | index: number, 36 | entries: Array, 37 | } 38 | 39 | declare export type Match = { 40 | params: { [key: string]: ?string }, 41 | isExact: boolean, 42 | path: string, 43 | url: string, 44 | } 45 | 46 | declare export type ContextRouter = {| 47 | history: RouterHistory, 48 | location: Location, 49 | match: ?Match, 50 | |} 51 | 52 | declare export type GetUserConfirmation = ( 53 | message: string, 54 | callback: (confirmed: boolean) => void, 55 | ) => void 56 | 57 | declare type StaticRouterContext = { 58 | url?: string, 59 | } 60 | 61 | declare export class StaticRouter extends React$Component<{ 62 | basename?: string, 63 | location?: string | Location, 64 | context: StaticRouterContext, 65 | children?: React$Node, 66 | }> {} 67 | 68 | declare export class MemoryRouter extends React$Component<{ 69 | initialEntries?: Array, 70 | initialIndex?: number, 71 | getUserConfirmation?: GetUserConfirmation, 72 | keyLength?: number, 73 | children?: React$Node, 74 | }> {} 75 | 76 | declare export class Router extends React$Component<{ 77 | history: RouterHistory, 78 | children?: React$Node, 79 | }> {} 80 | 81 | declare export class Prompt extends React$Component<{ 82 | message: string | ((location: Location) => string | true), 83 | when?: boolean, 84 | }> {} 85 | 86 | declare export class Redirect extends React$Component<{ 87 | to: string | LocationShape, 88 | push?: boolean, 89 | }> {} 90 | 91 | declare export class Route extends React$Component<{ 92 | component?: React$ComponentType, 93 | render?: (router: ContextRouter) => React$Node, 94 | children?: React$ComponentType | React$Node, 95 | path?: string, 96 | exact?: boolean, 97 | strict?: boolean, 98 | }> {} 99 | 100 | declare export class Switch extends React$Component<{ 101 | children?: React$Node, 102 | }> {} 103 | 104 | declare export function withRouter

( 105 | Component: React$ComponentType<*>, 106 | ): React$ComponentType

107 | 108 | declare export type MatchPathOptions = { 109 | path?: string, 110 | exact?: boolean, 111 | strict?: boolean, 112 | sensitive?: boolean, 113 | } 114 | declare export function matchPath( 115 | pathname: string, 116 | options?: MatchPathOptions | string, 117 | ): null | Match 118 | } 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "A complete navigation library for React Native", 4 | "license": "MIT", 5 | "scripts": { 6 | "format": "make format", 7 | "clean": "make clean", 8 | "test": "make test", 9 | "precommit": "make precommit" 10 | }, 11 | "author": "Léo Le Bras (https://github.com/winoteam/)", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/winoteam/react-router-navigation.git" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "ios", 19 | "android", 20 | "router", 21 | "navigation", 22 | "navigator" 23 | ], 24 | "engines": { 25 | "yarn": "^1.9.0" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/winoteam/react-router-navigation/issues" 29 | }, 30 | "homepage": "https://github.com/winoteam/react-router-navigation#readme", 31 | "devDependencies": { 32 | "babel-eslint": "7.2.3", 33 | "babel-jest": "18.0.0", 34 | "child_process": "^1.0.2", 35 | "eslint": "4.10.0", 36 | "eslint-config-react-app": "^2.0.1", 37 | "eslint-loader": "1.9.0", 38 | "eslint-plugin-flowtype": "2.39.1", 39 | "eslint-plugin-import": "2.8.0", 40 | "eslint-plugin-jsx-a11y": "5.1.1", 41 | "eslint-plugin-react": "7.4.0", 42 | "flow-bin": "0.67.1", 43 | "jest": "^23.6.0", 44 | "prettier": "^1.13.5", 45 | "pretty-quick": "^1.6.0", 46 | "trash-cli": "^1.4.0" 47 | }, 48 | "workspaces": { 49 | "packages": [ 50 | "packages/*", 51 | "examples/*" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-navigation-core", 3 | "version": "2.0.0-alpha.10", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "author": "Léo Le Bras (https://github.com/winoteam/)", 7 | "scripts": { 8 | "prepublish": "jest && yarn build", 9 | "build": "yarn build:cjs && yarn build:flow", 10 | "build:watch": "babel -d ./lib ./src/ --ignore __tests__ --watch", 11 | "build:cjs": "babel -d ./lib ./src/ --ignore __tests__", 12 | "build:flow": "for file in $(find ./src -name '*.js' -not -path '*/__tests__*'); do cp \"$file\" `echo \"$file\" | sed 's/\\/src\\//\\/lib\\//g'`.flow; done" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/winoteam/react-router-navigation.git" 17 | }, 18 | "keywords": [ 19 | "web", 20 | "react-native", 21 | "ios", 22 | "android", 23 | "router", 24 | "navigation", 25 | "navigator" 26 | ], 27 | "dependencies": { 28 | "fbjs": "0.8.17", 29 | "invariant": "^2.2.4", 30 | "iterall": "^1.2.2" 31 | }, 32 | "peerDependencies": { 33 | "react": "*", 34 | "react-router": "4.2.x" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "^6.26.0", 38 | "history": "4.7.2", 39 | "jest": "^23.6.0", 40 | "react": "16.3.1", 41 | "react-native": "0.55.4", 42 | "react-router": "4.2.0", 43 | "react-router-native": "4.2.0", 44 | "react-test-renderer": "16.3.1" 45 | }, 46 | "jest": { 47 | "preset": "react-native", 48 | "testRegex": "react-router-navigation-core/.*/__tests__/.*.spec.js$", 49 | "rootDir": "./../../", 50 | "transformIgnorePatterns": [ 51 | "node_modules/(?!(jest-)?react-native|react-navigation)" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/CardStack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { matchPath, type RouterHistory } from 'react-router' 5 | import invariant from 'invariant' 6 | import HistoryUtils from './HistoryUtils' 7 | import StackUtils from './StackUtils' 8 | import RouteUtils from './RouteUtils' 9 | import StateUtils from './StateUtils' 10 | import type { 11 | Route, 12 | CardsRendererProps, 13 | NavigationState, 14 | Card, 15 | BackHandler, 16 | } from './TypeDefinitions' 17 | 18 | type Props = { 19 | history: RouterHistory, 20 | children: React$Node[], 21 | backHandler: BackHandler, 22 | render: (props: CardsRendererProps) => React$Node, 23 | } 24 | 25 | type State = {| 26 | key: string, 27 | cards: Card[], 28 | navigationState: NavigationState<>, 29 | historyRootIndex: number, 30 | |} 31 | 32 | export default class CardStack extends React.Component { 33 | unlistenHistory: ?Function = null 34 | 35 | constructor(props: Props) { 36 | super(props) 37 | const { children, history } = props 38 | invariant( 39 | history, 40 | 'The prop `history` is marked as required in `CardStack`, but its value is `undefined` in CardStack', 41 | ) 42 | invariant( 43 | children || React.Children.count(children) > 0, 44 | 'A must have child elements', 45 | ) 46 | const { location } = history 47 | const entries = history.entries || [location] 48 | const cards = StackUtils.create(children, props) 49 | const key = `id-${Date.now()}` 50 | const navigationState = StateUtils.initialize( 51 | cards, 52 | location, 53 | entries, 54 | 'history', 55 | ) 56 | invariant( 57 | navigationState.index !== -1, 58 | 'There is no route defined for path « %s »', 59 | location.pathname, 60 | ) 61 | this.unlistenHistory = HistoryUtils.listen(history, this.onHistoryChange) 62 | const historyRootIndex = history.index - navigationState.index 63 | this.state = { key, cards, navigationState, historyRootIndex } 64 | } 65 | 66 | componentDidMount() { 67 | const { backHandler } = this.props 68 | backHandler.addEventListener('hardwareBackPress', this.onNavigateBack) 69 | } 70 | 71 | componentWillUnmount() { 72 | const { backHandler } = this.props 73 | if (this.unlistenHistory) this.unlistenHistory() 74 | backHandler.removeEventListener('hardwareBackPress', this.onNavigateBack) 75 | } 76 | 77 | componentWillReceiveProps(nextProps: Props) { 78 | const { children: nextChildren, history } = nextProps 79 | const { location } = history 80 | const { cards, historyRootIndex, navigationState } = this.state 81 | const nextCards = StackUtils.create(nextChildren, nextProps) 82 | const nextCard = nextCards.find(card => matchPath(location.pathname, card)) 83 | const nextRoute = nextCard && RouteUtils.create(nextCard, location) 84 | const isCorrumped = StateUtils.isCorrumped( 85 | navigationState, 86 | history, 87 | historyRootIndex, 88 | ) 89 | if ( 90 | nextRoute && 91 | nextCards && 92 | (!StackUtils.shallowEqual(cards, nextCards) || isCorrumped) 93 | ) { 94 | const { location, index } = history 95 | const entries = history.entries || [location] 96 | const newKey = `id-${Date.now()}` 97 | const nextNavigationState = StateUtils.initialize( 98 | nextCards, 99 | location, 100 | entries, 101 | 'history', 102 | navigationState, 103 | ) 104 | invariant( 105 | nextNavigationState.index !== -1, 106 | 'There is no route defined for path « %s »', 107 | location.pathname, 108 | ) 109 | const newHistoryRootIndex = index - nextNavigationState.index 110 | this.setState(prevState => ({ 111 | key: isCorrumped ? newKey : prevState.key, 112 | cards: nextCards, 113 | navigationState: nextNavigationState, 114 | historyRootIndex: newHistoryRootIndex, 115 | })) 116 | } 117 | } 118 | 119 | onHistoryChange = (history: RouterHistory, nextHistory: RouterHistory) => { 120 | const { index } = history 121 | const { location, action, index: nextIndex } = nextHistory 122 | const { navigationState, cards } = this.state 123 | const currentRoute = navigationState.routes[navigationState.index] 124 | const nextCard = cards.find(card => matchPath(location.pathname, card)) 125 | const nextRoute = nextCard && RouteUtils.create(nextCard, location) 126 | if (nextRoute && !RouteUtils.equal(currentRoute, nextRoute)) { 127 | switch (action) { 128 | case 'PUSH': { 129 | this.setState(prevState => ({ 130 | navigationState: StateUtils.push( 131 | prevState.navigationState, 132 | nextRoute, 133 | ), 134 | })) 135 | break 136 | } 137 | case 'POP': { 138 | const n = index - nextIndex 139 | this.setState(prevState => ({ 140 | navigationState: StateUtils.pop(prevState.navigationState, n), 141 | })) 142 | break 143 | } 144 | case 'REPLACE': { 145 | this.setState(prevState => ({ 146 | navigationState: StateUtils.replace( 147 | prevState.navigationState, 148 | prevState.navigationState.index, 149 | nextRoute, 150 | ), 151 | })) 152 | break 153 | } 154 | default: 155 | } 156 | } 157 | } 158 | 159 | onNavigateBack = () => { 160 | if (this.state.navigationState.index > 0) { 161 | this.props.history.goBack() 162 | return true 163 | } 164 | return false 165 | } 166 | 167 | shouldComponentUpdate(nextProps: Props, nextState: State) { 168 | return ( 169 | this.state.cards !== nextState.cards || 170 | this.state.navigationState !== nextState.navigationState 171 | ) 172 | } 173 | 174 | renderCard = (route: Route) => { 175 | const children = React.Children.toArray(this.props.children) 176 | const child = children.find(({ props }) => props.path === route.name) 177 | if (!child) return null 178 | return React.cloneElement(child, route) 179 | } 180 | 181 | render() { 182 | return this.props.render({ 183 | ...this.state, 184 | onNavigateBack: this.onNavigateBack, 185 | renderCard: this.renderCard, 186 | }) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/HistoryUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { createLocation } from 'history' 4 | import { type RouterHistory, type Location, matchPath } from 'react-router' 5 | import type { 6 | Route, 7 | RouteProps, 8 | HistoryNode, 9 | HistoryRootIndex, 10 | } from './TypeDefinitions' 11 | 12 | export default { 13 | listen(history: RouterHistory, callback: Function): Function { 14 | let lastHistory = { ...history } 15 | return history.listen(() => { 16 | callback(lastHistory, history) 17 | lastHistory = { ...history } 18 | }) 19 | }, 20 | 21 | createLocation(history: RouterHistory, route: RouteProps): Location { 22 | const path = route.initialPath || route.path 23 | return createLocation( 24 | path, 25 | history.location.state, 26 | undefined, 27 | history.location, 28 | ) 29 | }, 30 | 31 | saveNodes( 32 | source: Location | RouterHistory, 33 | route: Route, 34 | localHistoryState: { 35 | historyNodes: { [name: string]: HistoryNode }, 36 | historyRootIndex: number, 37 | }, 38 | ) { 39 | const { historyRootIndex, historyNodes } = localHistoryState 40 | if ('pathname' in source) { 41 | // $FlowFixMe 42 | const location: Location = source 43 | const historyNode = historyNodes[route.name] 44 | const index = historyNode ? historyNode.index : 0 45 | const entries = historyNode ? historyNode.entries : [location] 46 | return { ...historyNodes, [route.name]: { index, entries } } 47 | } 48 | // $FlowFixMe 49 | const history: RouterHistory = source 50 | const initialEntries = history.entries.slice(historyRootIndex) 51 | const initialIndex = initialEntries.findIndex(location => { 52 | return ( 53 | route.match && 54 | matchPath(location.pathname, { 55 | path: route.match.path, 56 | }) 57 | ) 58 | }) 59 | const index = history.index - historyRootIndex - initialIndex 60 | const entries = initialEntries.slice(initialIndex) 61 | return { ...historyNodes, [route.name]: { index, entries } } 62 | }, 63 | 64 | regenerateFromEntries( 65 | history: RouterHistory, 66 | historyNode: HistoryNode, 67 | historyRootIndex: HistoryRootIndex, 68 | ) { 69 | if ( 70 | historyNode.entries.length > 1 || 71 | history.entries.length > historyRootIndex + 1 72 | ) { 73 | history.entries = [ 74 | ...history.entries.slice(0, historyRootIndex), 75 | ...historyNode.entries, 76 | ] 77 | history.index = historyRootIndex + historyNode.index 78 | } 79 | history.replace(historyNode.entries[historyNode.index].pathname) 80 | }, 81 | 82 | regenerateFromLocation(history: RouterHistory, location: Location) { 83 | history.replace(location.pathname) 84 | }, 85 | } 86 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/PropTypes.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import PropTypes from 'prop-types' 4 | 5 | const MatchPropType = PropTypes.object 6 | 7 | const NavigationRoutePropType = PropTypes.shape({ 8 | key: PropTypes.string.isRequired, 9 | name: PropTypes.string.isRequired, 10 | match: MatchPropType, 11 | }) 12 | 13 | const NavigationStatePropType = PropTypes.shape({ 14 | routes: PropTypes.arrayOf(NavigationRoutePropType).isRequired, 15 | index: PropTypes.number.isRequired, 16 | }) 17 | 18 | export const RoutePropType = { 19 | path: PropTypes.string.isRequired, 20 | exact: PropTypes.bool, 21 | strict: PropTypes.bool, 22 | sensitive: PropTypes.bool, 23 | component: PropTypes.func, 24 | render: PropTypes.func, 25 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), 26 | } 27 | 28 | export const ItemPropType = PropTypes.shape({ 29 | ...RoutePropType, 30 | }) 31 | 32 | export const CardPropType = PropTypes.shape({ 33 | ...RoutePropType, 34 | }) 35 | 36 | export const TabPropType = PropTypes.shape({ 37 | ...RoutePropType, 38 | routePath: PropTypes.string, 39 | initialPath: PropTypes.string, 40 | }) 41 | 42 | export const CardRendererPropType = { 43 | navigationState: NavigationStatePropType, 44 | cards: PropTypes.arrayOf(CardPropType), 45 | renderCard: PropTypes.func.isRequired, 46 | onNavigateBack: PropTypes.func.isRequired, 47 | } 48 | 49 | export const TabsRendererPropType = { 50 | navigationState: NavigationStatePropType, 51 | tabs: PropTypes.arrayOf(TabPropType), 52 | renderTab: PropTypes.func.isRequired, 53 | onIndexChange: PropTypes.func.isRequired, 54 | } 55 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/RouteUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { matchPath, type Location } from 'react-router' 4 | import type { RouteProps, Route } from './TypeDefinitions' 5 | 6 | let uniqueBaseId = `id-${Date.now()}` 7 | let uuidCount = 0 8 | 9 | export default { 10 | create(item: RouteProps, location?: ?Location, staleRoute?: ?Route): ?Route { 11 | if (!item || !item.path) return null 12 | const routeName = item.path 13 | const routeMatch = location ? matchPath(location.pathname, item) : null 14 | const routePath = item.routePath || item.path 15 | const route = { ...item, path: routePath } 16 | const match = location && matchPath(location.pathname, route) 17 | const key = staleRoute ? staleRoute.key : match ? match.url : routeName 18 | return { 19 | key: staleRoute ? key : `${key}-${uniqueBaseId}-${uuidCount++}`, 20 | match: routeMatch, 21 | name: routeName, 22 | } 23 | }, 24 | 25 | equal(oldRoute: Route, newRoute: Route): boolean { 26 | if (!oldRoute || !newRoute) return false 27 | const { match: oldMatch } = oldRoute 28 | const { match: newMatch } = newRoute 29 | return !!(oldMatch && newMatch && oldMatch.url === newMatch.url) 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/SceneView.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { 5 | matchPath, 6 | type RouterHistory, 7 | type Location, 8 | type Match, 9 | } from 'react-router' 10 | import type { RouteProps } from './TypeDefinitions' 11 | 12 | type Props = RouteProps & { 13 | history?: RouterHistory, 14 | match?: ?Match, 15 | } 16 | 17 | type State = {| 18 | location: ?Location, 19 | match: ?Match, 20 | |} 21 | 22 | export default class SceneView extends React.Component { 23 | unlisten: ?Function 24 | 25 | constructor(props: Props) { 26 | super(props) 27 | const { history, match } = props 28 | this.state = { match: match || null, location: history && history.location } 29 | this.unlisten = history && history.listen(this.onHistoryChange) 30 | } 31 | 32 | componentWillUnmount() { 33 | if (this.unlisten) this.unlisten() 34 | } 35 | 36 | onHistoryChange = () => { 37 | const { history, routePath, path, exact, strict } = this.props 38 | if (history) { 39 | const { location } = history 40 | const { match: oldMatch } = this.state 41 | const minimalRoute = { path: routePath || path, exact, strict } 42 | const minimalMatch = matchPath(location.pathname, minimalRoute) 43 | const route = { path, exact, strict } 44 | const match = matchPath(location.pathname, route) 45 | const shouldUpdateLocation = !!match 46 | let shouldUpdateMatch = false 47 | if (match && minimalMatch) { 48 | if (!oldMatch) { 49 | shouldUpdateMatch = true 50 | } else if (oldMatch.url !== match.url) { 51 | if ( 52 | oldMatch.path !== minimalMatch.path && 53 | oldMatch.url.includes(minimalMatch.url) 54 | ) { 55 | shouldUpdateMatch = true 56 | } else if (routePath && oldMatch.path === minimalMatch.path) { 57 | shouldUpdateMatch = true 58 | } 59 | } 60 | } 61 | if (shouldUpdateMatch) { 62 | this.setState({ match, location }) 63 | } else if (shouldUpdateLocation) { 64 | this.setState({ location }) 65 | } 66 | } 67 | } 68 | 69 | shouldComponentUpdate(nextProps: Props, nextState: State) { 70 | return !!nextState.match 71 | } 72 | 73 | render() { 74 | const { render, children, component: Component, history } = this.props 75 | const { match, location } = this.state 76 | if (!history || !location) { 77 | return null 78 | } 79 | const contextRouter = { history, match, location } 80 | if (render) { 81 | return render(contextRouter) 82 | } else if (children && typeof children === 'function') { 83 | return children(contextRouter) 84 | } else if (children && React.Children.count(children) === 0) { 85 | return React.cloneElement(children, contextRouter) 86 | } else if (Component) { 87 | return 88 | } 89 | return null 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/StackUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { type Location, matchPath } from 'react-router' 5 | import shallowEqual from 'fbjs/lib/shallowEqual' 6 | import type { RouteProps } from './TypeDefinitions' 7 | 8 | export default { 9 | create(stackChildren: React$Node[], props: any) { 10 | // eslint-disable-next-line 11 | const { history, children, render, ...rest } = props 12 | return React.Children.toArray(stackChildren).reduce((stack, child) => { 13 | return [...stack, { ...rest, ...child.props }] 14 | }, []) 15 | }, 16 | 17 | shallowEqual(oldStack: T[], newStack: T[]): boolean { 18 | if (oldStack.length !== newStack.length) return false 19 | return oldStack.every((oldItem, index) => { 20 | return shallowEqual(oldItem, newStack[index]) 21 | }) 22 | }, 23 | 24 | getHistoryEntries( 25 | stack: RouteProps[], 26 | entries: Location[], 27 | location: Location, 28 | historyIndex?: number, 29 | ): Location[] { 30 | const startHistoryIndex = entries.reduce((acc, entry, index) => { 31 | if (stack.find(item => matchPath(entry.pathname, item))) { 32 | if (acc === -1) { 33 | return index 34 | } 35 | return acc 36 | } 37 | if (typeof historyIndex === 'number' && index > historyIndex) { 38 | return acc 39 | } 40 | return -1 41 | }, -1) 42 | const lastHistoryIndex = entries.reduce((acc, entry, index) => { 43 | if ( 44 | index < startHistoryIndex && 45 | typeof historyIndex === 'number' && 46 | index <= historyIndex 47 | ) { 48 | return -1 49 | } 50 | if (location.pathname === entry.pathname) { 51 | return index 52 | } 53 | return acc 54 | }, -1) 55 | return entries.slice(startHistoryIndex, lastHistoryIndex + 1) 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/StateUtils.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { forEach } from 'iterall' 4 | import { matchPath, type Location, type RouterHistory } from 'react-router' 5 | import RouteUtils from './RouteUtils' 6 | import StackUtils from './StackUtils' 7 | import type { NavigationState, RouteProps, Route } from './TypeDefinitions' 8 | 9 | export default { 10 | initialize( 11 | nodes: RouteProps[], 12 | location: Location, 13 | entries: Location[], 14 | buildFrom: 'history' | 'nodes', 15 | staleNavigationState?: ?NavigationState<>, 16 | index?: number, 17 | ): NavigationState<> { 18 | const historyEntries = StackUtils.getHistoryEntries( 19 | nodes, 20 | entries, 21 | location, 22 | index, 23 | ) 24 | const staleRoutes = staleNavigationState && staleNavigationState.routes 25 | if (buildFrom === 'nodes') { 26 | return nodes.reduce( 27 | (state, item) => { 28 | let entry 29 | forEach(historyEntries, (_entry, index) => { 30 | if (!entry) { 31 | const entryIndex = historyEntries.length - 1 - index 32 | const currentEntry = historyEntries[entryIndex] 33 | if (currentEntry && matchPath(currentEntry.pathname, item)) { 34 | entry = currentEntry 35 | } 36 | } 37 | }) 38 | const match = entry ? matchPath(entry.pathname, item) : null 39 | const staleRoute = staleRoutes && staleRoutes[state.routes.length] 40 | const route = RouteUtils.create(item, match && entry, staleRoute) 41 | if (!route) return state 42 | const isCurrentLocation = 43 | entry && entry.pathname === location.pathname 44 | return { 45 | routes: [...state.routes, route], 46 | index: isCurrentLocation ? state.routes.length : state.index, 47 | } 48 | }, 49 | { routes: [], index: -1 }, 50 | ) 51 | } 52 | return historyEntries.reduce( 53 | (state, entry) => { 54 | const item = nodes.find(route => { 55 | const routePath = route.routePath || route.path 56 | return matchPath(entry.pathname, { path: routePath, ...route }) 57 | }) 58 | if (!item || !item.path) return state 59 | const staleRoute = staleRoutes && staleRoutes[state.routes.length] 60 | const route = RouteUtils.create(item, entry, staleRoute) 61 | if (!route) return state 62 | const itemPath = item.routePath || item.path 63 | return { 64 | routes: [...state.routes, route], 65 | index: matchPath(location.pathname, { ...item, path: itemPath }) 66 | ? state.routes.length 67 | : state.index, 68 | } 69 | }, 70 | { routes: [], index: -1 }, 71 | ) 72 | }, 73 | 74 | getRouteIndex(state: NavigationState<>, arg: number | Route): number { 75 | if (typeof arg === 'number') { 76 | if (state.routes[arg]) return arg 77 | return -1 78 | } 79 | // $FlowFixMe 80 | return state.routes.findIndex(route => route.name === arg.name) 81 | }, 82 | 83 | isCorrumped( 84 | state: NavigationState<>, 85 | history: RouterHistory, 86 | historyRootIndex: number, 87 | ) { 88 | if (!Array.isArray(history.entries)) return false 89 | let isCorrumped = false 90 | forEach(state.routes.slice(0, state.index + 1), (route, index) => { 91 | const location = history.entries[historyRootIndex + index] 92 | const match = route ? route.match : null 93 | if ( 94 | !location || 95 | !(match && matchPath(location.pathname, { path: match.path })) 96 | ) { 97 | isCorrumped = true 98 | } 99 | }) 100 | return isCorrumped 101 | }, 102 | 103 | push(state: NavigationState<>, route: Route): NavigationState<> { 104 | const newRoutes = [...state.routes, route] 105 | return { 106 | ...state, 107 | index: newRoutes.length - 1, 108 | routes: newRoutes, 109 | } 110 | }, 111 | 112 | pop(state: NavigationState<>, n: number = 1): NavigationState<> { 113 | if (n <= 0) return state 114 | const newRoutes = state.routes.slice(0, Math.max(state.index + 1 - n, 1)) 115 | return { 116 | ...state, 117 | index: newRoutes.length - 1, 118 | routes: newRoutes, 119 | } 120 | }, 121 | 122 | replace( 123 | state: NavigationState<>, 124 | index: number, 125 | route: Route, 126 | ): NavigationState<> { 127 | if (state.routes[index] === route || index > state.routes.length - 1) { 128 | return state 129 | } 130 | const newRoutes = [ 131 | ...state.routes.slice(0, index), 132 | route, 133 | ...state.routes.slice(index + 1), 134 | ] 135 | return { 136 | ...state, 137 | index, 138 | routes: newRoutes, 139 | } 140 | }, 141 | 142 | changeIndex(state: NavigationState<>, arg: number | Route) { 143 | const index = 144 | typeof arg === 'number' 145 | ? arg 146 | : // $FlowFixMe 147 | state.routes.findIndex(route => route.name === arg.name) 148 | if (index === -1 || index > state.routes.length - 1) return state 149 | const routes = 150 | typeof arg === 'number' 151 | ? state.routes 152 | : [ 153 | ...state.routes.slice(0, index), 154 | arg, 155 | ...state.routes.slice(index + 1), 156 | ] 157 | return { ...state, routes, index } 158 | }, 159 | } 160 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/TabStack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { matchPath, type RouterHistory } from 'react-router' 5 | import invariant from 'invariant' 6 | import HistoryUtils from './HistoryUtils' 7 | import StackUtils from './StackUtils' 8 | import RouteUtils from './RouteUtils' 9 | import StateUtils from './StateUtils' 10 | import type { 11 | NavigationState, 12 | TabsRendererProps, 13 | Tab, 14 | Route, 15 | HistoryRootIndex, 16 | HistoryNodes, 17 | } from './TypeDefinitions' 18 | 19 | type Props = { 20 | history: RouterHistory, 21 | children: React$Node[], 22 | changeTabMode?: 'state' | 'history', 23 | render: (props: TabsRendererProps<>) => React$Node, 24 | lazy?: boolean, 25 | } 26 | 27 | type State = {| 28 | tabs: Tab[], 29 | navigationState: NavigationState<>, 30 | historyRootIndex: HistoryRootIndex, 31 | historyNodes: HistoryNodes, 32 | |} 33 | 34 | export default class TabStack extends React.Component { 35 | unlistenHistory: ?Function = null 36 | 37 | static defaultProps = { 38 | changeTabMode: 'state', 39 | } 40 | 41 | constructor(props: Props) { 42 | super(props) 43 | const { children, history } = props 44 | invariant( 45 | history, 46 | 'The prop `history` is marked as required in `TabStack`, but its value is `undefined` in TabStack', 47 | ) 48 | invariant( 49 | children || React.Children.count(children) > 0, 50 | 'A must have child elements', 51 | ) 52 | const { index, location } = history 53 | const entries = history.entries || [location] 54 | const tabs = children && StackUtils.create(children, props) 55 | const navigationState = StateUtils.initialize( 56 | tabs, 57 | location, 58 | entries, 59 | 'nodes', 60 | undefined, 61 | index, 62 | ) 63 | invariant( 64 | navigationState.index !== -1, 65 | 'There is no route defined for path « %s »', 66 | location.pathname, 67 | ) 68 | const initialRoute = navigationState.routes[navigationState.index] 69 | const historyRootIndex = index 70 | const historyNodes = { 71 | [initialRoute.name]: { index: 0, entries: entries.slice(index) }, 72 | } 73 | this.state = { tabs, navigationState, historyRootIndex, historyNodes } 74 | this.unlistenHistory = HistoryUtils.listen(history, this.onHistoryChange) 75 | } 76 | 77 | componentWillUnmount() { 78 | if (this.unlistenHistory) this.unlistenHistory() 79 | } 80 | 81 | componentWillReceiveProps(nextProps: Props) { 82 | const { tabs, navigationState } = this.state 83 | const { children: nextChildren, history } = nextProps 84 | const { location } = history 85 | const entries = history.entries || [location] 86 | const nextTabs = StackUtils.create(nextChildren, nextProps) 87 | const nextTab = nextTabs.find(tab => matchPath(location.pathname, tab)) 88 | const nextRoute = nextTab && RouteUtils.create(nextTab, location) 89 | if (nextRoute && !StackUtils.shallowEqual(tabs, nextTabs)) { 90 | const nextNavigationState = StateUtils.initialize( 91 | nextTabs, 92 | location, 93 | entries, 94 | 'nodes', 95 | navigationState, 96 | history.index, 97 | ) 98 | invariant( 99 | nextNavigationState.index !== -1, 100 | 'There is no route defined for path « %s »', 101 | location.pathname, 102 | ) 103 | this.setState({ 104 | tabs: nextTabs, 105 | navigationState: nextNavigationState, 106 | }) 107 | } 108 | } 109 | 110 | onHistoryChange = (history: RouterHistory, nextHistory: RouterHistory) => { 111 | const { location } = nextHistory 112 | const { navigationState, tabs } = this.state 113 | const { routes } = navigationState 114 | const currentRoute = routes[navigationState.index] 115 | const nextTab = tabs.find(tab => matchPath(location.pathname, tab)) 116 | if (nextTab) { 117 | const staleRoute = routes.find(route => route.name === nextTab.path) 118 | const nextRoute = RouteUtils.create(nextTab, location, staleRoute) 119 | this.setState(prevState => ({ 120 | navigationState: 121 | nextRoute && !RouteUtils.equal(currentRoute, nextRoute) 122 | ? StateUtils.changeIndex(prevState.navigationState, nextRoute) 123 | : prevState.navigationState, 124 | historyNodes: 125 | nextRoute && 'entries' in nextHistory 126 | ? HistoryUtils.saveNodes(nextHistory, nextRoute, prevState) 127 | : prevState.historyNodes, 128 | })) 129 | } 130 | } 131 | 132 | onIndexChange = (arg: number | Route) => { 133 | const { history, changeTabMode } = this.props 134 | const { tabs, navigationState, historyRootIndex } = this.state 135 | const index = StateUtils.getRouteIndex(navigationState, arg) 136 | const nextRoute = navigationState.routes[index] 137 | const nextTab = tabs.find(tab => tab.path === nextRoute.name) 138 | if (nextTab && index !== navigationState.index) { 139 | const location = HistoryUtils.createLocation(history, nextTab) 140 | this.setState( 141 | prevState => ({ 142 | navigationState: 143 | changeTabMode === 'state' 144 | ? StateUtils.changeIndex( 145 | navigationState, 146 | RouteUtils.create(nextTab, location, nextRoute) || index, 147 | ) 148 | : prevState.navigationState, 149 | historyNodes: HistoryUtils.saveNodes(location, nextRoute, prevState), 150 | }), 151 | () => { 152 | if ('entries' in history) { 153 | HistoryUtils.regenerateFromEntries( 154 | history, 155 | this.state.historyNodes[nextRoute.name], 156 | historyRootIndex, 157 | ) 158 | } else { 159 | HistoryUtils.regenerateFromLocation(history, location) 160 | } 161 | }, 162 | ) 163 | } else { 164 | const n = historyRootIndex - history.index 165 | if (n < 0) { 166 | history.go(n) 167 | } else if (nextTab) { 168 | if (typeof nextTab.onReset === 'function') { 169 | nextTab.onReset() 170 | } else if (nextTab.initialPath) { 171 | history.replace(nextTab.initialPath) 172 | } else if (nextTab.path) { 173 | history.replace(nextTab.path) 174 | } 175 | } 176 | } 177 | } 178 | 179 | shouldComponentUpdate(nextProps: Props, nextState: State) { 180 | return ( 181 | this.state.tabs !== nextState.tabs || 182 | this.state.navigationState !== nextState.navigationState 183 | ) 184 | } 185 | 186 | renderTab = (route: Route) => { 187 | const { lazy } = this.props 188 | const { historyNodes } = this.state 189 | if (lazy && !historyNodes[route.name]) return null 190 | const children = React.Children.toArray(this.props.children) 191 | const child = children.find(({ props }) => props.path === route.name) 192 | if (!child) return null 193 | return React.cloneElement(child, route) 194 | } 195 | 196 | render() { 197 | return this.props.render({ 198 | navigationState: this.state.navigationState, 199 | tabs: this.state.tabs, 200 | onIndexChange: this.onIndexChange, 201 | renderTab: this.renderTab, 202 | }) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/TypeDefinitions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { Location, ContextRouter, Match } from 'react-router' 4 | 5 | export type Route = { 6 | key: string, 7 | name: string, 8 | match: ?Match, 9 | } 10 | 11 | export type NavigationState = { 12 | index: number, 13 | routes: Array, 14 | } 15 | 16 | export type NavigationAction = { 17 | type: 'PUSH' | 'REPLACE' | 'POP' | 'CHANGE_INDEX', 18 | payload: { n?: number }, 19 | } 20 | 21 | export type RouteProps = { 22 | component?: React$ComponentType, 23 | render?: (router: ContextRouter) => React$Node, 24 | children?: (router: ContextRouter) => React$Node | React$Node, 25 | path?: string, 26 | routePath?: string, 27 | initialPath?: string, 28 | exact?: boolean, 29 | strict?: boolean, 30 | sensitive?: boolean, 31 | } 32 | 33 | export type Card = RouteProps 34 | 35 | export type CardsRendererProps = { 36 | renderCard: (route: Route) => React$Node, 37 | onNavigateBack: () => boolean, 38 | navigationState: NavigationState<>, 39 | cards: Card[], 40 | } 41 | 42 | export type Tab = RouteProps & { 43 | onReset?: () => void, 44 | } 45 | 46 | export type TabsRendererProps = { 47 | renderTab: (route: Route) => React$Node, 48 | onIndexChange: (index: number) => void, 49 | navigationState: NavigationState, 50 | tabs: Tab[], 51 | } 52 | 53 | export type HistoryNode = { 54 | index: number, 55 | entries: Location[], 56 | } 57 | 58 | export type HistoryNodes = { [name: string]: HistoryNode } 59 | 60 | export type HistoryRootIndex = number 61 | 62 | export type BackHandler = { 63 | addEventListener: (name: string, callback: Function) => void, 64 | removeEventListener: (name: string, callback: Function) => void, 65 | } 66 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/HistoryUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { createMemoryHistory } from 'history' 2 | import { matchPath } from 'react-router' 3 | import HistoryUtils from './../HistoryUtils' 4 | 5 | describe('HistoryUtils', () => { 6 | describe('listen', () => { 7 | it('should call callback', () => { 8 | const history = createMemoryHistory() 9 | const spy = jest.fn() 10 | HistoryUtils.listen(history, spy) 11 | history.push('/hello') 12 | expect(spy.mock.calls).toMatchObject([ 13 | [ 14 | { 15 | action: 'POP', 16 | index: 0, 17 | length: 1, 18 | location: { pathname: '/' }, 19 | entries: [{ pathname: '/' }], 20 | createHref: expect.any(Function), 21 | push: expect.any(Function), 22 | replace: expect.any(Function), 23 | go: expect.any(Function), 24 | goBack: expect.any(Function), 25 | goForward: expect.any(Function), 26 | canGo: expect.any(Function), 27 | block: expect.any(Function), 28 | listen: expect.any(Function), 29 | }, 30 | { 31 | action: 'PUSH', 32 | index: 1, 33 | length: 2, 34 | location: { pathname: '/hello' }, 35 | entries: [{ pathname: '/' }, { pathname: '/hello' }], 36 | createHref: expect.any(Function), 37 | push: expect.any(Function), 38 | replace: expect.any(Function), 39 | go: expect.any(Function), 40 | goBack: expect.any(Function), 41 | goForward: expect.any(Function), 42 | canGo: expect.any(Function), 43 | block: expect.any(Function), 44 | listen: expect.any(Function), 45 | }, 46 | ], 47 | ]) 48 | }) 49 | 50 | it('should return unlisten function', () => { 51 | const history = createMemoryHistory() 52 | const spy = jest.fn() 53 | const unlisten = HistoryUtils.listen(history, spy) 54 | unlisten() 55 | history.push('/hello') 56 | expect(spy.mock.calls).toHaveLength(0) 57 | }) 58 | }) 59 | 60 | describe('createLocation', () => { 61 | it('should return location with initial path', () => { 62 | const history = createMemoryHistory() 63 | const item = { path: '/:name', initialPath: '/hello' } 64 | expect(HistoryUtils.createLocation(history, item)).toMatchObject({ 65 | pathname: '/hello', 66 | }) 67 | }) 68 | 69 | it('should return location with path', () => { 70 | const history = createMemoryHistory() 71 | const item = { path: '/hello' } 72 | expect(HistoryUtils.createLocation(history, item)).toMatchObject({ 73 | pathname: '/hello', 74 | }) 75 | }) 76 | 77 | it('should return location with state', () => { 78 | const history = createMemoryHistory() 79 | history.push('/sayhello', { from: '/' }) 80 | const item = { path: '/hello' } 81 | expect(HistoryUtils.createLocation(history, item)).toMatchObject({ 82 | pathname: '/hello', 83 | state: { from: '/' }, 84 | }) 85 | }) 86 | }) 87 | 88 | describe('regenerateFromEntries', () => { 89 | it('should make a simple replace call (1)', () => { 90 | const historySpy = jest.fn() 91 | let history = createMemoryHistory({ 92 | initialIndex: 3, 93 | initialEntries: ['/', '/yolo', '/goodbye', '/hello'], 94 | }) 95 | history.listen(historySpy) 96 | HistoryUtils.regenerateFromEntries( 97 | history, 98 | { entries: [{ pathname: '/salut' }], index: 0 }, 99 | 3, 100 | ) 101 | expect(historySpy).toHaveBeenCalledTimes(1) 102 | expect(history).toMatchObject({ 103 | index: 3, 104 | entries: [ 105 | { pathname: '/' }, 106 | { pathname: '/yolo' }, 107 | { pathname: '/goodbye' }, 108 | { pathname: '/salut' }, 109 | ], 110 | location: { pathname: '/salut' }, 111 | }) 112 | }) 113 | 114 | it('should recreate history and make a simple replace call (1)', () => { 115 | const historySpy = jest.fn() 116 | let history = createMemoryHistory({ 117 | initialIndex: 3, 118 | initialEntries: ['/', '/yolo', '/goodbye', '/hello', '/hello/one'], 119 | }) 120 | history.listen(historySpy) 121 | HistoryUtils.regenerateFromEntries( 122 | history, 123 | { 124 | entries: [{ pathname: '/salut' }], 125 | index: 0, 126 | }, 127 | 3, 128 | ) 129 | expect(historySpy).toHaveBeenCalledTimes(1) 130 | expect(history).toMatchObject({ 131 | index: 3, 132 | entries: [ 133 | { pathname: '/' }, 134 | { pathname: '/yolo' }, 135 | { pathname: '/goodbye' }, 136 | { pathname: '/salut' }, 137 | ], 138 | location: { pathname: '/salut' }, 139 | }) 140 | }) 141 | 142 | it('should recreate history and make a simple replace call (2)', () => { 143 | const historySpy = jest.fn() 144 | let history = createMemoryHistory({ 145 | initialIndex: 3, 146 | initialEntries: ['/', '/yolo', '/goodbye', '/hello'], 147 | }) 148 | history.listen(historySpy) 149 | HistoryUtils.regenerateFromEntries( 150 | history, 151 | { 152 | entries: [{ pathname: '/salut' }, { pathname: '/salut/a' }], 153 | index: 1, 154 | }, 155 | 3, 156 | ) 157 | expect(historySpy).toHaveBeenCalledTimes(1) 158 | expect(history).toMatchObject({ 159 | index: 4, 160 | entries: [ 161 | { pathname: '/' }, 162 | { pathname: '/yolo' }, 163 | { pathname: '/goodbye' }, 164 | { pathname: '/salut' }, 165 | { pathname: '/salut/a' }, 166 | ], 167 | location: { pathname: '/salut/a' }, 168 | }) 169 | }) 170 | 171 | it('should recreate history and make a simple replace call (3)', () => { 172 | const historySpy = jest.fn() 173 | let history = createMemoryHistory({ 174 | initialIndex: 4, 175 | initialEntries: ['/', '/yolo', '/goodbye', '/hello', '/hello/one'], 176 | }) 177 | history.listen(historySpy) 178 | HistoryUtils.regenerateFromEntries( 179 | history, 180 | { 181 | entries: [{ pathname: '/salut' }, { pathname: '/salut/a' }], 182 | index: 1, 183 | }, 184 | 3, 185 | ) 186 | expect(historySpy).toHaveBeenCalledTimes(1) 187 | expect(history).toMatchObject({ 188 | index: 4, 189 | entries: [ 190 | { pathname: '/' }, 191 | { pathname: '/yolo' }, 192 | { pathname: '/goodbye' }, 193 | { pathname: '/salut' }, 194 | { pathname: '/salut/a' }, 195 | ], 196 | location: { pathname: '/salut/a' }, 197 | }) 198 | }) 199 | 200 | it('should recreate history and make a simple replace call (4)', () => { 201 | const historySpy = jest.fn() 202 | let history = createMemoryHistory({ 203 | initialIndex: 4, 204 | initialEntries: ['/', '/yolo', '/goodbye', '/hello', '/hello/one'], 205 | }) 206 | history.listen(historySpy) 207 | HistoryUtils.regenerateFromEntries( 208 | history, 209 | { 210 | entries: [{ pathname: '/salut' }, { pathname: '/salut/a' }], 211 | index: 0, 212 | }, 213 | 3, 214 | ) 215 | expect(historySpy).toHaveBeenCalledTimes(1) 216 | expect(history).toMatchObject({ 217 | index: 3, 218 | entries: [ 219 | { pathname: '/' }, 220 | { pathname: '/yolo' }, 221 | { pathname: '/goodbye' }, 222 | { pathname: '/salut' }, 223 | { pathname: '/salut/a' }, 224 | ], 225 | location: { pathname: '/salut' }, 226 | }) 227 | }) 228 | }) 229 | 230 | describe('regenerateFromLocation', () => { 231 | it('should recreate history and make a simple replace call (4)', () => { 232 | const historySpy = jest.fn() 233 | let history = createMemoryHistory({ 234 | initialIndex: 4, 235 | initialEntries: ['/', '/yolo', '/goodbye', '/hello', '/hello/one'], 236 | }) 237 | history.listen(historySpy) 238 | HistoryUtils.regenerateFromLocation(history, { pathname: '/salut/a' }) 239 | expect(historySpy).toHaveBeenCalledTimes(1) 240 | expect(history).toMatchObject({ 241 | index: 4, 242 | entries: [ 243 | { pathname: '/' }, 244 | { pathname: '/yolo' }, 245 | { pathname: '/goodbye' }, 246 | { pathname: '/hello' }, 247 | { pathname: '/salut/a' }, 248 | ], 249 | location: { pathname: '/salut/a' }, 250 | }) 251 | }) 252 | }) 253 | 254 | describe('saveNodes', () => { 255 | it('should return nodes from a location source', () => { 256 | const source = { pathname: '/a' } 257 | const route = { name: '/a', match: matchPath('/a', { path: '/a' }) } 258 | const localHistoryState = { 259 | historyNodes: { 260 | '/b': { 261 | index: 0, 262 | entries: [{ pathname: '/b' }], 263 | }, 264 | }, 265 | } 266 | expect( 267 | HistoryUtils.saveNodes(source, route, localHistoryState), 268 | ).toMatchObject({ 269 | '/a': { 270 | index: 0, 271 | entries: [{ pathname: '/a' }], 272 | }, 273 | '/b': { 274 | index: 0, 275 | entries: [{ pathname: '/b' }], 276 | }, 277 | }) 278 | }) 279 | 280 | it('should return nodes from a history source', () => { 281 | const source = { 282 | index: 1, 283 | entries: [{ pathname: '/a' }, { pathname: '/a' }], 284 | } 285 | const route = { name: '/a', match: matchPath('/a', { path: '/a' }) } 286 | const localHistoryState = { 287 | historyRootIndex: 1, 288 | historyNodes: { 289 | '/b': { 290 | index: 0, 291 | entries: [{ pathname: '/b' }], 292 | }, 293 | }, 294 | } 295 | expect( 296 | HistoryUtils.saveNodes(source, route, localHistoryState), 297 | ).toMatchObject({ 298 | '/a': { 299 | index: 0, 300 | entries: [{ pathname: '/a' }], 301 | }, 302 | '/b': { 303 | index: 0, 304 | entries: [{ pathname: '/b' }], 305 | }, 306 | }) 307 | }) 308 | 309 | it('should return nodes from a deep history source', () => { 310 | const source = { 311 | index: 2, 312 | entries: [{ pathname: '/d' }, { pathname: '/c' }, { pathname: '/a' }], 313 | } 314 | const route = { name: '/a', match: matchPath('/a', { path: '/a' }) } 315 | const localHistoryState = { 316 | historyRootIndex: 1, 317 | historyNodes: { '/b': { index: 0, entries: [{ pathname: '/b' }] } }, 318 | } 319 | expect( 320 | HistoryUtils.saveNodes(source, route, localHistoryState), 321 | ).toMatchObject({ 322 | '/a': { 323 | index: 0, 324 | entries: [{ pathname: '/a' }], 325 | }, 326 | '/b': { 327 | index: 0, 328 | entries: [{ pathname: '/b' }], 329 | }, 330 | }) 331 | }) 332 | 333 | it('should return nodes from a very deep history source', () => { 334 | const source = { 335 | index: 3, 336 | entries: [ 337 | { pathname: '/d' }, 338 | { pathname: '/c' }, 339 | { pathname: '/a' }, 340 | { pathname: '/a/a' }, 341 | ], 342 | } 343 | const route = { name: '/a', match: matchPath('/a', { path: '/a' }) } 344 | const localHistoryState = { 345 | historyRootIndex: 1, 346 | historyNodes: { '/b': { index: 0, entries: [{ pathname: '/b' }] } }, 347 | } 348 | expect( 349 | HistoryUtils.saveNodes(source, route, localHistoryState), 350 | ).toMatchObject({ 351 | '/a': { 352 | index: 1, 353 | entries: [{ pathname: '/a' }, { pathname: '/a/a' }], 354 | }, 355 | '/b': { 356 | index: 0, 357 | entries: [{ pathname: '/b' }], 358 | }, 359 | }) 360 | }) 361 | }) 362 | 363 | describe('regenerateFromEntries', () => { 364 | it('should regenerateFromEntries history in deep', () => { 365 | let history = createMemoryHistory({ 366 | initialIndex: 0, 367 | initialEntries: ['/a'], 368 | }) 369 | const spy = jest.fn() 370 | history.listen(spy) 371 | const historyNode = { 372 | index: 0, 373 | entries: [{ pathname: '/b' }], 374 | } 375 | const historyRootIndex = 0 376 | HistoryUtils.regenerateFromEntries(history, historyNode, historyRootIndex) 377 | expect(history).toMatchObject({ 378 | index: 0, 379 | length: 1, 380 | entries: [{ pathname: '/b' }], 381 | }) 382 | expect(spy).toHaveBeenCalledTimes(1) 383 | }) 384 | 385 | it('should regenerateFromEntries history in deep', () => { 386 | let history = createMemoryHistory({ 387 | initialIndex: 3, 388 | initialEntries: ['/', '/yolo', '/goodbye', '/hello', '/hello/one'], 389 | }) 390 | const spy = jest.fn() 391 | history.listen(spy) 392 | const historyNode = { 393 | index: 1, 394 | entries: [{ pathname: '/a' }, { pathname: '/b' }], 395 | } 396 | const historyRootIndex = 1 397 | HistoryUtils.regenerateFromEntries(history, historyNode, historyRootIndex) 398 | expect(history).toMatchObject({ 399 | index: 2, 400 | length: 3, 401 | entries: [{ pathname: '/' }, { pathname: '/a' }, { pathname: '/b' }], 402 | }) 403 | expect(spy).toHaveBeenCalledTimes(1) 404 | }) 405 | }) 406 | }) 407 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/RouteUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { createLocation } from 'history' 2 | import RouteUtils from './../RouteUtils' 3 | 4 | describe('RouteUtils', () => { 5 | describe('create', () => { 6 | it('should return Route object', () => { 7 | const location = createLocation('/contact') 8 | const route = RouteUtils.create({ path: '/contact' }, location) 9 | expect(route).toMatchObject({ 10 | name: '/contact', 11 | match: { 12 | url: '/contact', 13 | path: '/contact', 14 | params: {}, 15 | }, 16 | }) 17 | expect(route.key).toMatch('/contact') 18 | }) 19 | 20 | it('should return Route object with URL parameters', () => { 21 | const location = createLocation('/article/1') 22 | const route = RouteUtils.create({ path: '/article/:id' }, location) 23 | expect(route).toMatchObject({ 24 | name: '/article/:id', 25 | match: { 26 | url: '/article/1', 27 | path: '/article/:id', 28 | params: { id: '1' }, 29 | }, 30 | }) 31 | expect(route.key).toMatch('/article/1') 32 | }) 33 | 34 | it('should return Route object with URL parameters and staleRoute ', () => { 35 | const location = createLocation('/article/1') 36 | const route = RouteUtils.create({ path: '/article/:id' }, location, { 37 | key: '/article/:id', 38 | }) 39 | expect(route).toMatchObject({ 40 | name: '/article/:id', 41 | match: { 42 | url: '/article/1', 43 | path: '/article/:id', 44 | params: { id: '1' }, 45 | }, 46 | }) 47 | expect(route.key).toMatch('/article/:id') 48 | }) 49 | 50 | it('should return Route object without location argument', () => { 51 | const route = RouteUtils.create({ path: '/contact' }) 52 | expect(route).toMatchObject({ 53 | name: '/contact', 54 | match: null, 55 | }) 56 | expect(route.key).toMatch('/contact') 57 | }) 58 | 59 | it('should return Route object with URL parameters without location argument', () => { 60 | const route = RouteUtils.create({ path: '/article/:id' }) 61 | expect(route).toMatchObject({ 62 | name: '/article/:id', 63 | match: null, 64 | }) 65 | expect(route.key).toMatch('/article/:id') 66 | }) 67 | 68 | it('should return Route object with routePath defined', () => { 69 | const location = createLocation('/article/1/update') 70 | const route = RouteUtils.create( 71 | { 72 | path: '/article/:id/:method(create|update)?', 73 | routePath: '/article/:id', 74 | }, 75 | location, 76 | ) 77 | expect(route).toMatchObject({ 78 | name: '/article/:id/:method(create|update)?', 79 | match: { 80 | url: '/article/1/update', 81 | path: '/article/:id/:method(create|update)?', 82 | params: { id: '1', method: 'update' }, 83 | }, 84 | }) 85 | expect(route.key).toMatch('/article/1') 86 | }) 87 | 88 | it('should return null with no path defined', () => { 89 | expect(RouteUtils.create({})).toBe(null) 90 | }) 91 | }) 92 | 93 | describe('equal', () => { 94 | it('should return false if routes provided are null', () => { 95 | const routeA = RouteUtils.create() 96 | const routeB = RouteUtils.create() 97 | expect(RouteUtils.equal(routeA, routeB)).toBe(false) 98 | }) 99 | 100 | it('should return true if routes provided contain same paths', () => { 101 | const locationA = createLocation('/contact/email') 102 | const locationB = createLocation('/contact/twitter') 103 | const routeA = RouteUtils.create({ path: '/contact' }, locationA) 104 | const routeB = RouteUtils.create({ path: '/contact' }, locationB) 105 | expect(RouteUtils.equal(routeA, routeB)).toBe(true) 106 | }) 107 | 108 | it('should return fase if routes provided contain different paths', () => { 109 | const locationA = createLocation('/contact/email') 110 | const locationB = createLocation('/profiler') 111 | const routeA = RouteUtils.create({ path: '/contact' }, locationA) 112 | const routeB = RouteUtils.create({ path: '/profile' }, locationB) 113 | expect(RouteUtils.equal(routeA, routeB)).toBe(false) 114 | }) 115 | 116 | it('should return true if routes provided contain same URL parameters', () => { 117 | const locationA = createLocation('/article/1') 118 | const locationB = createLocation('/article/1') 119 | const routeA = RouteUtils.create({ path: '/article/:id' }, locationA) 120 | const routeB = RouteUtils.create({ path: '/article/:id' }, locationB) 121 | expect(RouteUtils.equal(routeA, routeB)).toBe(true) 122 | }) 123 | 124 | it('should return false if routes provided contain different URL parameters', () => { 125 | const locationA = createLocation('/article/1') 126 | const locationB = createLocation('/article/2') 127 | const routeA = RouteUtils.create({ path: '/article/:id' }, locationA) 128 | const routeB = RouteUtils.create({ path: '/article/:id' }, locationB) 129 | expect(RouteUtils.equal(routeA, routeB)).toBe(false) 130 | }) 131 | 132 | it('should return false if routes provided contain different URL parameters with routePath arg', () => { 133 | const locationA = createLocation('/article/1') 134 | const locationB = createLocation('/article/1/update') 135 | const routeA = RouteUtils.create( 136 | { path: '/article/:id/:method?', routePath: '/article/:id' }, 137 | locationA, 138 | ) 139 | const routeB = RouteUtils.create( 140 | { path: '/article/:id/:method?', routePath: '/article/:id' }, 141 | locationB, 142 | ) 143 | expect(RouteUtils.equal(routeA, routeB)).toBe(false) 144 | }) 145 | 146 | it('should return true if routes provided contain same URL parameters with routePath arg', () => { 147 | const locationA = createLocation('/article/1/update') 148 | const locationB = createLocation('/article/1/update') 149 | const routeA = RouteUtils.create( 150 | { path: '/article/:id/:method?', routePath: '/article/:id' }, 151 | locationA, 152 | ) 153 | const routeB = RouteUtils.create( 154 | { path: '/article/:id/:method?', routePath: '/article/:id' }, 155 | locationB, 156 | ) 157 | expect(RouteUtils.equal(routeA, routeB)).toBe(true) 158 | }) 159 | }) 160 | }) 161 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/SceneView.spec.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Text } from 'react-native' 3 | import { Router, Route } from 'react-router' 4 | import createHistory from 'history/createMemoryHistory' 5 | import renderer from 'react-test-renderer' 6 | import { componentFactory } from './utils' 7 | import './__mocks__' 8 | import SceneView from './../SceneView' 9 | 10 | describe('', () => { 11 | it('should render correctly', () => { 12 | const history = createHistory({ 13 | initialEntries: ['/1'], 14 | }) 15 | const SceneComponent = jest.fn(({ match }) => { 16 | return componentFactory()({ match }) 17 | }) 18 | const component = renderer.create( 19 | 20 | 21 | {contextRouter => ( 22 | 28 | )} 29 | 30 | , 31 | ) 32 | expect(component.toJSON()).toMatchSnapshot() 33 | expect(SceneComponent.mock.calls[0][0]).toMatchObject({ 34 | match: { url: '/1', params: { id: '1' } }, 35 | history: { location: { pathname: '/1' } }, 36 | location: { pathname: '/1' }, 37 | }) 38 | }) 39 | 40 | it('should re-render correctly on history change', async () => { 41 | const SceneComponent = jest.fn(({ match }) => { 42 | return componentFactory()({ match }) 43 | }) 44 | const history = createHistory({ 45 | initialEntries: ['/1'], 46 | }) 47 | const component = renderer.create( 48 | 49 | 50 | {contextRouter => ( 51 | 57 | )} 58 | 59 | , 60 | ) 61 | expect(component.toJSON()).toMatchSnapshot() 62 | history.push('/2') 63 | expect(component.toJSON()).toMatchSnapshot() 64 | expect(SceneComponent.mock.calls[0][0]).toMatchObject({ 65 | match: { url: '/1', params: { id: '1' } }, 66 | location: { pathname: '/1' }, 67 | }) 68 | expect(SceneComponent.mock.calls[1][0]).toMatchObject({ 69 | match: { url: '/1', params: { id: '1' } }, 70 | location: { pathname: '/1' }, 71 | }) 72 | expect(SceneComponent.mock.calls[2][0]).toMatchObject({ 73 | match: { url: '/1', params: { id: '1' } }, 74 | location: { pathname: '/2' }, 75 | }) 76 | expect(SceneComponent.mock.calls).toHaveLength(3) 77 | }) 78 | 79 | it('should re-render correctly on history change with no default route match prop', async () => { 80 | const SceneComponent = jest.fn(({ match }) => { 81 | return componentFactory()({ match }) 82 | }) 83 | const history = createHistory({ 84 | initialEntries: ['/1'], 85 | }) 86 | const component = renderer.create( 87 | 88 | 89 | {contextRouter => ( 90 | 95 | )} 96 | 97 | , 98 | ) 99 | expect(component.toJSON()).toMatchSnapshot() 100 | history.push('/2') 101 | expect(component.toJSON()).toMatchSnapshot() 102 | expect(SceneComponent.mock.calls[0][0]).toMatchObject({ 103 | match: null, 104 | location: { pathname: '/1' }, 105 | }) 106 | expect(SceneComponent.mock.calls[1][0]).toMatchObject({ 107 | match: { url: '/2', params: { id: '2' } }, 108 | location: { pathname: '/2' }, 109 | }) 110 | expect(SceneComponent.mock.calls).toHaveLength(2) 111 | }) 112 | 113 | it('should re-render correctly with routePath prop (1)', () => { 114 | const SceneComponent = jest.fn(({ match }) => { 115 | return componentFactory()({ match }) 116 | }) 117 | const history = createHistory({ initialEntries: ['/1'] }) 118 | const component = renderer.create( 119 | 120 | 121 | {contextRouter => ( 122 | 129 | )} 130 | 131 | , 132 | ) 133 | expect(component.toJSON()).toMatchSnapshot() 134 | history.push('/1/read') 135 | expect(component.toJSON()).toMatchSnapshot() 136 | history.push('/1/read/bookmarks') 137 | expect(component.toJSON()).toMatchSnapshot() 138 | history.push('/2/read') 139 | expect(component.toJSON()).toMatchSnapshot() 140 | expect(SceneComponent.mock.calls[0][0]).toMatchObject({ 141 | match: { url: '/1', params: { id: '1' } }, 142 | location: { pathname: '/1' }, 143 | }) 144 | expect(SceneComponent.mock.calls[1][0]).toMatchObject({ 145 | match: { url: '/1', params: { id: '1' } }, 146 | location: { pathname: '/1' }, 147 | }) 148 | expect(SceneComponent.mock.calls[2][0]).toMatchObject({ 149 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 150 | location: { pathname: '/1/read' }, 151 | }) 152 | expect(SceneComponent.mock.calls[3][0]).toMatchObject({ 153 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 154 | location: { pathname: '/1/read' }, 155 | }) 156 | expect(SceneComponent.mock.calls[4][0]).toMatchObject({ 157 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 158 | location: { pathname: '/1/read/bookmarks' }, 159 | }) 160 | expect(SceneComponent.mock.calls[5][0]).toMatchObject({ 161 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 162 | location: { pathname: '/1/read/bookmarks' }, 163 | }) 164 | expect(SceneComponent.mock.calls[6][0]).toMatchObject({ 165 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 166 | location: { pathname: '/2/read' }, 167 | }) 168 | expect(SceneComponent.mock.calls).toHaveLength(7) 169 | }) 170 | 171 | it('should re-render correctly with routePath prop (2)', () => { 172 | const SceneComponent = jest.fn(({ match }) => { 173 | return componentFactory()({ match }) 174 | }) 175 | const history = createHistory({ initialEntries: ['/1'] }) 176 | const component = renderer.create( 177 | 178 | 179 | {contextRouter => ( 180 | 191 | )} 192 | 193 | , 194 | ) 195 | expect(component.toJSON()).toMatchSnapshot() 196 | history.push('/1/read') 197 | expect(component.toJSON()).toMatchSnapshot() 198 | history.push('/1/read/bookmarks') 199 | expect(component.toJSON()).toMatchSnapshot() 200 | history.push('/1/update') 201 | expect(component.toJSON()).toMatchSnapshot() 202 | expect(SceneComponent.mock.calls[0][0]).toMatchObject({ 203 | match: { url: '/1', params: { id: '1' } }, 204 | }) 205 | expect(SceneComponent.mock.calls[1][0]).toMatchObject({ 206 | location: { pathname: '/1' }, 207 | }) 208 | expect(SceneComponent.mock.calls[2][0]).toMatchObject({ 209 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 210 | location: { pathname: '/1/read' }, 211 | }) 212 | expect(SceneComponent.mock.calls[3][0]).toMatchObject({ 213 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 214 | location: { pathname: '/1/read' }, 215 | }) 216 | expect(SceneComponent.mock.calls[4][0]).toMatchObject({ 217 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 218 | location: { pathname: '/1/read/bookmarks' }, 219 | }) 220 | expect(SceneComponent.mock.calls[5][0]).toMatchObject({ 221 | match: { url: '/1/read', params: { id: '1', method: 'read' } }, 222 | location: { pathname: '/1/read/bookmarks' }, 223 | }) 224 | expect(SceneComponent.mock.calls[6][0]).toMatchObject({ 225 | match: { url: '/1/update', params: { id: '1', method: 'update' } }, 226 | location: { pathname: '/1/update' }, 227 | }) 228 | expect(SceneComponent.mock.calls).toHaveLength(7) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/StackUtils.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createMemoryHistory } from 'history' 3 | import StackUtils from './../StackUtils' 4 | 5 | describe('StackUtils', () => { 6 | describe('create', () => { 7 | it('should return stack of items', () => { 8 | function Item() { 9 | return null 10 | } 11 | function rootRender() { 12 | return null 13 | } 14 | function rootChildren() { 15 | return null 16 | } 17 | function renderComponentA() { 18 | return null 19 | } 20 | function renderComponentB() { 21 | return null 22 | } 23 | expect( 24 | StackUtils.create( 25 | [ 26 | , 27 | , 28 | ], 29 | { 30 | history: createMemoryHistory(), 31 | render: rootRender, 32 | children: rootChildren, 33 | title: 'C', 34 | color: 'red', 35 | }, 36 | ), 37 | ).toEqual([ 38 | { 39 | path: '/a', 40 | title: 'A', 41 | render: renderComponentA, 42 | color: 'red', 43 | }, 44 | { 45 | path: '/b', 46 | title: 'B', 47 | component: renderComponentB, 48 | color: 'red', 49 | }, 50 | ]) 51 | }) 52 | }) 53 | 54 | describe('shallowEqual', () => { 55 | it('should return true if stack are equal', () => { 56 | expect( 57 | StackUtils.shallowEqual( 58 | [ 59 | { 60 | path: '/a', 61 | title: 'A', 62 | }, 63 | { 64 | path: '/b', 65 | title: 'B', 66 | }, 67 | ], 68 | [ 69 | { 70 | path: '/a', 71 | title: 'A', 72 | }, 73 | { 74 | path: '/b', 75 | title: 'B', 76 | }, 77 | ], 78 | ), 79 | ).toBe(true) 80 | }) 81 | 82 | it('should return false if stack are not equal', () => { 83 | expect( 84 | StackUtils.shallowEqual( 85 | [ 86 | { 87 | path: '/a', 88 | title: 'A', 89 | }, 90 | { 91 | path: '/b', 92 | title: 'B', 93 | }, 94 | ], 95 | [ 96 | { 97 | path: '/a', 98 | title: 'A', 99 | }, 100 | { 101 | path: '/b', 102 | title: 'B+', 103 | }, 104 | ], 105 | ), 106 | ).toBe(false) 107 | expect( 108 | StackUtils.shallowEqual( 109 | [ 110 | { 111 | path: '/a', 112 | title: 'A', 113 | }, 114 | { 115 | path: '/b', 116 | title: 'B', 117 | }, 118 | ], 119 | [ 120 | { 121 | path: '/a', 122 | title: 'A', 123 | }, 124 | { 125 | path: '/b', 126 | title: 'B', 127 | }, 128 | { 129 | path: '/c', 130 | title: 'C', 131 | }, 132 | ], 133 | ), 134 | ).toBe(false) 135 | }) 136 | }) 137 | 138 | describe('getHistoryEntries', () => { 139 | it('should return history entries', () => { 140 | const stack = [{ path: '/a' }, { path: '/b' }] 141 | const location = { pathname: '/a' } 142 | const entries = [ 143 | { pathname: '/c' }, 144 | { pathname: '/a' }, 145 | { pathname: '/d' }, 146 | { pathname: '/a' }, 147 | { pathname: '/b' }, 148 | { pathname: '/a' }, 149 | ] 150 | expect( 151 | StackUtils.getHistoryEntries(stack, entries, location), 152 | ).toMatchObject([ 153 | { pathname: '/a' }, 154 | { pathname: '/b' }, 155 | { pathname: '/a' }, 156 | ]) 157 | }) 158 | 159 | it('should return history entries with history index (1)', () => { 160 | const stack = [{ path: '/a' }, { path: '/b' }] 161 | const location = { pathname: '/a' } 162 | const entries = [ 163 | { pathname: '/c' }, 164 | { pathname: '/a' }, 165 | { pathname: '/d' }, 166 | { pathname: '/a' }, 167 | { pathname: '/b' }, 168 | { pathname: '/a' }, 169 | { pathname: '/c' }, 170 | ] 171 | const index = 5 172 | expect( 173 | StackUtils.getHistoryEntries(stack, entries, location, index), 174 | ).toMatchObject([ 175 | { pathname: '/a' }, 176 | { pathname: '/b' }, 177 | { pathname: '/a' }, 178 | ]) 179 | }) 180 | 181 | it('should return history entries with history index (2)', () => { 182 | const stack = [{ path: '/a' }, { path: '/b' }] 183 | const location = { pathname: '/a' } 184 | const entries = [ 185 | { pathname: '/c' }, 186 | { pathname: '/a' }, 187 | { pathname: '/d' }, 188 | { pathname: '/a' }, 189 | { pathname: '/b' }, 190 | { pathname: '/a' }, 191 | { pathname: '/c' }, 192 | ] 193 | const index = 6 194 | expect( 195 | StackUtils.getHistoryEntries(stack, entries, location, index), 196 | ).toMatchObject([]) 197 | }) 198 | }) 199 | }) 200 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/StateUtils.spec.js: -------------------------------------------------------------------------------- 1 | import { matchPath } from 'react-router' 2 | import StateUtils from './../StateUtils' 3 | 4 | describe('StateUtils', () => { 5 | describe('initialize', () => { 6 | it('should return initial state from nodes', () => { 7 | const nodes = [{ path: '/a' }, { path: '/b' }, { path: '/c' }] 8 | const location = { pathname: '/a' } 9 | const history = [{ pathname: '/b' }, location] 10 | const buildFrom = 'nodes' 11 | expect( 12 | StateUtils.initialize(nodes, location, history, buildFrom), 13 | ).toMatchObject({ 14 | index: 0, 15 | routes: [ 16 | { 17 | match: { isExact: true, params: {}, path: '/a', url: '/a' }, 18 | name: '/a', 19 | }, 20 | { 21 | match: { isExact: true, params: {}, path: '/b', url: '/b' }, 22 | name: '/b', 23 | }, 24 | { match: null, name: '/c' }, 25 | ], 26 | }) 27 | }) 28 | 29 | it('should return initial state from history', () => { 30 | const nodes = [{ path: '/a' }, { path: '/b' }, { path: '/c' }] 31 | const location = { pathname: '/a' } 32 | const history = [{ pathname: '/b' }, location] 33 | const buildFrom = 'history' 34 | expect( 35 | StateUtils.initialize(nodes, location, history, buildFrom), 36 | ).toMatchObject({ 37 | index: 1, 38 | routes: [ 39 | { 40 | match: { isExact: true, params: {}, path: '/b', url: '/b' }, 41 | name: '/b', 42 | }, 43 | { 44 | match: { isExact: true, params: {}, path: '/a', url: '/a' }, 45 | name: '/a', 46 | }, 47 | ], 48 | }) 49 | }) 50 | 51 | it('should return initial state from deep history', () => { 52 | const nodes = [{ path: '/a' }, { path: '/b' }, { path: '/c' }] 53 | const location = { pathname: '/a' } 54 | const entries = [ 55 | { pathname: '/b' }, 56 | { pathname: '/d' }, 57 | { pathname: '/c' }, 58 | location, 59 | ] 60 | const buildFrom = 'history' 61 | expect( 62 | StateUtils.initialize(nodes, location, entries, buildFrom), 63 | ).toMatchObject({ 64 | index: 1, 65 | routes: [ 66 | { 67 | match: { isExact: true, params: {}, path: '/c', url: '/c' }, 68 | name: '/c', 69 | }, 70 | { 71 | match: { isExact: true, params: {}, path: '/a', url: '/a' }, 72 | name: '/a', 73 | }, 74 | ], 75 | }) 76 | }) 77 | }) 78 | 79 | describe('getRouteIndex', () => { 80 | it('should return index with correct index arg', () => { 81 | const state = { 82 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 83 | index: 1, 84 | } 85 | expect(StateUtils.getRouteIndex(state, 0)).toBe(0) 86 | }) 87 | 88 | it('should return index with impossible index arg', () => { 89 | const state = { 90 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 91 | index: 1, 92 | } 93 | expect(StateUtils.getRouteIndex(state, 3)).toBe(-1) 94 | }) 95 | 96 | it('should return index with route arg', () => { 97 | const state = { 98 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 99 | index: 1, 100 | } 101 | expect(StateUtils.getRouteIndex(state, { name: '/a' })).toBe(0) 102 | }) 103 | }) 104 | 105 | describe('isCorrumped', () => { 106 | it('should return true', () => { 107 | const state = { 108 | routes: [ 109 | { match: matchPath('/a', { path: '/a' }) }, 110 | { match: matchPath('/b', { path: '/b' }) }, 111 | { match: matchPath('/c', { path: '/c' }) }, 112 | ], 113 | index: 1, 114 | } 115 | const history = { 116 | index: 1, 117 | entries: [{ pathname: '/c' }, { pathname: '/b' }], 118 | } 119 | expect(StateUtils.isCorrumped(state, history, 0)).toBe(true) 120 | }) 121 | 122 | it('should return false in web browser env', () => { 123 | const state = { 124 | routes: [ 125 | { match: matchPath('/a', { path: '/a' }) }, 126 | { match: matchPath('/b', { path: '/b' }) }, 127 | ], 128 | index: 1, 129 | } 130 | const history = { 131 | index: 1, 132 | } 133 | expect(StateUtils.isCorrumped(state, history, 0)).toBe(false) 134 | }) 135 | 136 | it('should return false with valid state', () => { 137 | const state = { 138 | routes: [ 139 | { match: matchPath('/a', { path: '/a' }) }, 140 | { match: matchPath('/b', { path: '/b' }) }, 141 | ], 142 | index: 1, 143 | } 144 | const history = { 145 | index: 1, 146 | entries: [{ pathname: '/a' }, { pathname: '/b' }], 147 | } 148 | expect(StateUtils.isCorrumped(state, history, 0)).toBe(false) 149 | }) 150 | }) 151 | 152 | describe('push', () => { 153 | it('should return new state', () => { 154 | const oldState = { 155 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 156 | index: 2, 157 | } 158 | expect(StateUtils.push(oldState, { name: '/d' })).toMatchObject({ 159 | routes: [ 160 | { name: '/a' }, 161 | { name: '/b' }, 162 | { name: '/c' }, 163 | { name: '/d' }, 164 | ], 165 | index: 3, 166 | }) 167 | }) 168 | }) 169 | 170 | describe('pop', () => { 171 | it('should return new state', () => { 172 | const oldState = { 173 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 174 | index: 2, 175 | } 176 | expect(StateUtils.pop(oldState, 1)).toMatchObject({ 177 | routes: [{ name: '/a' }, { name: '/b' }], 178 | index: 1, 179 | }) 180 | }) 181 | 182 | it('should return new state without n arg', () => { 183 | const oldState = { 184 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 185 | index: 2, 186 | } 187 | expect(StateUtils.pop(oldState)).toMatchObject({ 188 | routes: [{ name: '/a' }, { name: '/b' }], 189 | index: 1, 190 | }) 191 | }) 192 | 193 | it('should return new state with initial huge number of routes', () => { 194 | const oldState = { 195 | routes: [ 196 | { name: '/a' }, 197 | { name: '/b' }, 198 | { name: '/c' }, 199 | { name: '/d' }, 200 | ], 201 | index: 2, 202 | } 203 | expect(StateUtils.pop(oldState, 1)).toMatchObject({ 204 | routes: [{ name: '/a' }, { name: '/b' }], 205 | index: 1, 206 | }) 207 | }) 208 | 209 | it('should return new state with index arg that is not big', () => { 210 | const oldState = { 211 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 212 | index: 2, 213 | } 214 | expect(StateUtils.pop(oldState, 4)).toMatchObject({ 215 | routes: [{ name: '/a' }], 216 | index: 0, 217 | }) 218 | }) 219 | 220 | it('should return new state with index arg that is negative', () => { 221 | const oldState = { 222 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 223 | index: 1, 224 | } 225 | expect(StateUtils.changeIndex(oldState, -1)).toMatchObject({ 226 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 227 | index: 1, 228 | }) 229 | }) 230 | }) 231 | 232 | describe('replace', () => { 233 | it('should return new state', () => { 234 | const oldState = { 235 | routes: [ 236 | { name: '/a' }, 237 | { name: '/b' }, 238 | { name: '/c', title: 'Title' }, 239 | ], 240 | index: 1, 241 | } 242 | expect( 243 | StateUtils.replace(oldState, 2, { 244 | name: '/c', 245 | title: 'New title', 246 | }), 247 | ).toMatchObject({ 248 | routes: [ 249 | { name: '/a' }, 250 | { name: '/b' }, 251 | { name: '/c', title: 'New title' }, 252 | ], 253 | index: 2, 254 | }) 255 | }) 256 | 257 | it('should return new state with index arg that is greater than the length of the routes', () => { 258 | const oldState = { 259 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 260 | index: 2, 261 | } 262 | expect(StateUtils.replace(oldState, 3, { name: '/d' })).toMatchObject({ 263 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 264 | index: 2, 265 | }) 266 | }) 267 | 268 | it('should return new state with index arg that is negative', () => { 269 | const oldState = { 270 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 271 | index: 1, 272 | } 273 | expect( 274 | StateUtils.changeIndex(oldState, -1, { name: '/z' }), 275 | ).toMatchObject({ 276 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 277 | index: 1, 278 | }) 279 | }) 280 | }) 281 | 282 | describe('changeIndex', () => { 283 | it('should return new state with index arg', () => { 284 | const oldState = { 285 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 286 | index: 1, 287 | } 288 | expect(StateUtils.changeIndex(oldState, 2)).toMatchObject({ 289 | index: 2, 290 | }) 291 | }) 292 | 293 | it('should return new state with index arg that is greater than the length of the routes', () => { 294 | const oldState = { 295 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 296 | index: 1, 297 | } 298 | expect(StateUtils.changeIndex(oldState, 3)).toMatchObject({ 299 | index: 1, 300 | }) 301 | }) 302 | 303 | it('should return new state with index arg that is negative', () => { 304 | const oldState = { 305 | routes: [{ name: '/a' }, { name: '/b' }, { name: '/c' }], 306 | index: 1, 307 | } 308 | expect(StateUtils.changeIndex(oldState, -1)).toMatchObject({ 309 | index: 1, 310 | }) 311 | }) 312 | 313 | it('should return new state with route arg', () => { 314 | const oldState = { 315 | routes: [ 316 | { name: '/a' }, 317 | { name: '/b' }, 318 | { name: '/c', title: 'Title' }, 319 | ], 320 | index: 1, 321 | } 322 | expect( 323 | StateUtils.changeIndex(oldState, { 324 | name: '/c', 325 | title: 'New title', 326 | }), 327 | ).toMatchObject({ 328 | routes: [ 329 | { name: '/a' }, 330 | { name: '/b' }, 331 | { name: '/c', title: 'New title' }, 332 | ], 333 | index: 2, 334 | }) 335 | }) 336 | 337 | it('should return new state with route arg that is not exist in routes', () => { 338 | const oldState = { 339 | routes: [ 340 | { name: '/a' }, 341 | { name: '/b' }, 342 | { name: '/c', title: 'Title' }, 343 | ], 344 | index: 1, 345 | } 346 | expect( 347 | StateUtils.changeIndex(oldState, { 348 | name: '/d', 349 | }), 350 | ).toMatchObject({ 351 | routes: [ 352 | { name: '/a' }, 353 | { name: '/b' }, 354 | { name: '/c', title: 'Title' }, 355 | ], 356 | index: 1, 357 | }) 358 | }) 359 | }) 360 | }) 361 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/__mocks__/index.js: -------------------------------------------------------------------------------- 1 | jest.mock('View', () => { 2 | const RealComponent = require.requireActual('View') 3 | const React = require('react') 4 | const View = ({ children }) => { 5 | return React.createElement('View', {}, children) 6 | } 7 | View.propTypes = RealComponent.propTypes 8 | return View 9 | }) 10 | 11 | jest.mock('Text', () => { 12 | const RealComponent = require.requireActual('Text') 13 | const React = require('react') 14 | const Text = ({ children }) => { 15 | return React.createElement('Text', {}, children) 16 | } 17 | Text.propTypes = RealComponent.propTypes 18 | return Text 19 | }) 20 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/__snapshots__/CardStack.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should re render correctly when new cards are provided (1) 1`] = ` 4 | 5 | Index 6 | 7 | `; 8 | 9 | exports[` should re render correctly when new cards are provided (1) 2`] = ` 10 | 11 | Hello 12 | 13 | `; 14 | 15 | exports[` should re render correctly when new cards are provided (2) 1`] = ` 16 | 17 | Index 18 | 19 | `; 20 | 21 | exports[` should re render correctly when new cards are provided (2) 2`] = ` 22 | 23 | Hello 24 | 25 | `; 26 | 27 | exports[` should re-render correctly when "go" action is called 1`] = ` 28 | 29 | Goodbye 30 | 31 | `; 32 | 33 | exports[` should re-render correctly when "go" action is called 2`] = ` 34 | 35 | Index 36 | 37 | `; 38 | 39 | exports[` should re-render correctly when "go" action is called with URL parameters 1`] = ` 40 | 41 | {"id":"3"} 42 | 43 | `; 44 | 45 | exports[` should re-render correctly when "go" action is called with URL parameters 2`] = ` 46 | 47 | {"id":"1"} 48 | 49 | `; 50 | 51 | exports[` should re-render correctly when "goBack" action is called 1`] = ` 52 | 53 | Hello 54 | 55 | `; 56 | 57 | exports[` should re-render correctly when "goBack" action is called 2`] = ` 58 | 59 | Index 60 | 61 | `; 62 | 63 | exports[` should re-render correctly when "goBack" action is called with URL parameters 1`] = ` 64 | 65 | {"id":"2"} 66 | 67 | `; 68 | 69 | exports[` should re-render correctly when "goBack" action is called with URL parameters 2`] = ` 70 | 71 | {"id":"1"} 72 | 73 | `; 74 | 75 | exports[` should re-render correctly when "onNavigationBack" action is called 1`] = ` 76 | 77 | Hello 78 | 79 | `; 80 | 81 | exports[` should re-render correctly when "onNavigationBack" action is called 2`] = ` 82 | 83 | Index 84 | 85 | `; 86 | 87 | exports[` should re-render correctly when "push" action is called 1`] = ` 88 | 89 | Index 90 | 91 | `; 92 | 93 | exports[` should re-render correctly when "push" action is called 2`] = ` 94 | 95 | Hello 96 | 97 | `; 98 | 99 | exports[` should re-render correctly when "push" action is called with URL parameters 1`] = ` 100 | 101 | {"id":"1"} 102 | 103 | `; 104 | 105 | exports[` should re-render correctly when "push" action is called with URL parameters 2`] = ` 106 | 107 | {"id":"2"} 108 | 109 | `; 110 | 111 | exports[` should re-render correctly when "replace" action is called 1`] = ` 112 | 113 | Hello 114 | 115 | `; 116 | 117 | exports[` should re-render correctly when "replace" action is called 2`] = ` 118 | 119 | Goodbye 120 | 121 | `; 122 | 123 | exports[` should re-render correctly when "replace" action is called with URL parameters 1`] = ` 124 | 125 | {"id":"2"} 126 | 127 | `; 128 | 129 | exports[` should re-render correctly when "replace" action is called with URL parameters 2`] = ` 130 | 131 | {"id":"3"} 132 | 133 | `; 134 | 135 | exports[` should re-render correctly when "replace" action is called with routePath prop 1`] = ` 136 | 137 | {"id":"1"} 138 | 139 | `; 140 | 141 | exports[` should re-render correctly when "replace" action is called with routePath prop 2`] = ` 142 | 143 | {"id":"1","method":"update"} 144 | 145 | `; 146 | 147 | exports[` should re-render correctly when new history is provided 1`] = ` 148 | 149 | {"id":"1"} 150 | 151 | `; 152 | 153 | exports[` should re-render correctly when new history is provided 2`] = ` 154 | 155 | {"id":"3"} 156 | 157 | `; 158 | 159 | exports[` should re-render correctly when new history is provided 3`] = ` 160 | 161 | {"id":"5"} 162 | 163 | `; 164 | 165 | exports[` should render correctly 1`] = ` 166 | 167 | Index 168 | 169 | `; 170 | 171 | exports[` should render correctly with initialIndex and initialEntries props 1`] = ` 172 | 173 | Hello 174 | 175 | `; 176 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/__snapshots__/SceneView.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should re-render correctly on history change 1`] = ` 4 | 5 | {"id":"1"} 6 | 7 | `; 8 | 9 | exports[` should re-render correctly on history change 2`] = ` 10 | 11 | {"id":"1"} 12 | 13 | `; 14 | 15 | exports[` should re-render correctly on history change with no default route match prop 1`] = `null`; 16 | 17 | exports[` should re-render correctly on history change with no default route match prop 2`] = ` 18 | 19 | {"id":"2"} 20 | 21 | `; 22 | 23 | exports[` should re-render correctly with routePath prop (1) 1`] = ` 24 | 25 | {"id":"1"} 26 | 27 | `; 28 | 29 | exports[` should re-render correctly with routePath prop (1) 2`] = ` 30 | 31 | {"id":"1","method":"read"} 32 | 33 | `; 34 | 35 | exports[` should re-render correctly with routePath prop (1) 3`] = ` 36 | 37 | {"id":"1","method":"read"} 38 | 39 | `; 40 | 41 | exports[` should re-render correctly with routePath prop (1) 4`] = ` 42 | 43 | {"id":"1","method":"read"} 44 | 45 | `; 46 | 47 | exports[` should re-render correctly with routePath prop (2) 1`] = ` 48 | 49 | {"id":"1"} 50 | 51 | `; 52 | 53 | exports[` should re-render correctly with routePath prop (2) 2`] = ` 54 | 55 | {"id":"1","method":"read"} 56 | 57 | `; 58 | 59 | exports[` should re-render correctly with routePath prop (2) 3`] = ` 60 | 61 | {"id":"1","method":"read"} 62 | 63 | `; 64 | 65 | exports[` should re-render correctly with routePath prop (2) 4`] = ` 66 | 67 | {"id":"1","method":"update"} 68 | 69 | `; 70 | 71 | exports[` should render correctly 1`] = ` 72 | 73 | {"id":"1"} 74 | 75 | `; 76 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/__snapshots__/TabStack.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should not re-render correctly when "replace" action is called with not defined location 1`] = ` 4 | 5 | Hello 6 | 7 | `; 8 | 9 | exports[` should not re-render correctly when "replace" action is called with not defined location 2`] = ` 10 | 11 | Hello 12 | 13 | `; 14 | 15 | exports[` should re render correctly when new tabs are provided 1`] = ` 16 | 17 | Index 18 | 19 | `; 20 | 21 | exports[` should re render correctly when new tabs are provided 2`] = ` 22 | 23 | Hello 24 | 25 | `; 26 | 27 | exports[` should re-render correctly when "go" action is called 1`] = ` 28 | 29 | Index 30 | 31 | `; 32 | 33 | exports[` should re-render correctly when "go" action is called 2`] = ` 34 | 35 | Hello 36 | 37 | `; 38 | 39 | exports[` should re-render correctly when "goBack" action is called 1`] = ` 40 | 41 | Goodbye 42 | 43 | `; 44 | 45 | exports[` should re-render correctly when "goBack" action is called 2`] = ` 46 | 47 | Hello 48 | 49 | `; 50 | 51 | exports[` should re-render correctly when "onIndexChange" action is called with index arg 1`] = ` 52 | 53 | Index 54 | 55 | `; 56 | 57 | exports[` should re-render correctly when "onIndexChange" action is called with index arg 2`] = ` 58 | 59 | Goodbye 60 | 61 | `; 62 | 63 | exports[` should re-render correctly when "onIndexChange" action is called with index arg and initialPath prop 1`] = ` 64 | 65 | Index 66 | 67 | `; 68 | 69 | exports[` should re-render correctly when "onIndexChange" action is called with index arg and initialPath prop 2`] = ` 70 | 71 | {"name":"goodbye"} 72 | 73 | `; 74 | 75 | exports[` should re-render correctly when "onIndexChange" action is called with index arg inside advanced history nodes 1`] = ` 76 | 77 | Hello 78 | 79 | `; 80 | 81 | exports[` should re-render correctly when "onIndexChange" action is called with index arg inside advanced history nodes 2`] = ` 82 | 83 | Goodbye 84 | 85 | `; 86 | 87 | exports[` should re-render correctly when "onIndexChange" action is called with index arg inside advanced history nodes 3`] = ` 88 | 89 | Hello 90 | 91 | `; 92 | 93 | exports[` should re-render correctly when "onIndexChange" action is called with route arg 1`] = ` 94 | 95 | Index 96 | 97 | `; 98 | 99 | exports[` should re-render correctly when "onIndexChange" action is called with route arg 2`] = ` 100 | 101 | Goodbye 102 | 103 | `; 104 | 105 | exports[` should re-render correctly when "push" action is called 1`] = ` 106 | 107 | Index 108 | 109 | `; 110 | 111 | exports[` should re-render correctly when "push" action is called 2`] = ` 112 | 113 | Hello 114 | 115 | `; 116 | 117 | exports[` should re-render correctly when "replace" action is called 1`] = ` 118 | 119 | Index 120 | 121 | `; 122 | 123 | exports[` should re-render correctly when "replace" action is called 2`] = ` 124 | 125 | Hello 126 | 127 | `; 128 | 129 | exports[` should re-render correctly when "replace" action is called with routePath prop 1`] = ` 130 | 131 | {"language":"en"} 132 | 133 | `; 134 | 135 | exports[` should re-render correctly when "replace" action is called with routePath prop 2`] = ` 136 | 137 | {"language":"fr"} 138 | 139 | `; 140 | 141 | exports[` should re-render with lazy prop disabled 1`] = ` 142 | Array [ 143 | 144 | 145 | Index 146 | 147 | , 148 | 149 | 150 | Hello 151 | 152 | , 153 | 154 | 155 | Goodbye 156 | 157 | , 158 | ] 159 | `; 160 | 161 | exports[` should re-render with lazy prop disabled 2`] = ` 162 | Array [ 163 | 164 | 165 | Index 166 | 167 | , 168 | 169 | 170 | Hello 171 | 172 | , 173 | 174 | 175 | Goodbye 176 | 177 | , 178 | ] 179 | `; 180 | 181 | exports[` should re-render with lazy prop disabled 3`] = ` 182 | Array [ 183 | 184 | 185 | Index 186 | 187 | , 188 | 189 | 190 | Hello 191 | 192 | , 193 | 194 | 195 | Goodbye 196 | 197 | , 198 | ] 199 | `; 200 | 201 | exports[` should re-render with lazy prop enabled 1`] = ` 202 | Array [ 203 | , 204 | 205 | 206 | Hello 207 | 208 | , 209 | , 210 | ] 211 | `; 212 | 213 | exports[` should re-render with lazy prop enabled 2`] = ` 214 | Array [ 215 | 216 | 217 | Index 218 | 219 | , 220 | 221 | 222 | Hello 223 | 224 | , 225 | , 226 | ] 227 | `; 228 | 229 | exports[` should re-render with lazy prop enabled 3`] = ` 230 | Array [ 231 | 232 | 233 | Index 234 | 235 | , 236 | 237 | 238 | Hello 239 | 240 | , 241 | 242 | 243 | Goodbye 244 | 245 | , 246 | ] 247 | `; 248 | 249 | exports[` should render correctly 1`] = ` 250 | 251 | Index 252 | 253 | `; 254 | 255 | exports[` should render correctly with initialIndex and initialEntries props 1`] = ` 256 | 257 | Hello 258 | 259 | `; 260 | 261 | exports[` should reset tab by calling history.go function 1`] = ` 262 | 263 | Index 264 | 265 | `; 266 | 267 | exports[` should reset tab by calling history.go function 2`] = ` 268 | 269 | {"name":"goodbye"} 270 | 271 | `; 272 | 273 | exports[` should reset tab by calling history.replace function with initialPath prop 1`] = ` 274 | 275 | {"name":"goodbye"} 276 | 277 | `; 278 | 279 | exports[` should reset tab by calling history.replace function with initialPath prop 2`] = ` 280 | 281 | {"name":"goodbye"} 282 | 283 | `; 284 | 285 | exports[` should reset tab by calling history.replace function with path prop 1`] = ` 286 | 287 | Goodbye 288 | 289 | `; 290 | 291 | exports[` should reset tab by calling history.replace function with path prop 2`] = ` 292 | 293 | Goodbye 294 | 295 | `; 296 | 297 | exports[` should reset tab by calling onReset prop function 1`] = ` 298 | 299 | Goodbye 300 | 301 | `; 302 | 303 | exports[` should reset tab by calling onReset prop function 2`] = ` 304 | 305 | Goodbye 306 | 307 | `; 308 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/__tests__/utils.js: -------------------------------------------------------------------------------- 1 | /* @noflow */ 2 | 3 | import React, { createElement } from 'react' 4 | import { Text } from 'react-native' 5 | import * as StackUtils from './../StackUtils' 6 | 7 | export function componentFactory(message) { 8 | return function({ match }) { 9 | if (!match && !message) return null 10 | return ( 11 | 12 | {(match && 13 | Object.values(match.params).length > 0 && 14 | JSON.stringify(match.params)) || 15 | message} 16 | 17 | ) 18 | } 19 | } 20 | 21 | export function renderCardView({ navigationState, renderCard }) { 22 | const route = navigationState.routes[navigationState.index] 23 | return renderCard(route) 24 | } 25 | 26 | export function renderTabView({ navigationState, renderTab }) { 27 | const route = navigationState.routes[navigationState.index] 28 | return renderTab(route) 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-router-navigation-core/src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // High-level wrappers 4 | export { default as CardStack } from './CardStack' 5 | export { default as TabStack } from './TabStack' 6 | export { default as SceneView } from './SceneView' 7 | 8 | // Low-level building blocks 9 | export { default as HistoryUtils } from './HistoryUtils' 10 | export { default as RouteUtils } from './RouteUtils' 11 | export { default as StackUtils } from './StackUtils' 12 | export { default as StateUtils } from './StateUtils' 13 | 14 | // Prop types 15 | export { 16 | CardPropType, 17 | TabPropType, 18 | CardRendererPropType, 19 | TabsRendererPropType, 20 | } from './PropTypes' 21 | 22 | // Type definitions 23 | export type { 24 | Route, 25 | RouteProps, 26 | NavigationState, 27 | Card, 28 | CardsRendererProps, 29 | Tab, 30 | TabsRendererProps, 31 | } from './TypeDefinitions' 32 | -------------------------------------------------------------------------------- /packages/react-router-navigation/README.md: -------------------------------------------------------------------------------- 1 | # react-router-navigation 2 | 3 | Declarative routing for [React Native](https://facebook.github.io/react-native/) based on [`react-router`](https://reacttraining.com/react-router/) and [`react-navigation`](https://reactnavigation.org/). 4 | 5 | ## How to use 6 | 7 | Install: 8 | 9 | ```shell 10 | $ yarn add react-router react-router-native react-router-navigation 11 | ``` 12 | 13 | And then, enjoy it: 14 | 15 | ```js 16 | import * as React from 'react' 17 | import { Text } from 'react-native' 18 | import { NativeRouter, Link } from 'react-router-native' 19 | import { Navigation, Card } from 'react-router-navigation' 20 | 21 | const App = () => ( 22 | 23 | 24 | ( 28 | 29 | Press it 30 | 31 | )} 32 | /> 33 | Hello} 36 | /> 37 | 38 | 39 | ) 40 | ``` 41 | 42 | ## Questions 43 | 44 | If you have any questions, feel free to get in touch on Twitter [@Leo_LeBras](https://twitter.com/Leo_LeBras) or open an issue. 45 | 46 | ## Credits 47 | 48 | React Router Navigation is built and maintained by [Léo Le Bras](https://twitter.com/Leo_LeBras). 49 | -------------------------------------------------------------------------------- /packages/react-router-navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-navigation", 3 | "version": "2.0.0-alpha.10", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "author": "Léo Le Bras (https://github.com/winoteam/)", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/winoteam/react-router-navigation.git" 10 | }, 11 | "scripts": { 12 | "build": "yarn build:cjs && yarn build:flow", 13 | "build:watch": "babel -d ./lib ./src/ --ignore __tests__ --watch", 14 | "build:cjs": "babel -d ./lib ./src/ --ignore __tests__", 15 | "build:flow": "for file in $(find ./src -name '*.js' -not -path '*/__tests__*'); do cp \"$file\" `echo \"$file\" | sed 's/\\/src\\//\\/lib\\//g'`.flow; done" 16 | }, 17 | "keywords": [ 18 | "react-native", 19 | "ios", 20 | "android", 21 | "router", 22 | "navigation", 23 | "navigator" 24 | ], 25 | "peerDependencies": { 26 | "react": "*", 27 | "react-native": "*", 28 | "react-router": "4.2.x" 29 | }, 30 | "dependencies": { 31 | "react-native-tab-view": "1.2.0", 32 | "react-navigation": "1.5.11", 33 | "react-router-navigation-core": "2.0.0-alpha.10" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "^6.26.0", 37 | "react": "16.3.1", 38 | "react-native": "0.55.4", 39 | "react-router": "4.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/BottomNavigation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Dimensions } from 'react-native' 3 | import { Route } from 'react-router' 4 | import { TabStack } from 'react-router-navigation-core' 5 | import { PagerPan } from 'react-native-tab-view' 6 | import DefaultTabsRenderer from './DefaultTabsRenderer' 7 | import TabBarBottom from './TabBarBottom' 8 | import { BottomNavigationPropTypes } from './PropTypes' 9 | 10 | export default class BottomNavigation extends React.Component { 11 | static propTypes = BottomNavigationPropTypes 12 | 13 | static defaultProps = { 14 | lazy: true, 15 | initialLayout: Dimensions.get('window'), 16 | } 17 | 18 | renderPager = props => pagerProps => { 19 | return ( 20 | 26 | ) 27 | } 28 | 29 | renderTabBar = props => tabBarProps => { 30 | if (tabBarProps.hideTabBar) return null 31 | if (tabBarProps.renderTabBar) { 32 | return React.createElement(tabBarProps.renderTabBar, { 33 | ...props, 34 | ...tabBarProps, 35 | }) 36 | } 37 | return 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | {({ history }) => ( 44 | ( 49 | 57 | )} 58 | /> 59 | )} 60 | 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/Card.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Route } from 'react-router' 3 | import { SceneView } from 'react-router-navigation-core' 4 | import { CardPropTypes } from './PropTypes' 5 | 6 | export default class Card extends React.Component { 7 | static propTypes = CardPropTypes 8 | 9 | render() { 10 | return ( 11 | 12 | {({ history }) => { 13 | return 14 | }} 15 | 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/DefaultNavigationRenderer.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { NativeModules } from 'react-native' 3 | import { 4 | Transitioner, 5 | CardStack, 6 | StackRouter, 7 | NavigationActions, 8 | addNavigationHelpers, 9 | } from 'react-navigation' 10 | import CardStackStyleInterpolator from 'react-navigation/src/views/CardStack/CardStackStyleInterpolator' 11 | import TransitionConfigs from 'react-navigation/src/views/CardStack/TransitionConfigs' 12 | import { DefaultNavigationRendererPropTypes } from './PropTypes' 13 | 14 | const NativeAnimatedModule = NativeModules && NativeModules.NativeAnimatedModule 15 | 16 | export default class DefaultNavigationRenderer extends React.Component { 17 | static propTypes = DefaultNavigationRendererPropTypes 18 | 19 | constructor(props) { 20 | super(props) 21 | this.state = { router: this.getRouter(props) } 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (this.props.cards !== nextProps.cards) { 26 | this.setState({ router: this.getRouter(nextProps) }) 27 | } 28 | } 29 | 30 | getRouter = props => { 31 | const { renderHeader, cards } = props 32 | const routeConfigMap = cards.reduce((acc, card) => { 33 | return { 34 | ...acc, 35 | [card.path]: { 36 | screen: this.getScreenComponent(card), 37 | navigationOptions: { 38 | ...props, 39 | ...card, 40 | header: sceneProps => { 41 | return renderHeader({ ...props, ...sceneProps }) 42 | }, 43 | }, 44 | }, 45 | } 46 | }, {}) 47 | return StackRouter(routeConfigMap) 48 | } 49 | 50 | getScreenComponent = () => { 51 | return this.renderScreenComponent 52 | } 53 | 54 | renderScreenComponent = ({ navigation }) => { 55 | const { renderCard } = this.props 56 | const { state: route } = navigation 57 | return renderCard(route) 58 | } 59 | 60 | configureTransition = (transitionProps, prevTransitionProps) => { 61 | const isModal = this.props.mode === 'modal' 62 | const transitionSpec = { 63 | ...TransitionConfigs.getTransitionConfig( 64 | undefined, 65 | transitionProps, 66 | prevTransitionProps, 67 | isModal, 68 | ).transitionSpec, 69 | } 70 | if ( 71 | !!NativeAnimatedModule && 72 | CardStackStyleInterpolator.canUseNativeDriver() 73 | ) { 74 | transitionSpec.useNativeDriver = true 75 | } 76 | return transitionSpec 77 | } 78 | 79 | renderStack = (props, prevProps) => { 80 | const { cards } = this.props 81 | const { router } = this.state 82 | const { 83 | scene: { route }, 84 | } = props 85 | const cardStackProps = cards.find(card => card.path === route.name) 86 | const { 87 | screenProps, 88 | headerMode, 89 | headerTransitionPreset, 90 | mode, 91 | cardStyle, 92 | transitionConfig, 93 | } = cardStackProps 94 | return ( 95 | 106 | ) 107 | } 108 | 109 | render() { 110 | const { cards, navigationState, onNavigateBack } = this.props 111 | const route = navigationState.routes[navigationState.index] 112 | const transitionerProps = cards.find(card => card.path === route.name) 113 | const { 114 | configureTransition, 115 | onTransitionStart, 116 | onTransitionEnd, 117 | } = transitionerProps 118 | return ( 119 | ({ 128 | ...route, 129 | routeName: route.name, 130 | })), 131 | }, 132 | addListener: () => ({}), 133 | dispatch: action => { 134 | if (action.type === NavigationActions.back().type) { 135 | onNavigateBack() 136 | } 137 | }, 138 | })} 139 | /> 140 | ) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/DefaultTabsRenderer.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { StyleSheet } from 'react-native' 3 | import { TabView } from 'react-native-tab-view' 4 | import { DefaultTabsRendererPropTypes } from './PropTypes' 5 | 6 | const styles = StyleSheet.create({ 7 | container: { 8 | flex: 1, 9 | }, 10 | }) 11 | 12 | export default class DefaultTabsRenderer extends React.Component { 13 | static propTypes = DefaultTabsRendererPropTypes 14 | 15 | static defaultProps = { 16 | tabBarPosition: 'top', 17 | } 18 | 19 | renderTabBar = sceneProps => { 20 | const { tabs, renderTabBar, tabBarPosition } = this.props 21 | if (!renderTabBar) return null 22 | const { navigationState } = sceneProps 23 | const route = navigationState.routes[navigationState.index] 24 | const activeTab = tabs.find(tab => tab.path === route.name) 25 | if (!activeTab) return null 26 | const { path, ...tabProps } = activeTab 27 | const tabBarProps = { tabs, tabBarPosition, ...sceneProps, ...tabProps } 28 | if (tabBarProps.hideTabBar) return null 29 | return React.createElement(renderTabBar, { 30 | ...tabBarProps, 31 | ...sceneProps, 32 | style: tabBarProps.tabBarStyle, 33 | indicatorStyle: tabBarProps.tabBarIndicatorStyle, 34 | }) 35 | } 36 | 37 | renderScene = sceneProps => { 38 | const { renderTab } = this.props 39 | const { route } = sceneProps 40 | return renderTab(route) 41 | } 42 | 43 | render() { 44 | return ( 45 | 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/NavBar.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Header, HeaderTitle, HeaderBackButton } from 'react-navigation' 3 | import { NavBarPropTypes } from './PropTypes' 4 | 5 | export default class NavBar extends React.Component { 6 | static propTypes = NavBarPropTypes 7 | 8 | renderLeftComponent = sceneProps => { 9 | const { scenes, cards } = sceneProps 10 | if (sceneProps.renderLeftButton) { 11 | return sceneProps.renderLeftButton(sceneProps) 12 | } 13 | if ( 14 | sceneProps.scene.index === 0 || 15 | !sceneProps.onNavigateBack || 16 | sceneProps.hideBackButton 17 | ) { 18 | return null 19 | } 20 | const previousScene = scenes[Math.max(0, sceneProps.scene.index - 1)] 21 | const { name: previousRouteName } = previousScene.route 22 | const previousCard = cards.find(card => card.path === previousRouteName) 23 | const previousSceneProps = { ...previousScene, ...previousCard } 24 | return ( 25 | 30 | ) 31 | } 32 | 33 | renderTitleComponent = sceneProps => { 34 | if (sceneProps.renderTitle) { 35 | return sceneProps.renderTitle(sceneProps) 36 | } 37 | return ( 38 | 39 | {sceneProps.title} 40 | 41 | ) 42 | } 43 | 44 | renderRightComponent = sceneProps => { 45 | if (sceneProps.renderRightButton) { 46 | return sceneProps.renderRightButton(sceneProps) 47 | } 48 | return null 49 | } 50 | 51 | render() { 52 | return ( 53 |

{ 56 | const { route } = scene 57 | const activeCard = this.props.cards.find(card => { 58 | return card.path === route.name 59 | }) 60 | const sceneProps = { 61 | ...this.props, 62 | ...activeCard, 63 | ...scene.route, 64 | scene, 65 | } 66 | return { 67 | options: { 68 | headerStyle: sceneProps.navBarStyle, 69 | headerLeft: this.renderLeftComponent(sceneProps), 70 | headerTitle: this.renderTitleComponent(sceneProps), 71 | headerRight: this.renderRightComponent(sceneProps), 72 | }, 73 | } 74 | }} 75 | /> 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/Navigation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Platform, BackHandler } from 'react-native' 3 | import { Route } from 'react-router' 4 | import { CardStack } from 'react-router-navigation-core' 5 | import DefaultNavigationRenderer from './DefaultNavigationRenderer' 6 | import NavBar from './NavBar' 7 | import { NavigationPropTypes } from './PropTypes' 8 | 9 | export default class Navigation extends React.Component { 10 | static propTypes = NavigationPropTypes 11 | 12 | static defaultProps = { 13 | headerTransitionPreset: 14 | Platform.OS === 'android' ? 'fade-in-place' : 'uikit', 15 | } 16 | 17 | renderHeader = headerProps => { 18 | const { cards, scene } = headerProps 19 | const activeCard = cards.find(card => card.path === scene.route.name) 20 | const navBarProps = { ...headerProps, ...activeCard } 21 | if (navBarProps.hideNavBar) return null 22 | if (navBarProps.renderNavBar) { 23 | return navBarProps.renderNavBar(headerProps) 24 | } 25 | return 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | {({ history }) => ( 32 | ( 37 | 42 | )} 43 | /> 44 | )} 45 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/PropTypes.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import PropTypes from 'prop-types' 4 | import { ViewPropTypes, Text } from 'react-native' 5 | 6 | export const RoutePropTypes = { 7 | path: PropTypes.string.isRequired, 8 | exact: PropTypes.bool, 9 | strict: PropTypes.bool, 10 | sensitive: PropTypes.bool, 11 | component: PropTypes.func, 12 | render: PropTypes.func, 13 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), 14 | } 15 | 16 | export const NavBarPropTypes = { 17 | hideNavBar: PropTypes.bool, 18 | renderNavBar: PropTypes.func, 19 | navBarStyle: ViewPropTypes.style, 20 | hideBackButton: PropTypes.bool, 21 | backButtonTintColor: PropTypes.string, 22 | backButtonTitle: PropTypes.string, 23 | renderLeftButton: PropTypes.func, 24 | title: PropTypes.string, 25 | titleStyle: Text.propTypes.style, 26 | renderTitle: PropTypes.func, 27 | renderRightButton: PropTypes.func, 28 | } 29 | 30 | export const DefaultNavigationRendererPropTypes = { 31 | ...NavBarPropTypes, 32 | onTransitionStart: PropTypes.func, 33 | onTransitionEnd: PropTypes.func, 34 | cardStyle: ViewPropTypes.style, 35 | configureTransition: PropTypes.func, 36 | mode: PropTypes.string, 37 | gesturesEnabled: PropTypes.bool, 38 | } 39 | 40 | export const NavigationPropTypes = DefaultNavigationRendererPropTypes 41 | 42 | export const CardPropTypes = { 43 | ...RoutePropTypes, 44 | ...NavigationPropTypes, 45 | routePath: PropTypes.string, 46 | } 47 | 48 | export const TabBarPropTypes = { 49 | hideTabBar: PropTypes.bool, 50 | tabBarPosition: PropTypes.oneOf(['top', 'bottom']), 51 | tabBarIndicatorStyle: ViewPropTypes.style, 52 | tabBarStyle: ViewPropTypes.style, 53 | renderTabBar: PropTypes.func, 54 | tabStyle: ViewPropTypes.style, 55 | tabTintColor: PropTypes.string, 56 | tabActiveTintColor: PropTypes.string, 57 | label: PropTypes.string, 58 | labelStyle: Text.propTypes.style, 59 | renderLabel: PropTypes.func, 60 | renderTabIcon: PropTypes.func, 61 | } 62 | 63 | export const DefaultTabsRendererPropTypes = { 64 | ...TabBarPropTypes, 65 | style: ViewPropTypes.style, 66 | initialLayout: PropTypes.shape({ 67 | height: PropTypes.number.isRequired, 68 | width: PropTypes.number.isRequired, 69 | }), 70 | lazy: PropTypes.bool, 71 | } 72 | 73 | export const BottomNavigationPropTypes = DefaultTabsRendererPropTypes 74 | 75 | export const TabsPropTypes = DefaultTabsRendererPropTypes 76 | 77 | export const TabPropTypes = { 78 | ...RoutePropTypes, 79 | ...TabBarPropTypes, 80 | routePath: PropTypes.string, 81 | initialPath: PropTypes.string, 82 | onReset: PropTypes.func, 83 | } 84 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/Tab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Route } from 'react-router' 3 | import { SceneView } from 'react-router-navigation-core' 4 | import { TabPropTypes } from './PropTypes' 5 | 6 | export default class Tab extends React.Component { 7 | static propTypes = TabPropTypes 8 | 9 | render() { 10 | return ( 11 | 12 | {({ history }) => { 13 | return 14 | }} 15 | 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/TabBarBottom.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { StyleSheet, Platform, Text } from 'react-native' 3 | import { SafeAreaView } from 'react-navigation' 4 | import { TabBar } from 'react-native-tab-view' 5 | import { TabBarPropTypes } from './PropTypes' 6 | 7 | const TAB_HEIGHT = Platform.OS === 'ios' ? 49 : 56 8 | 9 | const styles = StyleSheet.create({ 10 | tabBar: { 11 | height: TAB_HEIGHT, 12 | borderTopWidth: StyleSheet.hairlineWidth, 13 | borderTopColor: 'rgba(0, 0, 0, .2)', 14 | ...Platform.select({ 15 | ios: { 16 | backgroundColor: '#f4f4f4', 17 | }, 18 | android: { 19 | backgroundColor: '#ffffff', 20 | }, 21 | }), 22 | }, 23 | tab: { 24 | flex: 1, 25 | paddingVertical: 4, 26 | height: TAB_HEIGHT, 27 | }, 28 | label: { 29 | textAlign: 'center', 30 | ...Platform.select({ 31 | ios: { 32 | fontSize: 10, 33 | }, 34 | android: { 35 | fontSize: 13, 36 | includeFontPadding: false, 37 | }, 38 | }), 39 | }, 40 | }) 41 | 42 | export default class TabBarBottom extends React.Component { 43 | static propTypes = TabBarPropTypes 44 | 45 | static defaultProps = { 46 | tabTintColor: '#929292', 47 | tabActiveTintColor: '#3478f6', 48 | } 49 | 50 | onTabPress = scene => { 51 | const { navigationState, onIndexChange } = this.props 52 | const { route } = scene 53 | const activeRoute = navigationState.routes[navigationState.index] 54 | if (activeRoute.key === route.key) { 55 | onIndexChange(route) 56 | } 57 | } 58 | 59 | renderIndicator = () => { 60 | return null 61 | } 62 | 63 | renderLabel = scene => { 64 | const { tabs, navigationState } = this.props 65 | const { route } = scene 66 | const activeRoute = navigationState.routes[navigationState.index] 67 | const activeTab = tabs.find(tab => tab.path === route.name) 68 | const labelprops = { ...activeTab, ...scene } 69 | const focused = activeRoute.key === route.key 70 | if (labelprops.renderLabel) return labelprops.renderLabel(labelprops, scene) 71 | const { label, tabTintColor, tabActiveTintColor } = labelprops 72 | if (!label) return null 73 | return ( 74 | 82 | {label} 83 | 84 | ) 85 | } 86 | 87 | renderIcon = scene => { 88 | const { tabs, navigationState } = this.props 89 | const { route } = scene 90 | const activeRoute = navigationState.routes[navigationState.index] 91 | const activeTab = tabs.find(tab => tab.path === route.name) 92 | const focused = activeRoute.key === route.key 93 | const iconProps = { ...scene, ...activeTab, focused } 94 | if (!iconProps.renderTabIcon) return null 95 | return iconProps.renderTabIcon(iconProps) 96 | } 97 | 98 | shouldComponentUpdate(nextProps) { 99 | const { index } = this.props.navigationState 100 | const { index: nextIndex } = nextProps.navigationState 101 | return index !== nextIndex 102 | } 103 | 104 | render() { 105 | const { label, renderTabIcon, renderLabel, ...props } = this.props 106 | const tabBarStyle = [ 107 | styles.tabBar, 108 | { justifyContent: label && renderTabIcon ? 'flex-end' : 'center' }, 109 | this.props.tabBarStyle, 110 | ] 111 | const flattenStyle = StyleSheet.flatten(tabBarStyle) 112 | return ( 113 | 119 | 135 | 136 | ) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/Tabs.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { StyleSheet, Text } from 'react-native' 3 | import { Route } from 'react-router' 4 | import { TabBar } from 'react-native-tab-view' 5 | import { TabStack } from 'react-router-navigation-core' 6 | import DefaultTabsRenderer from './DefaultTabsRenderer' 7 | import { TabsPropTypes } from './PropTypes' 8 | 9 | const styles = StyleSheet.create({ 10 | tabLabel: { 11 | backgroundColor: 'transparent', 12 | color: 'white', 13 | margin: 8, 14 | }, 15 | }) 16 | 17 | export default class Tabs extends React.Component { 18 | static propTypes = TabsPropTypes 19 | 20 | renderTabBar = tabBarProps => { 21 | const renderTabBar = tabBarProps.renderTabBar || this.props.renderTabBar 22 | if (tabBarProps.hideTabBar) return null 23 | if (renderTabBar) { 24 | return React.createElement(renderTabBar, { 25 | ...tabBarProps, 26 | renderLabel: scene => this.renderTabLabel(tabBarProps, scene), 27 | }) 28 | } 29 | return ( 30 | this.renderTabLabel(tabBarProps, scene)} 33 | /> 34 | ) 35 | } 36 | 37 | renderTabLabel = (tabLabelProps, scene) => { 38 | const { tabs } = tabLabelProps 39 | const { route, focused } = scene 40 | const activeTab = tabs.find(tab => tab.path === route.name) 41 | const tabsProps = { ...tabLabelProps, ...activeTab } 42 | const { tabTintColor, tabActiveTintColor } = tabsProps 43 | if (tabsProps.renderLabel) return tabsProps.renderLabel(tabsProps, scene) 44 | return ( 45 | 53 | {tabsProps && tabsProps.label} 54 | 55 | ) 56 | } 57 | 58 | render() { 59 | return ( 60 | 61 | {({ history }) => ( 62 | ( 67 | 72 | )} 73 | /> 74 | )} 75 | 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/react-router-navigation/src/index.js: -------------------------------------------------------------------------------- 1 | /* @noflow */ 2 | 3 | // High-level wrappers 4 | export { default as BottomNavigation } from './BottomNavigation' 5 | export { default as Card } from './Card' 6 | export { default as Tab } from './Tab' 7 | export { default as NavBar } from './NavBar' 8 | export { default as Navigation } from './Navigation' 9 | export { default as Tabs } from './Tabs' 10 | 11 | // Low-level building blocks 12 | export { default as DefaultNavigationRenderer } from './DefaultNavigationRenderer' 13 | export { default as DefaultTabsRenderer } from './DefaultTabsRenderer' 14 | -------------------------------------------------------------------------------- /packages/react-router-polaris/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-polaris", 3 | "version": "0.1.3", 4 | "license": "MIT", 5 | "main": "lib/index.js", 6 | "author": "Léo Le Bras (https://github.com/winoteam/)", 7 | "scripts": { 8 | "prepublish": "yarn build", 9 | "build": "yarn build:cjs && yarn build:flow", 10 | "build:watch": "babel -d ./lib ./src/ --ignore __tests__ --watch", 11 | "build:cjs": "babel -d ./lib ./src/ --ignore __tests__", 12 | "build:flow": "for file in $(find ./src -name '*.js' -not -path '*/__tests__*'); do cp \"$file\" `echo \"$file\" | sed 's/\\/src\\//\\/lib\\//g'`.flow; done" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/winoteam/react-router-navigation.git" 17 | }, 18 | "keywords": [ 19 | "web", 20 | "polaris", 21 | "router", 22 | "tabs", 23 | "navigation", 24 | "navigator" 25 | ], 26 | "peerDependencies": { 27 | "react": "*", 28 | "react-router": "4.2.x" 29 | }, 30 | "dependencies": { 31 | "@shopify/polaris": "^2.5.0", 32 | "react-router-navigation-core": "2.0.0-alpha.7" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "react": "16.3.1", 37 | "react-router": "4.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-router-polaris/src/DefaultTabsRenderer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import { Card, Tabs } from '@shopify/polaris' 5 | import { 6 | type TabsRendererProps, 7 | TabsRendererPropType, 8 | } from 'react-router-navigation-core' 9 | 10 | type Props = TabsRendererProps<> 11 | 12 | export default class DefaultTabsRenderer extends React.Component { 13 | static propTypes = TabsRendererPropType 14 | 15 | renderCurrentTab = () => { 16 | const { renderTab, navigationState } = this.props 17 | return renderTab(navigationState.routes[navigationState.index]) 18 | } 19 | 20 | render() { 21 | const { navigationState, onIndexChange, tabs } = this.props 22 | return ( 23 | 24 | { 28 | const item: { 29 | path?: string, 30 | label?: string, 31 | } = { ...tab } 32 | return { 33 | id: item.path, 34 | content: item.label, 35 | panelID: item.label, 36 | } 37 | })} 38 | /> 39 | {this.renderCurrentTab()} 40 | 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-router-polaris/src/Tab.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import PropTypes from 'prop-types' 5 | import * as ReactRouter from 'react-router' 6 | import { 7 | SceneView, 8 | TabPropType, 9 | type RouteProps, 10 | } from 'react-router-navigation-core' 11 | import { type TabOptions } from './TypeDefinitions' 12 | 13 | type Props = RouteProps & TabOptions 14 | 15 | export default class Tab extends React.Component { 16 | // $FlowFixMe 17 | static propTypes = { ...TabPropType, label: PropTypes.string.isRequired } 18 | 19 | render() { 20 | return ( 21 | 22 | {({ history }) => { 23 | return 24 | }} 25 | 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-router-polaris/src/Tabs.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react' 4 | import PropTypes from 'prop-types' 5 | import { Route } from 'react-router' 6 | import { TabStack } from 'react-router-navigation-core' 7 | import DefaultTabsRenderer from './DefaultTabsRenderer' 8 | 9 | type Props = { 10 | children?: React$Node[], 11 | } 12 | 13 | export default class Tabs extends React.Component { 14 | static defaultProps = { 15 | children: PropTypes.arrayOf(PropTypes.node).isRequired, 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | {contextRouter => ( 22 | ( 26 | 27 | )} 28 | /> 29 | )} 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/react-router-polaris/src/TypeDefinitions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type TabOptions = { 4 | label: string, 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-router-polaris/src/index.js: -------------------------------------------------------------------------------- 1 | /* @noflow */ 2 | 3 | // High-level wrappers 4 | export { default as Tab } from './Tab' 5 | export { default as Tabs } from './Tabs' 6 | 7 | // Low-level building blocks 8 | export { default as DefaultTabsRenderer } from './DefaultTabsRenderer' 9 | 10 | // Type definitions 11 | export type { TabOptions } from './TypeDefinitions' 12 | -------------------------------------------------------------------------------- /scripts/tests.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const rootPackageJson = require('./../package.json') 5 | 6 | function isDirectory(source) { 7 | return fs.lstatSync(source).isDirectory() 8 | } 9 | 10 | function isJestDirectory(source) { 11 | const sourcePackageJson = require(path.join(source + '/package.json')) 12 | return sourcePackageJson.jest 13 | } 14 | 15 | const { packages } = rootPackageJson.workspaces 16 | 17 | const jestDirectories = packages 18 | .map(packageDirectory => { 19 | const packageRootDirectory = packageDirectory.split('/')[0] 20 | return fs 21 | .readdirSync(packageRootDirectory) 22 | .map(name => path.join(__dirname + '/../' + packageRootDirectory, name)) 23 | .filter(isDirectory) 24 | .filter(isJestDirectory) 25 | }) 26 | .reduce((acc, directories) => [...acc, ...directories], []) 27 | 28 | jestDirectories.forEach(cwd => { 29 | child_process.exec( 30 | 'node_modules/.bin/jest', 31 | { cwd }, 32 | (err, stdout, stderr) => { 33 | if (err) throw Error(err) 34 | console.log(stderr) 35 | }, 36 | ) 37 | }) 38 | --------------------------------------------------------------------------------