Compare commits

...

43 Commits

Author SHA1 Message Date
satyajit.happy
4e0ebb05f9 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.7
 - @react-navigation/drawer@5.0.0-alpha.8
 - @react-navigation/example@5.0.0-alpha.4
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.7
 - @react-navigation/material-top-tabs@5.0.0-alpha.7
 - @react-navigation/routers@5.0.0-alpha.7
 - @react-navigation/stack@5.0.0-alpha.10
2019-08-31 10:22:57 +02:00
satyajit.happy
bf0b408238 refactor: change signature of interpolation props 2019-08-31 10:21:36 +02:00
Satyajit Sahoo
1b2983eaa9 fix: handle route names change when all routes are removed (#86) 2019-08-30 19:15:54 +01:00
satyajit.happy
b38ee1c162 chore: publish
- @react-navigation/core@5.0.0-alpha.5
 - @react-navigation/drawer@5.0.0-alpha.7
 - @react-navigation/stack@5.0.0-alpha.9
2019-08-30 15:34:34 +02:00
satyajit.happy
fdc24d2523 fix: rename contentContainerStyle to sceneContainerStyle for drawer 2019-08-30 15:30:18 +02:00
satyajit.happy
6a8242c31a fix: properly set animated node on gestureEnabled change 2019-08-29 19:55:42 +02:00
satyajit.happy
3ad2e6ebcf fix: change interpolated style when idle to avoid messing up reanimated 2019-08-29 16:12:22 +02:00
satyajit.happy
5c8d183d68 chore: rename BaseActions -> CommonActions 2019-08-29 13:10:36 +02:00
Michal Osadnik
f1b976b68c chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.6
 - @react-navigation/core@5.0.0-alpha.4
 - @react-navigation/drawer@5.0.0-alpha.6
 - @react-navigation/example@5.0.0-alpha.3
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.6
 - @react-navigation/material-top-tabs@5.0.0-alpha.6
 - @react-navigation/native@5.0.0-alpha.5
 - @react-navigation/routers@5.0.0-alpha.6
 - @react-navigation/stack@5.0.0-alpha.8
2019-08-29 10:40:13 +01:00
Michał Osadnik
6b75cbaaa6 feat: handle navigating with both with both key and name (#83) 2019-08-29 09:46:51 +02:00
satyajit.happy
c951027ebb fix: handle both null and undefined in useScrollToTop 2019-08-29 08:33:42 +02:00
Janic Duplessis
cdbf1e97f9 feat: handle animated component wrappers in useScrollToTop (#81) 2019-08-28 23:13:00 +01:00
Janic Duplessis
56a2ee99f9 chore: add prettier config to package.json so it is picked up by editors (#82) 2019-08-28 23:12:29 +01:00
satyajit.happy
a9d4813b47 fix: allow making params optional. fixes #80 2019-08-28 22:05:48 +02:00
satyajit.happy
74c3377b63 chore: fix typo in example 2019-08-28 21:48:58 +02:00
satyajit.happy
8c1acc33c6 fix: fix gestures not working in stack 2019-08-28 21:41:15 +02:00
satyajit.happy
d72a96d1ef chore: add example for pop to top on tab press 2019-08-28 21:28:18 +02:00
satyajit.happy
f9e8c7e80f feat: handle more methods in useScrollToTop 2019-08-28 21:17:48 +02:00
satyajit.happy
9245c79990 feat: export NavigationContext 2019-08-28 15:52:48 +02:00
satyajit.happy
ea4c753d0a refactor: rename 'timing' option to 'animation' in transition spec 2019-08-28 15:48:17 +02:00
satyajit.happy
01196d7b48 docs: add JSDoc for transition configurations 2019-08-28 15:41:11 +02:00
Janic Duplessis
b4a5c3c35e fix: types path (#75) 2019-08-28 11:36:37 +01:00
Michal Osadnik
f302416631 chore: publish
- @react-navigation/native@5.0.0-alpha.4
 - @react-navigation/stack@5.0.0-alpha.7
2019-08-28 11:24:44 +01:00
satyajit.happy
dead4e826a fix: fix stack nested in tab always getting reset 2019-08-28 12:23:15 +02:00
Michal Osadnik
d5b4210eb2 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.5
 - @react-navigation/drawer@5.0.0-alpha.5
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.5
 - @react-navigation/material-top-tabs@5.0.0-alpha.5
 - @react-navigation/routers@5.0.0-alpha.5
 - @react-navigation/stack@5.0.0-alpha.6
2019-08-28 11:22:18 +01:00
Michal Osadnik
38336b0290 feat: disable gesture logic when no gesture stack 2019-08-28 11:22:04 +01:00
satyajit.happy
fc37e93b5b chore: add some badges 2019-08-27 17:09:59 +02:00
satyajit.happy
093858b68b test: improve coverage for router tests 2019-08-27 16:58:43 +02:00
Michal Osadnik
3703ab6353 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.4
 - @react-navigation/core@5.0.0-alpha.3
 - @react-navigation/drawer@5.0.0-alpha.4
 - @react-navigation/example@5.0.0-alpha.2
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.4
 - @react-navigation/material-top-tabs@5.0.0-alpha.4
 - @react-navigation/native@5.0.0-alpha.3
 - @react-navigation/routers@5.0.0-alpha.4
 - @react-navigation/stack@5.0.0-alpha.5
2019-08-27 11:06:59 +01:00
Michal Osadnik
7990cf2575 feat: add memoization of spring for stack 2019-08-27 10:51:26 +01:00
satyajit.happy
fb9d1837a1 chore: configure jest to resolve source files for packages 2019-08-27 08:58:29 +02:00
satyajit.happy
935c588000 refactor: rename BaseActions tCommonActions 2019-08-27 08:45:37 +02:00
Michal Osadnik
7d526e5881 chore: add tests for drawer 2019-08-26 13:14:03 +01:00
Michal Osadnik
2adccdef1d chore: add tests for tabs 2019-08-26 13:07:12 +01:00
Michal Osadnik
dee25057e8 chore: add tests for stack 2019-08-26 12:52:00 +01:00
satyajit.happy
9e1104c31f feat: add hook to scroll to top on tab press 2019-08-24 12:14:49 +05:30
Michal Osadnik
469ec31cc5 fix: link proper descriptor for StackView 2019-08-24 02:12:48 +01:00
Michal Osadnik
d26b77f9c9 feat: add native container 2019-08-24 06:06:05 +05:30
Michal Osadnik
1bbd6ac422 fix: set correct pointer events when active prop changes 2019-08-22 16:08:51 +05:30
satyajit.happy
1a8281d37d refactor: change order of type parameters 2019-08-22 11:37:17 +05:30
satyajit.happy
b0a0857b0a refactor: typecheck events in navigation helpers 2019-08-22 11:33:15 +05:30
satyajit.happy
4e07461526 chore: publish
- @react-navigation/bottom-tabs@5.0.0-alpha.3
 - @react-navigation/core@5.0.0-alpha.2
 - @react-navigation/drawer@5.0.0-alpha.3
 - @react-navigation/material-bottom-tabs@5.0.0-alpha.3
 - @react-navigation/material-top-tabs@5.0.0-alpha.3
 - @react-navigation/native@5.0.0-alpha.2
 - @react-navigation/routers@5.0.0-alpha.3
 - @react-navigation/stack@5.0.0-alpha.4
2019-08-22 10:10:06 +05:30
satyajit.happy
f18231541b fix: fix path to typescript definitions 2019-08-22 10:09:16 +05:30
76 changed files with 3064 additions and 479 deletions

View File

@@ -42,6 +42,13 @@ jobs:
- store_artifacts: - store_artifacts:
path: coverage path: coverage
destination: coverage destination: coverage
build-packages:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn lerna run prepare
workflows: workflows:
version: 2 version: 2
@@ -54,3 +61,6 @@ workflows:
- unit-test: - unit-test:
requires: requires:
- install-dependencies - install-dependencies
- build-packages:
requires:
- install-dependencies

View File

@@ -1,5 +1,9 @@
# Rethinking Navigation # Rethinking Navigation
[![Build Status][build-badge]][build]
[![Code Coverage][coverage-badge]][coverage]
[![MIT License][license-badge]][license]
An exploration of a component-first API for React Navigation for building more dynamic navigation solutions. An exploration of a component-first API for React Navigation for building more dynamic navigation solutions.
## Considerations ## Considerations
@@ -31,6 +35,8 @@ Navigators bundle a router and a view which takes the navigation state and decid
A simple navigator could look like this: A simple navigator could look like this:
```js ```js
import { createNavigator } from '@react-navigation/core';
function StackNavigator({ initialRouteName, children, ...rest }) { function StackNavigator({ initialRouteName, children, ...rest }) {
// The `navigation` object contains the navigation state and some helpers (e.g. push, pop) // The `navigation` object contains the navigation state and some helpers (e.g. push, pop)
// The `descriptors` object contains the screen options and a helper for rendering a screen // The `descriptors` object contains the screen options and a helper for rendering a screen
@@ -127,8 +133,11 @@ It's also possible to disable bubbling of actions when dispatching them by addin
## Basic usage ## Basic usage
```js ```js
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Stack = createStackNavigator(); const Stack = createStackNavigator();
const Tab = createTabNavigator(); const Tab = createBottomTabNavigator();
function App() { function App() {
return ( return (
@@ -210,6 +219,8 @@ function Profile({ navigation }) {
} }
``` ```
The `navigation.addListener` method returns a function to remove the listener which can be returned as the cleanup function in an effect.
Navigators can also emit custom events using the `emit` method in the `navigation` object passed: Navigators can also emit custom events using the `emit` method in the `navigation` object passed:
```js ```js
@@ -245,6 +256,8 @@ Sometimes we want to run side-effects when a screen is focused. A side effect ma
To make this easier, the library exports a `useFocusEffect` hook: To make this easier, the library exports a `useFocusEffect` hook:
```js ```js
import { useFocusEffect } from '@react-navigation/core';
function Profile({ userId }) { function Profile({ userId }) {
const [user, setUser] = React.useState(null); const [user, setUser] = React.useState(null);
@@ -272,6 +285,10 @@ The `useFocusEffect` is analogous to React's `useEffect` hook. The only differen
We might want to render different content based on the current focus state of the screen. The library exports a `useIsFocused` hook to make this easier: We might want to render different content based on the current focus state of the screen. The library exports a `useIsFocused` hook to make this easier:
```js ```js
import { useIsFocused } from '@react-navigation/core';
// ...
const isFocused = useIsFocused(); const isFocused = useIsFocused();
``` ```
@@ -284,6 +301,10 @@ For proper UX in React Native, we need to respect platform behavior such as the
When the back button on the device is pressed, we also want to navigate back in the focused navigator. The library exports a `useBackButton` hook to handle this: When the back button on the device is pressed, we also want to navigate back in the focused navigator. The library exports a `useBackButton` hook to handle this:
```js ```js
import { useBackButton } from '@react-navigation/native';
// ...
const ref = React.useRef(); const ref = React.useRef();
useBackButton(ref); useBackButton(ref);
@@ -291,6 +312,24 @@ useBackButton(ref);
return <NavigationContainer ref={ref}>{/* content */}</NavigationContainer>; return <NavigationContainer ref={ref}>{/* content */}</NavigationContainer>;
``` ```
### Scroll to top on tab button press
When there's a scroll view in a tab and the user taps on the already focused tab bar again, we might want to scroll to top in our scroll view. The library exports a `useScrollToTop` hook to handle this:
```js
import { useScrollToTop } from '@react-navigation/native';
// ...
const ref = React.useRef();
useScrollToTop(ref);
return <ScrollView ref={ref}>{/* content */}</ScrollView>;
```
The hook can accept a ref object to any view that has a `scrollTo` method.
### Deep-link integration ### Deep-link integration
To handle incoming links, we need to handle 2 scenarios: To handle incoming links, we need to handle 2 scenarios:
@@ -325,6 +364,10 @@ For example, the path `/rooms/chat?user=jane` will be translated to a state obje
The `useLinking` hooks makes it easier to handle incoming links: The `useLinking` hooks makes it easier to handle incoming links:
```js ```js
import { useLinking } from '@react-navigation/native';
// ...
const ref = React.useRef(); const ref = React.useRef();
const { getInitialState } = useLinking(ref, { const { getInitialState } = useLinking(ref, {
@@ -451,10 +494,10 @@ Unfortunately it's not possible to verify that the type of children elements are
## Contributing ## Contributing
The project uses a monorepo structure for the packages managed by [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) and [lerna](https://lerna.js.org). To get started with the project, run `lerna bootstrap` to install the required dependencies for each package: The project uses a monorepo structure for the packages managed by [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/) and [lerna](https://lerna.js.org). To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
```sh ```sh
lerna bootstrap yarn
``` ```
While developing, you can run the [example app](/example/) with [Expo](https://expo.io/) to test your changes: While developing, you can run the [example app](/example/) with [Expo](https://expo.io/) to test your changes:
@@ -476,13 +519,7 @@ To fix formatting errors, run the following:
yarn lint --fix yarn lint --fix
``` ```
Remember to add tests for your change if possible. To run tests, first build all the files: Remember to add tests for your change if possible. Run the tests by:
```sh
lerna run prepare
```
Then run the tests:
```sh ```sh
yarn test yarn test
@@ -493,7 +530,16 @@ yarn test
To publish a new version, first we need to export a `GH_TOKEN` environment variable as mentioned [here](https://github.com/lerna/lerna/tree/master/commands/version#--create-release-type). Then run: To publish a new version, first we need to export a `GH_TOKEN` environment variable as mentioned [here](https://github.com/lerna/lerna/tree/master/commands/version#--create-release-type). Then run:
```sh ```sh
lerna publish yarn lerna publish
``` ```
This will automatically bump the version and publish the packages. It'll also publish the changelogs on GitHub for each package. This will automatically bump the version and publish the packages. It'll also publish the changelogs on GitHub for each package.
<!-- badges -->
[build-badge]: https://img.shields.io/circleci/project/github/react-navigation/navigation-ex/master.svg?style=flat-square
[build]: https://circleci.com/gh/react-navigation/navigation-ex
[coverage-badge]: https://img.shields.io/codecov/c/github/react-navigation/navigation-ex.svg?style=flat-square
[coverage]: https://codecov.io/github/react-navigation/navigation-ex
[license-badge]: https://img.shields.io/npm/l/@react-navigation/core.svg?style=flat-square
[license]: https://opensource.org/licenses/MIT

View File

@@ -14,7 +14,7 @@
"type": "git", "type": "git",
"url": "git+https://github.com/satya164/react-navigation.git" "url": "git+https://github.com/satya164/react-navigation.git"
}, },
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/)", "author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
"scripts": { "scripts": {
"lint": "eslint --ext '.js,.ts,.tsx' .", "lint": "eslint --ext '.js,.ts,.tsx' .",
"typescript": "tsc --noEmit", "typescript": "tsc --noEmit",
@@ -59,7 +59,16 @@
}, },
"setupFiles": [ "setupFiles": [
"<rootDir>/jest/setup.js" "<rootDir>/jest/setup.js"
] ],
"moduleNameMapper": {
"@react-navigation/([^/]+)": "<rootDir>/packages/$1/src"
}
},
"prettier": {
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "es5"
}, },
"name": "react-navigation" "name": "react-navigation"
} }

View File

@@ -3,6 +3,56 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.6...@react-navigation/bottom-tabs@5.0.0-alpha.7) (2019-08-31)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.5...@react-navigation/bottom-tabs@5.0.0-alpha.6) (2019-08-29)
### Bug Fixes
* allow making params optional. fixes [#80](https://github.com/react-navigation/navigation-ex/issues/80) ([a9d4813](https://github.com/react-navigation/navigation-ex/commit/a9d4813))
* types path ([#75](https://github.com/react-navigation/navigation-ex/issues/75)) ([b4a5c3c](https://github.com/react-navigation/navigation-ex/commit/b4a5c3c))
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.4...@react-navigation/bottom-tabs@5.0.0-alpha.5) (2019-08-28)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.3...@react-navigation/bottom-tabs@5.0.0-alpha.4) (2019-08-27)
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.2...@react-navigation/bottom-tabs@5.0.0-alpha.3) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.1...@react-navigation/bottom-tabs@5.0.0-alpha.2) (2019-08-22) # [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/bottom-tabs@5.0.0-alpha.1...@react-navigation/bottom-tabs@5.0.0-alpha.2) (2019-08-22)
**Note:** Version bump only for package @react-navigation/bottom-tabs **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -10,7 +10,7 @@
"android", "android",
"tab" "tab"
], ],
"version": "5.0.0-alpha.2", "version": "5.0.0-alpha.7",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,7 +20,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/bottom-tabs/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"
@@ -33,7 +33,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.2", "@react-navigation/routers": "^5.0.0-alpha.7",
"react-native-safe-area-view": "^0.14.6" "react-native-safe-area-view": "^0.14.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -13,6 +13,7 @@ import BottomTabView from '../views/BottomTabView';
import { import {
BottomTabNavigationConfig, BottomTabNavigationConfig,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap,
} from '../types'; } from '../types';
type Props = DefaultNavigatorOptions<BottomTabNavigationOptions> & type Props = DefaultNavigatorOptions<BottomTabNavigationOptions> &
@@ -28,8 +29,9 @@ function BottomTabNavigator({
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState,
TabRouterOptions,
BottomTabNavigationOptions, BottomTabNavigationOptions,
TabRouterOptions BottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
initialRouteName, initialRouteName,
backBehavior, backBehavior,

View File

@@ -17,10 +17,6 @@ import {
import { TabNavigationState } from '@react-navigation/routers'; import { TabNavigationState } from '@react-navigation/routers';
export type BottomTabNavigationEventMap = { export type BottomTabNavigationEventMap = {
/**
* Event which fires on tapping on the tab for an already focused screen.
*/
refocus: undefined;
/** /**
* Event which fires on tapping on the tab in the tab bar. * Event which fires on tapping on the tab in the tab bar.
*/ */
@@ -35,6 +31,11 @@ export type Orientation = 'horizontal' | 'vertical';
export type LabelPosition = 'beside-icon' | 'below-icon'; export type LabelPosition = 'beside-icon' | 'below-icon';
export type BottomTabNavigationHelpers = NavigationHelpers<
ParamListBase,
BottomTabNavigationEventMap
>;
export type BottomTabNavigationProp< export type BottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string RouteName extends keyof ParamList = string
@@ -52,8 +53,8 @@ export type BottomTabNavigationProp<
* @param [params] Params object for the route. * @param [params] Params object for the route.
*/ */
jumpTo<RouteName extends Extract<keyof ParamList, string>>( jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends void ...args: ParamList[RouteName] extends (undefined | any)
? [RouteName] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
}; };

View File

@@ -5,23 +5,22 @@ import {
AccessibilityRole, AccessibilityRole,
AccessibilityStates, AccessibilityStates,
} from 'react-native'; } from 'react-native';
import { import { Route, CommonActions } from '@react-navigation/core';
NavigationHelpers,
ParamListBase,
Route,
BaseActions,
} from '@react-navigation/core';
import { TabNavigationState } from '@react-navigation/routers'; import { TabNavigationState } from '@react-navigation/routers';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
import BottomTabBar from './BottomTabBar'; import BottomTabBar from './BottomTabBar';
import { BottomTabNavigationConfig, BottomTabDescriptorMap } from '../types'; import {
BottomTabNavigationConfig,
BottomTabDescriptorMap,
BottomTabNavigationHelpers,
} from '../types';
import ResourceSavingScene from './ResourceSavingScene'; import ResourceSavingScene from './ResourceSavingScene';
type Props = BottomTabNavigationConfig & { type Props = BottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: BottomTabNavigationHelpers;
descriptors: BottomTabDescriptorMap; descriptors: BottomTabDescriptorMap;
}; };
@@ -139,14 +138,12 @@ export default class BottomTabView extends React.Component<Props, State> {
target: route.key, target: route.key,
}); });
if (state.routes[state.index].key === route.key) { if (
navigation.emit({ state.routes[state.index].key !== route.key &&
type: 'refocus', !event.defaultPrevented
target: route.key, ) {
});
} else if (!event.defaultPrevented) {
navigation.dispatch({ navigation.dispatch({
...BaseActions.navigate(route.name), ...CommonActions.navigate(route.name),
target: state.key, target: state.key,
}); });
} }

View File

@@ -3,6 +3,54 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.4...@react-navigation/core@5.0.0-alpha.5) (2019-08-30)
**Note:** Version bump only for package @react-navigation/core
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.3...@react-navigation/core@5.0.0-alpha.4) (2019-08-29)
### Bug Fixes
* allow making params optional. fixes [#80](https://github.com/react-navigation/navigation-ex/issues/80) ([a9d4813](https://github.com/react-navigation/navigation-ex/commit/a9d4813))
### Features
* export NavigationContext ([9245c79](https://github.com/react-navigation/navigation-ex/commit/9245c79))
* handle navigating with both with both key and name ([#83](https://github.com/react-navigation/navigation-ex/issues/83)) ([6b75cba](https://github.com/react-navigation/navigation-ex/commit/6b75cba))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.2...@react-navigation/core@5.0.0-alpha.3) (2019-08-27)
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
* add native container ([d26b77f](https://github.com/react-navigation/navigation-ex/commit/d26b77f))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/core@5.0.0-alpha.1...@react-navigation/core@5.0.0-alpha.2) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# 5.0.0-alpha.1 (2019-08-21) # 5.0.0-alpha.1 (2019-08-21)

View File

@@ -6,7 +6,7 @@
"react-native", "react-native",
"react-navigation" "react-navigation"
], ],
"version": "5.0.0-alpha.1", "version": "5.0.0-alpha.5",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -16,7 +16,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"

View File

@@ -10,7 +10,8 @@ export type Action =
type: 'NAVIGATE'; type: 'NAVIGATE';
payload: payload:
| { name: string; key?: undefined; params?: object } | { name: string; key?: undefined; params?: object }
| { key: string; name?: undefined; params?: object }; | { key: string; name?: undefined; params?: object }
| { key: string; name: string; params?: object };
source?: string; source?: string;
target?: string; target?: string;
} }
@@ -38,7 +39,10 @@ export function goBack(): Action {
} }
export function navigate( export function navigate(
route: { key: string; params?: object } | { name: string; params?: object } route:
| { key: string; params?: object }
| { name: string; params?: object }
| { name: string; key: string; params?: object }
): Action; ): Action;
export function navigate(name: string, params?: object): Action; export function navigate(name: string, params?: object): Action;
export function navigate(...args: any): Action { export function navigate(...args: any): Action {
@@ -47,12 +51,9 @@ export function navigate(...args: any): Action {
} else { } else {
const payload = args[0]; const payload = args[0];
if ( if (!payload.hasOwnProperty('key') && !payload.hasOwnProperty('name')) {
(payload.hasOwnProperty('key') && payload.hasOwnProperty('name')) ||
(!payload.hasOwnProperty('key') && !payload.hasOwnProperty('name'))
) {
throw new Error( throw new Error(
'While calling navigate with an object as the argument, you need to specify either name or key' 'While calling navigate with an object as the argument, you need to specify name or key'
); );
} }

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import * as BaseActions from './BaseActions'; import * as CommonActions from './CommonActions';
import EnsureSingleNavigator from './EnsureSingleNavigator'; import EnsureSingleNavigator from './EnsureSingleNavigator';
import NavigationBuilderContext from './NavigationBuilderContext'; import NavigationBuilderContext from './NavigationBuilderContext';
import useFocusedListeners from './useFocusedListeners'; import useFocusedListeners from './useFocusedListeners';
@@ -12,16 +12,11 @@ import {
PartialState, PartialState,
NavigationAction, NavigationAction,
NavigationContainerRef, NavigationContainerRef,
NavigationContainerProps,
} from './types'; } from './types';
type State = NavigationState | PartialState<NavigationState> | undefined; type State = NavigationState | PartialState<NavigationState> | undefined;
type Props = {
initialState?: InitialState;
onStateChange?: (state: State) => void;
children: React.ReactNode;
};
const MISSING_CONTEXT_ERROR = const MISSING_CONTEXT_ERROR =
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"; "We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?";
@@ -84,7 +79,7 @@ const getPartialState = (
* @param props.ref Ref object which refers to the navigation object containing helper methods. * @param props.ref Ref object which refers to the navigation object containing helper methods.
*/ */
const Container = React.forwardRef(function NavigationContainer( const Container = React.forwardRef(function NavigationContainer(
{ initialState, onStateChange, children }: Props, { initialState, onStateChange, children }: NavigationContainerProps,
ref: React.Ref<NavigationContainerRef> ref: React.Ref<NavigationContainerRef>
) { ) {
const [state, setNavigationState] = React.useState<State>(() => const [state, setNavigationState] = React.useState<State>(() =>
@@ -110,13 +105,13 @@ const Container = React.forwardRef(function NavigationContainer(
}; };
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
...(Object.keys(BaseActions) as Array<keyof typeof BaseActions>).reduce< ...(Object.keys(CommonActions) as Array<keyof typeof CommonActions>).reduce<
any any
>((acc, name) => { >((acc, name) => {
acc[name] = (...args: any[]) => acc[name] = (...args: any[]) =>
dispatch( dispatch(
// eslint-disable-next-line import/namespace // eslint-disable-next-line import/namespace
BaseActions[name]( CommonActions[name](
// @ts-ignore // @ts-ignore
...args ...args
) )

View File

@@ -1,5 +1,5 @@
import BaseRouter from '../BaseRouter'; import BaseRouter from '../BaseRouter';
import * as BaseActions from '../BaseActions'; import * as CommonActions from '../CommonActions';
jest.mock('shortid', () => () => 'test'); jest.mock('shortid', () => () => 'test');
@@ -18,7 +18,7 @@ const STATE = {
it('replaces focused screen with REPLACE', () => { it('replaces focused screen with REPLACE', () => {
const result = BaseRouter.getStateForAction( const result = BaseRouter.getStateForAction(
STATE, STATE,
BaseActions.replace('qux', { answer: 42 }) CommonActions.replace('qux', { answer: 42 })
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -36,7 +36,7 @@ it('replaces focused screen with REPLACE', () => {
it('replaces source screen with REPLACE', () => { it('replaces source screen with REPLACE', () => {
const result = BaseRouter.getStateForAction(STATE, { const result = BaseRouter.getStateForAction(STATE, {
...BaseActions.replace('qux', { answer: 42 }), ...CommonActions.replace('qux', { answer: 42 }),
source: 'baz', source: 'baz',
}); });
@@ -55,7 +55,7 @@ it('replaces source screen with REPLACE', () => {
it("doesn't handle REPLACE if source key isn't present", () => { it("doesn't handle REPLACE if source key isn't present", () => {
const result = BaseRouter.getStateForAction(STATE, { const result = BaseRouter.getStateForAction(STATE, {
...BaseActions.replace('qux', { answer: 42 }), ...CommonActions.replace('qux', { answer: 42 }),
source: 'magic', source: 'magic',
}); });
@@ -65,7 +65,7 @@ it("doesn't handle REPLACE if source key isn't present", () => {
it('sets params for the focused screen with SET_PARAMS', () => { it('sets params for the focused screen with SET_PARAMS', () => {
const result = BaseRouter.getStateForAction( const result = BaseRouter.getStateForAction(
STATE, STATE,
BaseActions.setParams({ answer: 42 }) CommonActions.setParams({ answer: 42 })
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -83,7 +83,7 @@ it('sets params for the focused screen with SET_PARAMS', () => {
it('sets params for the source screen with SET_PARAMS', () => { it('sets params for the source screen with SET_PARAMS', () => {
const result = BaseRouter.getStateForAction(STATE, { const result = BaseRouter.getStateForAction(STATE, {
...BaseActions.setParams({ answer: 42 }), ...CommonActions.setParams({ answer: 42 }),
source: 'foo', source: 'foo',
}); });
@@ -102,7 +102,7 @@ it('sets params for the source screen with SET_PARAMS', () => {
it("doesn't handle SET_PARAMS if source key isn't present", () => { it("doesn't handle SET_PARAMS if source key isn't present", () => {
const result = BaseRouter.getStateForAction(STATE, { const result = BaseRouter.getStateForAction(STATE, {
...BaseActions.setParams({ answer: 42 }), ...CommonActions.setParams({ answer: 42 }),
source: 'magic', source: 'magic',
}); });
@@ -119,7 +119,7 @@ it('resets state to new state with RESET', () => {
const result = BaseRouter.getStateForAction( const result = BaseRouter.getStateForAction(
STATE, STATE,
BaseActions.reset({ CommonActions.reset({
index: 0, index: 0,
routes, routes,
}) })
@@ -132,7 +132,7 @@ it('ignores key and routeNames when resetting with RESET', () => {
const result = BaseRouter.getStateForAction( const result = BaseRouter.getStateForAction(
STATE, STATE,
// @ts-ignore // @ts-ignore
BaseActions.reset({ index: 2, key: 'foo', routeNames: ['test'] }) CommonActions.reset({ index: 2, key: 'foo', routeNames: ['test'] })
); );
expect(result).toEqual({ ...STATE, index: 2 }); expect(result).toEqual({ ...STATE, index: 2 });

View File

@@ -7,42 +7,7 @@ import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
beforeEach(() => (MockRouterKey.current = 0)); beforeEach(() => (MockRouterKey.current = 0));
it('throws if NAVIGATE dispatched with both key and name', () => { it('throws if NAVIGATE dispatched neither key nor name', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const FooScreen = (props: any) => {
React.useEffect(() => {
props.navigation.navigate({ key: '1', name: '2' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};
const onStateChange = jest.fn();
const element = (
<NavigationContainer onStateChange={onStateChange}>
<TestNavigator initialRouteName="foo">
<Screen
name="foo"
component={FooScreen}
initialParams={{ count: 10 }}
/>
</TestNavigator>
</NavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
'While calling navigate with an object as the argument, you need to specify either name or key'
);
});
it('throws if NAVIGATE dispatched neither both key nor name', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -73,6 +38,6 @@ it('throws if NAVIGATE dispatched neither both key nor name', () => {
); );
expect(() => render(element).update(element)).toThrowError( expect(() => render(element).update(element)).toThrowError(
'While calling navigate with an object as the argument, you need to specify either name or key' 'While calling navigate with an object as the argument, you need to specify name or key'
); );
}); });

View File

@@ -3,8 +3,8 @@ import { render, act } from 'react-native-testing-library';
import Screen from '../Screen'; import Screen from '../Screen';
import NavigationContainer from '../NavigationContainer'; import NavigationContainer from '../NavigationContainer';
import useNavigationBuilder from '../useNavigationBuilder'; import useNavigationBuilder from '../useNavigationBuilder';
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
import useNavigation from '../useNavigation'; import useNavigation from '../useNavigation';
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
import { NavigationState } from '../types'; import { NavigationState } from '../types';
beforeEach(() => (MockRouterKey.current = 0)); beforeEach(() => (MockRouterKey.current = 0));

View File

@@ -17,6 +17,7 @@ it('sets options with options prop as an object', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -61,6 +62,7 @@ it("returns correct value for canGoBack when it's not overridden", () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -123,6 +125,7 @@ it(`returns false for canGoBack when current router doesn't handle GO_BACK`, ()
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
any,
any any
>(TestRouter, props); >(TestRouter, props);
@@ -172,6 +175,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const ParentNavigator = (props: any) => { const ParentNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(ParentRouter, props); >(ParentRouter, props);
@@ -181,6 +185,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const ChildNavigator = (props: any) => { const ChildNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -237,6 +242,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const OverrodeNavigator = (props: any) => { const OverrodeNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(OverrodeRouter, props); >(OverrodeRouter, props);
@@ -246,6 +252,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -293,6 +300,7 @@ it('sets options with options prop as a fuction', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -338,6 +346,7 @@ it('sets initial options with setOptions', () => {
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any,
{ {
title?: string; title?: string;
color?: string; color?: string;
@@ -392,6 +401,7 @@ it('updates options with setOptions', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
any,
any any
>(MockRouter, props); >(MockRouter, props);
const { render, options } = descriptors[state.routes[state.index].key]; const { render, options } = descriptors[state.routes[state.index].key];

View File

@@ -1,11 +1,13 @@
import * as BaseActions from './BaseActions'; import * as CommonActions from './CommonActions';
export { BaseActions }; export { CommonActions };
export { default as BaseRouter } from './BaseRouter'; export { default as BaseRouter } from './BaseRouter';
export { default as NavigationContainer } from './NavigationContainer'; export { default as NavigationContainer } from './NavigationContainer';
export { default as createNavigator } from './createNavigator'; export { default as createNavigator } from './createNavigator';
export { default as NavigationContext } from './NavigationContext';
export { default as useNavigationBuilder } from './useNavigationBuilder'; export { default as useNavigationBuilder } from './useNavigationBuilder';
export { default as useNavigation } from './useNavigation'; export { default as useNavigation } from './useNavigation';
export { default as useFocusEffect } from './useFocusEffect'; export { default as useFocusEffect } from './useFocusEffect';

View File

@@ -1,6 +1,7 @@
import * as BaseActions from './BaseActions'; import * as CommonActions from './CommonActions';
import * as React from 'react';
export type CommonAction = BaseActions.Action; export type CommonAction = CommonActions.Action;
export type NavigationState = { export type NavigationState = {
/** /**
@@ -186,7 +187,7 @@ export type EventMapBase = {
blur: undefined; blur: undefined;
}; };
export type EventArg<EventName extends string, Data> = { export type EventArg<EventName extends string, Data = undefined> = {
/** /**
* Type of the event (e.g. `focus`, `blur`) * Type of the event (e.g. `focus`, `blur`)
*/ */
@@ -231,11 +232,14 @@ export type EventEmitter<EventMap extends { [key: string]: any }> = {
* @param [options.target] Key of the target route which should receive the event. * @param [options.target] Key of the target route which should receive the event.
* If not specified, all routes receive the event. * If not specified, all routes receive the event.
*/ */
emit<EventName extends Extract<keyof EventMap, string>>(options: { emit<EventName extends Extract<keyof EventMap, string>>(
type: EventName; options: {
data?: EventMap[EventName]; type: EventName;
target?: string; target?: string;
}): EventArg<EventName, EventMap[EventName]>; } & (EventMap[EventName] extends undefined
? {}
: { data: EventMap[EventName] })
): EventArg<EventName, EventMap[EventName]>;
}; };
export class PrivateValueStore<A, B, C> { export class PrivateValueStore<A, B, C> {
@@ -270,8 +274,8 @@ type NavigationHelpersCommon<
* @param [params] Params object for the route. * @param [params] Params object for the route.
*/ */
navigate<RouteName extends keyof ParamList>( navigate<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends undefined ...args: ParamList[RouteName] extends (undefined | any)
? [RouteName] | [RouteName, undefined] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
@@ -283,7 +287,7 @@ type NavigationHelpersCommon<
navigate<RouteName extends keyof ParamList>( navigate<RouteName extends keyof ParamList>(
route: route:
| { key: string; params?: ParamList[RouteName] } | { key: string; params?: ParamList[RouteName] }
| { name: RouteName; params: ParamList[RouteName] } | { name: RouteName; key?: string; params: ParamList[RouteName] }
): void; ): void;
/** /**
@@ -294,7 +298,7 @@ type NavigationHelpersCommon<
*/ */
replace<RouteName extends keyof ParamList>( replace<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends undefined ...args: ParamList[RouteName] extends undefined
? [RouteName] | [RouteName, undefined] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
@@ -326,9 +330,10 @@ type NavigationHelpersCommon<
} & PrivateValueStore<ParamList, keyof ParamList, {}>; } & PrivateValueStore<ParamList, keyof ParamList, {}>;
export type NavigationHelpers< export type NavigationHelpers<
ParamList extends ParamListBase ParamList extends ParamListBase,
EventMap extends { [key: string]: any } = {}
> = NavigationHelpersCommon<ParamList> & > = NavigationHelpersCommon<ParamList> &
EventEmitter<{ [key: string]: any }> & { EventEmitter<EventMap> & {
/** /**
* Update the param object for the route. * Update the param object for the route.
* The new params will be shallow merged with the old one. * The new params will be shallow merged with the old one.
@@ -340,6 +345,14 @@ export type NavigationHelpers<
): void; ): void;
}; };
export type NavigationContainerProps = {
initialState?: InitialState;
onStateChange?: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
children: React.ReactNode;
};
export type NavigationProp< export type NavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string, RouteName extends keyof ParamList = string,

View File

@@ -21,6 +21,7 @@ import {
RouterFactory, RouterFactory,
PartialState, PartialState,
PrivateValueStore, PrivateValueStore,
NavigationAction,
} from './types'; } from './types';
// This is to make TypeScript compiler happy // This is to make TypeScript compiler happy
@@ -84,8 +85,9 @@ const getRouteConfigsFromChildren = <ScreenOptions extends object>(
*/ */
export default function useNavigationBuilder< export default function useNavigationBuilder<
State extends NavigationState, State extends NavigationState,
RouterOptions extends DefaultRouterOptions,
ScreenOptions extends object, ScreenOptions extends object,
RouterOptions extends DefaultRouterOptions EventMap extends { [key: string]: any }
>( >(
createRouter: RouterFactory<State, any, RouterOptions>, createRouter: RouterFactory<State, any, RouterOptions>,
options: DefaultNavigatorOptions<ScreenOptions> & RouterOptions options: DefaultNavigatorOptions<ScreenOptions> & RouterOptions
@@ -240,7 +242,7 @@ export default function useNavigationBuilder<
setState, setState,
}); });
const navigation = useNavigationHelpers({ const navigation = useNavigationHelpers<State, NavigationAction, EventMap>({
onAction, onAction,
getState, getState,
setState, setState,

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import * as BaseActions from './BaseActions'; import * as CommonActions from './CommonActions';
import { NavigationEventEmitter } from './useEventEmitter'; import { NavigationEventEmitter } from './useEventEmitter';
import NavigationContext from './NavigationContext'; import NavigationContext from './NavigationContext';
@@ -53,7 +53,7 @@ export default function useNavigationCache<
const actions = { const actions = {
...router.actionCreators, ...router.actionCreators,
...BaseActions, ...CommonActions,
}; };
cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>( cache.current = state.routes.reduce<NavigationCache<State, ScreenOptions>>(

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import * as BaseActions from './BaseActions'; import * as CommonActions from './CommonActions';
import NavigationContext from './NavigationContext'; import NavigationContext from './NavigationContext';
import { NavigationStateContext } from './NavigationContainer'; import { NavigationStateContext } from './NavigationContainer';
import { NavigationEventEmitter } from './useEventEmitter'; import { NavigationEventEmitter } from './useEventEmitter';
@@ -34,7 +34,8 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
*/ */
export default function useNavigationHelpers< export default function useNavigationHelpers<
State extends NavigationState, State extends NavigationState,
Action extends NavigationAction Action extends NavigationAction,
EventMap extends { [key: string]: any }
>({ onAction, getState, setState, emitter, router }: Options<State, Action>) { >({ onAction, getState, setState, emitter, router }: Options<State, Action>) {
const parentNavigationHelpers = React.useContext(NavigationContext); const parentNavigationHelpers = React.useContext(NavigationContext);
const { performTransaction } = React.useContext(NavigationStateContext); const { performTransaction } = React.useContext(NavigationStateContext);
@@ -51,7 +52,7 @@ export default function useNavigationHelpers<
const actions = { const actions = {
...router.actionCreators, ...router.actionCreators,
...BaseActions, ...CommonActions,
}; };
const helpers = Object.keys(actions).reduce( const helpers = Object.keys(actions).reduce(
@@ -72,11 +73,13 @@ export default function useNavigationHelpers<
? parentNavigationHelpers.isFocused ? parentNavigationHelpers.isFocused
: () => true, : () => true,
canGoBack: () => canGoBack: () =>
router.getStateForAction(getState(), BaseActions.goBack() as Action) !== router.getStateForAction(
null || getState(),
CommonActions.goBack() as Action
) !== null ||
(parentNavigationHelpers && parentNavigationHelpers.canGoBack()) || (parentNavigationHelpers && parentNavigationHelpers.canGoBack()) ||
false, false,
} as NavigationHelpers<ParamListBase> & } as NavigationHelpers<ParamListBase, EventMap> &
(NavigationProp<ParamListBase, string, any, any, any> | undefined); (NavigationProp<ParamListBase, string, any, any, any> | undefined);
}, [ }, [
router, router,

View File

@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.8](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.7...@react-navigation/drawer@5.0.0-alpha.8) (2019-08-31)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.6...@react-navigation/drawer@5.0.0-alpha.7) (2019-08-30)
### Bug Fixes
* rename contentContainerStyle to sceneContainerStyle for drawer ([fdc24d2](https://github.com/react-navigation/navigation-ex/commit/fdc24d2))
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.5...@react-navigation/drawer@5.0.0-alpha.6) (2019-08-29)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.4...@react-navigation/drawer@5.0.0-alpha.5) (2019-08-28)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.3...@react-navigation/drawer@5.0.0-alpha.4) (2019-08-27)
**Note:** Version bump only for package @react-navigation/drawer
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.2...@react-navigation/drawer@5.0.0-alpha.3) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.1...@react-navigation/drawer@5.0.0-alpha.2) (2019-08-22) # [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/drawer@5.0.0-alpha.1...@react-navigation/drawer@5.0.0-alpha.2) (2019-08-22)
**Note:** Version bump only for package @react-navigation/drawer **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -11,7 +11,7 @@
"material", "material",
"drawer" "drawer"
], ],
"version": "5.0.0-alpha.2", "version": "5.0.0-alpha.8",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -21,7 +21,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/drawer/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"
@@ -34,7 +34,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.2", "@react-navigation/routers": "^5.0.0-alpha.7",
"react-native-safe-area-view": "^0.14.6" "react-native-safe-area-view": "^0.14.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -11,7 +11,11 @@ import {
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import DrawerView from '../views/DrawerView'; import DrawerView from '../views/DrawerView';
import { DrawerNavigationOptions, DrawerNavigationConfig } from '../types'; import {
DrawerNavigationOptions,
DrawerNavigationConfig,
DrawerNavigationEventMap,
} from '../types';
type Props = DefaultNavigatorOptions<DrawerNavigationOptions> & type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
DrawerRouterOptions & DrawerRouterOptions &
@@ -25,8 +29,9 @@ function DrawerNavigator({
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
DrawerNavigationState, DrawerNavigationState,
DrawerRouterOptions,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerRouterOptions DrawerNavigationEventMap
>(DrawerRouter, { >(DrawerRouter, {
initialRouteName, initialRouteName,
children, children,

View File

@@ -75,7 +75,7 @@ export type DrawerNavigationConfig = {
lazy: boolean; lazy: boolean;
/** /**
* Whether a screen should be unmounted when navigating away from it. * Whether a screen should be unmounted when navigating away from it.
* Defaults to `false`.. * Defaults to `false`.
*/ */
unmountInactiveRoutes?: boolean; unmountInactiveRoutes?: boolean;
/** /**
@@ -87,7 +87,10 @@ export type DrawerNavigationConfig = {
* Options for the content component which will be passed as props. * Options for the content component which will be passed as props.
*/ */
contentOptions?: object; contentOptions?: object;
contentContainerStyle?: StyleProp<ViewStyle>; /**
* Style object for the component wrapping the screen content.
*/
sceneContainerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
}; };
@@ -182,6 +185,11 @@ export type DrawerNavigationEventMap = {
drawerClose: undefined; drawerClose: undefined;
}; };
export type DrawerNavigationHelpers = NavigationHelpers<
ParamListBase,
DrawerNavigationEventMap
>;
export type DrawerNavigationProp< export type DrawerNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string RouteName extends keyof ParamList = string

View File

@@ -88,7 +88,7 @@ type Props = {
statusBarAnimation: 'slide' | 'none' | 'fade'; statusBarAnimation: 'slide' | 'none' | 'fade';
overlayStyle?: StyleProp<ViewStyle>; overlayStyle?: StyleProp<ViewStyle>;
drawerStyle?: StyleProp<ViewStyle>; drawerStyle?: StyleProp<ViewStyle>;
contentContainerStyle?: StyleProp<ViewStyle>; sceneContainerStyle?: StyleProp<ViewStyle>;
renderDrawerContent: Renderer; renderDrawerContent: Renderer;
renderSceneContent: Renderer; renderSceneContent: Renderer;
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>; gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
@@ -484,7 +484,7 @@ export default class DrawerView extends React.PureComponent<Props> {
drawerPosition, drawerPosition,
drawerType, drawerType,
swipeEdgeWidth, swipeEdgeWidth,
contentContainerStyle, sceneContainerStyle,
drawerStyle, drawerStyle,
overlayStyle, overlayStyle,
onGestureRef, onGestureRef,
@@ -534,7 +534,7 @@ export default class DrawerView extends React.PureComponent<Props> {
{ {
transform: [{ translateX: contentTranslateX }], transform: [{ translateX: contentTranslateX }],
}, },
contentContainerStyle as any, sceneContainerStyle as any,
]} ]}
> >
{renderSceneContent({ progress: this.progress })} {renderSceneContent({ progress: this.progress })}

View File

@@ -1,24 +1,24 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet, View, ViewStyle, StyleProp } from 'react-native'; import { StyleSheet, View, ViewStyle, StyleProp } from 'react-native';
import Animated from 'react-native-reanimated'; import Animated from 'react-native-reanimated';
import { import { Route, CommonActions } from '@react-navigation/core';
NavigationHelpers,
ParamListBase,
Route,
BaseActions,
} from '@react-navigation/core';
import { import {
DrawerActions, DrawerActions,
DrawerNavigationState, DrawerNavigationState,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import { Scene, ContentComponentProps, DrawerDescriptorMap } from '../types'; import {
Scene,
ContentComponentProps,
DrawerDescriptorMap,
DrawerNavigationHelpers,
} from '../types';
type Props = { type Props = {
contentComponent?: React.ComponentType<ContentComponentProps>; contentComponent?: React.ComponentType<ContentComponentProps>;
contentOptions?: object; contentOptions?: object;
state: DrawerNavigationState; state: DrawerNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
drawerOpenProgress: Animated.Node<number>; drawerOpenProgress: Animated.Node<number>;
drawerPosition: 'left' | 'right'; drawerPosition: 'left' | 'right';
@@ -78,7 +78,7 @@ class DrawerSidebar extends React.PureComponent<Props> {
navigation.dispatch({ navigation.dispatch({
...(focused ...(focused
? DrawerActions.closeDrawer() ? DrawerActions.closeDrawer()
: BaseActions.navigate(route.name)), : CommonActions.navigate(route.name)),
target: state.key, target: state.key,
}); });
}; };

View File

@@ -4,7 +4,6 @@ import { Dimensions, StyleSheet, I18nManager, Platform } from 'react-native';
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
import SafeAreaView from 'react-native-safe-area-view'; import SafeAreaView from 'react-native-safe-area-view';
import { PanGestureHandler, ScrollView } from 'react-native-gesture-handler'; import { PanGestureHandler, ScrollView } from 'react-native-gesture-handler';
import { ParamListBase, NavigationHelpers } from '@react-navigation/core';
import { import {
DrawerNavigationState, DrawerNavigationState,
DrawerActions, DrawerActions,
@@ -19,11 +18,12 @@ import {
DrawerDescriptorMap, DrawerDescriptorMap,
DrawerNavigationConfig, DrawerNavigationConfig,
ContentComponentProps, ContentComponentProps,
DrawerNavigationHelpers,
} from '../types'; } from '../types';
type Props = DrawerNavigationConfig & { type Props = DrawerNavigationConfig & {
state: DrawerNavigationState; state: DrawerNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
}; };
@@ -190,7 +190,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
drawerPosition, drawerPosition,
drawerBackgroundColor, drawerBackgroundColor,
overlayColor, overlayColor,
contentContainerStyle, sceneContainerStyle,
edgeWidth, edgeWidth,
minSwipeDistance, minSwipeDistance,
hideStatusBar, hideStatusBar,
@@ -222,7 +222,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
gestureHandlerProps={gestureHandlerProps} gestureHandlerProps={gestureHandlerProps}
drawerType={drawerType} drawerType={drawerType}
drawerPosition={drawerPosition} drawerPosition={drawerPosition}
contentContainerStyle={contentContainerStyle} sceneContainerStyle={sceneContainerStyle}
drawerStyle={{ drawerStyle={{
backgroundColor: drawerBackgroundColor || 'white', backgroundColor: drawerBackgroundColor || 'white',
width: this.state.drawerWidth, width: this.state.drawerWidth,

View File

@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.4](https://github.com/satya164/navigation-ex/compare/@react-navigation/example@5.0.0-alpha.3...@react-navigation/example@5.0.0-alpha.4) (2019-08-31)
### Bug Fixes
* handle route names change when all routes are removed ([#86](https://github.com/satya164/navigation-ex/issues/86)) ([1b2983e](https://github.com/satya164/navigation-ex/commit/1b2983e))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/example@5.0.0-alpha.2...@react-navigation/example@5.0.0-alpha.3) (2019-08-29)
### Features
* handle more methods in useScrollToTop ([f9e8c7e](https://github.com/react-navigation/navigation-ex/commit/f9e8c7e))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/example@5.0.0-alpha.1...@react-navigation/example@5.0.0-alpha.2) (2019-08-27)
### Features
* add native container ([d26b77f](https://github.com/react-navigation/navigation-ex/commit/d26b77f))
# 5.0.0-alpha.1 (2019-08-21) # 5.0.0-alpha.1 (2019-08-21)

View File

@@ -2,6 +2,6 @@
If you want to run the example from the repo, If you want to run the example from the repo,
- Clone the repository and run `lerna bootstrap` in the root - Clone the repository and run `yarn` in the project root
- Run `yarn example start` to start the packager - Run `yarn example start` to start the packager
- Follow the instructions to open it with the [Expo app](https://expo.io/) - Follow the instructions to open it with the [Expo app](https://expo.io/)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/example", "name": "@react-navigation/example",
"description": "Demo app to showcase various functionality of React Navigation", "description": "Demo app to showcase various functionality of React Navigation",
"version": "5.0.0-alpha.1", "version": "5.0.0-alpha.4",
"private": true, "private": true,
"workspaces": { "workspaces": {
"nohoist": [ "nohoist": [

View File

@@ -0,0 +1,128 @@
import * as React from 'react';
import { View, TextInput, ActivityIndicator, StyleSheet } from 'react-native';
import { Title, Button } from 'react-native-paper';
import { ParamListBase } from '@react-navigation/core';
import {
createStackNavigator,
HeaderBackButton,
StackNavigationProp,
} from '@react-navigation/stack';
type AuthStackParams = {
splash: undefined;
home: undefined;
'sign-in': undefined;
};
const SplashScreen = () => (
<View style={styles.content}>
<ActivityIndicator />
</View>
);
const SignInScreen = ({
setUserToken,
}: {
setUserToken: (token: string) => void;
}) => {
return (
<View style={styles.content}>
<TextInput placeholder="Username" style={styles.input} />
<TextInput placeholder="Password" secureTextEntry style={styles.input} />
<Button
mode="contained"
onPress={() => setUserToken('token')}
style={styles.button}
>
Sign in
</Button>
</View>
);
};
const HomeScreen = ({
setUserToken,
}: {
setUserToken: (token: undefined) => void;
}) => {
return (
<View style={styles.content}>
<Title style={styles.text}>Signed in successfully 🎉</Title>
<Button onPress={() => setUserToken(undefined)} style={styles.button}>
Sign out
</Button>
</View>
);
};
const SimpleStack = createStackNavigator<AuthStackParams>();
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function SimpleStackScreen({ navigation }: Props) {
const [isLoading, setIsLoading] = React.useState(true);
const [userToken, setUserToken] = React.useState<string | undefined>(
undefined
);
React.useEffect(() => {
const timer = setTimeout(() => setIsLoading(false), 1000);
return () => clearTimeout(timer);
}, []);
navigation.setOptions({
header: null,
});
return (
<SimpleStack.Navigator
screenOptions={{
headerLeft: () => (
<HeaderBackButton onPress={() => navigation.goBack()} />
),
}}
>
{isLoading ? (
<SimpleStack.Screen
name="splash"
component={SplashScreen}
options={{ title: `Auth Flow` }}
/>
) : userToken === undefined ? (
<SimpleStack.Screen name="sign-in" options={{ title: `Sign in` }}>
{() => <SignInScreen setUserToken={setUserToken} />}
</SimpleStack.Screen>
) : (
<SimpleStack.Screen name="home" options={{ title: 'Home' }}>
{() => <HomeScreen setUserToken={setUserToken} />}
</SimpleStack.Screen>
)}
</SimpleStack.Navigator>
);
}
const styles = StyleSheet.create({
content: {
flex: 1,
padding: 16,
justifyContent: 'center',
},
input: {
margin: 8,
padding: 10,
backgroundColor: 'white',
borderRadius: 3,
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(0, 0, 0, 0.08)',
},
button: {
margin: 8,
},
text: {
textAlign: 'center',
margin: 8,
},
});

View File

@@ -8,6 +8,7 @@ import TouchableBounce from 'react-native/Libraries/Components/Touchable/Touchab
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts'; import Contacts from '../Shared/Contacts';
import Chat from '../Shared/Chat'; import Chat from '../Shared/Chat';
import SimpleStackScreen from './SimpleStack';
const getTabBarIcon = (name: string) => ({ const getTabBarIcon = (name: string) => ({
tintColor, tintColor,
@@ -20,6 +21,7 @@ const getTabBarIcon = (name: string) => ({
); );
type BottomTabParams = { type BottomTabParams = {
article: undefined;
albums: undefined; albums: undefined;
contacts: undefined; contacts: undefined;
chat: undefined; chat: undefined;
@@ -30,6 +32,18 @@ const BottomTabs = createBottomTabNavigator<BottomTabParams>();
export default function BottomTabsScreen() { export default function BottomTabsScreen() {
return ( return (
<BottomTabs.Navigator> <BottomTabs.Navigator>
<BottomTabs.Screen
name="article"
options={{
title: 'Article',
tabBarIcon: getTabBarIcon('chrome-reader-mode'),
tabBarButtonComponent: TouchableBounce,
}}
>
{props => (
<SimpleStackScreen {...props} options={{ headerMode: 'none' }} />
)}
</BottomTabs.Screen>
<BottomTabs.Screen <BottomTabs.Screen
name="chat" name="chat"
component={Chat} component={Chat}

View File

@@ -1,10 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs'; import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts'; import Contacts from '../Shared/Contacts';
import Chat from '../Shared/Chat'; import Chat from '../Shared/Chat';
import SimpleStackScreen from './SimpleStack';
type MaterialBottomTabParams = { type MaterialBottomTabParams = {
article: undefined; article: undefined;
@@ -22,13 +22,16 @@ export default function MaterialBottomTabsScreen() {
<MaterialBottomTabs.Navigator barStyle={styles.tabBar}> <MaterialBottomTabs.Navigator barStyle={styles.tabBar}>
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="article" name="article"
component={Article}
options={{ options={{
tabBarLabel: 'Article', tabBarLabel: 'Article',
tabBarIcon: 'chrome-reader-mode', tabBarIcon: 'chrome-reader-mode',
tabBarColor: '#C9E7F8', tabBarColor: '#C9E7F8',
}} }}
/> >
{props => (
<SimpleStackScreen {...props} options={{ headerMode: 'none' }} />
)}
</MaterialBottomTabs.Screen>
<MaterialBottomTabs.Screen <MaterialBottomTabs.Screen
name="chat" name="chat"
component={Chat} component={Chat}

View File

@@ -72,17 +72,18 @@ const AlbumsScreen = ({
const SimpleStack = createStackNavigator<SimpleStackParams>(); const SimpleStack = createStackNavigator<SimpleStackParams>();
export default function SimpleStackScreen({ type Props = {
navigation, options?: React.ComponentProps<typeof SimpleStack.Navigator>;
}: {
navigation: StackNavigationProp<ParamListBase>; navigation: StackNavigationProp<ParamListBase>;
}) { };
export default function SimpleStackScreen({ navigation, options }: Props) {
navigation.setOptions({ navigation.setOptions({
header: null, header: null,
}); });
return ( return (
<SimpleStack.Navigator> <SimpleStack.Navigator {...options}>
<SimpleStack.Screen <SimpleStack.Screen
name="article" name="article"
component={ArticleScreen} component={ArticleScreen}

View File

@@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native'; import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
const COVERS = [ const COVERS = [
require('../../assets/album-art-1.jpg'), require('../../assets/album-art-1.jpg'),
@@ -12,19 +13,22 @@ const COVERS = [
require('../../assets/album-art-8.jpg'), require('../../assets/album-art-8.jpg'),
]; ];
export default class Albums extends React.Component { export default function Albums() {
render() { const ref = React.useRef<ScrollView>(null);
return (
<ScrollView useScrollToTop(ref);
style={styles.container}
contentContainerStyle={styles.content} return (
> <ScrollView
{COVERS.map((source, i) => ( ref={ref}
<Image key={i} source={source} style={styles.cover} /> style={styles.container}
))} contentContainerStyle={styles.content}
</ScrollView> >
); {COVERS.map((source, i) => (
} <Image key={i} source={source} style={styles.cover} />
))}
</ScrollView>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -1,63 +1,63 @@
import * as React from 'react'; import * as React from 'react';
import { View, Text, Image, ScrollView, StyleSheet } from 'react-native'; import { View, Text, Image, ScrollView, StyleSheet } from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
type Props = { type Props = {
date: string; date?: string;
author: { author?: {
name: string; name: string;
}; };
}; };
export default class Article extends React.Component<Props> { export default function Article({
static defaultProps = { date = '1st Jan 2025',
date: '1st Jan 2025', author = {
author: { name: 'Knowledge Bot',
name: 'Knowledge Bot', },
}, }: Props) {
}; const ref = React.useRef<ScrollView>(null);
render() { useScrollToTop(ref);
const { date, author } = this.props;
return ( return (
<ScrollView <ScrollView
style={styles.container} ref={ref}
contentContainerStyle={styles.content} style={styles.container}
> contentContainerStyle={styles.content}
<View style={styles.author}> >
<Image <View style={styles.author}>
style={styles.avatar} <Image
source={require('../../assets/avatar-1.png')} style={styles.avatar}
/> source={require('../../assets/avatar-1.png')}
<View style={styles.meta}> />
<Text style={styles.name}>{author.name}</Text> <View style={styles.meta}>
<Text style={styles.timestamp}>{date}</Text> <Text style={styles.name}>{author.name}</Text>
</View> <Text style={styles.timestamp}>{date}</Text>
</View> </View>
<Text style={styles.title}>Lorem Ipsum</Text> </View>
<Text style={styles.paragraph}> <Text style={styles.title}>Lorem Ipsum</Text>
Contrary to popular belief, Lorem Ipsum is not simply random text. It <Text style={styles.paragraph}>
has roots in a piece of classical Latin literature from 45 BC, making Contrary to popular belief, Lorem Ipsum is not simply random text. It
it over 2000 years old. has roots in a piece of classical Latin literature from 45 BC, making it
</Text> over 2000 years old.
<Image style={styles.image} source={require('../../assets/book.jpg')} /> </Text>
<Text style={styles.paragraph}> <Image style={styles.image} source={require('../../assets/book.jpg')} />
Richard McClintock, a Latin professor at Hampden-Sydney College in <Text style={styles.paragraph}>
Virginia, looked up one of the more obscure Latin words, consectetur, Richard McClintock, a Latin professor at Hampden-Sydney College in
from a Lorem Ipsum passage, and going through the cites of the word in Virginia, looked up one of the more obscure Latin words, consectetur,
classical literature, discovered the undoubtable source. from a Lorem Ipsum passage, and going through the cites of the word in
</Text> classical literature, discovered the undoubtable source.
<Text style={styles.paragraph}> </Text>
Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of &quot;de <Text style={styles.paragraph}>
Finibus Bonorum et Malorum&quot; (The Extremes of Good and Evil) by Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of &quot;de Finibus
Cicero, written in 45 BC. This book is a treatise on the theory of Bonorum et Malorum&quot; (The Extremes of Good and Evil) by Cicero,
ethics, very popular during the Renaissance. The first line of Lorem written in 45 BC. This book is a treatise on the theory of ethics, very
Ipsum, &quot;Lorem ipsum dolor sit amet..&quot;, comes from a line in popular during the Renaissance. The first line of Lorem Ipsum,
section 1.10.32. &quot;Lorem ipsum dolor sit amet..&quot;, comes from a line in section
</Text> 1.10.32.
</ScrollView> </Text>
); </ScrollView>
} );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -7,6 +7,7 @@ import {
ScrollView, ScrollView,
StyleSheet, StyleSheet,
} from 'react-native'; } from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
const MESSAGES = [ const MESSAGES = [
'okay', 'okay',
@@ -15,49 +16,51 @@ const MESSAGES = [
'make me a sandwich', 'make me a sandwich',
]; ];
export default class Chat extends React.Component { export default function Chat() {
render() { const ref = React.useRef<ScrollView>(null);
return (
<View style={styles.container}>
<ScrollView
style={styles.inverted}
contentContainerStyle={styles.content}
>
{MESSAGES.map((text, i) => {
const odd = i % 2;
return ( useScrollToTop(ref);
return (
<View style={styles.container}>
<ScrollView
style={styles.inverted}
contentContainerStyle={styles.content}
>
{MESSAGES.map((text, i) => {
const odd = i % 2;
return (
<View
key={i}
style={[odd ? styles.odd : styles.even, styles.inverted]}
>
<Image
style={styles.avatar}
source={
odd
? require('../../assets/avatar-2.png')
: require('../../assets/avatar-1.png')
}
/>
<View <View
key={i} style={[styles.bubble, odd ? styles.received : styles.sent]}
style={[odd ? styles.odd : styles.even, styles.inverted]}
> >
<Image <Text style={odd ? styles.receivedText : styles.sentText}>
style={styles.avatar} {text}
source={ </Text>
odd
? require('../../assets/avatar-2.png')
: require('../../assets/avatar-1.png')
}
/>
<View
style={[styles.bubble, odd ? styles.received : styles.sent]}
>
<Text style={odd ? styles.receivedText : styles.sentText}>
{text}
</Text>
</View>
</View> </View>
); </View>
})} );
</ScrollView> })}
<TextInput </ScrollView>
style={styles.input} <TextInput
placeholder="Write a message" style={styles.input}
underlineColorAndroid="transparent" placeholder="Write a message"
/> underlineColorAndroid="transparent"
</View> />
); </View>
} );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, FlatList, StyleSheet } from 'react-native';
import { FlatList } from 'react-native-gesture-handler'; import { useScrollToTop } from '@react-navigation/native';
type Item = { name: string; number: number }; type Item = { name: string; number: number };
@@ -79,23 +79,24 @@ class ContactItem extends React.PureComponent<{
} }
} }
export default class Contacts extends React.Component { export default function Contacts() {
private renderItem = ({ item }: { item: Item }) => ( const ref = React.useRef<FlatList<Item>>(null);
<ContactItem item={item} />
useScrollToTop(ref);
const renderItem = ({ item }: { item: Item }) => <ContactItem item={item} />;
const ItemSeparator = () => <View style={styles.separator} />;
return (
<FlatList
ref={ref}
data={CONTACTS}
keyExtractor={(_, i) => String(i)}
renderItem={renderItem}
ItemSeparatorComponent={ItemSeparator}
/>
); );
private ItemSeparator = () => <View style={styles.separator} />;
render() {
return (
<FlatList
data={CONTACTS}
keyExtractor={(_, i) => String(i)}
renderItem={this.renderItem}
ItemSeparatorComponent={this.ItemSeparator}
/>
);
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -4,12 +4,11 @@ import { Linking } from 'expo';
import { Appbar, List } from 'react-native-paper'; import { Appbar, List } from 'react-native-paper';
import { Asset } from 'expo-asset'; import { Asset } from 'expo-asset';
import { import {
NavigationContainer,
InitialState, InitialState,
getStateFromPath, getStateFromPath,
NavigationContainerRef, NavigationContainerRef,
} from '@react-navigation/core'; } from '@react-navigation/core';
import { useBackButton, useLinking } from '@react-navigation/native'; import { useLinking, NativeContainer } from '@react-navigation/native';
import { import {
createDrawerNavigator, createDrawerNavigator,
DrawerNavigationProp, DrawerNavigationProp,
@@ -24,6 +23,7 @@ import SimpleStackScreen from './Screens/SimpleStack';
import BottomTabsScreen from './Screens/BottomTabs'; import BottomTabsScreen from './Screens/BottomTabs';
import MaterialTopTabsScreen from './Screens/MaterialTopTabs'; import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MaterialBottomTabs from './Screens/MaterialBottomTabs'; import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import AuthFlow from './Screens/AuthFlow';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']); YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
@@ -48,6 +48,10 @@ const SCREENS = {
title: 'Material Bottom Tabs', title: 'Material Bottom Tabs',
component: MaterialBottomTabs, component: MaterialBottomTabs,
}, },
'auth-flow': {
title: 'Auth Flow',
component: AuthFlow,
},
}; };
const Drawer = createDrawerNavigator<RootDrawerParamList>(); const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -60,8 +64,6 @@ Asset.loadAsync(StackAssets);
export default function App() { export default function App() {
const containerRef = React.useRef<NavigationContainerRef>(); const containerRef = React.useRef<NavigationContainerRef>();
useBackButton(containerRef);
// To test deep linking on, run the following in the Terminal: // To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack" // Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack // iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
@@ -116,7 +118,7 @@ export default function App() {
} }
return ( return (
<NavigationContainer <NativeContainer
ref={containerRef} ref={containerRef}
initialState={initialState} initialState={initialState}
onStateChange={state => onStateChange={state =>
@@ -175,6 +177,6 @@ export default function App() {
)} )}
</Drawer.Screen> </Drawer.Screen>
</Drawer.Navigator> </Drawer.Navigator>
</NavigationContainer> </NativeContainer>
); );
} }

View File

@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.6...@react-navigation/material-bottom-tabs@5.0.0-alpha.7) (2019-08-31)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.5...@react-navigation/material-bottom-tabs@5.0.0-alpha.6) (2019-08-29)
### Bug Fixes
* allow making params optional. fixes [#80](https://github.com/react-navigation/navigation-ex/issues/80) ([a9d4813](https://github.com/react-navigation/navigation-ex/commit/a9d4813))
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.4...@react-navigation/material-bottom-tabs@5.0.0-alpha.5) (2019-08-28)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.3...@react-navigation/material-bottom-tabs@5.0.0-alpha.4) (2019-08-27)
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.2...@react-navigation/material-bottom-tabs@5.0.0-alpha.3) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.1...@react-navigation/material-bottom-tabs@5.0.0-alpha.2) (2019-08-22) # [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-bottom-tabs@5.0.0-alpha.1...@react-navigation/material-bottom-tabs@5.0.0-alpha.2) (2019-08-22)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -11,7 +11,7 @@
"material", "material",
"tab" "tab"
], ],
"version": "5.0.0-alpha.2", "version": "5.0.0-alpha.7",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -21,7 +21,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/material-bottom-tabs/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"
@@ -34,7 +34,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.2" "@react-navigation/routers": "^5.0.0-alpha.7"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",

View File

@@ -14,6 +14,7 @@ import MaterialBottomTabView from '../views/MaterialBottomTabView';
import { import {
MaterialBottomTabNavigationConfig, MaterialBottomTabNavigationConfig,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap,
} from '../types'; } from '../types';
type Props = DefaultNavigatorOptions<MaterialBottomTabNavigationOptions> & type Props = DefaultNavigatorOptions<MaterialBottomTabNavigationOptions> &
@@ -29,8 +30,9 @@ function MaterialBottomTabNavigator({
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState,
TabRouterOptions,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
TabRouterOptions MaterialBottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
initialRouteName, initialRouteName,
backBehavior, backBehavior,

View File

@@ -3,14 +3,22 @@ import {
ParamListBase, ParamListBase,
Descriptor, Descriptor,
NavigationProp, NavigationProp,
NavigationHelpers,
} from '@react-navigation/core'; } from '@react-navigation/core';
import { TabNavigationState } from '@react-navigation/routers'; import { TabNavigationState } from '@react-navigation/routers';
export type MaterialBottomTabNavigationEventMap = { export type MaterialBottomTabNavigationEventMap = {
refocus: undefined; /**
* Event which fires on tapping on the tab in the tab bar.
*/
tabPress: undefined; tabPress: undefined;
}; };
export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
ParamListBase,
MaterialBottomTabNavigationEventMap
>;
export type MaterialBottomTabNavigationProp< export type MaterialBottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string RouteName extends keyof ParamList = string
@@ -28,8 +36,8 @@ export type MaterialBottomTabNavigationProp<
* @param [params] Params object for the route. * @param [params] Params object for the route.
*/ */
jumpTo<RouteName extends Extract<keyof ParamList, string>>( jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends void ...args: ParamList[RouteName] extends (undefined | any)
? [RouteName] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
}; };

View File

@@ -2,21 +2,18 @@ import * as React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { BottomNavigation } from 'react-native-paper'; import { BottomNavigation } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialIcons';
import { import { Route } from '@react-navigation/core';
NavigationHelpers,
ParamListBase,
Route,
} from '@react-navigation/core';
import { TabNavigationState, TabActions } from '@react-navigation/routers'; import { TabNavigationState, TabActions } from '@react-navigation/routers';
import { import {
MaterialBottomTabDescriptorMap, MaterialBottomTabDescriptorMap,
MaterialBottomTabNavigationConfig, MaterialBottomTabNavigationConfig,
MaterialBottomTabNavigationHelpers,
} from '../types'; } from '../types';
type Props = MaterialBottomTabNavigationConfig & { type Props = MaterialBottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: MaterialBottomTabNavigationHelpers;
descriptors: MaterialBottomTabDescriptorMap; descriptors: MaterialBottomTabDescriptorMap;
}; };
@@ -65,19 +62,12 @@ export default class MaterialBottomTabView extends React.PureComponent<Props> {
}; };
private handleTabPress = ({ route }: Scene) => { private handleTabPress = ({ route }: Scene) => {
const { state, navigation } = this.props; const { navigation } = this.props;
navigation.emit({ navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
}); });
if (state.routes[state.index].key === route.key) {
navigation.emit({
type: 'refocus',
target: route.key,
});
}
}; };
private renderIcon = ({ private renderIcon = ({

View File

@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.6...@react-navigation/material-top-tabs@5.0.0-alpha.7) (2019-08-31)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.5...@react-navigation/material-top-tabs@5.0.0-alpha.6) (2019-08-29)
### Bug Fixes
* allow making params optional. fixes [#80](https://github.com/react-navigation/navigation-ex/issues/80) ([a9d4813](https://github.com/react-navigation/navigation-ex/commit/a9d4813))
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.4...@react-navigation/material-top-tabs@5.0.0-alpha.5) (2019-08-28)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.3...@react-navigation/material-top-tabs@5.0.0-alpha.4) (2019-08-27)
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.2...@react-navigation/material-top-tabs@5.0.0-alpha.3) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.1...@react-navigation/material-top-tabs@5.0.0-alpha.2) (2019-08-22) # [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/material-top-tabs@5.0.0-alpha.1...@react-navigation/material-top-tabs@5.0.0-alpha.2) (2019-08-22)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -11,7 +11,7 @@
"material", "material",
"tab" "tab"
], ],
"version": "5.0.0-alpha.2", "version": "5.0.0-alpha.7",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -21,7 +21,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/material-top-tabs/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"
@@ -34,7 +34,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.2" "@react-navigation/routers": "^5.0.0-alpha.7"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.7.0", "@react-native-community/bob": "^0.7.0",

View File

@@ -13,6 +13,7 @@ import MaterialTopTabView from '../views/MaterialTopTabView';
import { import {
MaterialTopTabNavigationConfig, MaterialTopTabNavigationConfig,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap,
} from '../types'; } from '../types';
type Props = DefaultNavigatorOptions<MaterialTopTabNavigationOptions> & type Props = DefaultNavigatorOptions<MaterialTopTabNavigationOptions> &
@@ -28,8 +29,9 @@ function MaterialTopTabNavigator({
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState,
TabRouterOptions,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
TabRouterOptions MaterialTopTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
initialRouteName, initialRouteName,
backBehavior, backBehavior,

View File

@@ -10,10 +10,6 @@ import {
import { TabNavigationState } from '@react-navigation/routers'; import { TabNavigationState } from '@react-navigation/routers';
export type MaterialTopTabNavigationEventMap = { export type MaterialTopTabNavigationEventMap = {
/**
* Event which fires on tapping on the tab for an already focused screen.
*/
refocus: undefined;
/** /**
* Event which fires on tapping on the tab in the tab bar. * Event which fires on tapping on the tab in the tab bar.
*/ */
@@ -32,6 +28,11 @@ export type MaterialTopTabNavigationEventMap = {
swipeEnd: undefined; swipeEnd: undefined;
}; };
export type MaterialTopTabNavigationHelpers = NavigationHelpers<
ParamListBase,
MaterialTopTabNavigationEventMap
>;
export type MaterialTopTabNavigationProp< export type MaterialTopTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string RouteName extends keyof ParamList = string
@@ -49,8 +50,8 @@ export type MaterialTopTabNavigationProp<
* @param [params] Params object for the route. * @param [params] Params object for the route.
*/ */
jumpTo<RouteName extends Extract<keyof ParamList, string>>( jumpTo<RouteName extends Extract<keyof ParamList, string>>(
...args: ParamList[RouteName] extends void ...args: ParamList[RouteName] extends (undefined | any)
? [RouteName] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
}; };
@@ -124,7 +125,7 @@ export type MaterialTopTabNavigationConfig = Partial<
* *
* This view is usually only shown for a split second. Keep it lightweight. * This view is usually only shown for a split second. Keep it lightweight.
* *
* By default, this renders null.. * By default, this renders null.
*/ */
lazyPlaceholderComponent?: React.ComponentType<{ route: Route<string> }>; lazyPlaceholderComponent?: React.ComponentType<{ route: Route<string> }>;
/** /**

View File

@@ -1,21 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { TabView, SceneRendererProps } from 'react-native-tab-view'; import { TabView, SceneRendererProps } from 'react-native-tab-view';
import { import { Route } from '@react-navigation/core';
NavigationHelpers,
ParamListBase,
Route,
} from '@react-navigation/core';
import { TabNavigationState, TabActions } from '@react-navigation/routers'; import { TabNavigationState, TabActions } from '@react-navigation/routers';
import MaterialTopTabBar from './MaterialTopTabBar'; import MaterialTopTabBar from './MaterialTopTabBar';
import { import {
MaterialTopTabDescriptorMap, MaterialTopTabDescriptorMap,
MaterialTopTabNavigationConfig, MaterialTopTabNavigationConfig,
MaterialTopTabNavigationHelpers,
} from '../types'; } from '../types';
type Props = MaterialTopTabNavigationConfig & { type Props = MaterialTopTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: MaterialTopTabNavigationHelpers;
descriptors: MaterialTopTabDescriptorMap; descriptors: MaterialTopTabDescriptorMap;
tabBarPosition: 'top' | 'bottom'; tabBarPosition: 'top' | 'bottom';
}; };
@@ -76,7 +73,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
route: Route<string>; route: Route<string>;
preventDefault: () => void; preventDefault: () => void;
}) => { }) => {
const { state, navigation } = this.props;
const event = this.props.navigation.emit({ const event = this.props.navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
@@ -85,13 +81,6 @@ export default class MaterialTopTabView extends React.PureComponent<Props> {
if (event.defaultPrevented) { if (event.defaultPrevented) {
preventDefault(); preventDefault();
} }
if (state.routes[state.index].key === route.key) {
navigation.emit({
type: 'refocus',
target: route.key,
});
}
}; };
private handleTabLongPress = ({ route }: { route: Route<string> }) => { private handleTabLongPress = ({ route }: { route: Route<string> }) => {

View File

@@ -3,6 +3,57 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.4...@react-navigation/native@5.0.0-alpha.5) (2019-08-29)
### Bug Fixes
* handle both null and undefined in useScrollToTop ([c951027](https://github.com/react-navigation/navigation-ex/commit/c951027))
### Features
* handle animated component wrappers in `useScrollToTop` ([#81](https://github.com/react-navigation/navigation-ex/issues/81)) ([cdbf1e9](https://github.com/react-navigation/navigation-ex/commit/cdbf1e9))
* handle more methods in useScrollToTop ([f9e8c7e](https://github.com/react-navigation/navigation-ex/commit/f9e8c7e))
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.3...@react-navigation/native@5.0.0-alpha.4) (2019-08-28)
### Bug Fixes
* fix stack nested in tab always getting reset ([dead4e8](https://github.com/react-navigation/navigation-ex/commit/dead4e8))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.2...@react-navigation/native@5.0.0-alpha.3) (2019-08-27)
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
* add native container ([d26b77f](https://github.com/react-navigation/navigation-ex/commit/d26b77f))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/native@5.0.0-alpha.1...@react-navigation/native@5.0.0-alpha.2) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# 5.0.0-alpha.1 (2019-08-21) # 5.0.0-alpha.1 (2019-08-21)

View File

@@ -7,7 +7,7 @@
"ios", "ios",
"android" "android"
], ],
"version": "5.0.0-alpha.1", "version": "5.0.0-alpha.5",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -17,7 +17,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/native/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"

View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import {
NavigationContainer,
NavigationContainerProps,
NavigationContainerRef,
} from '@react-navigation/core';
import useBackButton from './useBackButton';
/**
* Container component which holds the navigation state
* designed for mobile apps.
* This should be rendered at the root wrapping the whole app.
*
* @param props.initialState Initial state object for the navigation tree.
* @param props.onStateChange Callback which is called with the latest navigation state when it changes.
* @param props.children Child elements to render the content.
* @param props.ref Ref object which refers to the navigation object containing helper methods.
*/
const NativeContainer = React.forwardRef(function NativeContainer(
props: NavigationContainerProps,
ref: React.Ref<NavigationContainerRef>
) {
const refContainer = React.useRef<NavigationContainerRef>(null);
useBackButton(refContainer);
React.useImperativeHandle(ref, () => refContainer.current);
return (
<NavigationContainer
{...props}
ref={refContainer}
children={props.children}
/>
);
});
export default NativeContainer;

View File

@@ -1,2 +1,4 @@
export { default as NativeContainer } from './NativeContainer';
export { default as useBackButton } from './useBackButton'; export { default as useBackButton } from './useBackButton';
export { default as useLinking } from './useLinking'; export { default as useLinking } from './useLinking';
export { default as useScrollToTop } from './useScrollToTop';

View File

@@ -0,0 +1,69 @@
import * as React from 'react';
import { useNavigation, EventArg } from '@react-navigation/core';
type ScrollOptions = { y?: number; animated?: boolean };
type ScrollableView =
| { scrollToTop(): void }
| { scrollTo(options: ScrollOptions): void }
| { scrollToOffset(options: ScrollOptions): void }
| { scrollResponderScrollTo(options: ScrollOptions): void };
type ScrollableWrapper =
| { getScrollResponder(): ScrollableView }
| { getNode(): ScrollableView }
| ScrollableView;
function getScrollableNode(ref: React.RefObject<ScrollableWrapper>) {
if (ref.current == null) {
return null;
}
if ('getScrollResponder' in ref.current) {
// If the view is a wrapper like FlatList, SectionList etc.
// We need to use `getScrollResponder` to get access to the scroll responder
return ref.current.getScrollResponder();
} else if ('getNode' in ref.current) {
// When a `ScrollView` is wraped in `Animated.createAnimatedComponent`
// we need to use `getNode` to get the ref to the actual scrollview
return ref.current.getNode();
} else {
return ref.current;
}
}
export default function useScrollToTop(
ref: React.RefObject<ScrollableWrapper>
) {
const navigation = useNavigation();
React.useEffect(
() =>
// @ts-ignore
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
const isFocused = navigation.isFocused();
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
const scrollable = getScrollableNode(ref);
if (isFocused && !e.defaultPrevented && scrollable) {
// When user taps on already focused tab, scroll to top
if ('scrollToTop' in scrollable) {
scrollable.scrollToTop();
} else if ('scrollTo' in scrollable) {
scrollable.scrollTo({ y: 0, animated: true });
} else if ('scrollToOffset' in scrollable) {
scrollable.scrollToOffset({ y: 0, animated: true });
} else if ('scrollResponderScrollTo' in scrollable) {
scrollable.scrollResponderScrollTo({ y: 0, animated: true });
}
}
});
}),
[navigation, ref]
);
}

View File

@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.6...@react-navigation/routers@5.0.0-alpha.7) (2019-08-31)
### Bug Fixes
* handle route names change when all routes are removed ([#86](https://github.com/react-navigation/navigation-ex/issues/86)) ([1b2983e](https://github.com/react-navigation/navigation-ex/commit/1b2983e))
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.5...@react-navigation/routers@5.0.0-alpha.6) (2019-08-29)
### Features
* handle navigating with both with both key and name ([#83](https://github.com/react-navigation/navigation-ex/issues/83)) ([6b75cba](https://github.com/react-navigation/navigation-ex/commit/6b75cba))
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.4...@react-navigation/routers@5.0.0-alpha.5) (2019-08-28)
**Note:** Version bump only for package @react-navigation/routers
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.3...@react-navigation/routers@5.0.0-alpha.4) (2019-08-27)
**Note:** Version bump only for package @react-navigation/routers
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.2...@react-navigation/routers@5.0.0-alpha.3) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.1...@react-navigation/routers@5.0.0-alpha.2) (2019-08-22) # [5.0.0-alpha.2](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/routers@5.0.0-alpha.1...@react-navigation/routers@5.0.0-alpha.2) (2019-08-22)

View File

@@ -0,0 +1,440 @@
import { CommonActions } from '@react-navigation/core';
import { DrawerRouter, DrawerActions } from '../src';
jest.mock('shortid', () => () => 'test');
it('gets initial state from route names and params with initialRouteName', () => {
const router = DrawerRouter({ initialRouteName: 'baz' });
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 1,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it('gets initial state from route names and params without initialRouteName', () => {
const router = DrawerRouter({});
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 0,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it('gets rehydrated state from partial state', () => {
const router = DrawerRouter({});
const options = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
};
expect(
router.getRehydratedState(
{
routes: [{ key: 'bar-0', name: 'bar' }, { key: 'qux-1', name: 'qux' }],
},
options
)
).toEqual({
index: 0,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-1', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz' },
{ key: 'qux-2', name: 'qux' },
],
},
options
)
).toEqual({
index: 2,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz', params: { answer: 42 } },
{ key: 'qux-2', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 4,
routes: [],
},
options
)
).toEqual({
index: 0,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 1,
isDrawerOpen: true,
routeKeyHistory: ['bar-test', 'qux-test', 'foo-test'],
routes: [],
},
options
)
).toEqual({
index: 1,
key: 'drawer-test',
isDrawerOpen: true,
routeKeyHistory: ['bar-test', 'qux-test'],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it("doesn't rehydrate state if it's not stale", () => {
const router = DrawerRouter({});
const state = {
index: 0,
key: 'drawer-test',
isDrawerOpen: true,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false as const,
};
expect(
router.getRehydratedState(state, {
routeNames: [],
routeParamList: {},
})
).toBe(state);
});
it('handles navigate action', () => {
const router = DrawerRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
isDrawerOpen: false,
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('baz', { answer: 42 })
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
isDrawerOpen: false,
routeKeyHistory: ['bar'],
routes: [
{ key: 'baz', name: 'baz', params: { answer: 42 } },
{ key: 'bar', name: 'bar' },
],
});
});
it('handles open drawer action', () => {
const router = DrawerRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
isDrawerOpen: false,
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
DrawerActions.openDrawer()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: true,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
const state = {
stale: false as const,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: true,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
};
expect(router.getStateForAction(state, DrawerActions.openDrawer())).toBe(
state
);
});
it('handles close drawer action', () => {
const router = DrawerRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
isDrawerOpen: true,
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
DrawerActions.closeDrawer()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: false,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
const state = {
stale: false as const,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: false,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
};
expect(router.getStateForAction(state, DrawerActions.closeDrawer())).toBe(
state
);
});
it('handles toggle drawer action', () => {
const router = DrawerRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
isDrawerOpen: true,
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
DrawerActions.toggleDrawer()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: false,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
isDrawerOpen: false,
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
DrawerActions.toggleDrawer()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
isDrawerOpen: true,
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
});
it('updates route key history on focus change', () => {
const router = DrawerRouter({ backBehavior: 'history' });
const state = {
index: 0,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz', params: { answer: 42 } },
{ key: 'qux-0', name: 'qux', params: { name: 'Jane' } },
],
stale: false as const,
};
expect(router.getStateForRouteFocus(state, 'bar-0').routeKeyHistory).toEqual(
[]
);
expect(router.getStateForRouteFocus(state, 'baz-0').routeKeyHistory).toEqual([
'bar-0',
]);
});
it('closes drawer on focus change', () => {
const router = DrawerRouter({ backBehavior: 'history' });
expect(
router.getStateForRouteFocus(
{
index: 0,
key: 'drawer-test',
isDrawerOpen: false,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
},
'baz-0'
)
).toEqual({
index: 1,
isDrawerOpen: false,
key: 'drawer-test',
routeKeyHistory: ['bar-0'],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
});
expect(
router.getStateForRouteFocus(
{
index: 0,
key: 'drawer-test',
isDrawerOpen: true,
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
},
'baz-0'
)
).toEqual({
index: 1,
isDrawerOpen: false,
key: 'drawer-test',
routeKeyHistory: ['bar-0'],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
});
});

View File

@@ -0,0 +1,599 @@
import { CommonActions } from '@react-navigation/core';
import { StackRouter, StackActions } from '../src';
jest.mock('shortid', () => () => 'test');
it('gets initial state from route names and params with initialRouteName', () => {
const router = StackRouter({ initialRouteName: 'baz' });
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'baz-test', name: 'baz', params: { answer: 42 } }],
stale: false,
});
});
it('gets initial state from route names and params without initialRouteName', () => {
const router = StackRouter({});
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'bar-test', name: 'bar' }],
stale: false,
});
});
it('gets rehydrated state from partial state', () => {
const router = StackRouter({});
const options = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
};
expect(
router.getRehydratedState(
{
routes: [{ key: 'bar-0', name: 'bar' }, { key: 'qux-1', name: 'qux' }],
},
options
)
).toEqual({
index: 1,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'qux-1', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz' },
{ key: 'qux-2', name: 'qux' },
],
},
options
)
).toEqual({
index: 2,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz', params: { answer: 42 } },
{ key: 'qux-2', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 4,
routes: [],
},
options
)
).toEqual({
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'bar-test', name: 'bar' }],
stale: false,
});
});
it("doesn't rehydrate state if it's not stale", () => {
const router = StackRouter({});
const state = {
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'bar-test', name: 'bar' }],
stale: false as const,
};
expect(
router.getRehydratedState(state, {
routeNames: [],
routeParamList: {},
})
).toBe(state);
});
it('gets state on route names change', () => {
const router = StackRouter({});
expect(
router.getStateForRouteNamesChange(
{
index: 2,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
},
{
routeNames: ['qux', 'baz', 'foo', 'fiz'],
routeParamList: {
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
}
)
).toEqual({
index: 1,
key: 'stack-test',
routeNames: ['qux', 'baz', 'foo', 'fiz'],
routes: [
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getStateForRouteNamesChange(
{
index: 1,
key: 'stack-test',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo-test', name: 'foo' },
{ key: 'bar-test', name: 'bar' },
],
stale: false,
},
{
routeNames: ['baz', 'qux'],
routeParamList: {
baz: { name: 'John' },
},
}
)
).toEqual({
index: 0,
key: 'stack-test',
routeNames: ['baz', 'qux'],
routes: [{ key: 'baz-test', name: 'baz', params: { name: 'John' } }],
stale: false,
});
});
it('gets state on route names change with initialRouteName', () => {
const router = StackRouter({ initialRouteName: 'qux' });
expect(
router.getStateForRouteNamesChange(
{
index: 1,
key: 'stack-test',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo-test', name: 'foo' },
{ key: 'bar-test', name: 'bar' },
],
stale: false,
},
{
routeNames: ['baz', 'qux'],
routeParamList: {
baz: { name: 'John' },
},
}
)
).toEqual({
index: 0,
key: 'stack-test',
routeNames: ['baz', 'qux'],
routes: [{ key: 'qux-test', name: 'qux' }],
stale: false,
});
});
it('handles navigate action', () => {
const router = StackRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('qux', { answer: 42 })
)
).toEqual({
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{
key: 'qux-test',
name: 'qux',
params: { answer: 42 },
},
],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('baz', { answer: 42 })
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz', params: { answer: 42 } }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
],
},
CommonActions.navigate('bar', { answer: 96 })
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 96 } },
],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('unknown')
)
).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }, { key: 'bar-0', name: 'bar' }],
},
CommonActions.navigate({ key: 'unknown' })
)
).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
{
type: 'NAVIGATE',
payload: { key: 'baz-0', name: 'baz' },
}
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate({ key: 'baz-1', name: 'baz' })
)
).toEqual({
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz-0', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'baz-1', name: 'baz' },
],
});
});
it('handles go back action', () => {
const router = StackRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }],
},
CommonActions.goBack()
)
).toBe(null);
});
it('handles pop action', () => {
const router = StackRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux' },
],
},
StackActions.pop()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux' },
],
},
StackActions.pop(2)
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz-0', name: 'baz' },
{ key: 'bar-0', name: 'bar' },
{ key: 'qux-0', name: 'qux' },
],
},
{
...StackActions.pop(),
target: 'root',
source: 'bar-0',
}
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz-0', name: 'baz' }],
},
StackActions.pop()
)
).toBe(null);
});
it('handles pop to top action', () => {
const router = StackRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar' },
{ key: 'qux', name: 'qux' },
],
},
StackActions.popToTop()
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'baz', name: 'baz' }],
});
});
it('handles push action', () => {
const router = StackRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'bar', name: 'bar' }],
},
StackActions.push('baz')
)
).toEqual({
stale: false,
key: 'root',
index: 3,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'bar', name: 'bar' }, { key: 'baz-test', name: 'baz' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [{ key: 'bar', name: 'bar' }],
},
StackActions.push('unknown')
)
).toBe(null);
});
it('changes index on focus change', () => {
const router = StackRouter({});
expect(
router.getStateForRouteFocus(
{
index: 2,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
stale: false,
},
'baz-0'
)
).toEqual({
index: 1,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }],
stale: false,
});
const state = {
index: 0,
key: 'stack-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [{ key: 'bar-0', name: 'bar' }, { key: 'baz-0', name: 'baz' }],
stale: false as const,
};
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
});

View File

@@ -0,0 +1,526 @@
import { CommonActions } from '@react-navigation/core';
import { TabRouter, TabActions, TabNavigationState } from '../src';
jest.mock('shortid', () => () => 'test');
it('gets initial state from route names and params with initialRouteName', () => {
const router = TabRouter({ initialRouteName: 'baz' });
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 1,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it('gets initial state from route names and params without initialRouteName', () => {
const router = TabRouter({});
expect(
router.getInitialState({
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
})
).toEqual({
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it('gets rehydrated state from partial state', () => {
const router = TabRouter({});
const options = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {
baz: { answer: 42 },
qux: { name: 'Jane' },
},
};
expect(
router.getRehydratedState(
{
routes: [{ key: 'bar-0', name: 'bar' }, { key: 'qux-1', name: 'qux' }],
},
options
)
).toEqual({
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-1', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz' },
{ key: 'qux-2', name: 'qux' },
],
},
options
)
).toEqual({
index: 2,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-1', name: 'baz', params: { answer: 42 } },
{ key: 'qux-2', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 4,
routes: [],
},
options
)
).toEqual({
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
expect(
router.getRehydratedState(
{
index: 1,
routeKeyHistory: ['bar-test', 'qux-test', 'foo-test'],
routes: [],
},
options
)
).toEqual({
index: 1,
key: 'tab-test',
routeKeyHistory: ['bar-test', 'qux-test'],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
});
});
it("doesn't rehydrate state if it's not stale", () => {
const router = TabRouter({});
const state = {
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false as const,
};
expect(
router.getRehydratedState(state, {
routeNames: [],
routeParamList: {},
})
).toBe(state);
});
it('gets state on route names change', () => {
const router = TabRouter({});
expect(
router.getStateForRouteNamesChange(
{
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
stale: false,
},
{
routeNames: ['qux', 'baz', 'foo', 'fiz'],
routeParamList: {
qux: { name: 'John' },
fiz: { fruit: 'apple' },
},
}
)
).toEqual({
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['qux', 'baz', 'foo', 'fiz'],
routes: [
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'foo-test', name: 'foo' },
{ key: 'fiz-test', name: 'fiz', params: { fruit: 'apple' } },
],
stale: false,
});
});
it('handles navigate action', () => {
const router = TabRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz-1', name: 'baz' }, { key: 'bar-1', name: 'bar' }],
},
CommonActions.navigate({ key: 'bar-1', params: { answer: 42 } })
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: ['bar-1'],
routes: [
{ key: 'baz-1', name: 'baz' },
{ key: 'bar-1', name: 'bar', params: { answer: 42 } },
],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('baz', { answer: 42 })
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: ['bar'],
routes: [
{ key: 'baz', name: 'baz', params: { answer: 42 } },
{ key: 'bar', name: 'bar' },
],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.navigate('non-existent')
)
).toBe(null);
});
it('handles jump to action', () => {
const router = TabRouter({});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
TabActions.jumpTo('bar')
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: ['baz'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
});
it('handles back action with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' });
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: ['bar'],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toBe(null);
});
it('handles back action with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' });
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toEqual({
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toBe(null);
});
it('handles back action with backBehavior: initialRoute', () => {
const router = TabRouter({
backBehavior: 'initialRoute',
initialRouteName: 'bar',
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toEqual({
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
});
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 1,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toBe(null);
});
it('handles back action with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' });
expect(
router.getStateForAction(
{
stale: false,
key: 'root',
index: 0,
routeNames: ['baz', 'bar'],
routeKeyHistory: [],
routes: [{ key: 'baz', name: 'baz' }, { key: 'bar', name: 'bar' }],
},
CommonActions.goBack()
)
).toEqual(null);
});
it('updates route key history on navigate and jump to', () => {
const router = TabRouter({ backBehavior: 'history' });
let state: TabNavigationState = {
index: 1,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz', params: { answer: 42 } },
{ key: 'qux-0', name: 'qux', params: { name: 'Jane' } },
],
stale: false as const,
};
expect(state.routeKeyHistory).toEqual([]);
state = router.getStateForAction(
state,
TabActions.jumpTo('qux')
) as TabNavigationState;
expect(state.routeKeyHistory).toEqual(['baz-0']);
state = router.getStateForAction(
state,
CommonActions.navigate('bar')
) as TabNavigationState;
expect(state.routeKeyHistory).toEqual(['baz-0', 'qux-0']);
state = router.getStateForAction(
state,
TabActions.jumpTo('baz')
) as TabNavigationState;
expect(state.routeKeyHistory).toEqual(['qux-0', 'bar-0']);
state = router.getStateForAction(
state,
CommonActions.goBack()
) as TabNavigationState;
expect(state.routeKeyHistory).toEqual(['qux-0']);
state = router.getStateForAction(
state,
CommonActions.goBack()
) as TabNavigationState;
expect(state.routeKeyHistory).toEqual([]);
});
it('updates route key history on focus change', () => {
const router = TabRouter({ backBehavior: 'history' });
const state = {
index: 0,
key: 'tab-test',
routeKeyHistory: [],
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz', params: { answer: 42 } },
{ key: 'qux-0', name: 'qux', params: { name: 'Jane' } },
],
stale: false as const,
};
expect(router.getStateForRouteFocus(state, 'bar-0').routeKeyHistory).toEqual(
[]
);
expect(router.getStateForRouteFocus(state, 'baz-0').routeKeyHistory).toEqual([
'bar-0',
]);
});

View File

@@ -6,7 +6,7 @@
"react-native", "react-native",
"react-navigation" "react-navigation"
], ],
"version": "5.0.0-alpha.2", "version": "5.0.0-alpha.7",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -16,7 +16,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/routers/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"

View File

@@ -11,7 +11,7 @@ import {
export type StackActionType = export type StackActionType =
| { | {
type: 'PUSH'; type: 'PUSH';
payload: { name: string; params?: object }; payload: { name: string; key?: string | undefined; params?: object };
source?: string; source?: string;
target?: string; target?: string;
} }
@@ -114,11 +114,25 @@ export default function StackRouter(options: StackRouterOptions) {
}; };
}, },
getStateForRouteNamesChange(state, { routeNames }) { getStateForRouteNamesChange(state, { routeNames, routeParamList }) {
const routes = state.routes.filter(route => const routes = state.routes.filter(route =>
routeNames.includes(route.name) routeNames.includes(route.name)
); );
if (routes.length === 0) {
const initialRouteName =
options.initialRouteName !== undefined &&
routeNames.includes(options.initialRouteName)
? options.initialRouteName
: routeNames[0];
routes.push({
key: `${initialRouteName}-${shortid()}`,
name: initialRouteName,
params: routeParamList[initialRouteName],
});
}
return { return {
...state, ...state,
routeNames, routeNames,
@@ -151,7 +165,10 @@ export default function StackRouter(options: StackRouterOptions) {
routes: [ routes: [
...state.routes, ...state.routes,
{ {
key: `${action.payload.name}-${shortid()}`, key:
action.payload.key === undefined
? `${action.payload.name}-${shortid()}`
: action.payload.key,
name: action.payload.name, name: action.payload.name,
params: action.payload.params, params: action.payload.params,
}, },
@@ -199,14 +216,16 @@ export default function StackRouter(options: StackRouterOptions) {
let index = -1; let index = -1;
if ( if (
state.routes[state.index].name === action.payload.name || (state.routes[state.index].name === action.payload.name &&
action.payload.key === undefined) ||
state.routes[state.index].key === action.payload.key state.routes[state.index].key === action.payload.key
) { ) {
index = state.index; index = state.index;
} else { } else {
for (let i = state.routes.length - 1; i >= 0; i--) { for (let i = state.routes.length - 1; i >= 0; i--) {
if ( if (
state.routes[i].name === action.payload.name || (state.routes[i].name === action.payload.name &&
action.payload.key === undefined) ||
state.routes[i].key === action.payload.key state.routes[i].key === action.payload.key
) { ) {
index = i; index = i;
@@ -215,7 +234,11 @@ export default function StackRouter(options: StackRouterOptions) {
} }
} }
if (index === -1 && action.payload.key) { if (
index === -1 &&
action.payload.key &&
action.payload.name === undefined
) {
return null; return null;
} }
@@ -223,6 +246,7 @@ export default function StackRouter(options: StackRouterOptions) {
return router.getStateForAction(state, { return router.getStateForAction(state, {
type: 'PUSH', type: 'PUSH',
payload: { payload: {
key: action.payload.key,
name: action.payload.name, name: action.payload.name,
params: action.payload.params, params: action.payload.params,
}, },

View File

@@ -3,6 +3,89 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.0.0-alpha.10](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.9...@react-navigation/stack@5.0.0-alpha.10) (2019-08-31)
**Note:** Version bump only for package @react-navigation/stack
# [5.0.0-alpha.9](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.8...@react-navigation/stack@5.0.0-alpha.9) (2019-08-30)
### Bug Fixes
* change interpolated style when idle to avoid messing up reanimated ([3ad2e6e](https://github.com/react-navigation/navigation-ex/commit/3ad2e6e))
* properly set animated node on gestureEnabled change ([6a8242c](https://github.com/react-navigation/navigation-ex/commit/6a8242c))
# [5.0.0-alpha.8](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.7...@react-navigation/stack@5.0.0-alpha.8) (2019-08-29)
### Bug Fixes
* allow making params optional. fixes [#80](https://github.com/react-navigation/navigation-ex/issues/80) ([a9d4813](https://github.com/react-navigation/navigation-ex/commit/a9d4813))
* fix gestures not working in stack ([8c1acc3](https://github.com/react-navigation/navigation-ex/commit/8c1acc3))
# [5.0.0-alpha.7](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.6...@react-navigation/stack@5.0.0-alpha.7) (2019-08-28)
### Bug Fixes
* fix stack nested in tab always getting reset ([dead4e8](https://github.com/react-navigation/navigation-ex/commit/dead4e8))
# [5.0.0-alpha.6](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.5...@react-navigation/stack@5.0.0-alpha.6) (2019-08-28)
### Features
* disable gesture logic when no gesture stack ([38336b0](https://github.com/react-navigation/navigation-ex/commit/38336b0))
# [5.0.0-alpha.5](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.4...@react-navigation/stack@5.0.0-alpha.5) (2019-08-27)
### Bug Fixes
* link proper descriptor for StackView ([469ec31](https://github.com/react-navigation/navigation-ex/commit/469ec31))
* set correct pointer events when active prop changes ([1bbd6ac](https://github.com/react-navigation/navigation-ex/commit/1bbd6ac))
### Features
* add hook to scroll to top on tab press ([9e1104c](https://github.com/react-navigation/navigation-ex/commit/9e1104c))
* add memoization of spring for stack ([7990cf2](https://github.com/react-navigation/navigation-ex/commit/7990cf2))
# [5.0.0-alpha.4](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.3...@react-navigation/stack@5.0.0-alpha.4) (2019-08-22)
### Bug Fixes
* fix path to typescript definitions ([f182315](https://github.com/react-navigation/navigation-ex/commit/f182315))
# [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.2...@react-navigation/stack@5.0.0-alpha.3) (2019-08-22) # [5.0.0-alpha.3](https://github.com/react-navigation/navigation-ex/compare/@react-navigation/stack@5.0.0-alpha.2...@react-navigation/stack@5.0.0-alpha.3) (2019-08-22)
**Note:** Version bump only for package @react-navigation/stack **Note:** Version bump only for package @react-navigation/stack

View File

@@ -10,7 +10,7 @@
"android", "android",
"stack" "stack"
], ],
"version": "5.0.0-alpha.3", "version": "5.0.0-alpha.10",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,7 +20,7 @@
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/stack/src/index.d.ts",
"files": [ "files": [
"src", "src",
"lib" "lib"
@@ -33,7 +33,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.0.0-alpha.2", "@react-navigation/routers": "^5.0.0-alpha.7",
"react-native-safe-area-view": "^0.14.6" "react-native-safe-area-view": "^0.14.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -9,15 +9,16 @@ const { cond, add, multiply, interpolate } = Animated;
* Standard iOS-style slide in from the right. * Standard iOS-style slide in from the right.
*/ */
export function forHorizontalIOS({ export function forHorizontalIOS({
progress: { current, next }, current,
next,
layouts: { screen }, layouts: { screen },
}: CardInterpolationProps): CardInterpolatedStyle { }: CardInterpolationProps): CardInterpolatedStyle {
const translateFocused = interpolate(current, { const translateFocused = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [I18nManager.isRTL ? -screen.width : screen.width, 0], outputRange: [I18nManager.isRTL ? -screen.width : screen.width, 0],
}); });
const translateUnfocused = next const translateUnfocused = next
? interpolate(next, { ? interpolate(next.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [ outputRange: [
0, 0,
@@ -26,12 +27,12 @@ export function forHorizontalIOS({
}) })
: 0; : 0;
const overlayOpacity = interpolate(current, { const overlayOpacity = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 0.07], outputRange: [0, 0.07],
}); });
const shadowOpacity = interpolate(current, { const shadowOpacity = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, 0.3], outputRange: [0, 0.3],
}); });
@@ -54,10 +55,10 @@ export function forHorizontalIOS({
* Standard iOS-style slide in from the bottom (used for modals). * Standard iOS-style slide in from the bottom (used for modals).
*/ */
export function forVerticalIOS({ export function forVerticalIOS({
progress: { current }, current,
layouts: { screen }, layouts: { screen },
}: CardInterpolationProps): CardInterpolatedStyle { }: CardInterpolationProps): CardInterpolatedStyle {
const translateY = interpolate(current, { const translateY = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [screen.height, 0], outputRange: [screen.height, 0],
}); });
@@ -77,14 +78,15 @@ export function forVerticalIOS({
*/ */
export function forModalPresentationIOS({ export function forModalPresentationIOS({
index, index,
progress: { current, next }, current,
next,
layouts: { screen }, layouts: { screen },
}: CardInterpolationProps): CardInterpolatedStyle { }: CardInterpolationProps): CardInterpolatedStyle {
const topOffset = 10; const topOffset = 10;
const statusBarHeight = getStatusBarHeight(screen.width > screen.height); const statusBarHeight = getStatusBarHeight(screen.width > screen.height);
const aspectRatio = screen.height / screen.width; const aspectRatio = screen.height / screen.width;
const progress = add(current, next ? next : 0); const progress = add(current.progress, next ? next.progress : 0);
const translateY = interpolate(progress, { const translateY = interpolate(progress, {
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
@@ -129,19 +131,19 @@ export function forModalPresentationIOS({
* Standard Android-style fade in from the bottom for Android Oreo. * Standard Android-style fade in from the bottom for Android Oreo.
*/ */
export function forFadeFromBottomAndroid({ export function forFadeFromBottomAndroid({
progress: { current }, current,
layouts: { screen }, layouts: { screen },
closing, closing,
}: CardInterpolationProps): CardInterpolatedStyle { }: CardInterpolationProps): CardInterpolatedStyle {
const translateY = interpolate(current, { const translateY = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [multiply(screen.height, 0.08), 0], outputRange: [multiply(screen.height, 0.08), 0],
}); });
const opacity = cond( const opacity = cond(
closing, closing,
current, current.progress,
interpolate(current, { interpolate(current.progress, {
inputRange: [0, 0.5, 0.9, 1], inputRange: [0, 0.5, 0.9, 1],
outputRange: [0, 0.25, 0.7, 1], outputRange: [0, 0.25, 0.7, 1],
}) })
@@ -156,27 +158,28 @@ export function forFadeFromBottomAndroid({
} }
/** /**
* Standard Android-style wipe from the bottom for Android Pie. * Standard Android-style reveal from the bottom for Android Pie.
*/ */
export function forRevealFromBottomAndroid({ export function forRevealFromBottomAndroid({
progress: { current, next }, current,
next,
layouts: { screen }, layouts: { screen },
}: CardInterpolationProps): CardInterpolatedStyle { }: CardInterpolationProps): CardInterpolatedStyle {
const containerTranslateY = interpolate(current, { const containerTranslateY = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [screen.height, 0], outputRange: [screen.height, 0],
}); });
const cardTranslateYFocused = interpolate(current, { const cardTranslateYFocused = interpolate(current.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [multiply(screen.height, 95.9 / 100, -1), 0], outputRange: [multiply(screen.height, 95.9 / 100, -1), 0],
}); });
const cardTranslateYUnfocused = next const cardTranslateYUnfocused = next
? interpolate(next, { ? interpolate(next.progress, {
inputRange: [0, 1], inputRange: [0, 1],
outputRange: [0, multiply(screen.height, 2 / 100, -1)], outputRange: [0, multiply(screen.height, 2 / 100, -1)],
}) })
: 0; : 0;
const overlayOpacity = interpolate(current, { const overlayOpacity = interpolate(current.progress, {
inputRange: [0, 0.36, 1], inputRange: [0, 0.36, 1],
outputRange: [0, 0.1, 0.1], outputRange: [0, 0.1, 0.1],
}); });

View File

@@ -4,8 +4,12 @@ import { HeaderInterpolationProps, HeaderInterpolatedStyle } from '../types';
const { interpolate, add } = Animated; const { interpolate, add } = Animated;
/**
* Standard UIKit style animation for the header where the title fades into the back button label.
*/
export function forUIKit({ export function forUIKit({
progress: { current, next }, current,
next,
layouts, layouts,
}: HeaderInterpolationProps): HeaderInterpolatedStyle { }: HeaderInterpolationProps): HeaderInterpolatedStyle {
const defaultOffset = 100; const defaultOffset = 100;
@@ -27,7 +31,7 @@ export function forUIKit({
// The back title also animates in from this position // The back title also animates in from this position
const rightOffset = layouts.screen.width / 4; const rightOffset = layouts.screen.width / 4;
const progress = add(current, next ? next : 0); const progress = add(current.progress, next ? next.progress : 0);
return { return {
leftButtonStyle: { leftButtonStyle: {
@@ -85,10 +89,14 @@ export function forUIKit({
}; };
} }
/**
* Simple fade animation for the header elements.
*/
export function forFade({ export function forFade({
progress: { current, next }, current,
next,
}: HeaderInterpolationProps): HeaderInterpolatedStyle { }: HeaderInterpolationProps): HeaderInterpolatedStyle {
const progress = add(current, next ? next : 0); const progress = add(current.progress, next ? next.progress : 0);
const opacity = interpolate(progress, { const opacity = interpolate(progress, {
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: [0, 1, 0], outputRange: [0, 1, 0],
@@ -102,11 +110,15 @@ export function forFade({
}; };
} }
/**
* Simple translate animation to translate the header along with the sliding screen.
*/
export function forStatic({ export function forStatic({
progress: { current, next }, current,
next,
layouts: { screen }, layouts: { screen },
}: HeaderInterpolationProps): HeaderInterpolatedStyle { }: HeaderInterpolationProps): HeaderInterpolatedStyle {
const progress = add(current, next ? next : 0); const progress = add(current.progress, next ? next.progress : 0);
const translateX = interpolate(progress, { const translateX = interpolate(progress, {
inputRange: [0, 1, 2], inputRange: [0, 1, 2],
outputRange: I18nManager.isRTL outputRange: I18nManager.isRTL

View File

@@ -17,7 +17,9 @@ import { Platform } from 'react-native';
const ANDROID_VERSION_PIE = 28; const ANDROID_VERSION_PIE = 28;
// Standard iOS navigation transition /**
* Standard iOS navigation transition.
*/
export const SlideFromRightIOS: TransitionPreset = { export const SlideFromRightIOS: TransitionPreset = {
gestureDirection: 'horizontal', gestureDirection: 'horizontal',
transitionSpec: { transitionSpec: {
@@ -28,7 +30,9 @@ export const SlideFromRightIOS: TransitionPreset = {
headerStyleInterpolator: forFade, headerStyleInterpolator: forFade,
}; };
// Standard iOS navigation transition for modals /**
* Standard iOS navigation transition for modals.
*/
export const ModalSlideFromBottomIOS: TransitionPreset = { export const ModalSlideFromBottomIOS: TransitionPreset = {
gestureDirection: 'vertical', gestureDirection: 'vertical',
transitionSpec: { transitionSpec: {
@@ -39,7 +43,9 @@ export const ModalSlideFromBottomIOS: TransitionPreset = {
headerStyleInterpolator: forNoAnimation, headerStyleInterpolator: forNoAnimation,
}; };
// Standard iOS modal presentation style (introduced in iOS 13) /**
* Standard iOS modal presentation style (introduced in iOS 13).
*/
export const ModalPresentationIOS: TransitionPreset = { export const ModalPresentationIOS: TransitionPreset = {
gestureDirection: 'vertical', gestureDirection: 'vertical',
transitionSpec: { transitionSpec: {
@@ -50,7 +56,9 @@ export const ModalPresentationIOS: TransitionPreset = {
headerStyleInterpolator: forNoAnimation, headerStyleInterpolator: forNoAnimation,
}; };
// Standard Android navigation transition when opening or closing an Activity on Android < 9 /**
* Standard Android navigation transition when opening or closing an Activity on Android < 9 (Oreo).
*/
export const FadeFromBottomAndroid: TransitionPreset = { export const FadeFromBottomAndroid: TransitionPreset = {
gestureDirection: 'vertical', gestureDirection: 'vertical',
transitionSpec: { transitionSpec: {
@@ -61,7 +69,9 @@ export const FadeFromBottomAndroid: TransitionPreset = {
headerStyleInterpolator: forNoAnimation, headerStyleInterpolator: forNoAnimation,
}; };
// Standard Android navigation transition when opening or closing an Activity on Android >= 9 /**
* Standard Android navigation transition when opening or closing an Activity on Android >= 9 (Pie).
*/
export const RevealFromBottomAndroid: TransitionPreset = { export const RevealFromBottomAndroid: TransitionPreset = {
gestureDirection: 'vertical', gestureDirection: 'vertical',
transitionSpec: { transitionSpec: {
@@ -72,6 +82,9 @@ export const RevealFromBottomAndroid: TransitionPreset = {
headerStyleInterpolator: forNoAnimation, headerStyleInterpolator: forNoAnimation,
}; };
/**
* Default navigation transition for the current platform.
*/
export const DefaultTransition = Platform.select({ export const DefaultTransition = Platform.select({
ios: SlideFromRightIOS, ios: SlideFromRightIOS,
default: default:
@@ -80,6 +93,9 @@ export const DefaultTransition = Platform.select({
: RevealFromBottomAndroid, : RevealFromBottomAndroid,
}); });
/**
* Default modal transition for the current platform.
*/
export const ModalTransition = Platform.select({ export const ModalTransition = Platform.select({
ios: ModalSlideFromBottomIOS, ios: ModalSlideFromBottomIOS,
default: DefaultTransition, default: DefaultTransition,

View File

@@ -1,9 +1,11 @@
import { Easing } from 'react-native-reanimated'; import { Easing } from 'react-native-reanimated';
import { TransitionSpec } from '../types'; import { TransitionSpec } from '../types';
// These are the exact values from UINavigationController's animation configuration /**
* Exact values from UINavigationController's animation configuration.
*/
export const TransitionIOSSpec: TransitionSpec = { export const TransitionIOSSpec: TransitionSpec = {
timing: 'spring', animation: 'spring',
config: { config: {
stiffness: 1000, stiffness: 1000,
damping: 500, damping: 500,
@@ -14,27 +16,36 @@ export const TransitionIOSSpec: TransitionSpec = {
}, },
}; };
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml /**
* Configuration for activity open animation from Android Nougat.
* See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
*/
export const FadeInFromBottomAndroidSpec: TransitionSpec = { export const FadeInFromBottomAndroidSpec: TransitionSpec = {
timing: 'timing', animation: 'timing',
config: { config: {
duration: 350, duration: 350,
easing: Easing.out(Easing.poly(5)), easing: Easing.out(Easing.poly(5)),
}, },
}; };
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml /**
* Configuration for activity close animation from Android Nougat.
* See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
*/
export const FadeOutToBottomAndroidSpec: TransitionSpec = { export const FadeOutToBottomAndroidSpec: TransitionSpec = {
timing: 'timing', animation: 'timing',
config: { config: {
duration: 150, duration: 150,
easing: Easing.in(Easing.linear), easing: Easing.in(Easing.linear),
}, },
}; };
// See http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml /**
* Approximate configuration for activity open animation from Android Pie.
* See http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
*/
export const RevealFromBottomAndroidSpec: TransitionSpec = { export const RevealFromBottomAndroidSpec: TransitionSpec = {
timing: 'timing', animation: 'timing',
config: { config: {
duration: 425, duration: 425,
// This is super rough approximation of the path used for the curve by android // This is super rough approximation of the path used for the curve by android

View File

@@ -13,7 +13,11 @@ import {
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import KeyboardManager from '../views/KeyboardManager'; import KeyboardManager from '../views/KeyboardManager';
import StackView from '../views/Stack/StackView'; import StackView from '../views/Stack/StackView';
import { StackNavigationConfig, StackNavigationOptions } from '../types'; import {
StackNavigationConfig,
StackNavigationOptions,
StackNavigationEventMap,
} from '../types';
type Props = DefaultNavigatorOptions<StackNavigationOptions> & type Props = DefaultNavigatorOptions<StackNavigationOptions> &
StackRouterOptions & StackRouterOptions &
@@ -28,8 +32,9 @@ function StackNavigator({
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState, StackNavigationState,
StackRouterOptions,
StackNavigationOptions, StackNavigationOptions,
StackRouterOptions StackNavigationEventMap
>(StackRouter, { >(StackRouter, {
initialRouteName, initialRouteName,
children, children,
@@ -39,13 +44,21 @@ function StackNavigator({
React.useEffect( React.useEffect(
() => () =>
navigation.addListener && navigation.addListener &&
navigation.addListener('refocus', (e: EventArg<'refocus', undefined>) => { navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
if (state.index > 0 && !e.defaultPrevented) { const isFocused = navigation.isFocused();
navigation.dispatch({
...StackActions.popToTop(), // Run the operation in the next frame so we're sure all listeners have been run
target: state.key, // This is necessary to know if preventDefault() has been called
}); requestAnimationFrame(() => {
} if (state.index > 0 && isFocused && !e.defaultPrevented) {
// When user taps on already focused tab and we're inside the tab,
// reset the stack to replicate native behaviour
navigation.dispatch({
...StackActions.popToTop(),
target: state.key,
});
}
});
}), }),
[navigation, state.index, state.key] [navigation, state.index, state.key]
); );

View File

@@ -10,6 +10,7 @@ import {
ParamListBase, ParamListBase,
Descriptor, Descriptor,
Route, Route,
NavigationHelpers,
} from '@react-navigation/core'; } from '@react-navigation/core';
import { StackNavigationState } from '@react-navigation/routers'; import { StackNavigationState } from '@react-navigation/routers';
@@ -24,6 +25,11 @@ export type StackNavigationEventMap = {
transitionEnd: { closing: boolean }; transitionEnd: { closing: boolean };
}; };
export type StackNavigationHelpers = NavigationHelpers<
ParamListBase,
StackNavigationEventMap
>;
export type StackNavigationProp< export type StackNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string RouteName extends keyof ParamList = string
@@ -41,8 +47,8 @@ export type StackNavigationProp<
* @param [params] Params object for the route. * @param [params] Params object for the route.
*/ */
push<RouteName extends keyof ParamList>( push<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends void ...args: ParamList[RouteName] extends (undefined | any)
? [RouteName] ? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]] : [RouteName, ParamList[RouteName]]
): void; ): void;
@@ -399,28 +405,33 @@ export type TimingConfig = {
}; };
export type TransitionSpec = export type TransitionSpec =
| { timing: 'spring'; config: SpringConfig } | { animation: 'spring'; config: SpringConfig }
| { timing: 'timing'; config: TimingConfig }; | { animation: 'timing'; config: TimingConfig };
export type CardInterpolationProps = { export type CardInterpolationProps = {
/**
* Values for the current screen.
*/
current: {
/**
* Animated node representing the progress value of the current screen.
*/
progress: Animated.Node<number>;
};
/**
* Values for the current screen the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: {
/**
* Animated node representing the progress value of the next screen.
*/
progress: Animated.Node<number>;
};
/** /**
* The index of the card in the stack. * The index of the card in the stack.
*/ */
index: number; index: number;
/**
* Animated nodes representing the progress of the animation.
*/
progress: {
/**
* Progress value of the current screen.
*/
current: Animated.Node<number>;
/**
* Progress value for the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: Animated.Node<number>;
};
/** /**
* Animated node representing whether the card is closing. * Animated node representing whether the card is closing.
*/ */
@@ -461,18 +472,23 @@ export type CardStyleInterpolator = (
export type HeaderInterpolationProps = { export type HeaderInterpolationProps = {
/** /**
* Animated nodes representing the progress of the animation. * Values for the current screen (the screen which owns this header).
*/ */
progress: { current: {
/** /**
* Progress value of the current screen (the screen which owns this header). * Animated node representing the progress value of the current screen.
*/ */
current: Animated.Node<number>; progress: Animated.Node<number>;
};
/**
* Values for the current screen the screen after this one in the stack.
* This can be `undefined` in case the screen animating is the last one.
*/
next?: {
/** /**
* Progress value for the screen after this one in the stack. * Animated node representing the progress value of the next screen.
* This can be `undefined` in case the screen animating is the last one.
*/ */
next?: Animated.Node<number>; progress: Animated.Node<number>;
}; };
/** /**
* Layout measurements for various items we use for animation. * Layout measurements for various items we use for animation.

View File

@@ -121,10 +121,8 @@ export default class HeaderSegment extends React.Component<Props, State> {
leftLabelLayout: Layout | undefined leftLabelLayout: Layout | undefined
) => ) =>
styleInterpolator({ styleInterpolator({
progress: { current: { progress: current },
current, next: next && { progress: next },
next,
},
layouts: { layouts: {
screen: layout, screen: layout,
title: titleLayout, title: titleLayout,

View File

@@ -94,6 +94,86 @@ const {
Value, Value,
} = Animated; } = Animated;
// We need to be prepared for both version of reanimated. With and w/out proc
let memoizedSpring = spring;
// @ts-ignore
if (Animated.proc) {
// @ts-ignore
const springHelper = Animated.proc(
(
finished: Animated.Value<number>,
velocity: Animated.Value<number>,
position: Animated.Value<number>,
time: Animated.Value<number>,
prevPosition: Animated.Value<number>,
toValue: Animated.Adaptable<number>,
damping: Animated.Adaptable<number>,
mass: Animated.Adaptable<number>,
stiffness: Animated.Adaptable<number>,
overshootClamping: Animated.Adaptable<number>,
restSpeedThreshold: Animated.Adaptable<number>,
restDisplacementThreshold: Animated.Adaptable<number>,
clock: Animated.Clock
) =>
spring(
clock,
{
finished,
velocity,
position,
time,
// @ts-ignore
prevPosition,
},
{
toValue,
damping,
mass,
stiffness,
overshootClamping,
restDisplacementThreshold,
restSpeedThreshold,
}
)
);
// @ts-ignore
memoizedSpring = function(
clock: Animated.Clock,
state: {
finished: Animated.Value<number>;
velocity: Animated.Value<number>;
position: Animated.Value<number>;
time: Animated.Value<number>;
},
config: {
toValue: Animated.Adaptable<number>;
damping: Animated.Adaptable<number>;
mass: Animated.Adaptable<number>;
stiffness: Animated.Adaptable<number>;
overshootClamping: Animated.Adaptable<boolean>;
restSpeedThreshold: Animated.Adaptable<number>;
restDisplacementThreshold: Animated.Adaptable<number>;
}
) {
return springHelper(
state.finished,
state.velocity,
state.position,
state.time,
new Value(0),
config.toValue,
config.damping,
config.mass,
config.stiffness,
config.overshootClamping,
config.restSpeedThreshold,
config.restDisplacementThreshold,
clock
);
};
}
export default class Card extends React.Component<Props> { export default class Card extends React.Component<Props> {
static defaultProps = { static defaultProps = {
overlayEnabled: Platform.OS !== 'ios', overlayEnabled: Platform.OS !== 'ios',
@@ -122,7 +202,11 @@ export default class Card extends React.Component<Props> {
} }
if (closing !== prevProps.closing) { if (closing !== prevProps.closing) {
this.isClosing.setValue(closing ? TRUE : FALSE); // If the style updates during render, setting the value here doesn't work
// We need to defer it a bit so the animation starts properly
requestAnimationFrame(() =>
this.isClosing.setValue(closing ? TRUE : FALSE)
);
} }
} }
@@ -187,6 +271,17 @@ export default class Card extends React.Component<Props> {
finished: new Value(FALSE), finished: new Value(FALSE),
}; };
private handleTransitionEnd = () => {
this.isRunningAnimation = false;
this.interpolatedStyle = this.getInterpolatedStyle(
this.props.styleInterpolator,
this.props.index,
this.props.current,
this.props.next,
this.props.layout
);
};
private runTransition = (isVisible: Binary | Animated.Node<number>) => { private runTransition = (isVisible: Binary | Animated.Node<number>) => {
const { open: openingSpec, close: closingSpec } = this.props.transitionSpec; const { open: openingSpec, close: closingSpec } = this.props.transitionSpec;
@@ -218,8 +313,8 @@ export default class Card extends React.Component<Props> {
]), ]),
cond( cond(
eq(isVisible, 1), eq(isVisible, 1),
openingSpec.timing === 'spring' openingSpec.animation === 'spring'
? spring( ? memoizedSpring(
this.clock, this.clock,
{ ...this.transitionState, velocity: this.transitionVelocity }, { ...this.transitionState, velocity: this.transitionVelocity },
{ ...openingSpec.config, toValue: this.toValue } { ...openingSpec.config, toValue: this.toValue }
@@ -229,8 +324,8 @@ export default class Card extends React.Component<Props> {
{ ...this.transitionState, frameTime: this.frameTime }, { ...this.transitionState, frameTime: this.frameTime },
{ ...openingSpec.config, toValue: this.toValue } { ...openingSpec.config, toValue: this.toValue }
), ),
closingSpec.timing === 'spring' closingSpec.animation === 'spring'
? spring( ? memoizedSpring(
this.clock, this.clock,
{ ...this.transitionState, velocity: this.transitionVelocity }, { ...this.transitionState, velocity: this.transitionVelocity },
{ ...closingSpec.config, toValue: this.toValue } { ...closingSpec.config, toValue: this.toValue }
@@ -251,7 +346,9 @@ export default class Card extends React.Component<Props> {
call([this.isVisible], ([value]: ReadonlyArray<Binary>) => { call([this.isVisible], ([value]: ReadonlyArray<Binary>) => {
const isOpen = Boolean(value); const isOpen = Boolean(value);
const { onOpen, onClose } = this.props; const { onOpen, onClose } = this.props;
this.isRunningAnimation = false;
this.handleTransitionEnd();
if (isOpen) { if (isOpen) {
onOpen(true); onOpen(true);
} else { } else {
@@ -277,7 +374,7 @@ export default class Card extends React.Component<Props> {
cond(neq(this.nextIsVisible, UNSET), [ cond(neq(this.nextIsVisible, UNSET), [
// Stop any running animations // Stop any running animations
cond(clockRunning(this.clock), [ cond(clockRunning(this.clock), [
call([], () => (this.isRunningAnimation = false)), call([], this.handleTransitionEnd),
stopClock(this.clock), stopClock(this.clock),
]), ]),
set(this.gesture, 0), set(this.gesture, 0),
@@ -286,6 +383,15 @@ export default class Card extends React.Component<Props> {
set(this.nextIsVisible, UNSET), set(this.nextIsVisible, UNSET),
]) ])
), ),
onChange(
this.isVisible,
call([this.isVisible], ([isVisible]) => (this.isVisibleValue = isVisible))
),
]);
private execNoGesture = this.runTransition(this.isVisible);
private execWithGesture = block([
onChange( onChange(
this.isSwiping, this.isSwiping,
call( call(
@@ -374,10 +480,6 @@ export default class Card extends React.Component<Props> {
), ),
] ]
), ),
onChange(
this.isVisible,
call([this.isVisible], ([isVisible]) => (this.isVisibleValue = isVisible))
),
]); ]);
private handleGestureEventHorizontal = Animated.event([ private handleGestureEventHorizontal = Animated.event([
@@ -415,10 +517,8 @@ export default class Card extends React.Component<Props> {
) => ) =>
styleInterpolator({ styleInterpolator({
index, index,
progress: { current: { progress: current },
current, next: next && { progress: next },
next,
},
closing: this.isClosing, closing: this.isClosing,
layouts: { layouts: {
screen: layout, screen: layout,
@@ -426,6 +526,18 @@ export default class Card extends React.Component<Props> {
}) })
); );
// Keep track of the style in a property to avoid changing the animated node when deps change
// The style shouldn't change in the middle of the animation and should refer to what was there at the start of it
// Which will be the last value when just before the render which started the animation
// We need to make sure to update this when the running animation ends
private interpolatedStyle = this.getInterpolatedStyle(
this.props.styleInterpolator,
this.props.index,
this.props.current,
this.props.next,
this.props.layout
);
private gestureActivationCriteria() { private gestureActivationCriteria() {
const { layout, gestureDirection, gestureResponseDistance } = this.props; const { layout, gestureDirection, gestureResponseDistance } = this.props;
@@ -466,45 +578,54 @@ export default class Card extends React.Component<Props> {
render() { render() {
const { const {
index,
active, active,
transparent, transparent,
layout, styleInterpolator,
index,
current, current,
next, next,
layout,
overlayEnabled, overlayEnabled,
shadowEnabled, shadowEnabled,
gestureEnabled, gestureEnabled,
gestureDirection, gestureDirection,
children, children,
styleInterpolator,
containerStyle: customContainerStyle, containerStyle: customContainerStyle,
contentStyle, contentStyle,
...rest ...rest
} = this.props; } = this.props;
if (!this.isRunningAnimation) {
this.interpolatedStyle = this.getInterpolatedStyle(
styleInterpolator,
index,
current,
next,
layout
);
}
const { const {
containerStyle, containerStyle,
cardStyle, cardStyle,
overlayStyle, overlayStyle,
shadowStyle, shadowStyle,
} = this.getInterpolatedStyle( } = this.interpolatedStyle;
styleInterpolator,
index,
current,
next,
layout
);
const handleGestureEvent = const handleGestureEvent = gestureEnabled
gestureDirection === 'vertical' ? gestureDirection === 'vertical'
? this.handleGestureEventVertical ? this.handleGestureEventVertical
: this.handleGestureEventHorizontal; : this.handleGestureEventHorizontal
: undefined;
return ( return (
<StackGestureContext.Provider value={this.gestureRef}> <StackGestureContext.Provider value={this.gestureRef}>
<View pointerEvents="box-none" {...rest}> <View pointerEvents="box-none" {...rest}>
<Animated.Code exec={this.exec} /> <Animated.Code exec={this.exec} />
<Animated.Code
key={gestureEnabled ? 'gesture-code' : 'no-gesture-code'}
exec={gestureEnabled ? this.execWithGesture : this.execNoGesture}
/>
{overlayEnabled && overlayStyle ? ( {overlayEnabled && overlayStyle ? (
<Animated.View <Animated.View
pointerEvents="none" pointerEvents="none"

View File

@@ -23,6 +23,13 @@ const { block, greaterThan, cond, set, call, onChange } = Animated;
* whenever position changes. * whenever position changes.
*/ */
export default class PointerEventsView extends React.Component<Props> { export default class PointerEventsView extends React.Component<Props> {
componentDidUpdate(prevProps: Props) {
if (this.props.active !== prevProps.active) {
this.pointerEventsEnabled.setValue(this.props.active ? TRUE : FALSE);
this.setPointerEventsEnabled(this.props.active);
}
}
private pointerEventsEnabled = new Animated.Value<Binary>( private pointerEventsEnabled = new Animated.Value<Binary>(
this.props.active ? TRUE : FALSE this.props.active ? TRUE : FALSE
); );
@@ -40,13 +47,17 @@ export default class PointerEventsView extends React.Component<Props> {
onChange( onChange(
this.pointerEventsEnabled, this.pointerEventsEnabled,
call([this.pointerEventsEnabled], ([value]) => { call([this.pointerEventsEnabled], ([value]) => {
const pointerEvents = this.props.active && value ? 'box-none' : 'none'; this.setPointerEventsEnabled(Boolean(this.props.active && value));
this.root && this.root.setNativeProps({ pointerEvents });
}) })
), ),
]); ]);
private setPointerEventsEnabled = (enabled: boolean) => {
const pointerEvents = enabled ? 'box-none' : 'none';
this.root && this.root.setNativeProps({ pointerEvents });
};
private root: View | null = null; private root: View | null = null;
render() { render() {

View File

@@ -10,11 +10,7 @@ import {
import Animated from 'react-native-reanimated'; import Animated from 'react-native-reanimated';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import * as Screens from 'react-native-screens'; // Import with * as to prevent getters being called import * as Screens from 'react-native-screens'; // Import with * as to prevent getters being called
import { import { Route } from '@react-navigation/core';
Route,
NavigationHelpers,
ParamListBase,
} from '@react-navigation/core';
import { StackNavigationState } from '@react-navigation/routers'; import { StackNavigationState } from '@react-navigation/routers';
import { getDefaultHeaderHeight } from '../Header/HeaderSegment'; import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
@@ -31,6 +27,7 @@ import {
HeaderScene, HeaderScene,
StackDescriptorMap, StackDescriptorMap,
StackNavigationOptions, StackNavigationOptions,
StackNavigationHelpers,
} from '../../types'; } from '../../types';
type ProgressValues = { type ProgressValues = {
@@ -40,7 +37,7 @@ type ProgressValues = {
type Props = { type Props = {
mode: 'card' | 'modal'; mode: 'card' | 'modal';
state: StackNavigationState; state: StackNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: StackNavigationHelpers;
descriptors: StackDescriptorMap; descriptors: StackDescriptorMap;
routes: Route<string>[]; routes: Route<string>[];
openingRoutes: string[]; openingRoutes: string[];
@@ -170,11 +167,14 @@ export default class Stack extends React.Component<Props, State> {
: undefined; : undefined;
const next = nextRoute ? progress[nextRoute.key] : undefined; const next = nextRoute ? progress[nextRoute.key] : undefined;
const oldScene = state.scenes[index];
const scene = { const scene = {
route, route,
previous: previousRoute, previous: previousRoute,
descriptor: descriptor:
props.descriptors[route.key] || state.descriptors[route.key], props.descriptors[route.key] ||
state.descriptors[route.key] ||
(oldScene ? oldScene.descriptor : { options: {} }),
progress: { progress: {
current, current,
next, next,
@@ -182,8 +182,6 @@ export default class Stack extends React.Component<Props, State> {
}, },
}; };
const oldScene = state.scenes[index];
if ( if (
oldScene && oldScene &&
scene.route === oldScene.route && scene.route === oldScene.route &&
@@ -331,8 +329,8 @@ export default class Stack extends React.Component<Props, State> {
{routes.map((route, index, self) => { {routes.map((route, index, self) => {
const focused = focusedRoute.key === route.key; const focused = focusedRoute.key === route.key;
const current = progress[route.key]; const current = progress[route.key];
const descriptor = descriptors[route.key];
const scene = scenes[index]; const scene = scenes[index];
const descriptor = scene.descriptor;
const next = self[index + 1] const next = self[index + 1]
? progress[self[index + 1].key] ? progress[self[index + 1].key]
: ANIMATED_ONE; : ANIMATED_ONE;

View File

@@ -2,14 +2,16 @@ import * as React from 'react';
import { StyleSheet, Platform, StyleProp, ViewStyle } from 'react-native'; import { StyleSheet, Platform, StyleProp, ViewStyle } from 'react-native';
import Animated from 'react-native-reanimated'; import Animated from 'react-native-reanimated';
import { StackNavigationState } from '@react-navigation/routers'; import { StackNavigationState } from '@react-navigation/routers';
import { import { Route } from '@react-navigation/core';
Route,
NavigationHelpers,
ParamListBase,
} from '@react-navigation/core';
import { Props as HeaderContainerProps } from '../Header/HeaderContainer'; import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import Card from './Card'; import Card from './Card';
import { HeaderScene, Layout, HeaderMode, TransitionPreset } from '../../types'; import {
StackNavigationHelpers,
HeaderScene,
Layout,
HeaderMode,
TransitionPreset,
} from '../../types';
type Props = TransitionPreset & { type Props = TransitionPreset & {
index: number; index: number;
@@ -21,7 +23,7 @@ type Props = TransitionPreset & {
previousScene?: HeaderScene<Route<string>>; previousScene?: HeaderScene<Route<string>>;
scene: HeaderScene<Route<string>>; scene: HeaderScene<Route<string>>;
state: StackNavigationState; state: StackNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: StackNavigationHelpers;
cardTransparent?: boolean; cardTransparent?: boolean;
cardOverlayEnabled?: boolean; cardOverlayEnabled?: boolean;
cardShadowEnabled?: boolean; cardShadowEnabled?: boolean;

View File

@@ -1,21 +1,21 @@
import * as React from 'react'; import * as React from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { import { Route } from '@react-navigation/core';
ParamListBase,
Route,
NavigationHelpers,
} from '@react-navigation/core';
import { StackActions, StackNavigationState } from '@react-navigation/routers'; import { StackActions, StackNavigationState } from '@react-navigation/routers';
import Stack from './Stack'; import Stack from './Stack';
import HeaderContainer, { import HeaderContainer, {
Props as HeaderContainerProps, Props as HeaderContainerProps,
} from '../Header/HeaderContainer'; } from '../Header/HeaderContainer';
import { StackNavigationConfig, StackDescriptorMap } from '../../types'; import {
StackNavigationHelpers,
StackNavigationConfig,
StackDescriptorMap,
} from '../../types';
type Props = StackNavigationConfig & { type Props = StackNavigationConfig & {
state: StackNavigationState; state: StackNavigationState;
navigation: NavigationHelpers<ParamListBase>; navigation: StackNavigationHelpers;
descriptors: StackDescriptorMap; descriptors: StackDescriptorMap;
onPageChangeStart?: () => void; onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void; onPageChangeConfirm?: () => void;

View File

@@ -2,7 +2,10 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@react-navigation/*": ["./packages/*/src", "./packages/*/lib/typescript"], "@react-navigation/*": [
"./packages/*/src",
"./packages/*/lib/typescript"
],
"use-subscription": ["./typings/use-subscription.d"] "use-subscription": ["./typings/use-subscription.d"]
}, },
"allowUnreachableCode": false, "allowUnreachableCode": false,