Compare commits

..

38 Commits

Author SHA1 Message Date
Satyajit Sahoo
bd9f0ad5f6 chore: publish
- @react-navigation/bottom-tabs@5.10.3
 - @react-navigation/compat@5.3.3
 - @react-navigation/core@5.13.3
 - @react-navigation/devtools@5.1.11
 - @react-navigation/drawer@5.10.3
 - @react-navigation/material-bottom-tabs@5.3.3
 - @react-navigation/material-top-tabs@5.3.3
 - @react-navigation/native@5.8.3
 - @react-navigation/stack@5.12.0
2020-11-03 06:31:58 +01:00
Satyajit Sahoo
c326c106f9 feat: add a headerBackAccessibilityLabel option in stack
closes #9016
2020-11-03 06:22:51 +01:00
Satyajit Sahoo
52451d1109 fix: make sure that invalid linking config doesn't work if app is open 2020-11-03 06:15:44 +01:00
Satyajit Sahoo
0945689b70 fix: handle navigating to same screen again for nested screens 2020-11-03 05:51:52 +01:00
Satyajit Sahoo
37b9454f3e chore: publish
- @react-navigation/bottom-tabs@5.10.2
 - @react-navigation/compat@5.3.2
 - @react-navigation/core@5.13.2
 - @react-navigation/devtools@5.1.10
 - @react-navigation/drawer@5.10.2
 - @react-navigation/material-bottom-tabs@5.3.2
 - @react-navigation/material-top-tabs@5.3.2
 - @react-navigation/native@5.8.2
 - @react-navigation/stack@5.11.1
2020-10-30 13:42:48 +01:00
Satyajit Sahoo
fb7ac960c8 fix: trim routes if an index is specified in state 2020-10-30 13:41:28 +01:00
Satyajit Sahoo
e8515f9cd9 fix: fix params from for the root screen when creating action
closes #9006
2020-10-30 13:26:52 +01:00
Satyajit Sahoo
5eee804e7f chore: publish
- @react-navigation/bottom-tabs@5.10.1
 - @react-navigation/compat@5.3.1
 - @react-navigation/core@5.13.1
 - @react-navigation/devtools@5.1.9
 - @react-navigation/drawer@5.10.1
 - @react-navigation/material-bottom-tabs@5.3.1
 - @react-navigation/material-top-tabs@5.3.1
 - @react-navigation/native@5.8.1
 - @react-navigation/routers@5.5.1
 - @react-navigation/stack@5.11.0
2020-10-28 22:21:16 +01:00
Satyajit Sahoo
45dbe5c40e feat: enable react-native-screens in Stack by default on iOS 2020-10-28 22:15:37 +01:00
Satyajit Sahoo
d26bcc057e fix: improve types for route prop in screenOptions 2020-10-28 22:06:52 +01:00
Satyajit Sahoo
836ca4482e chore: fix loading indicator not visible in auth example 2020-10-27 01:37:53 +01:00
Satyajit Sahoo
fdd549a536 chore: migrate example to community async-storage 2020-10-27 00:44:30 +01:00
Satyajit Sahoo
128bbbe62a chore: add ability to manually run expo publish workflow 2020-10-25 02:05:18 +02:00
Satyajit Sahoo
a186b445b4 chore: fix slug for example app 2020-10-25 01:58:38 +02:00
Satyajit Sahoo
ac11a3bded chore: publish
- @react-navigation/bottom-tabs@5.10.0
 - @react-navigation/compat@5.3.0
 - @react-navigation/core@5.13.0
 - @react-navigation/devtools@5.1.8
 - @react-navigation/drawer@5.10.0
 - @react-navigation/material-bottom-tabs@5.3.0
 - @react-navigation/material-top-tabs@5.3.0
 - @react-navigation/native@5.8.0
 - @react-navigation/routers@5.5.0
 - @react-navigation/stack@5.10.0
2020-10-25 01:38:02 +02:00
Satyajit Sahoo
55d635f53e chore: fix custom link button example on web 2020-10-25 01:32:36 +02:00
Satyajit Sahoo
95600500a4 chore: upgrade depenendecies 2020-10-25 01:28:19 +02:00
Satyajit Sahoo
6cf124a190 docs: improve jsdoc for linking 2020-10-24 23:51:59 +02:00
Satyajit Sahoo
bfd0d94985 docs: fix incorrect comment 2020-10-24 23:35:50 +02:00
Satyajit Sahoo
748e92f120 feat: add getInitialURL and subscribe options to linking config
For apps with push notifications linking to screens inside the app, currently we need to handle them separately (e.g. [instructions for firebase](https://rnfirebase.io/messaging/notifications#handling-interaction), [instructions for expo notifications](https://docs.expo.io/push-notifications/receiving-notifications/)). But if we add a link in the notification to use for deep linking, we can instead reuse the same deep linking logic instead.

This commit adds the `getInitialURL` and `subscribe` options which internally used `Linking` API to allow more advanced implementations by combining it with other sources such as push notifications.

Example usage with Firebase notifications could look like this:

```js
const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  async getInitialURL() {
    // Check if app was opened from a deep link
    const url = await Linking.getInitialURL();

    if (url != null) {
      return url;
    }

    // Check if there is an initial firebase notification
    const message = await messaging().getInitialNotification();

    // Get the `url` property from the notification which corresponds to a screen
    // This property needs to be set on the notification payload when sending it
    return message?.notification.url;
  },
  subscribe(listener) {
    const onReceiveURL = ({ url }: { url: string }) => listener(url);

    // Listen to incoming links from deep linking
    Linking.addEventListener('url', onReceiveURL);

    // Listen to firebase push notifications
    const unsubscribeNotification = messaging().onNotificationOpenedApp(
      (message) => {
        const url = message.notification.url;

        if (url) {
          // If the notification has a `url` property, use it for linking
          listener(url);
        }
      }
    );

    return () => {
      // Clean up the event listeners
      Linking.removeEventListener('url', onReceiveURL);
      unsubscribeNotification();
    };
  },
  config,
};
```
2020-10-24 23:32:51 +02:00
Satyajit Sahoo
7f3b27a9ec feat: allow deep linking to reset state (#8973)
Currently when we receive a deep link after the app is rendered, it always results in a `navigate` action. While it's ok with the default configuration, it may result in incorrect behaviour when a custom `getStateForPath` function is provided and it returns a routes array different than the initial route and new route pair.

The commit changes 2 things:

1. Add ability to reset state via params of `navigate` by specifying a `state` property instead of `screen`
2. Update `getStateForAction` to return an action for reset when necessary according to the deep linking configuration

Closes #8952
2020-10-24 15:27:06 +02:00
Satyajit Sahoo
f51086edea feat: update helper types to have navigator specific methods 2020-10-23 18:12:36 +02:00
Wojciech Lewicki
7196889bf1 feat: add optional screens per navigator (#8805)
Changes done here will work properly with https://github.com/software-mansion/react-native-screens/pull/624 merged and released. The documentation of `screensEnabled` and `activeLimit` props should also be added. It also enabled `Screens` in iOS stack-navigator by default.

New things:
- `detachInactiveScreens` - prop for navigators with `react-native-screens` integration that can be set by user. It controls if the `react-native-screens` are used by the navigator.
- `detachPreviousScreen` - option that tells to keep the previous screen active. It can be set by user, defaults to `true` for normal mode and `false` for the last screen for mode = “modal”.

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-23 17:59:26 +02:00
Satyajit Sahoo
7dc2f5832e feat: improve types for navigation state (#8980)
The commit improves the navigation state object to have more specific types.
e.g. The `routeNames` array will now have proper type instead of `string[]`
2020-10-23 17:06:31 +02:00
Satyajit Sahoo
8ec6c1a603 chore: remove test code from example app 2020-10-23 16:51:44 +02:00
Satyajit Sahoo
960f0a5281 refactor: make sure height set on header container is focused header height 2020-10-23 03:32:40 +02:00
Satyajit Sahoo
da91cec941 fix: add missing check for parent header when calculating height 2020-10-23 02:37:36 +02:00
Satyajit Sahoo
38e17aae93 fix: don't set statusbar height on nested header by default 2020-10-23 02:27:50 +02:00
Satyajit Sahoo
0f60b4617f refactor: refactor HeaderSegment to function component 2020-10-23 02:17:04 +02:00
Drew Miller
f01bb4834b feat: add sceneContainerStyle prop to bottom-tabs navigator (#8947)
This feature adds the sceneContainerStyle prop to the bottom-tabs navigator, to allow setting styles on the container's view. It's already implemented in the material-top-tabs and drawer navigators, I've simply ported it into the bottom-tabs navigator.

It also fixes this issue:

https://github.com/react-navigation/react-navigation/issues/8076

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-23 02:16:09 +02:00
Satyajit Sahoo
261a33a0d0 fix: fix imports from query-string. closes #8971 (#8976) 2020-10-23 02:15:47 +02:00
Listi
d6cac6713a fix: fix header buttons not pressable when headerTransparent=true & headerMode=float (#8804)
Motivation
--
Previously when using `headerMode="float"` with headerTransparent set to true, we cant press header buttons in Android. This PR fixes this. (resolves #8731 )
Been doing some debugging and found out that this is caused by `HeaderContainer` being set as `absolute`. Initially it didn't have width & height in Android when it's set to default, that's why we can't access the children.
So, the solution in this PR is to define the height by using headerHeight. But, since we can't access headerHeight from header, Im using local state for keeping up with the headerHeight. Or should I move the HeaderHeight provider out of StackContainer? I'm not sure, since I think it was intended to be kept inside the StackContainer

Test Plan
--
With this config, now the header button can be clicked. Tested in both platform
```typescript
  <Stack.Navigator
    headerMode="float"
    screenOptions={{
      headerTransparent: true
    }}
  >
     <Stack.Screen  
      name="Home Screen"
      component={Home}
      />
      <Stack.Screen  
      name="Details Screen"
      component={Details}
      />
  </Stack.Navigator>
```

Android:
-
![Kapture 2020-09-30 at 19 01 21](https://user-images.githubusercontent.com/24470609/94682575-5b0fe480-034f-11eb-880a-318643d4eb00.gif)

iOS:
--
<img width="300" src="https://user-images.githubusercontent.com/24470609/94682743-9a3e3580-034f-11eb-8e90-2d31748bde5c.gif" />
2020-10-23 01:52:34 +02:00
Satyajit Sahoo
8ee0dda155 fix: set needsOffscreenAlphaCompositing and update default android animation
closes #8696
2020-10-23 01:38:14 +02:00
David Pett
8585f97226 docs: fix comments for gestureResponseDistance (#8954)
gestureResponseDistance vertical and horizontal documentations were swapped
2020-10-21 13:12:54 +02:00
Hossein Mohammadi
23ab350492 feat: support wildcard string prefixes (#8942)
Prefixes should be more flexible for situations like wild card subdomain. On android and IOS we can define wild cards by * but react-navigation does not work, In this PR I added support for RegExp Prefixes.

For Example
```js
{
  prefixes: [
    /^[^.s]+.example.com/g
 ],
}
```
I tested this work well.

Closes #8941 

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2020-10-20 12:01:49 +02:00
ahmadj-levelbenefits
80ff5a9c54 feat: add an unhandled action listener (#8895)
Often developers miss these console messages: this allows missed routes to be emitted to whatever event logger users prefer.
2020-10-09 16:49:20 +02:00
Kaushil Ruparelia
90ebfc40b3 feat: make react-native-vector-icons optional (#8936)
Referenced from 4b26429c49/src/components/MaterialCommunityIcon.tsx (L14)

https://callstack.github.io/react-native-paper/getting-started.html
> To get smaller bundle size by excluding modules you don't use, you can use our optional babel plugin. The plugin automatically rewrites the import statements so that only the modules you use are imported instead of the whole library. Add react-native-paper/babel to the plugins section in your babel.config.js for production environment. It should look like this:
> ```
> module.exports = {
>   presets: ['module:metro-react-native-babel-preset'],
>   env: {
>     production: {
>       plugins: ['react-native-paper/babel'],
>     },
>   },
> };
> ```
> If you created your project using Expo, it'll look something like this:
> ```
> module.exports = function(api) {
>   api.cache(true);
>   return {
>     presets: ['babel-preset-expo'],
>     env: {
>       production: {
>         plugins: ['react-native-paper/babel'],
>       },
>     },
>   };
> };
> ```

Closes #8821
2020-10-09 13:41:37 +02:00
Satyajit Sahoo
091b2a2038 fix: handle pushing a route with duplicate key
Currently, stack router adds a duplicate route when pushing a new route with a key that already exists. This is a buggy behaviour since keys need to be unique in the stack.

This commit fixes the behaviour to bring the existing route with the same key to focus (and merge new params if any) instead of adding a duplicate route.
2020-10-09 00:28:45 +02:00
85 changed files with 4285 additions and 2401 deletions

View File

@@ -87,6 +87,9 @@ jobs:
- run: - run:
name: Build packages in the monorepo name: Build packages in the monorepo
command: yarn lerna run prepare command: yarn lerna run prepare
- run:
name: Verify built type definitions are correct
command: yarn typescript
- run: - run:
name: Verify paths for types name: Verify paths for types
command: node scripts/check-types-path.js command: node scripts/check-types-path.js

View File

@@ -3,6 +3,7 @@ on:
push: push:
branches: branches:
- main - main
workflow_dispatch:
jobs: jobs:
publish: publish:

View File

@@ -4,7 +4,7 @@
"expo": { "expo": {
"name": "React Navigation", "name": "React Navigation",
"owner": "react-navigation", "owner": "react-navigation",
"slug": "NavigationPlayground", "slug": "react-navigation-example",
"description": "Demonstrates the functionality and various capabilities of React Navigation.", "description": "Demonstrates the functionality and various capabilities of React Navigation.",
"privacy": "public", "privacy": "public",
"version": "1.0.0", "version": "1.0.0",

View File

@@ -14,8 +14,9 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.0.0",
"@react-native-async-storage/async-storage": "^1.13.1",
"@react-native-community/masked-view": "0.1.10", "@react-native-community/masked-view": "0.1.10",
"color": "^3.1.2", "color": "^3.1.3",
"expo": "^39.0.0", "expo": "^39.0.0",
"expo-asset": "~8.2.0", "expo-asset": "~8.2.0",
"expo-blur": "~8.2.0", "expo-blur": "~8.2.0",
@@ -36,25 +37,25 @@
"react-native-web": "^0.13.16" "react-native-web": "^0.13.16"
}, },
"devDependencies": { "devDependencies": {
"@babel/node": "^7.10.1", "@babel/node": "^7.12.1",
"@expo/webpack-config": "^0.12.38", "@expo/webpack-config": "^0.12.40",
"@types/cheerio": "^0.22.22", "@types/cheerio": "^0.22.22",
"@types/jest-dev-server": "^4.2.0", "@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.11.4", "@types/koa": "^2.11.6",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/react": "~16.9.51", "@types/react": "~16.9.53",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-native": "~0.63.25", "@types/react-native": "~0.63.30",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.3.0", "babel-preset-expo": "^8.3.0",
"cheerio": "^1.0.0-rc.3", "cheerio": "^1.0.0-rc.3",
"expo-cli": "^3.27.14", "expo-cli": "^3.28.2",
"jest": "^26.5.2", "jest": "^26.6.1",
"jest-dev-server": "^4.4.0", "jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1", "mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"nodemon": "^2.0.4", "nodemon": "^2.0.6",
"playwright": "^0.14.0", "playwright": "^0.14.0",
"serve": "^11.3.0", "serve": "^11.3.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"

View File

@@ -1,3 +0,0 @@
import { AsyncStorage } from 'react-native';
export default AsyncStorage;

View File

@@ -1,14 +0,0 @@
export default {
getItem(key: string) {
return Promise.resolve(localStorage.getItem(key));
},
setItem(key: string, value: string) {
return Promise.resolve(localStorage.setItem(key, value));
},
removeItem(key: string) {
return Promise.resolve(localStorage.removeItem(key));
},
clear() {
return Promise.resolve(localStorage.clear());
},
};

View File

@@ -31,9 +31,11 @@ const AuthContext = React.createContext<{
}); });
const SplashScreen = () => { const SplashScreen = () => {
const { colors } = useTheme();
return ( return (
<View style={styles.content}> <View style={styles.content}>
<ActivityIndicator /> <ActivityIndicator color={colors.primary} />
</View> </View>
); );
}; };

View File

@@ -25,18 +25,9 @@ const LinkButton = ({
to, to,
...rest ...rest
}: React.ComponentProps<typeof Button> & { to: string }) => { }: React.ComponentProps<typeof Button> & { to: string }) => {
const { onPress, ...props } = useLinkProps({ to }); const props = useLinkProps({ to });
return ( return <Button {...props} {...rest} />;
<Button
{...props}
{...rest}
{...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
})}
/>
);
}; };
const ArticleScreen = ({ const ArticleScreen = ({

View File

@@ -1,13 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { import {
ScrollView, ScrollView,
YellowBox,
Platform, Platform,
StatusBar, StatusBar,
I18nManager, I18nManager,
Dimensions, Dimensions,
ScaledSize, ScaledSize,
Linking, Linking,
LogBox,
} from 'react-native'; } from 'react-native';
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
@@ -38,9 +38,9 @@ import {
HeaderStyleInterpolators, HeaderStyleInterpolators,
} from '@react-navigation/stack'; } from '@react-navigation/stack';
import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { useReduxDevToolsExtension } from '@react-navigation/devtools';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { restartApp } from './Restart'; import { restartApp } from './Restart';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes'; import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem'; import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack'; import SimpleStack from './Screens/SimpleStack';
@@ -58,7 +58,9 @@ import PreventRemove from './Screens/PreventRemove';
import CompatAPI from './Screens/CompatAPI'; import CompatAPI from './Screens/CompatAPI';
import LinkComponent from './Screens/LinkComponent'; import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']); if (Platform.OS !== 'web') {
LogBox.ignoreLogs(['Require cycle:']);
}
enableScreens(); enableScreens();

View File

@@ -26,14 +26,14 @@
}, },
"devDependencies": { "devDependencies": {
"@commitlint/config-conventional": "^11.0.0", "@commitlint/config-conventional": "^11.0.0",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"babel-jest": "^26.5.2", "babel-jest": "^26.6.1",
"codecov": "^3.8.0", "codecov": "^3.8.0",
"commitlint": "^11.0.0", "commitlint": "^11.0.0",
"eslint": "^7.10.0", "eslint": "^7.12.0",
"eslint-config-satya164": "^3.1.8", "eslint-config-satya164": "^3.1.8",
"husky": "^4.3.0", "husky": "^4.3.0",
"jest": "^26.5.2", "jest": "^26.6.1",
"lerna": "^3.22.1", "lerna": "^3.22.1",
"metro-react-native-babel-preset": "^0.63.0", "metro-react-native-babel-preset": "^0.63.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",

View File

@@ -3,6 +3,44 @@
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.10.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.2...@react-navigation/bottom-tabs@5.10.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.1...@react-navigation/bottom-tabs@5.10.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.10.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.10.0...@react-navigation/bottom-tabs@5.10.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.10.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.2...@react-navigation/bottom-tabs@5.10.0) (2020-10-24)
### Features
* add optional screens per navigator ([#8805](https://github.com/react-navigation/react-navigation/issues/8805)) ([7196889](https://github.com/react-navigation/react-navigation/commit/7196889bf1218eb6a736d9475e33a909c2248c3b))
* add sceneContainerStyle prop to bottom-tabs navigator ([#8947](https://github.com/react-navigation/react-navigation/issues/8947)) ([f01bb48](https://github.com/react-navigation/react-navigation/commit/f01bb4834b01e13ab9a6b220328349f77ca49428))
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.1...@react-navigation/bottom-tabs@5.9.2) (2020-10-07) ## [5.9.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.9.1...@react-navigation/bottom-tabs@5.9.2) (2020-10-07)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.9.2", "version": "5.10.3",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -36,16 +36,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2", "color": "^3.1.3",
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import BottomTabView from '../views/BottomTabView'; import BottomTabView from '../views/BottomTabView';
import type { import type {
@@ -23,11 +25,13 @@ function BottomTabNavigator({
backBehavior, backBehavior,
children, children,
screenOptions, screenOptions,
sceneContainerStyle,
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -43,12 +47,13 @@ function BottomTabNavigator({
state={state} state={state}
navigation={navigation} navigation={navigation}
descriptors={descriptors} descriptors={descriptors}
sceneContainerStyle={sceneContainerStyle}
/> />
); );
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap, BottomTabNavigationEventMap,
typeof BottomTabNavigator typeof BottomTabNavigator

View File

@@ -33,7 +33,8 @@ export type LabelPosition = 'beside-icon' | 'below-icon';
export type BottomTabNavigationHelpers = NavigationHelpers< export type BottomTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
BottomTabNavigationEventMap BottomTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type BottomTabNavigationProp< export type BottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -41,7 +42,7 @@ export type BottomTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationEventMap BottomTabNavigationEventMap
> & > &
@@ -148,7 +149,7 @@ export type BottomTabNavigationOptions = {
export type BottomTabDescriptor = Descriptor< export type BottomTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
BottomTabNavigationOptions BottomTabNavigationOptions
>; >;
@@ -170,6 +171,16 @@ export type BottomTabNavigationConfig<T = BottomTabBarOptions> = {
* Options for the tab bar which will be passed as props to the tab bar component. * Options for the tab bar which will be passed as props to the tab bar component.
*/ */
tabBarOptions?: T; tabBarOptions?: T;
/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true`.
*/
detachInactiveScreens?: boolean;
/**
* Style object for the component wrapping the screen content.
*/
sceneContainerStyle?: StyleProp<ViewStyle>;
}; };
export type BottomTabBarOptions = { export type BottomTabBarOptions = {
@@ -240,7 +251,7 @@ export type BottomTabBarOptions = {
}; };
export type BottomTabBarProps<T = BottomTabBarOptions> = T & { export type BottomTabBarProps<T = BottomTabBarOptions> = T & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
descriptors: BottomTabDescriptorMap; descriptors: BottomTabDescriptorMap;
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>; navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
}; };

View File

@@ -1,8 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
ParamListBase,
TabNavigationState, TabNavigationState,
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
@@ -19,7 +20,7 @@ import type {
} from '../types'; } from '../types';
type Props = BottomTabNavigationConfig & { type Props = BottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: BottomTabNavigationHelpers; navigation: BottomTabNavigationHelpers;
descriptors: BottomTabDescriptorMap; descriptors: BottomTabDescriptorMap;
}; };
@@ -31,9 +32,11 @@ type State = {
function SceneContent({ function SceneContent({
isFocused, isFocused,
children, children,
style,
}: { }: {
isFocused: boolean; isFocused: boolean;
children: React.ReactNode; children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}) { }) {
const { colors } = useTheme(); const { colors } = useTheme();
@@ -41,7 +44,7 @@ function SceneContent({
<View <View
accessibilityElementsHidden={!isFocused} accessibilityElementsHidden={!isFocused}
importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'} importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'}
style={[styles.content, { backgroundColor: colors.background }]} style={[styles.content, { backgroundColor: colors.background }, style]}
> >
{children} {children}
</View> </View>
@@ -85,7 +88,14 @@ export default class BottomTabView extends React.Component<Props, State> {
}; };
render() { render() {
const { state, descriptors, navigation, lazy } = this.props; const {
state,
descriptors,
navigation,
lazy,
detachInactiveScreens = true,
sceneContainerStyle,
} = this.props;
const { routes } = state; const { routes } = state;
const { loaded } = this.state; const { loaded } = this.state;
@@ -93,7 +103,11 @@ export default class BottomTabView extends React.Component<Props, State> {
<NavigationHelpersContext.Provider value={navigation}> <NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat> <SafeAreaProviderCompat>
<View style={styles.container}> <View style={styles.container}>
<ScreenContainer style={styles.pages}> <ScreenContainer
// @ts-ignore
enabled={detachInactiveScreens}
style={styles.pages}
>
{routes.map((route, index) => { {routes.map((route, index) => {
const descriptor = descriptors[route.key]; const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options; const { unmountOnBlur } = descriptor.options;
@@ -113,8 +127,12 @@ export default class BottomTabView extends React.Component<Props, State> {
key={route.key} key={route.key}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens}
> >
<SceneContent isFocused={isFocused}> <SceneContent
isFocused={isFocused}
style={sceneContainerStyle}
>
{descriptor.render()} {descriptor.render()}
</SceneContent> </SceneContent>
</ResourceSavingScene> </ResourceSavingScene>

View File

@@ -1,10 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native'; import { Platform, StyleSheet, View } from 'react-native';
import { Screen, screensEnabled } from 'react-native-screens'; import {
Screen,
screensEnabled,
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
type Props = { type Props = {
isVisible: boolean; isVisible: boolean;
children: React.ReactNode; children: React.ReactNode;
enabled: boolean;
style?: any; style?: any;
}; };
@@ -16,8 +22,17 @@ export default class ResourceSavingScene extends React.Component<Props> {
if (screensEnabled?.() && Platform.OS !== 'web') { if (screensEnabled?.() && Platform.OS !== 'web') {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-expect-error: stackPresentation is incorrectly marked as required if (shouldUseActivityState) {
return <Screen active={isVisible ? 1 : 0} {...rest} />; return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screen activityState={isVisible ? 2 : 0} {...rest} />
);
} else {
return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screen active={isVisible ? 1 : 0} {...rest} />
);
}
} }
const { isVisible, children, style, ...rest } = this.props; const { isVisible, children, style, ...rest } = this.props;

View File

@@ -3,6 +3,42 @@
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.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.2...@react-navigation/compat@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.1...@react-navigation/compat@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.3.0...@react-navigation/compat@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/compat
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.8...@react-navigation/compat@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.7...@react-navigation/compat@5.2.8) (2020-10-07) ## [5.2.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.7...@react-navigation/compat@5.2.8) (2020-10-07)
**Note:** Version bump only for package @react-navigation/compat **Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.2.8", "version": "5.3.3",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -32,8 +32,8 @@
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"react": "~16.13.1", "react": "~16.13.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"
}, },

View File

@@ -19,7 +19,11 @@ type EventName =
export default function createCompatNavigationProp< export default function createCompatNavigationProp<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase : ParamListBase

View File

@@ -32,7 +32,11 @@ export default function createCompatNavigatorFactory<
const createCompatNavigator = < const createCompatNavigator = <
NavigationPropType extends NavigationProp<any, any, any, any, any>, NavigationPropType extends NavigationProp<any, any, any, any, any>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase, : ParamListBase,

View File

@@ -5,6 +5,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import createCompatNavigatorFactory from './createCompatNavigatorFactory'; import createCompatNavigatorFactory from './createCompatNavigatorFactory';
@@ -12,17 +14,21 @@ type Props = DefaultNavigatorOptions<{}> & TabRouterOptions;
function SwitchNavigator(props: Props) { function SwitchNavigator(props: Props) {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
{}, {},
{} {},
TabActionHelpers<ParamListBase>
>(TabRouter, props); >(TabRouter, props);
return descriptors[state.routes[state.index].key].render(); return descriptors[state.routes[state.index].key].render();
} }
export default createCompatNavigatorFactory( export default createCompatNavigatorFactory(
createNavigatorFactory<TabNavigationState, {}, {}, typeof SwitchNavigator>( createNavigatorFactory<
SwitchNavigator TabNavigationState<ParamListBase>,
) {},
{},
typeof SwitchNavigator
>(SwitchNavigator)
); );

View File

@@ -8,7 +8,11 @@ import type * as helpers from './helpers';
export type CompatNavigationProp< export type CompatNavigationProp<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase, : ParamListBase,
@@ -67,7 +71,11 @@ export type CompatScreenType<
export type CompatRouteConfig< export type CompatRouteConfig<
NavigationPropType extends NavigationProp<ParamListBase>, NavigationPropType extends NavigationProp<ParamListBase>,
ParamList extends ParamListBase = NavigationPropType extends NavigationProp< ParamList extends ParamListBase = NavigationPropType extends NavigationProp<
infer P infer P,
any,
any,
any,
any
> >
? P ? P
: ParamListBase : ParamListBase

View File

@@ -3,6 +3,59 @@
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.13.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.2...@react-navigation/core@5.13.3) (2020-11-03)
### Bug Fixes
* handle navigating to same screen again for nested screens ([0945689](https://github.com/react-navigation/react-navigation/commit/0945689b70d71a4b5d766c61d57009761c460bf6))
## [5.13.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.1...@react-navigation/core@5.13.2) (2020-10-30)
### Bug Fixes
* fix params from for the root screen when creating action ([e8515f9](https://github.com/react-navigation/react-navigation/commit/e8515f9cd94a912c107a407dea3d953c4172393f)), closes [#9006](https://github.com/react-navigation/react-navigation/issues/9006)
* trim routes if an index is specified in state ([fb7ac96](https://github.com/react-navigation/react-navigation/commit/fb7ac960c8e1ffca200ecb12696ce5531a139e50))
## [5.13.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.13.0...@react-navigation/core@5.13.1) (2020-10-28)
### Bug Fixes
* improve types for route prop in screenOptions ([d26bcc0](https://github.com/react-navigation/react-navigation/commit/d26bcc057ef31f8950f909adf83e263171a42d74))
# [5.13.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.5...@react-navigation/core@5.13.0) (2020-10-24)
### Bug Fixes
* fix imports from query-string. closes [#8971](https://github.com/react-navigation/react-navigation/issues/8971) ([#8976](https://github.com/react-navigation/react-navigation/issues/8976)) ([261a33a](https://github.com/react-navigation/react-navigation/commit/261a33a0d03150c87b06f01aeace4926b1c03eb6))
### Features
* add an unhandled action listener ([#8895](https://github.com/react-navigation/react-navigation/issues/8895)) ([80ff5a9](https://github.com/react-navigation/react-navigation/commit/80ff5a9c543a44fa2fd7ba7fda0598f1b0d52a64))
* allow deep linking to reset state ([#8973](https://github.com/react-navigation/react-navigation/issues/8973)) ([7f3b27a](https://github.com/react-navigation/react-navigation/commit/7f3b27a9ec8edd9604ac19774baa1f60963ccdc9)), closes [#8952](https://github.com/react-navigation/react-navigation/issues/8952)
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.4...@react-navigation/core@5.12.5) (2020-10-07) ## [5.12.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.4...@react-navigation/core@5.12.5) (2020-10-07)
**Note:** Version bump only for package @react-navigation/core **Note:** Version bump only for package @react-navigation/core

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.12.5", "version": "5.13.3",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -35,17 +35,17 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.4.12", "@react-navigation/routers": "^5.5.1",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.12", "nanoid": "^3.1.15",
"query-string": "^6.13.5", "query-string": "^6.13.6",
"react-is": "^16.13.0", "react-is": "^16.13.0",
"use-subscription": "^1.4.0" "use-subscription": "^1.5.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-is": "^16.7.1", "@types/react-is": "^16.7.1",
"@types/use-subscription": "^1.0.0", "@types/use-subscription": "^1.0.0",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",

View File

@@ -94,6 +94,7 @@ const BaseNavigationContainer = React.forwardRef(
{ {
initialState, initialState,
onStateChange, onStateChange,
onUnhandledAction,
independent, independent,
children, children,
}: NavigationContainerProps, }: NavigationContainerProps,
@@ -342,51 +343,56 @@ const BaseNavigationContainer = React.forwardRef(
isFirstMountRef.current = false; isFirstMountRef.current = false;
}, [getRootState, emitter, state]); }, [getRootState, emitter, state]);
const onUnhandledAction = React.useCallback((action: NavigationAction) => { const defaultOnUnhandledAction = React.useCallback(
if (process.env.NODE_ENV === 'production') { (action: NavigationAction) => {
return; if (process.env.NODE_ENV === 'production') {
} return;
}
const payload: Record<string, any> | undefined = action.payload; const payload: Record<string, any> | undefined = action.payload;
let message = `The action '${action.type}'${ let message = `The action '${action.type}'${
payload ? ` with payload ${JSON.stringify(action.payload)}` : '' payload ? ` with payload ${JSON.stringify(action.payload)}` : ''
} was not handled by any navigator.`; } was not handled by any navigator.`;
switch (action.type) { switch (action.type) {
case 'NAVIGATE': case 'NAVIGATE':
case 'PUSH': case 'PUSH':
case 'REPLACE': case 'REPLACE':
case 'JUMP_TO': case 'JUMP_TO':
if (payload?.name) { if (payload?.name) {
message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`; message += `\n\nDo you have a screen named '${payload.name}'?\n\nIf you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.`;
} else { } else {
message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`; message += `\n\nYou need to pass the name of the screen to navigate to.\n\nSee https://reactnavigation.org/docs/navigation-actions for usage.`;
} }
break; break;
case 'GO_BACK': case 'GO_BACK':
case 'POP': case 'POP':
case 'POP_TO_TOP': case 'POP_TO_TOP':
message += `\n\nIs there any screen to go back to?`; message += `\n\nIs there any screen to go back to?`;
break; break;
case 'OPEN_DRAWER': case 'OPEN_DRAWER':
case 'CLOSE_DRAWER': case 'CLOSE_DRAWER':
case 'TOGGLE_DRAWER': case 'TOGGLE_DRAWER':
message += `\n\nIs your screen inside a Drawer navigator?`; message += `\n\nIs your screen inside a Drawer navigator?`;
break; break;
} }
message += `\n\nThis is a development-only warning and won't be shown in production.`; message += `\n\nThis is a development-only warning and won't be shown in production.`;
console.error(message); console.error(message);
}, []); },
[]
);
return ( return (
<ScheduleUpdateContext.Provider value={scheduleContext}> <ScheduleUpdateContext.Provider value={scheduleContext}>
<NavigationBuilderContext.Provider value={builderContext}> <NavigationBuilderContext.Provider value={builderContext}>
<NavigationStateContext.Provider value={context}> <NavigationStateContext.Provider value={context}>
<UnhandledActionContext.Provider value={onUnhandledAction}> <UnhandledActionContext.Provider
value={onUnhandledAction ?? defaultOnUnhandledAction}
>
<EnsureSingleNavigator>{children}</EnsureSingleNavigator> <EnsureSingleNavigator>{children}</EnsureSingleNavigator>
</UnhandledActionContext.Provider> </UnhandledActionContext.Provider>
</NavigationStateContext.Provider> </NavigationStateContext.Provider>

View File

@@ -721,3 +721,39 @@ it("throws if the ref hasn't finished initializing", () => {
render(element); render(element);
}); });
it('invokes the unhandled action listener with the unhandled action', () => {
const ref = React.createRef<NavigationContainerRef>();
const fn = jest.fn();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const TestScreen = () => <></>;
render(
<BaseNavigationContainer ref={ref} onUnhandledAction={fn}>
<TestNavigator>
<Screen name="foo" component={TestScreen} />
<Screen name="bar" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
act(() => ref.current!.navigate('bar'));
act(() => ref.current!.navigate('baz'));
expect(fn).toHaveBeenCalledWith({
payload: {
name: 'baz',
},
type: 'NAVIGATE',
});
});

View File

@@ -132,6 +132,17 @@ export default function MockRouter(options: DefaultRouterOptions) {
}; };
} }
case 'GO_BACK': {
if (state.index === 0) {
return null;
}
return {
...state,
index: state.index - 1,
};
}
default: default:
return BaseRouter.getStateForAction(state, action); return BaseRouter.getStateForAction(state, action);
} }

View File

@@ -43,32 +43,435 @@ it('gets navigate action from state', () => {
}, },
type: 'NAVIGATE', type: 'NAVIGATE',
}); });
});
expect( it('gets navigate action from state for top-level screen', () => {
getActionFromState({ const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: { answer: 42 },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
routes: [ routes: [
{ {
name: 'foo', name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
},
type: 'RESET',
});
});
it('gets navigate action from state for top-level screen with 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'bar',
params: { author: 'jane' },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with more than 2 screens with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
},
type: 'RESET',
});
});
it('gets navigate action from state for top-level screen with more than 2 screens with config with lower index', () => {
const state = {
index: 1,
routes: [
{
name: 'foo',
params: { answer: 42 },
},
{
name: 'bar',
params: { author: 'jane' },
},
{ name: 'baz' },
],
};
const config = {
initialRouteName: 'foo',
screens: {
bar: 'bar',
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'bar',
params: { author: 'jane' },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
state: { state: {
routes: [ routes: [
{ {
name: 'bar', name: 'qux',
state: { params: {
routes: [ author: 'jane',
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
}, },
}, },
{ name: 'quz' },
], ],
}, },
}, },
], },
}) },
).toEqual({ type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens with lower index', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
index: 0,
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
screen: 'qux',
initial: true,
params: {
author: 'jane',
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
],
},
},
],
};
expect(getActionFromState(state)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
params: { answer: 42 },
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
params: {
answer: 42,
params: {
author: 'jane',
},
screen: 'qux',
initial: true,
},
screen: 'bar',
initial: true,
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state for top-level screen with config', () => {
const state = {
routes: [
{
name: 'foo',
params: { answer: 42 },
},
],
};
const config = {
screens: {
initialRouteName: 'bar',
foo: {
path: 'some-path/:answer',
parse: {
answer: Number,
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: { answer: 42 },
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens including initial route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: { payload: {
name: 'foo', name: 'foo',
params: { params: {
@@ -84,6 +487,262 @@ it('gets navigate action from state', () => {
}); });
}); });
it('gets navigate action from state with 2 screens without initial route and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'quz',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with 2 screens including route with key and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
key: 'test',
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
state: {
routes: [
{
key: 'test',
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens and with config', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
routes: [
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'qux',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
state: {
routes: [
{
name: 'qux',
params: {
author: 'jane',
},
},
{ name: 'quz' },
{ name: 'qua' },
],
},
},
},
},
type: 'NAVIGATE',
});
});
it('gets navigate action from state with more than 2 screens with lower index', () => {
const state = {
routes: [
{
name: 'foo',
state: {
routes: [
{
name: 'bar',
state: {
index: 1,
routes: [
{ name: 'quu' },
{
name: 'qux',
params: { author: 'jane' },
},
{ name: 'quz' },
],
},
},
],
},
},
],
};
const config = {
screens: {
foo: {
initialRouteName: 'bar',
screens: {
bar: {
initialRouteName: 'quu',
},
},
},
},
};
expect(getActionFromState(state, config)).toEqual({
payload: {
name: 'foo',
params: {
screen: 'bar',
initial: true,
params: {
screen: 'qux',
initial: false,
params: {
author: 'jane',
},
},
},
},
type: 'NAVIGATE',
});
});
it("doesn't return action if no routes are provided'", () => {
expect(getActionFromState({ routes: [] })).toBe(undefined);
});
it('gets reset action from state', () => { it('gets reset action from state', () => {
const state = { const state = {
routes: [ routes: [

View File

@@ -735,6 +735,20 @@ it('navigates to nested child in a navigator', () => {
expect(element).toMatchInlineSnapshot( expect(element).toMatchInlineSnapshot(
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"` `"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
); );
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
act(() => navigation.current?.goBack());
expect(element).toMatchInlineSnapshot(
`"[bar-a, {\\"lol\\":\\"why\\",\\"whoa\\":\\"test\\"}]"`
);
act(() => navigation.current?.navigate('bar', { screen: 'bar-b' }));
expect(element).toMatchInlineSnapshot(
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
);
}); });
it('navigates to nested child in a navigator with initial: false', () => { it('navigates to nested child in a navigator with initial: false', () => {
@@ -1093,6 +1107,194 @@ it('navigates to nested child in a navigator with initial: false', () => {
}); });
}); });
it('resets state of a nested child in a navigator', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestComponent = ({ route }: any): any =>
`[${route.name}, ${JSON.stringify(route.params)}]`;
const onStateChange = jest.fn();
const navigation = React.createRef<NavigationContainerRef>();
const first = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen name="bar-a" component={TestComponent} />
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '1',
routeNames: ['foo-a', 'foo-b'],
routes: [
{
key: 'foo-a',
name: 'foo-a',
},
{
key: 'foo-b',
name: 'foo-b',
},
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(() =>
navigation.current?.navigate('bar', {
state: {
routes: [{ name: 'bar-a' }, { name: 'bar-b' }],
},
})
);
expect(first).toMatchInlineSnapshot(`"[bar-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
params: {
state: {
routes: [{ name: 'bar-a' }, { name: 'bar-b' }],
},
},
state: {
index: 0,
key: '4',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a-2',
name: 'bar-a',
},
{
key: 'bar-b-3',
name: 'bar-b',
params: { some: 'stuff' },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
act(() =>
navigation.current?.navigate('bar', {
state: {
index: 2,
routes: [
{ key: '37', name: 'bar-b' },
{ name: 'bar-b' },
{ name: 'bar-a', params: { test: 18 } },
],
},
})
);
expect(first).toMatchInlineSnapshot(`"[bar-a, {\\"test\\":18}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
params: {
state: {
index: 2,
routes: [
{ key: '37', name: 'bar-b' },
{ name: 'bar-b' },
{ name: 'bar-a', params: { test: 18 } },
],
},
},
state: {
index: 2,
key: '7',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: '37',
name: 'bar-b',
params: { some: 'stuff' },
},
{
key: 'bar-b-5',
name: 'bar-b',
params: { some: 'stuff' },
},
{
key: 'bar-a-6',
name: 'bar-a',
params: { test: 18 },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
});
it('gives access to internal state', () => { it('gives access to internal state', () => {
const TestNavigator = (props: any): any => { const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);

View File

@@ -22,6 +22,7 @@ it('sets options with options prop as an object', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -67,6 +68,7 @@ it('sets options with options prop as a fuction', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -113,6 +115,7 @@ it('sets options with screenOptions prop as an object', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -173,6 +176,7 @@ it('sets options with screenOptions prop as a fuction', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -245,6 +249,7 @@ it('sets initial options with setOptions', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ {
title?: string; title?: string;
color?: string; color?: string;
@@ -302,6 +307,7 @@ it('updates options with setOptions', () => {
NavigationState, NavigationState,
any, any,
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];
@@ -378,6 +384,7 @@ it("returns correct value for canGoBack when it's not overridden", () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -441,6 +448,7 @@ it(`returns false for canGoBack when current router doesn't handle GO_BACK`, ()
NavigationState, NavigationState,
any, any,
any, any,
any,
any any
>(TestRouter, props); >(TestRouter, props);
@@ -491,6 +499,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(ParentRouter, props); >(ParentRouter, props);
@@ -501,6 +510,7 @@ it('returns true for canGoBack when current router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);
@@ -558,6 +568,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(OverrodeRouter, props); >(OverrodeRouter, props);
@@ -568,6 +579,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
const { state, descriptors } = useNavigationBuilder< const { state, descriptors } = useNavigationBuilder<
NavigationState, NavigationState,
any, any,
{},
{ title?: string }, { title?: string },
any any
>(MockRouter, props); >(MockRouter, props);

View File

@@ -1,43 +1,94 @@
import type { PartialState, NavigationState } from '@react-navigation/routers'; import type {
Route,
PartialRoute,
NavigationState,
PartialState,
CommonActions,
} from '@react-navigation/routers';
import type { PathConfig, PathConfigMap, NestedNavigateParams } from './types';
type NavigateParams = { type ConfigItem = {
screen?: string; initialRouteName?: string;
params?: NavigateParams; screens?: Record<string, ConfigItem>;
initial?: boolean;
}; };
type NavigateAction = { type Options = { initialRouteName?: string; screens: PathConfigMap };
type NavigateAction<State extends NavigationState> = {
type: 'NAVIGATE'; type: 'NAVIGATE';
payload: { name: string; params: NavigateParams }; payload: {
name: string;
params?: NestedNavigateParams<State>;
};
}; };
export default function getActionFromState( export default function getActionFromState(
state: PartialState<NavigationState> state: PartialState<NavigationState>,
): NavigateAction | undefined { options?: Options
if (state.routes.length === 0) { ): NavigateAction<NavigationState> | CommonActions.Action | undefined {
// Create a normalized configs object which will be easier to use
const normalizedConfig = options ? createNormalizedConfigItem(options) : {};
const routes =
state.index != null ? state.routes.slice(0, state.index + 1) : state.routes;
if (routes.length === 0) {
return undefined; return undefined;
} }
// Try to construct payload for a `NAVIGATE` action from the state if (
// This lets us preserve the navigation state and not lose it !(
let route = state.routes[state.routes.length - 1]; routes.length === 1 ||
(routes.length === 2 &&
routes[0].name === normalizedConfig?.initialRouteName)
)
) {
return {
type: 'RESET',
payload: state,
};
}
let payload: { name: string; params: NavigateParams } = { const route = state.routes[state.index ?? state.routes.length - 1];
name: route.name,
params: { ...route.params },
};
let current = route.state; let current: PartialState<NavigationState> | undefined = route?.state;
let params = payload.params; let config: ConfigItem | undefined = normalizedConfig?.screens?.[route?.name];
let params: NestedNavigateParams<NavigationState> = { ...route.params };
let payload = route ? { name: route.name, params } : undefined;
while (current) { while (current) {
if (current.routes.length === 0) { if (current.routes.length === 0) {
return undefined; return undefined;
} }
route = current.routes[current.routes.length - 1]; const routes =
params.initial = current.routes.length === 1; current.index != null
params.screen = route.name; ? current.routes.slice(0, current.index + 1)
: current.routes;
const route: Route<string> | PartialRoute<Route<string>> =
routes[routes.length - 1];
if (routes.length === 1) {
params.initial = true;
params.screen = route.name;
params.state = undefined; // Explicitly set to override existing value when merging params
} else if (
routes.length === 2 &&
routes[0].key === undefined &&
routes[0].name === config?.initialRouteName
) {
params.initial = false;
params.screen = route.name;
params.state = undefined;
} else {
params.initial = undefined;
params.screen = undefined;
params.params = undefined;
params.state = current;
break;
}
if (route.state) { if (route.state) {
params.params = { ...route.params }; params.params = { ...route.params };
@@ -47,10 +98,34 @@ export default function getActionFromState(
} }
current = route.state; current = route.state;
config = config?.screens?.[route.name];
} }
if (!payload) {
return;
}
// Try to construct payload for a `NAVIGATE` action from the state
// This lets us preserve the navigation state and not lose it
return { return {
type: 'NAVIGATE', type: 'NAVIGATE',
payload, payload,
}; };
} }
const createNormalizedConfigItem = (config: PathConfig | string) =>
typeof config === 'object' && config != null
? {
initialRouteName: config.initialRouteName,
screens:
config.screens != null
? createNormalizedConfigs(config.screens)
: undefined,
}
: {};
const createNormalizedConfigs = (options: PathConfigMap) =>
Object.entries(options).reduce<Record<string, ConfigItem>>((acc, [k, v]) => {
acc[k] = createNormalizedConfigItem(v);
return acc;
}, {});

View File

@@ -1,4 +1,4 @@
import queryString from 'query-string'; import * as queryString from 'query-string';
import type { import type {
NavigationState, NavigationState,
PartialState, PartialState,
@@ -35,7 +35,7 @@ const getActiveRoute = (state: State): { name: string; params?: object } => {
/** /**
* Utility to serialize a navigation state object to a path string. * Utility to serialize a navigation state object to a path string.
* *
* Example: * @example
* ```js * ```js
* getPathFromState( * getPathFromState(
* { * {

View File

@@ -1,5 +1,5 @@
import escape from 'escape-string-regexp'; import escape from 'escape-string-regexp';
import queryString from 'query-string'; import * as queryString from 'query-string';
import type { import type {
NavigationState, NavigationState,
PartialState, PartialState,
@@ -37,7 +37,7 @@ type ResultState = PartialState<NavigationState> & {
* Utility to parse a path string to initial state object accepted by the container. * Utility to parse a path string to initial state object accepted by the container.
* This is useful for deep linking when we need to handle the incoming URL. * This is useful for deep linking when we need to handle the incoming URL.
* *
* Example: * @example
* ```js * ```js
* getStateFromPath( * getStateFromPath(
* '/chat/jane/42', * '/chat/jane/42',

View File

@@ -10,8 +10,9 @@ import type {
} from '@react-navigation/routers'; } from '@react-navigation/routers';
export type DefaultNavigatorOptions< export type DefaultNavigatorOptions<
ScreenOptions extends {} ScreenOptions extends {},
> = DefaultRouterOptions & { ParamList extends ParamListBase = ParamListBase
> = DefaultRouterOptions<Extract<keyof ParamList, string>> & {
/** /**
* Children React Elements to extract the route configuration from. * Children React Elements to extract the route configuration from.
* Only `Screen` components are supported as children. * Only `Screen` components are supported as children.
@@ -23,7 +24,7 @@ export type DefaultNavigatorOptions<
screenOptions?: screenOptions?:
| ScreenOptions | ScreenOptions
| ((props: { | ((props: {
route: RouteProp<ParamListBase, string>; route: RouteProp<ParamList, keyof ParamList>;
navigation: any; navigation: any;
}) => ScreenOptions); }) => ScreenOptions);
}; };
@@ -237,6 +238,10 @@ export type NavigationContainerProps = {
* Callback which is called with the latest navigation state when it changes. * Callback which is called with the latest navigation state when it changes.
*/ */
onStateChange?: (state: NavigationState | undefined) => void; onStateChange?: (state: NavigationState | undefined) => void;
/**
* Callback which is called when an action is not handled.
*/
onUnhandledAction?: (action: NavigationAction) => void;
/** /**
* Whether this navigation container should be independent of parent containers. * Whether this navigation container should be independent of parent containers.
* If this is not set to `true`, this container cannot be nested inside another container. * If this is not set to `true`, this container cannot be nested inside another container.
@@ -252,7 +257,7 @@ export type NavigationContainerProps = {
export type NavigationProp< export type NavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList = string, RouteName extends keyof ParamList = string,
State extends NavigationState = NavigationState, State extends NavigationState = NavigationState<ParamList>,
ScreenOptions extends {} = {}, ScreenOptions extends {} = {},
EventMap extends EventMapBase = {} EventMap extends EventMapBase = {}
> = NavigationHelpersCommon<ParamList, State> & { > = NavigationHelpersCommon<ParamList, State> & {
@@ -277,20 +282,7 @@ export type NavigationProp<
export type RouteProp< export type RouteProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
RouteName extends keyof ParamList RouteName extends keyof ParamList
> = Omit<Route<Extract<RouteName, string>>, 'params'> & > = Route<Extract<RouteName, string>, ParamList[RouteName]>;
(undefined extends ParamList[RouteName]
? Readonly<{
/**
* Params for this route
*/
params?: Readonly<ParamList[RouteName]>;
}>
: Readonly<{
/**
* Params for this route
*/
params: Readonly<ParamList[RouteName]>;
}>);
export type CompositeNavigationProp< export type CompositeNavigationProp<
A extends NavigationProp<ParamListBase, string, any, any>, A extends NavigationProp<ParamListBase, string, any, any>,
@@ -498,14 +490,11 @@ export type TypedNavigator<
* Navigator component which manages the child screens. * Navigator component which manages the child screens.
*/ */
Navigator: React.ComponentType< Navigator: React.ComponentType<
Omit<React.ComponentProps<Navigator>, keyof DefaultNavigatorOptions<any>> & Omit<
Omit<DefaultNavigatorOptions<ScreenOptions>, 'initialRouteName'> & { React.ComponentProps<Navigator>,
/** keyof DefaultNavigatorOptions<any, any>
* Name of the route to focus by on initial render. > &
* If not specified, usually the first route is used. DefaultNavigatorOptions<ScreenOptions, ParamList>
*/
initialRouteName?: keyof ParamList;
}
>; >;
/** /**
* Component used for specifying route configuration. * Component used for specifying route configuration.
@@ -515,6 +504,20 @@ export type TypedNavigator<
) => null; ) => null;
}; };
export type NestedNavigateParams<State extends NavigationState> =
| {
screen?: string;
params?: object;
initial?: boolean;
state?: never;
}
| {
screen?: never;
params?: never;
initial?: never;
state?: PartialState<State> | State;
};
export type PathConfig = { export type PathConfig = {
path?: string; path?: string;
exact?: boolean; exact?: boolean;

View File

@@ -23,30 +23,27 @@ import useFocusEvents from './useFocusEvents';
import useOnRouteFocus from './useOnRouteFocus'; import useOnRouteFocus from './useOnRouteFocus';
import useChildListeners from './useChildListeners'; import useChildListeners from './useChildListeners';
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter'; import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
import { import {
DefaultNavigatorOptions, DefaultNavigatorOptions,
RouteConfig, RouteConfig,
PrivateValueStore, PrivateValueStore,
EventMapBase, EventMapBase,
EventMapCore, EventMapCore,
NestedNavigateParams,
} from './types'; } from './types';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
// This is to make TypeScript compiler happy // This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions // eslint-disable-next-line babel/no-unused-expressions
PrivateValueStore; PrivateValueStore;
type NavigatorRoute = { type NavigatorRoute<State extends NavigationState> = {
key: string; key: string;
params?: { params?: NestedNavigateParams<State>;
screen?: string;
params?: object;
initial?: boolean;
};
}; };
/** /**
@@ -182,6 +179,7 @@ const getRouteConfigsFromChildren = <
export default function useNavigationBuilder< export default function useNavigationBuilder<
State extends NavigationState, State extends NavigationState,
RouterOptions extends DefaultRouterOptions, RouterOptions extends DefaultRouterOptions,
ActionHelpers extends Record<string, () => void>,
ScreenOptions extends {}, ScreenOptions extends {},
EventMap extends Record<string, any> EventMap extends Record<string, any>
>( >(
@@ -191,20 +189,15 @@ export default function useNavigationBuilder<
const navigatorKey = useRegisterNavigator(); const navigatorKey = useRegisterNavigator();
const route = React.useContext(NavigationRouteContext) as const route = React.useContext(NavigationRouteContext) as
| NavigatorRoute | NavigatorRoute<State>
| undefined; | undefined;
const previousNestedParamsRef = React.useRef(route?.params);
React.useEffect(() => {
previousNestedParamsRef.current = route?.params;
}, [route]);
const { children, ...rest } = options; const { children, ...rest } = options;
const { current: router } = React.useRef<Router<State, any>>( const { current: router } = React.useRef<Router<State, any>>(
createRouter({ createRouter({
...((rest as unknown) as RouterOptions), ...((rest as unknown) as RouterOptions),
...(route?.params && ...(route?.params &&
route.params.state == null &&
route.params.initial !== false && route.params.initial !== false &&
typeof route.params.screen === 'string' typeof route.params.screen === 'string'
? { initialRouteName: route.params.screen } ? { initialRouteName: route.params.screen }
@@ -239,7 +232,9 @@ export default function useNavigationBuilder<
(acc, curr) => { (acc, curr) => {
const { initialParams } = screens[curr]; const { initialParams } = screens[curr];
const initialParamsFromParams = const initialParamsFromParams =
route?.params?.initial !== false && route?.params?.screen === curr route?.params?.state == null &&
route?.params?.initial !== false &&
route?.params?.screen === curr
? route.params.params ? route.params.params
: undefined; : undefined;
@@ -287,7 +282,10 @@ export default function useNavigationBuilder<
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset) // We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
// Otherwise assume that the state was provided as initial state // Otherwise assume that the state was provided as initial state
// So we need to rehydrate it to make it usable // So we need to rehydrate it to make it usable
if (currentState === undefined || !isStateValid(currentState)) { if (
(currentState === undefined || !isStateValid(currentState)) &&
route?.params?.state == null
) {
return [ return [
router.getInitialState({ router.getInitialState({
routeNames, routeNames,
@@ -297,10 +295,13 @@ export default function useNavigationBuilder<
]; ];
} else { } else {
return [ return [
router.getRehydratedState(currentState as PartialState<State>, { router.getRehydratedState(
routeNames, route?.params?.state ?? (currentState as PartialState<State>),
routeParamList, {
}), routeNames,
routeParamList,
}
),
false, false,
]; ];
} }
@@ -331,21 +332,54 @@ export default function useNavigationBuilder<
}); });
} }
if ( const previousNestedParamsRef = React.useRef(route?.params);
typeof route?.params?.screen === 'string' &&
(route.params !== previousNestedParamsRef.current || React.useEffect(() => {
(route.params.initial === false && isFirstStateInitialization)) previousNestedParamsRef.current = route?.params;
) { }, [route?.params]);
// If the route was updated with new name and/or params, we should navigate there
if (route?.params) {
const previousParams = previousNestedParamsRef.current;
let action: CommonActions.Action | undefined;
if (
typeof route.params.state === 'object' &&
route.params.state != null &&
route.params.state !== previousParams?.state
) {
// If the route was updated with new state, we should reset to it
action = CommonActions.reset(route.params.state);
} else if (
typeof route.params.screen === 'string' &&
((route.params.initial === false && isFirstStateInitialization) ||
route.params !== previousParams)
) {
// FIXME: Since params are merged, `route.params.params` might contain params from an older route
// So we need to make sure to reuse it only if:
// - The screen is the same, so navigation happened with same params
// - Params have actually changed
// - It's the first navigation during initialization
const params = (
route.params.screen === nextState.routes[nextState.index].name
? route.params.screen === previousParams?.screen
: route.params.params !== previousParams?.params ||
(route.params.initial === false && isFirstStateInitialization)
)
? route.params.params
: undefined;
// If the route was updated with new screen name and/or params, we should navigate there
action = CommonActions.navigate(route.params.screen, params);
}
// The update should be limited to current navigator only, so we call the router manually // The update should be limited to current navigator only, so we call the router manually
const updatedState = router.getStateForAction( const updatedState = action
nextState, ? router.getStateForAction(nextState, action, {
CommonActions.navigate(route.params.screen, route.params.params), routeNames,
{ routeParamList,
routeNames, })
routeParamList, : null;
}
);
nextState = nextState =
updatedState !== null updatedState !== null
@@ -484,7 +518,12 @@ export default function useNavigationBuilder<
setState, setState,
}); });
const navigation = useNavigationHelpers<State, NavigationAction, EventMap>({ const navigation = useNavigationHelpers<
State,
ActionHelpers,
NavigationAction,
EventMap
>({
onAction, onAction,
getState, getState,
emitter, emitter,

View File

@@ -31,6 +31,7 @@ type Options<State extends NavigationState, Action extends NavigationAction> = {
*/ */
export default function useNavigationHelpers< export default function useNavigationHelpers<
State extends NavigationState, State extends NavigationState,
ActionHelpers extends Record<string, () => void>,
Action extends NavigationAction, Action extends NavigationAction,
EventMap extends Record<string, any> EventMap extends Record<string, any>
>({ onAction, getState, emitter, router }: Options<State, Action>) { >({ onAction, getState, emitter, router }: Options<State, Action>) {
@@ -85,7 +86,8 @@ export default function useNavigationHelpers<
dangerouslyGetParent: () => parentNavigationHelpers as any, dangerouslyGetParent: () => parentNavigationHelpers as any,
dangerouslyGetState: getState, dangerouslyGetState: getState,
} as NavigationHelpers<ParamListBase, EventMap> & } as NavigationHelpers<ParamListBase, EventMap> &
(NavigationProp<ParamListBase, string, any, any, any> | undefined); (NavigationProp<ParamListBase, string, any, any, any> | undefined) &
ActionHelpers;
}, [ }, [
emitter.emit, emitter.emit,
getState, getState,

View File

@@ -3,6 +3,38 @@
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.1.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.10...@react-navigation/devtools@5.1.11) (2020-11-03)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.9...@react-navigation/devtools@5.1.10) (2020-10-30)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.8...@react-navigation/devtools@5.1.9) (2020-10-28)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.7...@react-navigation/devtools@5.1.8) (2020-10-24)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.6...@react-navigation/devtools@5.1.7) (2020-10-07) ## [5.1.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.6...@react-navigation/devtools@5.1.7) (2020-10-07)
**Note:** Version bump only for package @react-navigation/devtools **Note:** Version bump only for package @react-navigation/devtools

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/devtools", "name": "@react-navigation/devtools",
"description": "Developer tools for React Navigation", "description": "Developer tools for React Navigation",
"version": "5.1.7", "version": "5.1.11",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -36,14 +36,14 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.12.5", "@react-navigation/core": "^5.13.3",
"deep-equal": "^2.0.4" "deep-equal": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"typescript": "^4.0.3" "typescript": "^4.0.3"

View File

@@ -3,6 +3,43 @@
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.10.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.2...@react-navigation/drawer@5.10.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.1...@react-navigation/drawer@5.10.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/drawer
## [5.10.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.10.0...@react-navigation/drawer@5.10.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/drawer
# [5.10.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.3...@react-navigation/drawer@5.10.0) (2020-10-24)
### Features
* add optional screens per navigator ([#8805](https://github.com/react-navigation/react-navigation/issues/8805)) ([7196889](https://github.com/react-navigation/react-navigation/commit/7196889bf1218eb6a736d9475e33a909c2248c3b))
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.2...@react-navigation/drawer@5.9.3) (2020-10-07) ## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.9.2...@react-navigation/drawer@5.9.3) (2020-10-07)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/drawer", "name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess", "description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.9.3", "version": "5.10.3",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -41,15 +41,15 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2", "color": "^3.1.3",
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

@@ -6,6 +6,8 @@ import {
DrawerNavigationState, DrawerNavigationState,
DrawerRouterOptions, DrawerRouterOptions,
DrawerRouter, DrawerRouter,
DrawerActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import DrawerView from '../views/DrawerView'; import DrawerView from '../views/DrawerView';
@@ -28,8 +30,9 @@ function DrawerNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerRouterOptions, DrawerRouterOptions,
DrawerActionHelpers<ParamListBase>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap DrawerNavigationEventMap
>(DrawerRouter, { >(DrawerRouter, {
@@ -51,7 +54,7 @@ function DrawerNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap, DrawerNavigationEventMap,
typeof DrawerNavigator typeof DrawerNavigator

View File

@@ -86,6 +86,12 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
* You can pass a custom background color for a drawer or a custom width here. * You can pass a custom background color for a drawer or a custom width here.
*/ */
drawerStyle?: StyleProp<ViewStyle>; drawerStyle?: StyleProp<ViewStyle>;
/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true`.
*/
detachInactiveScreens?: boolean;
}; };
export type DrawerNavigationOptions = { export type DrawerNavigationOptions = {
@@ -136,7 +142,7 @@ export type DrawerNavigationOptions = {
}; };
export type DrawerContentComponentProps<T = DrawerContentOptions> = T & { export type DrawerContentComponentProps<T = DrawerContentOptions> = T & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
/** /**
@@ -195,7 +201,8 @@ export type DrawerNavigationEventMap = {
export type DrawerNavigationHelpers = NavigationHelpers< export type DrawerNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
DrawerNavigationEventMap DrawerNavigationEventMap
>; > &
DrawerActionHelpers<ParamListBase>;
export type DrawerNavigationProp< export type DrawerNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -203,7 +210,7 @@ export type DrawerNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
DrawerNavigationState, DrawerNavigationState<ParamList>,
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationEventMap DrawerNavigationEventMap
> & > &
@@ -220,7 +227,7 @@ export type DrawerScreenProps<
export type DrawerDescriptor = Descriptor< export type DrawerDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerNavigationOptions DrawerNavigationOptions
>; >;

View File

@@ -3,6 +3,7 @@ import {
CommonActions, CommonActions,
DrawerActions, DrawerActions,
DrawerNavigationState, DrawerNavigationState,
ParamListBase,
useLinkBuilder, useLinkBuilder,
} from '@react-navigation/native'; } from '@react-navigation/native';
import DrawerItem from './DrawerItem'; import DrawerItem from './DrawerItem';
@@ -13,7 +14,7 @@ import type {
} from '../types'; } from '../types';
type Props = Omit<DrawerContentOptions, 'contentContainerStyle' | 'style'> & { type Props = Omit<DrawerContentOptions, 'contentContainerStyle' | 'style'> & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
}; };

View File

@@ -13,6 +13,7 @@ import {
DrawerNavigationState, DrawerNavigationState,
DrawerActions, DrawerActions,
useTheme, useTheme,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { GestureHandlerRootView } from './GestureHandler'; import { GestureHandlerRootView } from './GestureHandler';
@@ -31,7 +32,7 @@ import type {
} from '../types'; } from '../types';
type Props = DrawerNavigationConfig & { type Props = DrawerNavigationConfig & {
state: DrawerNavigationState; state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers; navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap; descriptors: DrawerDescriptorMap;
}; };
@@ -82,6 +83,7 @@ export default function DrawerView({
gestureHandlerProps, gestureHandlerProps,
minSwipeDistance, minSwipeDistance,
sceneContainerStyle, sceneContainerStyle,
detachInactiveScreens = true,
}: Props) { }: Props) {
const [loaded, setLoaded] = React.useState([state.routes[state.index].key]); const [loaded, setLoaded] = React.useState([state.routes[state.index].key]);
const dimensions = useWindowDimensions(); const dimensions = useWindowDimensions();
@@ -151,7 +153,8 @@ export default function DrawerView({
const renderContent = () => { const renderContent = () => {
return ( return (
<ScreenContainer style={styles.content}> // @ts-ignore
<ScreenContainer enabled={detachInactiveScreens} style={styles.content}>
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
const descriptor = descriptors[route.key]; const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options; const { unmountOnBlur } = descriptor.options;
@@ -171,6 +174,7 @@ export default function DrawerView({
key={route.key} key={route.key}
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]} style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
isVisible={isFocused} isVisible={isFocused}
enabled={detachInactiveScreens}
> >
{descriptor.render()} {descriptor.render()}
</ResourceSavingScene> </ResourceSavingScene>

View File

@@ -1,10 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native'; import { Platform, StyleSheet, View } from 'react-native';
import { Screen, screensEnabled } from 'react-native-screens'; import {
Screen,
screensEnabled,
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
type Props = { type Props = {
isVisible: boolean; isVisible: boolean;
children: React.ReactNode; children: React.ReactNode;
enabled: boolean;
style?: any; style?: any;
}; };
@@ -16,8 +22,17 @@ export default class ResourceSavingScene extends React.Component<Props> {
if (screensEnabled?.() && Platform.OS !== 'web') { if (screensEnabled?.() && Platform.OS !== 'web') {
const { isVisible, ...rest } = this.props; const { isVisible, ...rest } = this.props;
// @ts-expect-error: stackPresentation is incorrectly marked as required if (shouldUseActivityState) {
return <Screen active={isVisible ? 1 : 0} {...rest} />; return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screen activityState={isVisible ? 2 : 0} {...rest} />
);
} else {
return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screen active={isVisible ? 1 : 0} {...rest} />
);
}
} }
const { isVisible, children, style, ...rest } = this.props; const { isVisible, children, style, ...rest } = this.props;

View File

@@ -3,6 +3,43 @@
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.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.2...@react-navigation/material-bottom-tabs@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.1...@react-navigation/material-bottom-tabs@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.3.0...@react-navigation/material-bottom-tabs@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.19...@react-navigation/material-bottom-tabs@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* make react-native-vector-icons optional ([#8936](https://github.com/react-navigation/react-navigation/issues/8936)) ([90ebfc4](https://github.com/react-navigation/react-navigation/commit/90ebfc40b387b209031e6275aaa0be95192f7d04)), closes [/github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14](https://github.com//github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx/issues/L14) [#8821](https://github.com/react-navigation/react-navigation/issues/8821)
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.18...@react-navigation/material-bottom-tabs@5.2.19) (2020-10-07) ## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.18...@react-navigation/material-bottom-tabs@5.2.19) (2020-10-07)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-bottom-tabs", "name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper", "description": "Integration for bottom navigation component from react-native-paper",
"version": "5.2.19", "version": "5.3.3",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -42,10 +42,10 @@
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"@types/react-native-vector-icons": "^6.4.6", "@types/react-native-vector-icons": "^6.4.6",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import MaterialBottomTabView from '../views/MaterialBottomTabView'; import MaterialBottomTabView from '../views/MaterialBottomTabView';
@@ -27,8 +29,9 @@ function MaterialBottomTabNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -49,7 +52,7 @@ function MaterialBottomTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap, MaterialBottomTabNavigationEventMap,
typeof MaterialBottomTabNavigator typeof MaterialBottomTabNavigator

View File

@@ -19,7 +19,8 @@ export type MaterialBottomTabNavigationEventMap = {
export type MaterialBottomTabNavigationHelpers = NavigationHelpers< export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type MaterialBottomTabNavigationProp< export type MaterialBottomTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -27,7 +28,7 @@ export type MaterialBottomTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationEventMap MaterialBottomTabNavigationEventMap
> & > &
@@ -84,7 +85,7 @@ export type MaterialBottomTabNavigationOptions = {
export type MaterialBottomTabDescriptor = Descriptor< export type MaterialBottomTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialBottomTabNavigationOptions MaterialBottomTabNavigationOptions
>; >;

View File

@@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper'; import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
Route, Route,
@@ -10,6 +9,7 @@ import {
useTheme, useTheme,
useLinkBuilder, useLinkBuilder,
Link, Link,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import type { import type {
@@ -19,13 +19,55 @@ import type {
} from '../types'; } from '../types';
type Props = MaterialBottomTabNavigationConfig & { type Props = MaterialBottomTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: MaterialBottomTabNavigationHelpers; navigation: MaterialBottomTabNavigationHelpers;
descriptors: MaterialBottomTabDescriptorMap; descriptors: MaterialBottomTabDescriptorMap;
}; };
type Scene = { route: { key: string } }; type Scene = { route: { key: string } };
// Optionally require vector-icons referenced from react-native-paper:
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
let MaterialCommunityIcons: any;
try {
// Optionally require vector-icons
MaterialCommunityIcons = require('react-native-vector-icons/MaterialCommunityIcons')
.default;
} catch (e) {
// @ts-expect-error
if (global.__expo?.Icon?.MaterialCommunityIcons) {
// Snack doesn't properly bundle vector icons from sub-path
// Use icons from the __expo global if available
// @ts-expect-error
MaterialCommunityIcons = global.__expo.Icon.MaterialCommunityIcons;
} else {
let isErrorLogged = false;
// Fallback component for icons
MaterialCommunityIcons = () => {
if (!isErrorLogged) {
if (
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
e.message
)
) {
console.error(e);
}
console.warn(
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons' could not be loaded.`,
`To remove this warning, try installing 'react-native-vector-icons' or use another method.`
);
isErrorLogged = true;
}
return null;
};
}
}
function MaterialBottomTabViewInner({ function MaterialBottomTabViewInner({
state, state,
navigation, navigation,

View File

@@ -3,6 +3,42 @@
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.3.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.2...@react-navigation/material-top-tabs@5.3.3) (2020-11-03)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.1...@react-navigation/material-top-tabs@5.3.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.3.0...@react-navigation/material-top-tabs@5.3.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.19...@react-navigation/material-top-tabs@5.3.0) (2020-10-24)
### Features
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.18...@react-navigation/material-top-tabs@5.2.19) (2020-10-07) ## [5.2.19](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.18...@react-navigation/material-top-tabs@5.2.19) (2020-10-07)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-top-tabs", "name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view", "description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.2.19", "version": "5.3.3",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -41,14 +41,14 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2" "color": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

@@ -6,6 +6,8 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
TabNavigationState, TabNavigationState,
TabActionHelpers,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import MaterialTopTabView from '../views/MaterialTopTabView'; import MaterialTopTabView from '../views/MaterialTopTabView';
import type { import type {
@@ -26,8 +28,9 @@ function MaterialTopTabNavigator({
...rest ...rest
}: Props) { }: Props) {
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
TabNavigationState, TabNavigationState<ParamListBase>,
TabRouterOptions, TabRouterOptions,
TabActionHelpers<ParamListBase>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
>(TabRouter, { >(TabRouter, {
@@ -48,7 +51,7 @@ function MaterialTopTabNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap, MaterialTopTabNavigationEventMap,
typeof MaterialTopTabNavigator typeof MaterialTopTabNavigator

View File

@@ -37,7 +37,8 @@ export type MaterialTopTabNavigationEventMap = {
export type MaterialTopTabNavigationHelpers = NavigationHelpers< export type MaterialTopTabNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
>; > &
TabActionHelpers<ParamListBase>;
export type MaterialTopTabNavigationProp< export type MaterialTopTabNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -45,7 +46,7 @@ export type MaterialTopTabNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
TabNavigationState, TabNavigationState<ParamList>,
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap
> & > &
@@ -94,7 +95,7 @@ export type MaterialTopTabNavigationOptions = {
export type MaterialTopTabDescriptor = Descriptor< export type MaterialTopTabDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
TabNavigationState, TabNavigationState<ParamListBase>,
MaterialTopTabNavigationOptions MaterialTopTabNavigationOptions
>; >;
@@ -192,7 +193,7 @@ export type MaterialTopTabBarOptions = Partial<
export type MaterialTopTabBarProps = MaterialTopTabBarOptions & export type MaterialTopTabBarProps = MaterialTopTabBarOptions &
SceneRendererProps & { SceneRendererProps & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: NavigationHelpers< navigation: NavigationHelpers<
ParamListBase, ParamListBase,
MaterialTopTabNavigationEventMap MaterialTopTabNavigationEventMap

View File

@@ -4,6 +4,7 @@ import {
NavigationHelpersContext, NavigationHelpersContext,
TabNavigationState, TabNavigationState,
TabActions, TabActions,
ParamListBase,
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
@@ -16,7 +17,7 @@ import type {
} from '../types'; } from '../types';
type Props = MaterialTopTabNavigationConfig & { type Props = MaterialTopTabNavigationConfig & {
state: TabNavigationState; state: TabNavigationState<ParamListBase>;
navigation: MaterialTopTabNavigationHelpers; navigation: MaterialTopTabNavigationHelpers;
descriptors: MaterialTopTabDescriptorMap; descriptors: MaterialTopTabDescriptorMap;
tabBarPosition?: 'top' | 'bottom'; tabBarPosition?: 'top' | 'bottom';

View File

@@ -3,6 +3,46 @@
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.8.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.2...@react-navigation/native@5.8.3) (2020-11-03)
### Bug Fixes
* make sure that invalid linking config doesn't work if app is open ([52451d1](https://github.com/react-navigation/react-navigation/commit/52451d11094b8551e3c6950b3e005d68225c7da9))
## [5.8.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.1...@react-navigation/native@5.8.2) (2020-10-30)
**Note:** Version bump only for package @react-navigation/native
## [5.8.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.8.0...@react-navigation/native@5.8.1) (2020-10-28)
**Note:** Version bump only for package @react-navigation/native
# [5.8.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.6...@react-navigation/native@5.8.0) (2020-10-24)
### Features
* add `getInitialURL` and `subscribe` options to linking config ([748e92f](https://github.com/react-navigation/react-navigation/commit/748e92f120b9ff73c6b1e14515f60c76701081db))
* allow deep linking to reset state ([#8973](https://github.com/react-navigation/react-navigation/issues/8973)) ([7f3b27a](https://github.com/react-navigation/react-navigation/commit/7f3b27a9ec8edd9604ac19774baa1f60963ccdc9)), closes [#8952](https://github.com/react-navigation/react-navigation/issues/8952)
* support wildcard string prefixes ([#8942](https://github.com/react-navigation/react-navigation/issues/8942)) ([23ab350](https://github.com/react-navigation/react-navigation/commit/23ab3504921b7e741a48d66c6a953905206df4b7)), closes [#8941](https://github.com/react-navigation/react-navigation/issues/8941)
## [5.7.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.5...@react-navigation/native@5.7.6) (2020-10-07) ## [5.7.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.5...@react-navigation/native@5.7.6) (2020-10-07)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/native", "name": "@react-navigation/native",
"description": "React Native integration for React Navigation", "description": "React Native integration for React Navigation",
"version": "5.7.6", "version": "5.8.3",
"keywords": [ "keywords": [
"react-native", "react-native",
"react-navigation", "react-navigation",
@@ -37,15 +37,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.12.5", "@react-navigation/core": "^5.13.3",
"nanoid": "^3.1.12" "escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",

View File

@@ -27,12 +27,23 @@ export type LinkingOptions = {
* The prefixes are stripped from the URL before parsing them. * The prefixes are stripped from the URL before parsing them.
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`) * Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)
* Only applicable on Android and iOS. * Only applicable on Android and iOS.
*
* @example
* ```js
* {
* prefixes: [
* "myapp://", // App-specific scheme
* "https://example.com", // Prefix for universal links
* "https://*.example.com" // Prefix which matches any subdomain
* ]
* }
* ```
*/ */
prefixes: string[]; prefixes: string[];
/** /**
* Config to fine-tune how to parse the path. * Config to fine-tune how to parse the path.
* *
* Example: * @example
* ```js * ```js
* { * {
* Chat: { * Chat: {
@@ -43,13 +54,47 @@ export type LinkingOptions = {
* ``` * ```
*/ */
config?: { initialRouteName?: string; screens: PathConfigMap }; config?: { initialRouteName?: string; screens: PathConfigMap };
/**
* Custom function to get the initial URL used for linking.
* Uses `Linking.getInitialURL()` by default.
* Not supported on Web.
*
* @example
* ```js
* {
* getInitialURL () => Linking.getInitialURL(),
* }
* ```
*/
getInitialURL?: () => Promise<string | null | undefined>;
/**
* Custom function to get subscribe to URL updates.
* Uses `Linking.addEventListener('url', callback)` by default.
* Not supported on Web.
*
* @example
* ```js
* {
* subscribe: (listener) => {
* const onReceiveURL = ({ url }) => listener(url);
*
* Linking.addEventListener('url', onReceiveURL);
*
* return () => Linking.removeEventListener('url', onReceiveURL);
* }
* }
* ```
*/
subscribe?: (
listener: (url: string) => void
) => undefined | void | (() => void);
/** /**
* Custom function to parse the URL to a valid navigation state (advanced). * Custom function to parse the URL to a valid navigation state (advanced).
* Only applicable on Web.
*/ */
getStateFromPath?: typeof getStateFromPathDefault; getStateFromPath?: typeof getStateFromPathDefault;
/** /**
* Custom function to convert the state object to a valid URL (advanced). * Custom function to convert the state object to a valid URL (advanced).
* Only applicable on Web.
*/ */
getPathFromState?: typeof getPathFromStateDefault; getPathFromState?: typeof getPathFromStateDefault;
}; };

View File

@@ -22,7 +22,7 @@ export default function useLinkProps({ to, action }: Props) {
const linkTo = useLinkTo(); const linkTo = useLinkTo();
const onPress = ( const onPress = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent e?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => { ) => {
let shouldHandle = false; let shouldHandle = false;

View File

@@ -37,7 +37,7 @@ export default function useLinkTo() {
root = current; root = current;
} }
const action = getActionFromState(state); const action = getActionFromState(state, options?.config);
if (action !== undefined) { if (action !== undefined) {
root.dispatch(action); root.dispatch(action);

View File

@@ -6,6 +6,7 @@ import {
NavigationContainerRef, NavigationContainerRef,
} from '@react-navigation/core'; } from '@react-navigation/core';
import type { LinkingOptions } from './types'; import type { LinkingOptions } from './types';
import escapeStringRegexp from 'escape-string-regexp';
let isUsingLinking = false; let isUsingLinking = false;
@@ -15,6 +16,22 @@ export default function useLinking(
enabled = true, enabled = true,
prefixes, prefixes,
config, config,
getInitialURL = () =>
Promise.race([
Linking.getInitialURL(),
new Promise<undefined>((resolve) =>
// Timeout in 150ms if `getInitialState` doesn't resolve
// Workaround for https://github.com/facebook/react-native/issues/25675
setTimeout(resolve, 150)
),
]),
subscribe = (listener) => {
const callback = ({ url }: { url: string }) => listener(url);
Linking.addEventListener('url', callback);
return () => Linking.removeEventListener('url', callback);
},
getStateFromPath = getStateFromPathDefault, getStateFromPath = getStateFromPathDefault,
}: LinkingOptions }: LinkingOptions
) { ) {
@@ -47,19 +64,29 @@ export default function useLinking(
const enabledRef = React.useRef(enabled); const enabledRef = React.useRef(enabled);
const prefixesRef = React.useRef(prefixes); const prefixesRef = React.useRef(prefixes);
const configRef = React.useRef(config); const configRef = React.useRef(config);
const getInitialURLRef = React.useRef(getInitialURL);
const getStateFromPathRef = React.useRef(getStateFromPath); const getStateFromPathRef = React.useRef(getStateFromPath);
React.useEffect(() => { React.useEffect(() => {
enabledRef.current = enabled; enabledRef.current = enabled;
prefixesRef.current = prefixes; prefixesRef.current = prefixes;
configRef.current = config; configRef.current = config;
getInitialURLRef.current = getInitialURL;
getStateFromPathRef.current = getStateFromPath; getStateFromPathRef.current = getStateFromPath;
}, [config, enabled, getStateFromPath, prefixes]); }, [config, enabled, prefixes, getInitialURL, getStateFromPath]);
const extractPathFromURL = React.useCallback((url: string) => { const extractPathFromURL = React.useCallback((url: string) => {
for (const prefix of prefixesRef.current) { for (const prefix of prefixesRef.current) {
if (url.startsWith(prefix)) { const protocol = prefix.match(/^[^:]+:\/\//)?.[0] ?? '';
return url.replace(prefix, ''); const host = prefix.replace(protocol, '');
const prefixRegex = new RegExp(
`^${escapeStringRegexp(protocol)}${host
.split('.')
.map((it) => (it === '*' ? '[^/]+' : escapeStringRegexp(it)))
.join('\\.')}`
);
if (prefixRegex.test(url)) {
return url.replace(prefixRegex, '');
} }
} }
@@ -71,15 +98,7 @@ export default function useLinking(
return undefined; return undefined;
} }
const url = await (Promise.race([ const url = await getInitialURLRef.current();
Linking.getInitialURL(),
new Promise((resolve) =>
// Timeout in 150ms if `getInitialState` doesn't resolve
// Workaround for https://github.com/facebook/react-native/issues/25675
setTimeout(resolve, 150)
),
]) as Promise<string | null | undefined>);
const path = url ? extractPathFromURL(url) : null; const path = url ? extractPathFromURL(url) : null;
if (path) { if (path) {
@@ -90,7 +109,7 @@ export default function useLinking(
}, [extractPathFromURL]); }, [extractPathFromURL]);
React.useEffect(() => { React.useEffect(() => {
const listener = ({ url }: { url: string }) => { const listener = (url: string) => {
if (!enabled) { if (!enabled) {
return; return;
} }
@@ -102,7 +121,20 @@ export default function useLinking(
const state = getStateFromPathRef.current(path, configRef.current); const state = getStateFromPathRef.current(path, configRef.current);
if (state) { if (state) {
const action = getActionFromState(state); // Make sure that the routes in the state exist in the root navigator
// Otherwise there's an error in the linking configuration
const rootState = navigation.getRootState();
if (
state.routes.some((r) => !rootState?.routeNames.includes(r.name))
) {
console.warn(
"The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration."
);
return;
}
const action = getActionFromState(state, configRef.current);
if (action !== undefined) { if (action !== undefined) {
navigation.dispatch(action); navigation.dispatch(action);
@@ -113,10 +145,8 @@ export default function useLinking(
} }
}; };
Linking.addEventListener('url', listener); return subscribe(listener);
}, [enabled, ref, subscribe, extractPathFromURL]);
return () => Linking.removeEventListener('url', listener);
}, [enabled, extractPathFromURL, ref]);
return { return {
getInitialState, getInitialState,

View File

@@ -399,11 +399,26 @@ export default function useLinking(
// We should only dispatch an action when going forward // We should only dispatch an action when going forward
// Otherwise the action will likely add items to history, which would mess things up // Otherwise the action will likely add items to history, which would mess things up
if (state && index > previousIndex) { if (state) {
const action = getActionFromState(state); // Make sure that the routes in the state exist in the root navigator
// Otherwise there's an error in the linking configuration
const rootState = navigation.getRootState();
if (action !== undefined) { if (state.routes.some((r) => !rootState?.routeNames.includes(r.name))) {
navigation.dispatch(action); console.warn(
"The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration."
);
return;
}
if (index > previousIndex) {
const action = getActionFromState(state, configRef.current);
if (action !== undefined) {
navigation.dispatch(action);
} else {
navigation.resetRoot(state);
}
} else { } else {
navigation.resetRoot(state); navigation.resetRoot(state);
} }

View File

@@ -3,6 +3,34 @@
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.5.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.5.0...@react-navigation/routers@5.5.1) (2020-10-28)
### Bug Fixes
* improve types for route prop in screenOptions ([d26bcc0](https://github.com/react-navigation/react-navigation/commit/d26bcc057ef31f8950f909adf83e263171a42d74))
# [5.5.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.12...@react-navigation/routers@5.5.0) (2020-10-24)
### Bug Fixes
* handle pushing a route with duplicate key ([091b2a2](https://github.com/react-navigation/react-navigation/commit/091b2a2038af1097be57a1bb451623d64a1efc92))
### Features
* allow deep linking to reset state ([#8973](https://github.com/react-navigation/react-navigation/issues/8973)) ([7f3b27a](https://github.com/react-navigation/react-navigation/commit/7f3b27a9ec8edd9604ac19774baa1f60963ccdc9)), closes [#8952](https://github.com/react-navigation/react-navigation/issues/8952)
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
## [5.4.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.11...@react-navigation/routers@5.4.12) (2020-09-22) ## [5.4.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.11...@react-navigation/routers@5.4.12) (2020-09-22)
**Note:** Version bump only for package @react-navigation/routers **Note:** Version bump only for package @react-navigation/routers

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/routers", "name": "@react-navigation/routers",
"description": "Routers to help build custom navigators", "description": "Routers to help build custom navigators",
"version": "5.4.12", "version": "5.5.1",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -36,7 +36,7 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"nanoid": "^3.1.12" "nanoid": "^3.1.15"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",

View File

@@ -25,8 +25,8 @@ export type DrawerRouterOptions = TabRouterOptions & {
openByDefault?: boolean; openByDefault?: boolean;
}; };
export type DrawerNavigationState = Omit< export type DrawerNavigationState<ParamList extends ParamListBase> = Omit<
TabNavigationState, TabNavigationState<ParamList>,
'type' | 'history' 'type' | 'history'
> & { > & {
/** /**
@@ -72,10 +72,14 @@ export const DrawerActions = {
}; };
const isDrawerOpen = ( const isDrawerOpen = (
state: DrawerNavigationState | PartialState<DrawerNavigationState> state:
| DrawerNavigationState<ParamListBase>
| PartialState<DrawerNavigationState<ParamListBase>>
) => Boolean(state.history?.find((it) => it.type === 'drawer')); ) => Boolean(state.history?.find((it) => it.type === 'drawer'));
const openDrawer = (state: DrawerNavigationState): DrawerNavigationState => { const openDrawer = (
state: DrawerNavigationState<ParamListBase>
): DrawerNavigationState<ParamListBase> => {
if (isDrawerOpen(state)) { if (isDrawerOpen(state)) {
return state; return state;
} }
@@ -86,7 +90,9 @@ const openDrawer = (state: DrawerNavigationState): DrawerNavigationState => {
}; };
}; };
const closeDrawer = (state: DrawerNavigationState): DrawerNavigationState => { const closeDrawer = (
state: DrawerNavigationState<ParamListBase>
): DrawerNavigationState<ParamListBase> => {
if (!isDrawerOpen(state)) { if (!isDrawerOpen(state)) {
return state; return state;
} }
@@ -101,11 +107,11 @@ export default function DrawerRouter({
openByDefault, openByDefault,
...rest ...rest
}: DrawerRouterOptions): Router< }: DrawerRouterOptions): Router<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
DrawerActionType | CommonNavigationAction DrawerActionType | CommonNavigationAction
> { > {
const router = (TabRouter(rest) as unknown) as Router< const router = (TabRouter(rest) as unknown) as Router<
DrawerNavigationState, DrawerNavigationState<ParamListBase>,
TabActionType | CommonNavigationAction TabActionType | CommonNavigationAction
>; >;

View File

@@ -36,7 +36,9 @@ export type StackActionType =
export type StackRouterOptions = DefaultRouterOptions; export type StackRouterOptions = DefaultRouterOptions;
export type StackNavigationState = NavigationState & { export type StackNavigationState<
ParamList extends ParamListBase
> = NavigationState<ParamList> & {
/** /**
* Type of the router, in this case, it's stack. * Type of the router, in this case, it's stack.
*/ */
@@ -96,7 +98,7 @@ export const StackActions = {
export default function StackRouter(options: StackRouterOptions) { export default function StackRouter(options: StackRouterOptions) {
const router: Router< const router: Router<
StackNavigationState, StackNavigationState<ParamListBase>,
CommonNavigationAction | StackActionType CommonNavigationAction | StackActionType
> = { > = {
...BaseRouter, ...BaseRouter,
@@ -256,10 +258,35 @@ export default function StackRouter(options: StackRouterOptions) {
case 'PUSH': case 'PUSH':
if (state.routeNames.includes(action.payload.name)) { if (state.routeNames.includes(action.payload.name)) {
return { const route =
...state, action.payload.name && action.payload.key
index: state.index + 1, ? state.routes.find(
routes: [ (route) =>
route.name === action.payload.name &&
route.key === action.payload.key
)
: undefined;
let routes: Route<string>[];
if (route) {
routes = state.routes.filter((r) => r.key !== route.key);
routes.push(
action.payload.params
? {
...route,
params:
action.payload.params !== undefined
? {
...route.params,
...action.payload.params,
}
: route.params,
}
: route
);
} else {
routes = [
...state.routes, ...state.routes,
{ {
key: key:
@@ -275,7 +302,13 @@ export default function StackRouter(options: StackRouterOptions) {
} }
: action.payload.params, : action.payload.params,
}, },
], ];
}
return {
...state,
index: routes.length - 1,
routes,
}; };
} }

View File

@@ -23,7 +23,10 @@ export type TabRouterOptions = DefaultRouterOptions & {
backBehavior?: BackBehavior; backBehavior?: BackBehavior;
}; };
export type TabNavigationState = Omit<NavigationState, 'history'> & { export type TabNavigationState<ParamList extends ParamListBase> = Omit<
NavigationState<ParamList>,
'history'
> & {
/** /**
* Type of the router, in this case, it's tab. * Type of the router, in this case, it's tab.
*/ */
@@ -93,7 +96,7 @@ const getRouteHistory = (
}; };
const changeIndex = ( const changeIndex = (
state: TabNavigationState, state: TabNavigationState<ParamListBase>,
index: number, index: number,
backBehavior: BackBehavior, backBehavior: BackBehavior,
initialRouteName: string | undefined initialRouteName: string | undefined
@@ -127,7 +130,7 @@ export default function TabRouter({
backBehavior = 'history', backBehavior = 'history',
}: TabRouterOptions) { }: TabRouterOptions) {
const router: Router< const router: Router<
TabNavigationState, TabNavigationState<ParamListBase>,
TabActionType | CommonNavigationAction TabActionType | CommonNavigationAction
> = { > = {
...BaseRouter, ...BaseRouter,
@@ -172,9 +175,9 @@ export default function TabRouter({
} }
const routes = routeNames.map((name) => { const routes = routeNames.map((name) => {
const route = (state as PartialState<TabNavigationState>).routes.find( const route = (state as PartialState<
(r) => r.name === name TabNavigationState<ParamListBase>
); >).routes.find((r) => r.name === name);
return { return {
...route, ...route,

View File

@@ -3,6 +3,7 @@ import {
DrawerRouter, DrawerRouter,
DrawerActions, DrawerActions,
DrawerNavigationState, DrawerNavigationState,
ParamListBase,
} from '..'; } from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' })); jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
@@ -199,7 +200,7 @@ it('gets rehydrated state from partial state', () => {
it("doesn't rehydrate state if it's not stale", () => { it("doesn't rehydrate state if it's not stale", () => {
const router = DrawerRouter({}); const router = DrawerRouter({});
const state: DrawerNavigationState = { const state: DrawerNavigationState<ParamListBase> = {
index: 0, index: 0,
key: 'drawer-test', key: 'drawer-test',
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
@@ -340,7 +341,7 @@ it('handles open drawer action', () => {
history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }], history: [{ type: 'route', key: 'bar' }, { type: 'drawer' }],
}); });
const state: DrawerNavigationState = { const state: DrawerNavigationState<ParamListBase> = {
stale: false as const, stale: false as const,
type: 'drawer' as const, type: 'drawer' as const,
key: 'root', key: 'root',
@@ -395,7 +396,7 @@ it('handles close drawer action', () => {
history: [{ type: 'route', key: 'bar' }], history: [{ type: 'route', key: 'bar' }],
}); });
const state: DrawerNavigationState = { const state: DrawerNavigationState<ParamListBase> = {
stale: false as const, stale: false as const,
type: 'drawer' as const, type: 'drawer' as const,
key: 'root', key: 'root',
@@ -487,7 +488,7 @@ it('handles toggle drawer action', () => {
it('updates history on focus change', () => { it('updates history on focus change', () => {
const router = DrawerRouter({ backBehavior: 'history' }); const router = DrawerRouter({ backBehavior: 'history' });
const state: DrawerNavigationState = { const state: DrawerNavigationState<ParamListBase> = {
index: 0, index: 0,
key: 'drawer-test', key: 'drawer-test',
routeNames: ['baz', 'bar'], routeNames: ['baz', 'bar'],

View File

@@ -848,7 +848,7 @@ it('handles push action', () => {
stale: false, stale: false,
type: 'stack', type: 'stack',
key: 'root', key: 'root',
index: 3, index: 1,
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routes: [ routes: [
{ key: 'bar', name: 'bar' }, { key: 'bar', name: 'bar' },
@@ -873,7 +873,7 @@ it('handles push action', () => {
stale: false, stale: false,
type: 'stack', type: 'stack',
key: 'root', key: 'root',
index: 3, index: 1,
routeNames: ['baz', 'bar', 'qux'], routeNames: ['baz', 'bar', 'qux'],
routes: [ routes: [
{ key: 'bar', name: 'bar' }, { key: 'bar', name: 'bar' },
@@ -895,6 +895,73 @@ it('handles push action', () => {
options options
) )
).toBe(null); ).toBe(null);
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar-3', name: 'bar' },
{ key: 'bar-4', name: 'bar', params: { foo: 21 } },
{ key: 'baz-5', name: 'baz' },
],
},
{
type: 'PUSH',
payload: { name: 'bar', key: 'bar-4', params: { bar: 29 } },
},
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar-3', name: 'bar' },
{ key: 'baz-5', name: 'baz' },
{ key: 'bar-4', name: 'bar', params: { foo: 21, bar: 29 } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar-3', name: 'bar' },
{ key: 'bar-4', name: 'bar' },
{ key: 'baz-5', name: 'baz' },
],
},
{
type: 'PUSH',
payload: { name: 'bar', key: 'bar-6', params: { bar: 29 } },
},
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 3,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'bar-3', name: 'bar' },
{ key: 'bar-4', name: 'bar' },
{ key: 'baz-5', name: 'baz' },
{ key: 'bar-6', name: 'bar', params: { bar: 29 } },
],
});
}); });
it('changes index on focus change', () => { it('changes index on focus change', () => {

View File

@@ -1,4 +1,10 @@
import { CommonActions, TabRouter, TabActions, TabNavigationState } from '..'; import {
CommonActions,
TabRouter,
TabActions,
TabNavigationState,
ParamListBase,
} from '..';
jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' })); jest.mock('nanoid/non-secure', () => ({ nanoid: () => 'test' }));
@@ -217,7 +223,7 @@ it('gets rehydrated state from partial state', () => {
it("doesn't rehydrate state if it's not stale", () => { it("doesn't rehydrate state if it's not stale", () => {
const router = TabRouter({}); const router = TabRouter({});
const state: TabNavigationState = { const state: TabNavigationState<ParamListBase> = {
index: 0, index: 0,
key: 'tab-test', key: 'tab-test',
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
@@ -699,7 +705,7 @@ it('handles back action with backBehavior: history', () => {
state, state,
TabActions.jumpTo('qux'), TabActions.jumpTo('qux'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -721,7 +727,7 @@ it('handles back action with backBehavior: history', () => {
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -746,7 +752,7 @@ it('handles back action with backBehavior: history', () => {
state, state,
TabActions.jumpTo('bar'), TabActions.jumpTo('bar'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -785,7 +791,7 @@ it('handles back action with backBehavior: order', () => {
state, state,
TabActions.jumpTo('qux'), TabActions.jumpTo('qux'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -810,7 +816,7 @@ it('handles back action with backBehavior: order', () => {
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -832,7 +838,7 @@ it('handles back action with backBehavior: order', () => {
state, state,
TabActions.jumpTo('bar'), TabActions.jumpTo('bar'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -856,7 +862,7 @@ it('handles back action with backBehavior: initialRoute', () => {
state, state,
TabActions.jumpTo('qux'), TabActions.jumpTo('qux'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -878,7 +884,7 @@ it('handles back action with backBehavior: initialRoute', () => {
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -900,7 +906,7 @@ it('handles back action with backBehavior: initialRoute', () => {
state, state,
TabActions.jumpTo('bar'), TabActions.jumpTo('bar'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -928,7 +934,7 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
state, state,
TabActions.jumpTo('qux'), TabActions.jumpTo('qux'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -950,7 +956,7 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
state, state,
TabActions.jumpTo('bar'), TabActions.jumpTo('bar'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -972,7 +978,7 @@ it('handles back action with backBehavior: initialRoute and initialRouteName', (
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -992,7 +998,7 @@ it('handles back action with backBehavior: none', () => {
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect( expect(
router.getStateForAction(state, CommonActions.goBack(), options) router.getStateForAction(state, CommonActions.goBack(), options)
@@ -1006,7 +1012,7 @@ it('updates route key history on navigate and jump to', () => {
routeParamList: {}, routeParamList: {},
}; };
let state: TabNavigationState = { let state: TabNavigationState<ParamListBase> = {
index: 1, index: 1,
key: 'tab-test', key: 'tab-test',
routeNames: ['bar', 'baz', 'qux'], routeNames: ['bar', 'baz', 'qux'],
@@ -1024,7 +1030,7 @@ it('updates route key history on navigate and jump to', () => {
state, state,
TabActions.jumpTo('qux'), TabActions.jumpTo('qux'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect(state.history).toEqual([ expect(state.history).toEqual([
{ type: 'route', key: 'baz-0' }, { type: 'route', key: 'baz-0' },
@@ -1035,7 +1041,7 @@ it('updates route key history on navigate and jump to', () => {
state, state,
CommonActions.navigate('bar'), CommonActions.navigate('bar'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect(state.history).toEqual([ expect(state.history).toEqual([
{ type: 'route', key: 'baz-0' }, { type: 'route', key: 'baz-0' },
@@ -1047,7 +1053,7 @@ it('updates route key history on navigate and jump to', () => {
state, state,
TabActions.jumpTo('baz'), TabActions.jumpTo('baz'),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect(state.history).toEqual([ expect(state.history).toEqual([
{ type: 'route', key: 'qux-0' }, { type: 'route', key: 'qux-0' },
@@ -1059,7 +1065,7 @@ it('updates route key history on navigate and jump to', () => {
state, state,
CommonActions.goBack(), CommonActions.goBack(),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect(state.history).toEqual([ expect(state.history).toEqual([
{ type: 'route', key: 'qux-0' }, { type: 'route', key: 'qux-0' },
@@ -1070,7 +1076,7 @@ it('updates route key history on navigate and jump to', () => {
state, state,
CommonActions.goBack(), CommonActions.goBack(),
options options
) as TabNavigationState; ) as TabNavigationState<ParamListBase>;
expect(state.history).toEqual([{ type: 'route', key: 'qux-0' }]); expect(state.history).toEqual([{ type: 'route', key: 'qux-0' }]);
}); });

View File

@@ -2,7 +2,16 @@ import type * as CommonActions from './CommonActions';
export type CommonNavigationAction = CommonActions.Action; export type CommonNavigationAction = CommonActions.Action;
export type NavigationState = Readonly<{ type NavigationRoute<
ParamList extends ParamListBase,
RouteName extends keyof ParamList
> = Route<Extract<RouteName, string>, ParamList[RouteName]> & {
state?: NavigationState | PartialState<NavigationState>;
};
export type NavigationState<
ParamList extends ParamListBase = ParamListBase
> = Readonly<{
/** /**
* Unique key for the navigation state. * Unique key for the navigation state.
*/ */
@@ -14,7 +23,7 @@ export type NavigationState = Readonly<{
/** /**
* List of valid route names as defined in the screen components. * List of valid route names as defined in the screen components.
*/ */
routeNames: string[]; routeNames: Extract<keyof ParamList, string>[];
/** /**
* Alternative entries for history. * Alternative entries for history.
*/ */
@@ -22,9 +31,7 @@ export type NavigationState = Readonly<{
/** /**
* List of rendered routes. * List of rendered routes.
*/ */
routes: (Route<string> & { routes: NavigationRoute<ParamList, keyof ParamList>[];
state?: NavigationState | PartialState<NavigationState>;
})[];
/** /**
* Custom type for the state, whether it's for tab, stack, drawer etc. * Custom type for the state, whether it's for tab, stack, drawer etc.
* During rehydration, the state will be discarded if type doesn't match with router type. * During rehydration, the state will be discarded if type doesn't match with router type.
@@ -43,19 +50,24 @@ export type InitialState = Readonly<
} }
>; >;
export type PartialRoute<R extends Route<string>> = Omit<R, 'key'> & {
key?: string;
state?: PartialState<NavigationState>;
};
export type PartialState<State extends NavigationState> = Partial< export type PartialState<State extends NavigationState> = Partial<
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'> Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
> & > &
Readonly<{ Readonly<{
stale?: true; stale?: true;
type?: string; type?: string;
routes: (Omit<Route<string>, 'key'> & { routes: PartialRoute<Route<string>>[];
key?: string;
state?: InitialState;
})[];
}>; }>;
export type Route<RouteName extends string> = Readonly<{ export type Route<
RouteName extends string,
Params extends object | undefined = object | undefined
> = Readonly<{
/** /**
* Unique key for the route. * Unique key for the route.
*/ */
@@ -64,11 +76,20 @@ export type Route<RouteName extends string> = Readonly<{
* User-provided name for the route. * User-provided name for the route.
*/ */
name: RouteName; name: RouteName;
/** }> &
* Params for the route. (undefined extends Params
*/ ? Readonly<{
params?: object; /**
}>; * Params for this route
*/
params?: Readonly<Params>;
}>
: Readonly<{
/**
* Params for this route
*/
params: Readonly<Params>;
}>);
export type ParamListBase = Record<string, object | undefined>; export type ParamListBase = Record<string, object | undefined>;
@@ -95,12 +116,12 @@ export type ActionCreators<Action extends NavigationAction> = {
[key: string]: (...args: any) => Action; [key: string]: (...args: any) => Action;
}; };
export type DefaultRouterOptions = { export type DefaultRouterOptions<RouteName extends string = string> = {
/** /**
* Name of the route to focus by on initial render. * Name of the route to focus by on initial render.
* If not specified, usually the first route is used. * If not specified, usually the first route is used.
*/ */
initialRouteName?: string; initialRouteName?: RouteName;
}; };
export type RouterFactory< export type RouterFactory<

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.12.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.11.1...@react-navigation/stack@5.12.0) (2020-11-03)
### Features
* add a headerBackAccessibilityLabel option in stack ([c326c10](https://github.com/react-navigation/react-navigation/commit/c326c106f9a2492ff45bdc8da9bfbc404e48786a)), closes [#9016](https://github.com/react-navigation/react-navigation/issues/9016)
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.11.0...@react-navigation/stack@5.11.1) (2020-10-30)
**Note:** Version bump only for package @react-navigation/stack
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.10.0...@react-navigation/stack@5.11.0) (2020-10-28)
### Features
* enable react-native-screens in Stack by default on iOS ([45dbe5c](https://github.com/react-navigation/react-navigation/commit/45dbe5c40ebc66c62593b3fad35cff3048b888a4))
# [5.10.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.9.3...@react-navigation/stack@5.10.0) (2020-10-24)
### Bug Fixes
* add missing check for parent header when calculating height ([da91cec](https://github.com/react-navigation/react-navigation/commit/da91cec941e7dec352ba1910901904d769c9f3a3))
* don't set statusbar height on nested header by default ([38e17aa](https://github.com/react-navigation/react-navigation/commit/38e17aae939974b47fe5c77d8670b9a4544250f2))
* fix header buttons not pressable when headerTransparent=true & headerMode=float ([#8804](https://github.com/react-navigation/react-navigation/issues/8804)) ([d6cac67](https://github.com/react-navigation/react-navigation/commit/d6cac6713a51e4de320fc1c7ece72a2b92241574)), closes [#8731](https://github.com/react-navigation/react-navigation/issues/8731)
* set needsOffscreenAlphaCompositing and update default android animation ([8ee0dda](https://github.com/react-navigation/react-navigation/commit/8ee0dda155b4cde43be1e58170e44823b54e7d4f)), closes [#8696](https://github.com/react-navigation/react-navigation/issues/8696)
### Features
* add optional screens per navigator ([#8805](https://github.com/react-navigation/react-navigation/issues/8805)) ([7196889](https://github.com/react-navigation/react-navigation/commit/7196889bf1218eb6a736d9475e33a909c2248c3b))
* improve types for navigation state ([#8980](https://github.com/react-navigation/react-navigation/issues/8980)) ([7dc2f58](https://github.com/react-navigation/react-navigation/commit/7dc2f5832e371473f3263c01ab39824eb9e2057d))
* update helper types to have navigator specific methods ([f51086e](https://github.com/react-navigation/react-navigation/commit/f51086edea42f2382dac8c6914aac8574132114b))
## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.9.2...@react-navigation/stack@5.9.3) (2020-10-07) ## [5.9.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.9.2...@react-navigation/stack@5.9.3) (2020-10-07)
**Note:** Version bump only for package @react-navigation/stack **Note:** Version bump only for package @react-navigation/stack

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/stack", "name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures", "description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "5.9.3", "version": "5.12.0",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -40,17 +40,17 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"color": "^3.1.2", "color": "^3.1.3",
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.16.2", "@react-native-community/bob": "^0.16.2",
"@react-native-community/masked-view": "^0.1.10", "@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.7.6", "@react-navigation/native": "^5.8.3",
"@testing-library/react-native": "^7.0.2", "@testing-library/react-native": "^7.1.0",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.51", "@types/react": "^16.9.53",
"@types/react-native": "^0.63.25", "@types/react-native": "^0.63.30",
"del-cli": "^3.0.1", "del-cli": "^3.0.1",
"react": "~16.13.1", "react": "~16.13.1",
"react-native": "~0.63.2", "react-native": "~0.63.2",

View File

@@ -289,8 +289,8 @@ export function forScaleFromCenterAndroid({
); );
const opacity = progress.interpolate({ const opacity = progress.interpolate({
inputRange: [0, 0.8, 1, 1.2, 2], inputRange: [0, 0.75, 0.875, 1, 1.0825, 1.2075, 2],
outputRange: [0, 0.5, 1, 0.33, 0], outputRange: [0, 0, 1, 1, 1, 1, 0],
}); });
const scale = conditional( const scale = conditional(

View File

@@ -18,6 +18,7 @@ import {
import type { TransitionPreset } from '../types'; import type { TransitionPreset } from '../types';
const ANDROID_VERSION_PIE = 28; const ANDROID_VERSION_PIE = 28;
const ANDROID_VERSION_10 = 29;
/** /**
* Standard iOS navigation transition. * Standard iOS navigation transition.
@@ -102,10 +103,13 @@ export const ScaleFromCenterAndroid: TransitionPreset = {
*/ */
export const DefaultTransition = Platform.select({ export const DefaultTransition = Platform.select({
ios: SlideFromRightIOS, ios: SlideFromRightIOS,
default: android:
Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_PIE Platform.Version >= ANDROID_VERSION_10
? ScaleFromCenterAndroid
: Platform.Version >= ANDROID_VERSION_PIE
? RevealFromBottomAndroid ? RevealFromBottomAndroid
: FadeFromBottomAndroid, : FadeFromBottomAndroid,
default: ScaleFromCenterAndroid,
}); });
/** /**

View File

@@ -9,6 +9,8 @@ import {
StackRouterOptions, StackRouterOptions,
StackNavigationState, StackNavigationState,
StackActions, StackActions,
ParamListBase,
StackActionHelpers,
} from '@react-navigation/native'; } from '@react-navigation/native';
import StackView from '../views/Stack/StackView'; import StackView from '../views/Stack/StackView';
import type { import type {
@@ -36,8 +38,9 @@ function StackNavigator({
}; };
const { state, descriptors, navigation } = useNavigationBuilder< const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState, StackNavigationState<ParamListBase>,
StackRouterOptions, StackRouterOptions,
StackActionHelpers<ParamListBase>,
StackNavigationOptions, StackNavigationOptions,
StackNavigationEventMap StackNavigationEventMap
>(StackRouter, { >(StackRouter, {
@@ -91,7 +94,7 @@ function StackNavigator({
} }
export default createNavigatorFactory< export default createNavigatorFactory<
StackNavigationState, StackNavigationState<ParamListBase>,
StackNavigationOptions, StackNavigationOptions,
StackNavigationEventMap, StackNavigationEventMap,
typeof StackNavigator typeof StackNavigator

View File

@@ -44,7 +44,8 @@ export type StackNavigationEventMap = {
export type StackNavigationHelpers = NavigationHelpers< export type StackNavigationHelpers = NavigationHelpers<
ParamListBase, ParamListBase,
StackNavigationEventMap StackNavigationEventMap
>; > &
StackActionHelpers<ParamListBase>;
export type StackNavigationProp< export type StackNavigationProp<
ParamList extends ParamListBase, ParamList extends ParamListBase,
@@ -52,7 +53,7 @@ export type StackNavigationProp<
> = NavigationProp< > = NavigationProp<
ParamList, ParamList,
RouteName, RouteName,
StackNavigationState, StackNavigationState<ParamList>,
StackNavigationOptions, StackNavigationOptions,
StackNavigationEventMap StackNavigationEventMap
> & > &
@@ -144,6 +145,10 @@ export type StackHeaderOptions = {
* Whether back button title font should scale to respect Text Size accessibility settings. Defaults to `false`. * Whether back button title font should scale to respect Text Size accessibility settings. Defaults to `false`.
*/ */
headerBackAllowFontScaling?: boolean; headerBackAllowFontScaling?: boolean;
/**
* Accessibility label for the header back button.
*/
headerBackAccessibilityLabel?: string;
/** /**
* Title string used by the back button on iOS. Defaults to the previous scene's `headerTitle`. * Title string used by the back button on iOS. Defaults to the previous scene's `headerTitle`.
* Use `headerBackTitleVisible: false` to hide it. * Use `headerBackTitleVisible: false` to hide it.
@@ -250,7 +255,7 @@ export type StackHeaderProps = {
export type StackDescriptor = Descriptor< export type StackDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,
StackNavigationState, StackNavigationState<ParamListBase>,
StackNavigationOptions StackNavigationOptions
>; >;
@@ -320,11 +325,11 @@ export type StackNavigationOptions = StackHeaderOptions &
*/ */
gestureResponseDistance?: { gestureResponseDistance?: {
/** /**
* Distance for horizontal direction. Defaults to 25. * Distance for vertical direction. Defaults to 135.
*/ */
vertical?: number; vertical?: number;
/** /**
* Distance for vertical direction. Defaults to 135. * Distance for horizontal direction. Defaults to 25.
*/ */
horizontal?: number; horizontal?: number;
}; };
@@ -344,6 +349,13 @@ export type StackNavigationOptions = StackHeaderOptions &
bottom?: number; bottom?: number;
left?: number; left?: number;
}; };
/**
* Whether to detach the previous screen from the view hierarchy to save memory.
* Set it to `false` if you need the previous screen to be seen through the active screen.
* Only applicable if `detachInactiveScreens` isn't set to `false`.
* Defaults to `false` for the last screen when mode='modal', otherwise `true`.
*/
detachPreviousScreen?: boolean;
}; };
export type StackNavigationConfig = { export type StackNavigationConfig = {
@@ -354,6 +366,12 @@ export type StackNavigationConfig = {
* Defaults to `true`. * Defaults to `true`.
*/ */
keyboardHandlingEnabled?: boolean; keyboardHandlingEnabled?: boolean;
/**
* Whether inactive screens should be detached from the view hierarchy to save memory.
* Make sure to call `enableScreens` from `react-native-screens` to make it work.
* Defaults to `true`.
*/
detachInactiveScreens?: boolean;
}; };
export type StackHeaderLeftButtonProps = { export type StackHeaderLeftButtonProps = {

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; import { Animated, View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { import {
NavigationContext, NavigationContext,
NavigationRouteContext, NavigationRouteContext,
@@ -60,7 +60,7 @@ export default function HeaderContainer({
const parentPreviousScene = React.useContext(PreviousSceneContext); const parentPreviousScene = React.useContext(PreviousSceneContext);
return ( return (
<View pointerEvents="box-none" style={style}> <Animated.View pointerEvents="box-none" style={style}>
{scenes.slice(-3).map((scene, i, self) => { {scenes.slice(-3).map((scene, i, self) => {
if ((mode === 'screen' && i !== self.length - 1) || !scene) { if ((mode === 'screen' && i !== self.length - 1) || !scene) {
return null; return null;
@@ -130,11 +130,14 @@ export default function HeaderContainer({
<View <View
onLayout={ onLayout={
onContentHeightChange onContentHeightChange
? (e) => ? (e) => {
const { height } = e.nativeEvent.layout;
onContentHeightChange({ onContentHeightChange({
route: scene.route, route: scene.route,
height: e.nativeEvent.layout.height, height,
}) });
}
: undefined : undefined
} }
pointerEvents={isFocused ? 'box-none' : 'none'} pointerEvents={isFocused ? 'box-none' : 'none'}
@@ -156,7 +159,7 @@ export default function HeaderContainer({
</NavigationContext.Provider> </NavigationContext.Provider>
); );
})} })}
</View> </Animated.View>
); );
} }

View File

@@ -11,6 +11,7 @@ import type { EdgeInsets } from 'react-native-safe-area-context';
import type { Route } from '@react-navigation/native'; import type { Route } from '@react-navigation/native';
import HeaderBackButton from './HeaderBackButton'; import HeaderBackButton from './HeaderBackButton';
import HeaderBackground from './HeaderBackground'; import HeaderBackground from './HeaderBackground';
import HeaderShownContext from '../../utils/HeaderShownContext';
import memoize from '../../utils/memoize'; import memoize from '../../utils/memoize';
import type { import type {
Layout, Layout,
@@ -32,11 +33,6 @@ type Props = StackHeaderOptions & {
styleInterpolator: StackHeaderStyleInterpolator; styleInterpolator: StackHeaderStyleInterpolator;
}; };
type State = {
titleLayout?: Layout;
leftLabelLayout?: Layout;
};
const warnIfHeaderStylesDefined = (styles: Record<string, any>) => { const warnIfHeaderStylesDefined = (styles: Record<string, any>) => {
Object.keys(styles).forEach((styleProp) => { Object.keys(styles).forEach((styleProp) => {
const value = styles[styleProp]; const value = styles[styleProp];
@@ -76,30 +72,35 @@ export const getDefaultHeaderHeight = (
return headerHeight + statusBarHeight; return headerHeight + statusBarHeight;
}; };
export default class HeaderSegment extends React.Component<Props, State> { export default function HeaderSegment(props: Props) {
state: State = {}; const [leftLabelLayout, setLeftLabelLayout] = React.useState<
Layout | undefined
>(undefined);
private handleTitleLayout = (e: LayoutChangeEvent) => { const [titleLayout, setTitleLayout] = React.useState<Layout | undefined>(
undefined
);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const handleTitleLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout; const { height, width } = e.nativeEvent.layout;
this.setState(({ titleLayout }) => { setTitleLayout((titleLayout) => {
if ( if (
titleLayout && titleLayout &&
height === titleLayout.height && height === titleLayout.height &&
width === titleLayout.width width === titleLayout.width
) { ) {
return null; return titleLayout;
} }
return { return { height, width };
titleLayout: { height, width },
};
}); });
}; };
private handleLeftLabelLayout = (e: LayoutChangeEvent) => { const handleLeftLabelLayout = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout; const { height, width } = e.nativeEvent.layout;
const { leftLabelLayout } = this.state;
if ( if (
leftLabelLayout && leftLabelLayout &&
@@ -109,10 +110,10 @@ export default class HeaderSegment extends React.Component<Props, State> {
return; return;
} }
this.setState({ leftLabelLayout: { height, width } }); setLeftLabelLayout({ height, width });
}; };
private getInterpolatedStyle = memoize( const getInterpolatedStyle = memoize(
( (
styleInterpolator: StackHeaderStyleInterpolator, styleInterpolator: StackHeaderStyleInterpolator,
layout: Layout, layout: Layout,
@@ -137,258 +138,253 @@ export default class HeaderSegment extends React.Component<Props, State> {
}) })
); );
render() { const {
const { scene,
scene, layout,
layout, insets,
insets, title: currentTitle,
title: currentTitle, leftLabel: previousTitle,
leftLabel: previousTitle, onGoBack,
onGoBack, headerTitle,
headerTitle, headerTitleAlign = Platform.select({
headerTitleAlign = Platform.select({ ios: 'center',
ios: 'center', default: 'left',
default: 'left', }),
}), headerLeft: left = onGoBack
headerLeft: left = onGoBack ? (props: StackHeaderLeftButtonProps) => <HeaderBackButton {...props} />
? (props: StackHeaderLeftButtonProps) => <HeaderBackButton {...props} /> : undefined,
: undefined, headerTransparent,
headerTransparent, headerTintColor,
headerTintColor, headerBackground,
headerBackground, headerRight: right,
headerRight: right, headerBackImage: backImage,
headerBackImage: backImage, headerBackTitle: leftLabel,
headerBackTitle: leftLabel, headerBackTitleVisible,
headerBackTitleVisible, headerTruncatedBackTitle: truncatedLabel,
headerTruncatedBackTitle: truncatedLabel, headerPressColorAndroid: pressColorAndroid,
headerPressColorAndroid: pressColorAndroid, headerBackAccessibilityLabel: backAccessibilityLabel,
headerBackAllowFontScaling: backAllowFontScaling, headerBackAllowFontScaling: backAllowFontScaling,
headerTitleAllowFontScaling: titleAllowFontScaling, headerTitleAllowFontScaling: titleAllowFontScaling,
headerTitleStyle: customTitleStyle, headerTitleStyle: customTitleStyle,
headerBackTitleStyle: customLeftLabelStyle, headerBackTitleStyle: customLeftLabelStyle,
headerLeftContainerStyle: leftContainerStyle, headerLeftContainerStyle: leftContainerStyle,
headerRightContainerStyle: rightContainerStyle, headerRightContainerStyle: rightContainerStyle,
headerTitleContainerStyle: titleContainerStyle, headerTitleContainerStyle: titleContainerStyle,
headerStyle: customHeaderStyle, headerStyle: customHeaderStyle,
headerStatusBarHeight = insets.top, headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
styleInterpolator, styleInterpolator,
} = this.props; } = props;
const { leftLabelLayout, titleLayout } = this.state; const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight);
const defaultHeight = getDefaultHeaderHeight(layout, headerStatusBarHeight); const {
height = defaultHeight,
minHeight,
maxHeight,
backgroundColor,
borderBottomColor,
borderBottomEndRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderBottomStartRadius,
borderBottomWidth,
borderColor,
borderEndColor,
borderEndWidth,
borderLeftColor,
borderLeftWidth,
borderRadius,
borderRightColor,
borderRightWidth,
borderStartColor,
borderStartWidth,
borderStyle,
borderTopColor,
borderTopEndRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderTopStartRadius,
borderTopWidth,
borderWidth,
// @ts-expect-error: web support for shadow
boxShadow,
elevation,
shadowColor,
shadowOffset,
shadowOpacity,
shadowRadius,
opacity,
transform,
...unsafeStyles
} = StyleSheet.flatten(customHeaderStyle || {}) as ViewStyle;
const { if (process.env.NODE_ENV !== 'production') {
height = defaultHeight, warnIfHeaderStylesDefined(unsafeStyles);
minHeight, }
maxHeight,
backgroundColor,
borderBottomColor,
borderBottomEndRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderBottomStartRadius,
borderBottomWidth,
borderColor,
borderEndColor,
borderEndWidth,
borderLeftColor,
borderLeftWidth,
borderRadius,
borderRightColor,
borderRightWidth,
borderStartColor,
borderStartWidth,
borderStyle,
borderTopColor,
borderTopEndRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderTopStartRadius,
borderTopWidth,
borderWidth,
// @ts-expect-error: web support for shadow
boxShadow,
elevation,
shadowColor,
shadowOffset,
shadowOpacity,
shadowRadius,
opacity,
transform,
...unsafeStyles
} = StyleSheet.flatten(customHeaderStyle || {}) as ViewStyle;
if (process.env.NODE_ENV !== 'production') { const safeStyles: ViewStyle = {
warnIfHeaderStylesDefined(unsafeStyles); backgroundColor,
borderBottomColor,
borderBottomEndRadius,
borderBottomLeftRadius,
borderBottomRightRadius,
borderBottomStartRadius,
borderBottomWidth,
borderColor,
borderEndColor,
borderEndWidth,
borderLeftColor,
borderLeftWidth,
borderRadius,
borderRightColor,
borderRightWidth,
borderStartColor,
borderStartWidth,
borderStyle,
borderTopColor,
borderTopEndRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderTopStartRadius,
borderTopWidth,
borderWidth,
// @ts-expect-error: boxShadow is only for Web
boxShadow,
elevation,
shadowColor,
shadowOffset,
shadowOpacity,
shadowRadius,
opacity,
transform,
};
// Setting a property to undefined triggers default style
// So we need to filter them out
// Users can use `null` instead
for (const styleProp in safeStyles) {
// @ts-expect-error: typescript wrongly complains that styleProp cannot be used to index safeStyles
if (safeStyles[styleProp] === undefined) {
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete safeStyles[styleProp];
} }
}
const safeStyles: ViewStyle = { const {
backgroundColor, titleStyle,
borderBottomColor, leftButtonStyle,
borderBottomEndRadius, leftLabelStyle,
borderBottomLeftRadius, rightButtonStyle,
borderBottomRightRadius, backgroundStyle,
borderBottomStartRadius, } = getInterpolatedStyle(
borderBottomWidth, styleInterpolator,
borderColor, layout,
borderEndColor, scene.progress.current,
borderEndWidth, scene.progress.next,
borderLeftColor, titleLayout,
borderLeftWidth, previousTitle ? leftLabelLayout : undefined,
borderRadius, typeof height === 'number' ? height : defaultHeight
borderRightColor, );
borderRightWidth,
borderStartColor,
borderStartWidth,
borderStyle,
borderTopColor,
borderTopEndRadius,
borderTopLeftRadius,
borderTopRightRadius,
borderTopStartRadius,
borderTopWidth,
borderWidth,
// @ts-expect-error: boxShadow is only for Web
boxShadow,
elevation,
shadowColor,
shadowOffset,
shadowOpacity,
shadowRadius,
opacity,
transform,
};
// Setting a property to undefined triggers default style const leftButton = left
// So we need to filter them out ? left({
// Users can use `null` instead backImage,
for (const styleProp in safeStyles) { pressColorAndroid,
// @ts-expect-error: typescript wrongly complains that styleProp cannot be used to index safeStyles accessibilityLabel: backAccessibilityLabel,
if (safeStyles[styleProp] === undefined) { allowFontScaling: backAllowFontScaling,
// @ts-expect-error onPress: onGoBack,
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete labelVisible: headerBackTitleVisible,
delete safeStyles[styleProp]; label: leftLabel !== undefined ? leftLabel : previousTitle,
} truncatedLabel,
} labelStyle: [leftLabelStyle, customLeftLabelStyle],
onLabelLayout: handleLeftLabelLayout,
screenLayout: layout,
titleLayout,
tintColor: headerTintColor,
canGoBack: Boolean(onGoBack),
})
: null;
const { const rightButton = right ? right({ tintColor: headerTintColor }) : null;
titleStyle,
leftButtonStyle,
leftLabelStyle,
rightButtonStyle,
backgroundStyle,
} = this.getInterpolatedStyle(
styleInterpolator,
layout,
scene.progress.current,
scene.progress.next,
titleLayout,
previousTitle ? leftLabelLayout : undefined,
typeof height === 'number' ? height : defaultHeight
);
const leftButton = left return (
? left({ <React.Fragment>
backImage, <Animated.View
pressColorAndroid, pointerEvents="box-none"
allowFontScaling: backAllowFontScaling, style={[StyleSheet.absoluteFill, { zIndex: 0 }, backgroundStyle]}
onPress: onGoBack, >
labelVisible: headerBackTitleVisible, {headerBackground ? (
label: leftLabel !== undefined ? leftLabel : previousTitle, headerBackground({ style: safeStyles })
truncatedLabel, ) : headerTransparent ? null : (
labelStyle: [leftLabelStyle, customLeftLabelStyle], <HeaderBackground style={safeStyles} />
onLabelLayout: this.handleLeftLabelLayout, )}
screenLayout: layout, </Animated.View>
titleLayout, <Animated.View
tintColor: headerTintColor, pointerEvents="box-none"
canGoBack: Boolean(onGoBack), style={[{ height, minHeight, maxHeight, opacity, transform }]}
}) >
: null; <View pointerEvents="none" style={{ height: headerStatusBarHeight }} />
<View pointerEvents="box-none" style={styles.content}>
const rightButton = right ? right({ tintColor: headerTintColor }) : null; {leftButton ? (
return (
<React.Fragment>
<Animated.View
pointerEvents="box-none"
style={[StyleSheet.absoluteFill, { zIndex: 0 }, backgroundStyle]}
>
{headerBackground ? (
headerBackground({ style: safeStyles })
) : headerTransparent ? null : (
<HeaderBackground style={safeStyles} />
)}
</Animated.View>
<Animated.View
pointerEvents="box-none"
style={[{ height, minHeight, maxHeight, opacity, transform }]}
>
<View
pointerEvents="none"
style={{ height: headerStatusBarHeight }}
/>
<View pointerEvents="box-none" style={styles.content}>
{leftButton ? (
<Animated.View
pointerEvents="box-none"
style={[
styles.left,
{ left: insets.left },
leftButtonStyle,
leftContainerStyle,
]}
>
{leftButton}
</Animated.View>
) : null}
<Animated.View <Animated.View
pointerEvents="box-none" pointerEvents="box-none"
style={[ style={[
headerTitleAlign === 'left' styles.left,
? { { left: insets.left },
position: 'absolute', leftButtonStyle,
left: (leftButton ? 72 : 16) + insets.left, leftContainerStyle,
right: (rightButton ? 72 : 16) + insets.right,
}
: {
marginHorizontal:
(leftButton ? 32 : 16) +
(leftButton && headerBackTitleVisible !== false
? 40
: 0) +
Math.max(insets.left, insets.right),
},
titleStyle,
titleContainerStyle,
]} ]}
> >
{headerTitle({ {leftButton}
children: currentTitle,
onLayout: this.handleTitleLayout,
allowFontScaling: titleAllowFontScaling,
tintColor: headerTintColor,
style: customTitleStyle,
})}
</Animated.View> </Animated.View>
{rightButton ? ( ) : null}
<Animated.View <Animated.View
pointerEvents="box-none" pointerEvents="box-none"
style={[ style={[
styles.right, headerTitleAlign === 'left'
{ right: insets.right }, ? {
rightButtonStyle, position: 'absolute',
rightContainerStyle, left: (leftButton ? 72 : 16) + insets.left,
]} right: (rightButton ? 72 : 16) + insets.right,
> }
{rightButton} : {
</Animated.View> marginHorizontal:
) : null} (leftButton ? 32 : 16) +
</View> (leftButton && headerBackTitleVisible !== false
</Animated.View> ? 40
</React.Fragment> : 0) +
); Math.max(insets.left, insets.right),
} },
titleStyle,
titleContainerStyle,
]}
>
{headerTitle({
children: currentTitle,
onLayout: handleTitleLayout,
allowFontScaling: titleAllowFontScaling,
tintColor: headerTintColor,
style: customTitleStyle,
})}
</Animated.View>
{rightButton ? (
<Animated.View
pointerEvents="box-none"
style={[
styles.right,
{ right: insets.right },
rightButtonStyle,
rightContainerStyle,
]}
>
{rightButton}
</Animated.View>
) : null}
</View>
</Animated.View>
</React.Fragment>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -34,6 +34,9 @@ class WebScreen extends React.Component<
const AnimatedWebScreen = Animated.createAnimatedComponent(WebScreen); const AnimatedWebScreen = Animated.createAnimatedComponent(WebScreen);
// @ts-ignore
export const shouldUseActivityState = Screens?.shouldUseActivityState;
export const MaybeScreenContainer = ({ export const MaybeScreenContainer = ({
enabled, enabled,
...rest ...rest
@@ -41,8 +44,11 @@ export const MaybeScreenContainer = ({
enabled: boolean; enabled: boolean;
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
if (enabled && Platform.OS !== 'web' && Screens && Screens.screensEnabled()) { if (enabled && Platform.OS !== 'web' && Screens?.screensEnabled()) {
return <Screens.ScreenContainer {...rest} />; return (
// @ts-ignore
<Screens.ScreenContainer enabled={enabled} {...rest} />
);
} }
return <View {...rest} />; return <View {...rest} />;
@@ -54,16 +60,25 @@ export const MaybeScreen = ({
...rest ...rest
}: ViewProps & { }: ViewProps & {
enabled: boolean; enabled: boolean;
active: 0 | 1 | Animated.AnimatedInterpolation; active: 0 | 1 | 2 | Animated.AnimatedInterpolation;
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
if (enabled && Platform.OS === 'web') { if (enabled && Platform.OS === 'web') {
return <AnimatedWebScreen active={active} {...rest} />; return <AnimatedWebScreen active={active} {...rest} />;
} }
if (enabled && Screens && Screens.screensEnabled()) { if (enabled && Screens?.screensEnabled()) {
// @ts-expect-error: stackPresentation is incorrectly marked as required if (shouldUseActivityState) {
return <Screens.Screen active={active} {...rest} />; return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screens.Screen enabled={enabled} activityState={active} {...rest} />
);
} else {
return (
// @ts-expect-error: there was an `active` prop and no `activityState` in older version and stackPresentation was required
<Screens.Screen enabled={enabled} active={active} {...rest} />
);
}
} }
return <View {...rest} />; return <View {...rest} />;

View File

@@ -79,6 +79,15 @@ const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
const useNativeDriver = Platform.OS !== 'web'; const useNativeDriver = Platform.OS !== 'web';
const hasOpacityStyle = (style: any) => {
if (style) {
const flattenedStyle = StyleSheet.flatten(style);
return flattenedStyle.opacity != null;
}
return false;
};
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',
@@ -533,6 +542,7 @@ export default class Card extends React.Component<Props> {
</View> </View>
) : null} ) : null}
<Animated.View <Animated.View
needsOffscreenAlphaCompositing={hasOpacityStyle(containerStyle)}
style={[styles.container, containerStyle, customContainerStyle]} style={[styles.container, containerStyle, customContainerStyle]}
pointerEvents="box-none" pointerEvents="box-none"
> >

View File

@@ -66,6 +66,7 @@ type Props = TransitionPreset & {
route: Route<string>; route: Route<string>;
height: number; height: number;
}) => void; }) => void;
isParentHeaderShown: boolean;
}; };
const EPSILON = 0.1; const EPSILON = 0.1;
@@ -93,6 +94,7 @@ function CardContainer({
hasAbsoluteHeader, hasAbsoluteHeader,
headerHeight, headerHeight,
onHeaderHeightChange, onHeaderHeightChange,
isParentHeaderShown,
index, index,
layout, layout,
onCloseRoute, onCloseRoute,
@@ -181,7 +183,6 @@ function CardContainer({
}; };
}, [pointerEvents, scene.progress.next]); }, [pointerEvents, scene.progress.next]);
const isParentHeaderShown = React.useContext(HeaderShownContext);
const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false; const isCurrentHeaderShown = headerMode !== 'none' && headerShown !== false;
const previousScene = getPreviousScene({ route: scene.route }); const previousScene = getPreviousScene({ route: scene.route });

View File

@@ -4,12 +4,19 @@ import {
StyleSheet, StyleSheet,
LayoutChangeEvent, LayoutChangeEvent,
Dimensions, Dimensions,
Platform,
} from 'react-native'; } from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context'; import type { EdgeInsets } from 'react-native-safe-area-context';
import type { Route, StackNavigationState } from '@react-navigation/native'; import type {
ParamListBase,
Route,
StackNavigationState,
} from '@react-navigation/native';
import { MaybeScreenContainer, MaybeScreen } from '../Screens'; import {
MaybeScreenContainer,
MaybeScreen,
shouldUseActivityState,
} from '../Screens';
import { getDefaultHeaderHeight } from '../Header/HeaderSegment'; import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
import type { Props as HeaderContainerProps } from '../Header/HeaderContainer'; import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import CardContainer from './CardContainer'; import CardContainer from './CardContainer';
@@ -19,7 +26,6 @@ import {
} from '../../TransitionConfigs/TransitionPresets'; } from '../../TransitionConfigs/TransitionPresets';
import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators'; import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators';
import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators'; import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
import HeaderShownContext from '../../utils/HeaderShownContext';
import getDistanceForDirection from '../../utils/getDistanceForDirection'; import getDistanceForDirection from '../../utils/getDistanceForDirection';
import type { import type {
Layout, Layout,
@@ -38,7 +44,7 @@ type GestureValues = {
type Props = { type Props = {
mode: StackCardMode; mode: StackCardMode;
insets: EdgeInsets; insets: EdgeInsets;
state: StackNavigationState; state: StackNavigationState<ParamListBase>;
descriptors: StackDescriptorMap; descriptors: StackDescriptorMap;
routes: Route<string>[]; routes: Route<string>[];
openingRouteKeys: string[]; openingRouteKeys: string[];
@@ -52,6 +58,7 @@ type Props = {
renderHeader: (props: HeaderContainerProps) => React.ReactNode; renderHeader: (props: HeaderContainerProps) => React.ReactNode;
renderScene: (props: { route: Route<string> }) => React.ReactNode; renderScene: (props: { route: Route<string> }) => React.ReactNode;
headerMode: StackHeaderMode; headerMode: StackHeaderMode;
isParentHeaderShown: boolean;
onTransitionStart: ( onTransitionStart: (
props: { route: Route<string> }, props: { route: Route<string> },
closing: boolean closing: boolean
@@ -63,6 +70,7 @@ type Props = {
onGestureStart?: (props: { route: Route<string> }) => void; onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void; onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void; onGestureCancel?: (props: { route: Route<string> }) => void;
detachInactiveScreens?: boolean;
}; };
type State = { type State = {
@@ -76,11 +84,16 @@ type State = {
const EPSILON = 0.01; const EPSILON = 0.01;
const STATE_INACTIVE = 0;
const STATE_TRANSITIONING_OR_BELOW_TOP = 1;
const STATE_ON_TOP = 2;
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} }); const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
const getHeaderHeights = ( const getHeaderHeights = (
routes: Route<string>[], routes: Route<string>[],
insets: EdgeInsets, insets: EdgeInsets,
isParentHeaderShown: boolean,
descriptors: StackDescriptorMap, descriptors: StackDescriptorMap,
layout: Layout, layout: Layout,
previous: Record<string, number> previous: Record<string, number>
@@ -97,7 +110,9 @@ const getHeaderHeights = (
...options.safeAreaInsets, ...options.safeAreaInsets,
}; };
const { headerStatusBarHeight = safeAreaInsets.top } = options; const {
headerStatusBarHeight = isParentHeaderShown ? 0 : safeAreaInsets.top,
} = options;
acc[curr.key] = acc[curr.key] =
typeof height === 'number' typeof height === 'number'
@@ -260,6 +275,7 @@ export default class CardStack extends React.Component<Props, State> {
headerHeights: getHeaderHeights( headerHeights: getHeaderHeights(
props.routes, props.routes,
props.insets, props.insets,
props.isParentHeaderShown,
state.descriptors, state.descriptors,
state.layout, state.layout,
state.headerHeights state.headerHeights
@@ -302,6 +318,7 @@ export default class CardStack extends React.Component<Props, State> {
headerHeights: getHeaderHeights( headerHeights: getHeaderHeights(
props.routes, props.routes,
props.insets, props.insets,
props.isParentHeaderShown,
state.descriptors, state.descriptors,
layout, layout,
state.headerHeights state.headerHeights
@@ -370,6 +387,7 @@ export default class CardStack extends React.Component<Props, State> {
renderHeader, renderHeader,
renderScene, renderScene,
headerMode, headerMode,
isParentHeaderShown,
onTransitionStart, onTransitionStart,
onTransitionEnd, onTransitionEnd,
onPageChangeStart, onPageChangeStart,
@@ -378,6 +396,8 @@ export default class CardStack extends React.Component<Props, State> {
onGestureStart, onGestureStart,
onGestureEnd, onGestureEnd,
onGestureCancel, onGestureCancel,
// Enable on new versions of screens or for non modals on older versions
detachInactiveScreens = shouldUseActivityState || mode !== 'modal',
} = this.props; } = this.props;
const { scenes, layout, gestures, headerHeights } = this.state; const { scenes, layout, gestures, headerHeights } = this.state;
@@ -385,6 +405,7 @@ export default class CardStack extends React.Component<Props, State> {
const focusedRoute = state.routes[state.index]; const focusedRoute = state.routes[state.index];
const focusedDescriptor = descriptors[focusedRoute.key]; const focusedDescriptor = descriptors[focusedRoute.key];
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {}; const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
const focusedHeaderHeight = headerHeights[focusedRoute.key];
let defaultTransitionPreset = let defaultTransitionPreset =
mode === 'modal' ? ModalTransition : DefaultTransition; mode === 'modal' ? ModalTransition : DefaultTransition;
@@ -403,212 +424,250 @@ export default class CardStack extends React.Component<Props, State> {
left = insets.left, left = insets.left,
} = focusedOptions.safeAreaInsets || {}; } = focusedOptions.safeAreaInsets || {};
// Screens is buggy on iOS and web, so we only enable it on Android let activeScreensLimit = 1;
// For modals, usually we want the screen underneath to be visible, so also disable it there
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal'; for (let i = scenes.length - 1; i >= 0; i--) {
const {
// By default, we don't want to detach the previous screen of the active one for modals
detachPreviousScreen = mode === 'modal'
? i !== scenes.length - 1
: true,
} = scenes[i].descriptor.options;
if (detachPreviousScreen === false) {
activeScreensLimit++;
} else {
break;
}
}
const isFloatHeaderAbsolute =
headerMode === 'float'
? this.state.scenes.slice(-2).some((scene) => {
const { descriptor } = scene;
const options = descriptor ? descriptor.options : {};
const {
headerTransparent,
headerShown = isParentHeaderShown === false,
} = options;
if (headerTransparent || headerShown === false) {
return true;
}
return false;
})
: false;
const floatingHeader =
headerMode === 'float' ? (
<React.Fragment key="header">
{renderHeader({
mode: 'float',
layout,
insets: { top, right, bottom, left },
scenes,
getPreviousScene: this.getPreviousScene,
getFocusedRoute: this.getFocusedRoute,
onContentHeightChange: this.handleHeaderLayout,
gestureDirection:
focusedOptions.gestureDirection !== undefined
? focusedOptions.gestureDirection
: defaultTransitionPreset.gestureDirection,
styleInterpolator:
focusedOptions.headerStyleInterpolator !== undefined
? focusedOptions.headerStyleInterpolator
: defaultTransitionPreset.headerStyleInterpolator,
style: [
styles.floating,
isFloatHeaderAbsolute && [
// Without this, the header buttons won't be touchable on Android when headerTransparent: true
{ height: focusedHeaderHeight },
styles.absolute,
],
],
})}
</React.Fragment>
) : null;
return ( return (
<HeaderShownContext.Consumer> <React.Fragment>
{(isParentHeaderShown) => { {isFloatHeaderAbsolute ? null : floatingHeader}
const isFloatHeaderAbsolute = <MaybeScreenContainer
headerMode === 'float' enabled={detachInactiveScreens}
? this.state.scenes.slice(-2).some((scene) => { style={styles.container}
const { descriptor } = scene; onLayout={this.handleLayout}
const options = descriptor ? descriptor.options : {}; >
const { {routes.map((route, index, self) => {
headerTransparent, const focused = focusedRoute.key === route.key;
headerShown = isParentHeaderShown === false, const gesture = gestures[route.key];
} = options; const scene = scenes[index];
if (headerTransparent || headerShown === false) { // For the screens that shouldn't be active, the value is 0
return true; // For those that should be active, but are not the top screen, the value is 1
} // For those on top of the stack and with interaction enabled, the value is 2
// For the old implementation, it stays the same it was
let isScreenActive: Animated.AnimatedInterpolation | 2 | 1 | 0 = 1;
return false; if (shouldUseActivityState) {
}) if (index < self.length - activeScreensLimit - 1) {
: false; // screen should be inactive because it is too deep in the stack
isScreenActive = STATE_INACTIVE;
} else {
const sceneForActivity = scenes[self.length - 1];
const outputValue =
index === self.length - 1
? STATE_ON_TOP // the screen is on top after the transition
: index >= self.length - activeScreensLimit
? STATE_TRANSITIONING_OR_BELOW_TOP // the screen should stay active after the transition, it is not on top but is in activeLimit
: STATE_INACTIVE; // the screen should be active only during the transition, it is at the edge of activeLimit
isScreenActive = sceneForActivity
? sceneForActivity.progress.current.interpolate({
inputRange: [0, 1 - EPSILON, 1],
outputRange: [1, 1, outputValue],
extrapolate: 'clamp',
})
: STATE_TRANSITIONING_OR_BELOW_TOP;
}
} else {
isScreenActive = scene.progress.next
? scene.progress.next.interpolate({
inputRange: [0, 1 - EPSILON, 1],
outputRange: [1, 1, 0],
extrapolate: 'clamp',
})
: 1;
}
const floatingHeader = const {
headerMode === 'float' ? ( safeAreaInsets,
<React.Fragment key="header"> headerShown = isParentHeaderShown === false,
{renderHeader({ headerTransparent,
mode: 'float', cardShadowEnabled,
layout, cardOverlayEnabled,
insets: { top, right, bottom, left }, cardOverlay,
scenes, cardStyle,
getPreviousScene: this.getPreviousScene, animationEnabled,
getFocusedRoute: this.getFocusedRoute, gestureResponseDistance,
onContentHeightChange: this.handleHeaderLayout, gestureVelocityImpact,
gestureDirection: gestureDirection = defaultTransitionPreset.gestureDirection,
focusedOptions.gestureDirection !== undefined transitionSpec = defaultTransitionPreset.transitionSpec,
? focusedOptions.gestureDirection cardStyleInterpolator = animationEnabled === false
: defaultTransitionPreset.gestureDirection, ? forNoAnimationCard
styleInterpolator: : defaultTransitionPreset.cardStyleInterpolator,
focusedOptions.headerStyleInterpolator !== undefined headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
? focusedOptions.headerStyleInterpolator } = scene.descriptor
: defaultTransitionPreset.headerStyleInterpolator, ? scene.descriptor.options
style: [ : ({} as StackNavigationOptions);
styles.floating,
isFloatHeaderAbsolute && styles.absolute,
],
})}
</React.Fragment>
) : null;
return ( let transitionConfig = {
<React.Fragment> gestureDirection,
{isFloatHeaderAbsolute ? null : floatingHeader} transitionSpec,
<MaybeScreenContainer cardStyleInterpolator,
enabled={isScreensEnabled} headerStyleInterpolator,
style={styles.container} };
onLayout={this.handleLayout}
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
if (index !== self.length - 1) {
const nextScene = scenes[index + 1];
if (nextScene) {
const {
animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
} = nextScene.descriptor
? nextScene.descriptor.options
: ({} as StackNavigationOptions);
transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
};
}
}
const {
top: safeAreaInsetTop = insets.top,
right: safeAreaInsetRight = insets.right,
bottom: safeAreaInsetBottom = insets.bottom,
left: safeAreaInsetLeft = insets.left,
} = safeAreaInsets || {};
const headerHeight =
headerMode !== 'none' && headerShown !== false
? headerHeights[route.key]
: 0;
return (
<MaybeScreen
key={route.key}
style={StyleSheet.absoluteFill}
enabled={detachInactiveScreens}
active={isScreenActive}
pointerEvents="box-none"
> >
{routes.map((route, index, self) => { <CardContainer
const focused = focusedRoute.key === route.key; index={index}
const gesture = gestures[route.key]; active={index === self.length - 1}
const scene = scenes[index]; focused={focused}
closing={closingRouteKeys.includes(route.key)}
const isScreenActive = scene.progress.next layout={layout}
? scene.progress.next.interpolate({ gesture={gesture}
inputRange: [0, 1 - EPSILON, 1], scene={scene}
outputRange: [1, 1, 0], safeAreaInsetTop={safeAreaInsetTop}
extrapolate: 'clamp', safeAreaInsetRight={safeAreaInsetRight}
}) safeAreaInsetBottom={safeAreaInsetBottom}
: 1; safeAreaInsetLeft={safeAreaInsetLeft}
cardOverlay={cardOverlay}
const { cardOverlayEnabled={cardOverlayEnabled}
safeAreaInsets, cardShadowEnabled={cardShadowEnabled}
headerShown = isParentHeaderShown === false, cardStyle={cardStyle}
headerTransparent, onPageChangeStart={onPageChangeStart}
cardShadowEnabled, onPageChangeConfirm={onPageChangeConfirm}
cardOverlayEnabled, onPageChangeCancel={onPageChangeCancel}
cardOverlay, onGestureStart={onGestureStart}
cardStyle, onGestureCancel={onGestureCancel}
animationEnabled, onGestureEnd={onGestureEnd}
gestureResponseDistance, gestureResponseDistance={gestureResponseDistance}
gestureVelocityImpact, headerHeight={headerHeight}
gestureDirection = defaultTransitionPreset.gestureDirection, isParentHeaderShown={isParentHeaderShown}
transitionSpec = defaultTransitionPreset.transitionSpec, onHeaderHeightChange={this.handleHeaderLayout}
cardStyleInterpolator = animationEnabled === false getPreviousScene={this.getPreviousScene}
? forNoAnimationCard getFocusedRoute={this.getFocusedRoute}
: defaultTransitionPreset.cardStyleInterpolator, mode={mode}
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator, headerMode={headerMode}
} = scene.descriptor headerShown={headerShown}
? scene.descriptor.options hasAbsoluteHeader={
: ({} as StackNavigationOptions); isFloatHeaderAbsolute && !headerTransparent
let transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
};
// When a screen is not the last, it should use next screen's transition config
// Many transitions also animate the previous screen, so using 2 different transitions doesn't look right
// For example combining a slide and a modal transition would look wrong otherwise
// With this approach, combining different transition styles in the same navigator mostly looks right
// This will still be broken when 2 transitions have different idle state (e.g. modal presentation),
// but majority of the transitions look alright
if (index !== self.length - 1) {
const nextScene = scenes[index + 1];
if (nextScene) {
const {
animationEnabled,
gestureDirection = defaultTransitionPreset.gestureDirection,
transitionSpec = defaultTransitionPreset.transitionSpec,
cardStyleInterpolator = animationEnabled === false
? forNoAnimationCard
: defaultTransitionPreset.cardStyleInterpolator,
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
} = nextScene.descriptor
? nextScene.descriptor.options
: ({} as StackNavigationOptions);
transitionConfig = {
gestureDirection,
transitionSpec,
cardStyleInterpolator,
headerStyleInterpolator,
};
}
} }
renderHeader={renderHeader}
const { renderScene={renderScene}
top: safeAreaInsetTop = insets.top, onOpenRoute={onOpenRoute}
right: safeAreaInsetRight = insets.right, onCloseRoute={onCloseRoute}
bottom: safeAreaInsetBottom = insets.bottom, onTransitionStart={onTransitionStart}
left: safeAreaInsetLeft = insets.left, onTransitionEnd={onTransitionEnd}
} = safeAreaInsets || {}; gestureEnabled={index !== 0 && getGesturesEnabled({ route })}
gestureVelocityImpact={gestureVelocityImpact}
const headerHeight = {...transitionConfig}
headerMode !== 'none' && headerShown !== false />
? headerHeights[route.key] </MaybeScreen>
: 0; );
})}
return ( </MaybeScreenContainer>
<MaybeScreen {isFloatHeaderAbsolute ? floatingHeader : null}
key={route.key} </React.Fragment>
style={StyleSheet.absoluteFill}
enabled={isScreensEnabled}
active={isScreenActive}
pointerEvents="box-none"
>
<CardContainer
index={index}
active={index === self.length - 1}
focused={focused}
closing={closingRouteKeys.includes(route.key)}
layout={layout}
gesture={gesture}
scene={scene}
safeAreaInsetTop={safeAreaInsetTop}
safeAreaInsetRight={safeAreaInsetRight}
safeAreaInsetBottom={safeAreaInsetBottom}
safeAreaInsetLeft={safeAreaInsetLeft}
cardOverlay={cardOverlay}
cardOverlayEnabled={cardOverlayEnabled}
cardShadowEnabled={cardShadowEnabled}
cardStyle={cardStyle}
onPageChangeStart={onPageChangeStart}
onPageChangeConfirm={onPageChangeConfirm}
onPageChangeCancel={onPageChangeCancel}
onGestureStart={onGestureStart}
onGestureCancel={onGestureCancel}
onGestureEnd={onGestureEnd}
gestureResponseDistance={gestureResponseDistance}
headerHeight={headerHeight}
onHeaderHeightChange={this.handleHeaderLayout}
getPreviousScene={this.getPreviousScene}
getFocusedRoute={this.getFocusedRoute}
mode={mode}
headerMode={headerMode}
headerShown={headerShown}
hasAbsoluteHeader={
isFloatHeaderAbsolute && !headerTransparent
}
renderHeader={renderHeader}
renderScene={renderScene}
onOpenRoute={onOpenRoute}
onCloseRoute={onCloseRoute}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
gestureEnabled={
index !== 0 && getGesturesEnabled({ route })
}
gestureVelocityImpact={gestureVelocityImpact}
{...transitionConfig}
/>
</MaybeScreen>
);
})}
</MaybeScreenContainer>
{isFloatHeaderAbsolute ? floatingHeader : null}
</React.Fragment>
);
}}
</HeaderShownContext.Consumer>
); );
} }
} }

View File

@@ -6,6 +6,7 @@ import {
StackActions, StackActions,
StackNavigationState, StackNavigationState,
Route, Route,
ParamListBase,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { GestureHandlerRootView } from '../GestureHandler'; import { GestureHandlerRootView } from '../GestureHandler';
@@ -20,9 +21,10 @@ import type {
StackNavigationConfig, StackNavigationConfig,
StackDescriptorMap, StackDescriptorMap,
} from '../../types'; } from '../../types';
import HeaderShownContext from '../../utils/HeaderShownContext';
type Props = StackNavigationConfig & { type Props = StackNavigationConfig & {
state: StackNavigationState; state: StackNavigationState<ParamListBase>;
navigation: StackNavigationHelpers; navigation: StackNavigationHelpers;
descriptors: StackDescriptorMap; descriptors: StackDescriptorMap;
}; };
@@ -455,29 +457,34 @@ export default class StackView extends React.Component<Props, State> {
{(insets) => ( {(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}> <KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => ( {(props) => (
<CardStack <HeaderShownContext.Consumer>
mode={mode} {(isParentHeaderShown) => (
insets={insets as EdgeInsets} <CardStack
getPreviousRoute={this.getPreviousRoute} mode={mode}
getGesturesEnabled={this.getGesturesEnabled} insets={insets as EdgeInsets}
routes={routes} isParentHeaderShown={isParentHeaderShown}
openingRouteKeys={openingRouteKeys} getPreviousRoute={this.getPreviousRoute}
closingRouteKeys={closingRouteKeys} getGesturesEnabled={this.getGesturesEnabled}
onOpenRoute={this.handleOpenRoute} routes={routes}
onCloseRoute={this.handleCloseRoute} openingRouteKeys={openingRouteKeys}
onTransitionStart={this.handleTransitionStart} closingRouteKeys={closingRouteKeys}
onTransitionEnd={this.handleTransitionEnd} onOpenRoute={this.handleOpenRoute}
renderHeader={this.renderHeader} onCloseRoute={this.handleCloseRoute}
renderScene={this.renderScene} onTransitionStart={this.handleTransitionStart}
headerMode={headerMode} onTransitionEnd={this.handleTransitionEnd}
state={state} renderHeader={this.renderHeader}
descriptors={descriptors} renderScene={this.renderScene}
onGestureStart={this.handleGestureStart} headerMode={headerMode}
onGestureEnd={this.handleGestureEnd} state={state}
onGestureCancel={this.handleGestureCancel} descriptors={descriptors}
{...rest} onGestureStart={this.handleGestureStart}
{...props} onGestureEnd={this.handleGestureEnd}
/> onGestureCancel={this.handleGestureCancel}
{...rest}
{...props}
/>
)}
</HeaderShownContext.Consumer>
)} )}
</KeyboardManager> </KeyboardManager>
)} )}

3002
yarn.lock

File diff suppressed because it is too large Load Diff