Compare commits

..

23 Commits

Author SHA1 Message Date
Satyajit Sahoo
21f61d6eeb chore: publish
- @react-navigation/bottom-tabs@5.5.2
 - @react-navigation/compat@5.1.26
 - @react-navigation/core@5.10.0
 - @react-navigation/drawer@5.8.2
 - @react-navigation/material-bottom-tabs@5.2.10
 - @react-navigation/material-top-tabs@5.2.10
 - @react-navigation/native@5.5.1
 - @react-navigation/stack@5.4.2
2020-06-06 02:15:26 +02:00
Jean Regisser
8774ca97e1 fix: catch missing params when they are required in navigate (#8389)
There is a problem with the enforcement of required params in `navigation.navigate(...)` in TypeScript as described in https://github.com/react-navigation/react-navigation/issues/7936

@Miyou found a fix 🥳. All credits go to him.

I needed this so went ahead and submitted a PR and added a test to avoid it from breaking in the future.

However note that until this project switches to TypeScript 3.9 with support for [`@ts-expect-error`](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9-beta/#ts-expect-error-comments), it can still break silently.

Before the change, this is how the test looks:
<img width="861" alt="Screenshot 2020-06-05 at 22 51 00" src="https://user-images.githubusercontent.com/57791/83923057-c53dc180-a781-11ea-8c35-36406a23a717.png">
As you can see it doesn't catch the missing params which are required on line 28.

After the change, all expected errors are raised:
<img width="812" alt="Screenshot 2020-06-05 at 22 51 59" src="https://user-images.githubusercontent.com/57791/83923413-80665a80-a782-11ea-8ff2-f5af3f4e1f32.png">

Let me know what you think.
2020-06-06 00:57:28 +02:00
Satyajit Sahoo
e653d55479 refactor: minor tweaks 2020-06-06 00:38:38 +02:00
Ashoat Tevosyan
78afbffe97 fix: relatively position float Header if !headerTransparent (#8285)
## Motivation

Right now `headerMode: float` renders an absolutely-positioned header. To offset the content appropriately, it then measures the height of the header and compensates with a margin. This approach unfortunately doesn't work well for animations.

Before             |  After
:-------------------------:|:-------------------------:
<img src="http://ashoat.com/jerky_absolute.gif" width="300" />  |  <img src="http://ashoat.com/smooth_relative.gif" width="300" />

## Approach

When rendering the header absolutely we want to render it above (after, in sibling order) the content. But when rendering it relatively we want to render it first (before, in sibling order).

The margin compensation code is no longer necessary so I removed it.

## Test plan

I used the `StackHeaderCustomization` example to make sure transitions between `headerTransparent` and `!headerTransparent` looked good. I added a custom (taller) header to test if height transitions looked good, and toggled `headerShown` to make sure that transitioned well too.

Would be open to any other suggestions of things to test!
2020-06-06 00:12:00 +02:00
Diego Mello
762cc44578 fix: typo on drawerPosition default props (#8357)
Fix a minor typo from `drawerPostion` to `drawerPosition` :)

Fix https://github.com/react-navigation/react-navigation/issues/8358
2020-06-05 23:48:10 +02:00
Satyajit Sahoo
c3bd349d77 fix: make sure the wildcard pattern catches nested unmatched routes 2020-06-05 23:03:37 +02:00
Satyajit Sahoo
5dcaf903f3 refactor: rework history stack integration (#8367)
The PR reworks history integration to better integrate with browser's history stack and supports nested navigation more reliably:

- On each navigation, save the navigation in memory and use it to reset the state when user presses back/forward
- Improve heuristic to determine if we should do a push, replace or back

This closes #8230, closes #8284 and closes #8344
2020-06-05 23:02:35 +02:00
Satyajit Sahoo
2d66ef93ec fix: only use the query params for focused route in path 2020-06-05 20:01:52 +02:00
Satyajit Sahoo
4fe72e3ce7 feat: add wildcard patterns for paths
Currently, if we don't have matching routes for a path, we'll reuse the path name for the route name. This doesn't produce an error, and renders the initial route in the navigator. However, the user doesn't have a way of handling this with the default configuration.

This PR adds support for a wildcard pattern ('*'). The wildcard pattern will be matched after all other patterns were matched and will always match unmatched screens. This allows the user to implement a 404 screen.

Example:

```js
{
  Home: '',
  Profile: 'user/:id',
  404: '*',
}
```

This config will return the `404` route for paths which didn't match `Home` or `Profile`, e.g. - `/test`

Closes #8019

Co-authored-by: Evan Bacon <baconbrix@gmail.com>
2020-06-05 17:13:00 +02:00
Satyajit Sahoo
ab1f79c096 fix: prevent state change being emitted unnecessarily 2020-06-01 21:32:08 +02:00
Satyajit Sahoo
9305bfa939 chore: try to fix yarn caching on gh actions 2020-05-27 20:12:33 +02:00
Satyajit Sahoo
0c3c450f5f chore: tweak SSR output 2020-05-27 19:55:37 +02:00
Satyajit Sahoo
7ac4c13d44 chore: publish
- @react-navigation/bottom-tabs@5.5.1
 - @react-navigation/compat@5.1.25
 - @react-navigation/core@5.9.0
 - @react-navigation/drawer@5.8.1
 - @react-navigation/material-bottom-tabs@5.2.9
 - @react-navigation/material-top-tabs@5.2.9
 - @react-navigation/native@5.5.0
 - @react-navigation/stack@5.4.1
2020-05-27 18:32:30 +02:00
Raviraj
a0b9f94120 refactor: remove unnecessary check for type of bottom tab bar label 2020-05-27 18:12:21 +02:00
Satyajit Sahoo
717dffdb81 chore: improve caching of yarn on gh actions 2020-05-27 13:31:50 +02:00
Satyajit Sahoo
9016ba00e3 chore: improve caching of yarn on circleci 2020-05-27 13:19:41 +02:00
Satyajit Sahoo
9d822b95a6 fix: fix type of style for various options 2020-05-26 17:33:50 +02:00
Satyajit Sahoo
52d5cb4179 chore: add an example for SSR (#8298)
<img width="740" alt="Screen Shot 2020-05-20 at 16 31 30" src="https://user-images.githubusercontent.com/1174278/82458770-673d8880-9ab7-11ea-81d3-8ac0c1e52705.png">
2020-05-26 16:07:47 +02:00
Satyajit Sahoo
af1722d1e9 fix: export types from /native 2020-05-26 14:11:48 +02:00
Satyajit Sahoo
0b1a718756 feat: add ref to get current options in ServerContainer (#8333)
User can pass a `ref` to the container to get current options, like they can with `NavigationContainer`:

```js
const ref = React.createRef();

const html = renderToString(
  <ServerContainer ref={ref}>
    <App />
  </ServerContainer>
);

ref.current.getCurrentOptions(); // Options for screen
```
2020-05-26 13:55:06 +02:00
Michał Osadnik
9ab29558d0 refactor: remove useless callback arg in useSubscription (#8332) 2020-05-26 13:21:51 +02:00
Bright Lee
00c23f2c9e fix: allow HeaderBackground's subViews to be touchable (#8317) 2020-05-25 15:50:24 +02:00
Satyajit Sahoo
68e750d5a6 feat: add a ServerContainer component for SSR (#8297)
When doing SSR, the app needs to be aware of request URL to render correct navigation state.
The `ServerContainer` component lets us pass the `location` object to use for SSR.
The shape of the `location` object matches the `location` object in the browser.

Usage:

```js
ReactDOM.renderToString(
  <ServerContainer location={{ pathname: req.path, search: req.search }}>
    <App />
  </ServerContainer>
);
```

Updated example: https://github.com/react-navigation/react-navigation/pull/8298
2020-05-24 14:28:16 +02:00
68 changed files with 2644 additions and 460 deletions

View File

@@ -22,13 +22,14 @@ jobs:
- attach_project
- restore_cache:
keys:
- v2-dependencies-{{ checksum "yarn.lock" }}
- v2-dependencies-
- yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
- yarn-packages-v1-{{ .Branch }}-
- yarn-packages-v1-
- run:
name: Install project dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v2-dependencies-{{ checksum "yarn.lock" }}
key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths: ~/.cache/yarn
- persist_to_workspace:
root: .

View File

@@ -23,20 +23,16 @@ jobs:
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-cache: true
- name: Get yarn cache
- name: Restore yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Check yarn cache
uses: actions/cache@v1
uses: actions/cache@master
with:
path: ${{ steps.yarn-cache.outputs.dir }}
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Publish Expo app
working-directory: ./example

View File

@@ -25,19 +25,16 @@ jobs:
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-cache: true
- name: Get yarn cache
- name: Restore yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
uses: actions/cache@master
with:
path: ${{ steps.yarn-cache.outputs.dir }}
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Publish Expo app
working-directory: ./example

View File

@@ -1,6 +1,10 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import { Asset } from 'expo-asset';
import { Assets as StackAssets } from '@react-navigation/stack';
import App from './src/index';
Asset.loadAsync(StackAssets);
registerRootComponent(App);

View File

@@ -0,0 +1,13 @@
import fetch from 'node-fetch';
import cheerio from 'cheerio';
const server = 'http://localhost:3275';
it('renders the home page', async () => {
const res = await fetch(server);
const html = await res.text();
const $ = cheerio.load(html);
expect($('title').text()).toBe('Examples');
});

View File

@@ -1,8 +1,16 @@
import { setup } from 'jest-dev-server';
export default async function () {
await setup({
command: 'yarn serve -l 3579 web-build',
port: 3579,
});
await setup([
{
command: 'yarn serve -l 3579 web-build',
launchTimeout: 50000,
port: 3579,
},
{
command: 'yarn server',
launchTimeout: 50000,
port: 3275,
},
]);
}

View File

@@ -9,6 +9,7 @@
"native": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"server": "nodemon -e '.js,.ts,.tsx' --exec \"babel-node -i '/node_modules[/\\](?react-native)/' -x '.web.tsx,.web.ts,.web.js,.tsx,.ts,.js' --config-file ./server/babel.config.js server\"",
"test": "jest"
},
"dependencies": {
@@ -18,6 +19,7 @@
"expo": "^37.0.8",
"expo-asset": "~8.1.3",
"expo-blur": "~8.1.0",
"koa": "^2.12.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "~0.61.5",
@@ -29,17 +31,28 @@
"react-native-screens": "^2.7.0",
"react-native-tab-view": "2.14.0",
"react-native-unimodules": "~0.9.1",
"react-native-vector-icons": "^6.6.0",
"react-native-web": "^0.11.7"
},
"devDependencies": {
"@babel/node": "^7.8.7",
"@expo/webpack-config": "^0.11.19",
"@types/cheerio": "^0.22.18",
"@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.11.3",
"@types/node-fetch": "^2.5.7",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.8",
"@types/react-native": "^0.62.7",
"babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.1.0",
"cheerio": "^1.0.0-rc.3",
"expo-cli": "^3.20.1",
"jest": "^26.0.1",
"jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.0",
"nodemon": "^2.0.4",
"playwright": "^0.14.0",
"serve": "^11.3.0",
"typescript": "^3.8.3"

View File

@@ -0,0 +1,40 @@
const path = require('path');
const fs = require('fs');
const packages = path.resolve(__dirname, '..', '..', 'packages');
const alias = Object.fromEntries(
fs
.readdirSync(packages)
.filter((name) => !name.startsWith('.'))
.map((name) => [
`@react-navigation/${name}`,
path.resolve(
packages,
name,
require(`../../packages/${name}/package.json`).source
),
])
);
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-flow',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-class-properties',
[
'module-resolver',
{
root: ['..'],
alias: {
'react-native': 'react-native-web',
...alias,
},
},
],
],
};

54
example/server/index.tsx Normal file
View File

@@ -0,0 +1,54 @@
import './resolve-hooks';
import Koa from 'koa';
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';
import { AppRegistry } from 'react-native-web';
import { ServerContainer, ServerContainerRef } from '@react-navigation/native';
import App from '../src/index';
AppRegistry.registerComponent('App', () => App);
const PORT = process.env.PORT || 3275;
const app = new Koa();
app.use(async (ctx) => {
const { element, getStyleElement } = AppRegistry.getApplication('App');
const ref = React.createRef<ServerContainerRef>();
const html = ReactDOMServer.renderToString(
<ServerContainer
ref={ref}
location={{ pathname: ctx.path, search: ctx.search }}
>
{element}
</ServerContainer>
);
const css = ReactDOMServer.renderToStaticMarkup(getStyleElement());
const document = `
<!DOCTYPE html>
<html style="height: 100%">
<meta charset="utf-8">
<meta httpEquiv="X-UA-Compatible" content="IE=edge">
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover"
>
${css}
<title>${ref.current?.getCurrentOptions()?.title}</title>
<body style="min-height: 100%">
<div id="root" style="display: flex; min-height: 100vh">
${html}
</div>
`;
ctx.body = document;
});
app.listen(PORT, () => {
console.log(`Running at http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,12 @@
import 'mock-require-assets';
import Module from 'module';
// We need to make sure that .web.xx extensions are resolved before .xx
// @ts-ignore
Module._extensions = Object.fromEntries(
// @ts-ignore
Object.entries(Module._extensions).sort((a, b) => {
return b[0].split('.').length - a[0].split('.').length;
})
);

View File

@@ -0,0 +1,11 @@
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
export function restartApp() {
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
}

1
example/src/Restart.tsx Normal file
View File

@@ -0,0 +1 @@
export function restartApp() {}

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, ScrollView, StyleSheet, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {
createBottomTabNavigator,
BottomTabNavigationProp,

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { Title, Button } from 'react-native-paper';
import { Feather } from '@expo/vector-icons';
import Feather from 'react-native-vector-icons/Feather';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
type BottomTabParams = {

View File

@@ -0,0 +1,40 @@
import { StackNavigationProp } from '@react-navigation/stack';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-paper';
const NotFoundScreen = ({
navigation,
}: {
navigation: StackNavigationProp<{ Home: undefined }>;
}) => {
return (
<View style={styles.container}>
<Text style={styles.title}>404 Not Found</Text>
<Button
mode="contained"
onPress={() => navigation.navigate('Home')}
style={styles.button}
>
Go to home
</Button>
</View>
);
};
export default NotFoundScreen;
const styles = StyleSheet.create({
title: {
fontSize: 36,
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 8,
},
button: {
margin: 24,
},
});

View File

@@ -1,15 +1,24 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
import {
Animated,
View,
StyleSheet,
ScrollView,
Alert,
Platform,
} from 'react-native';
import { Button, Appbar } from 'react-native-paper';
import { BlurView } from 'expo-blur';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
HeaderBackground,
useHeaderHeight,
Header,
StackHeaderProps,
} from '@react-navigation/stack';
import BlurView from '../Shared/BlurView';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
@@ -91,6 +100,25 @@ type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
function CustomHeader(props: StackHeaderProps) {
const { current, next } = props.scene.progress;
const progress = Animated.add(current, next || 0);
const opacity = progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [0, 1, 0],
});
return (
<>
<Header {...props} />
<Animated.Text style={[styles.banner, { opacity }]}>
Why hello there, pardner!
</Animated.Text>
</>
);
}
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
@@ -103,6 +131,7 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params?.author}`,
header: CustomHeader,
headerTintColor: '#fff',
headerStyle: { backgroundColor: '#ff005d' },
headerBackTitleVisible: false,
@@ -160,4 +189,10 @@ const styles = StyleSheet.create({
button: {
margin: 8,
},
banner: {
textAlign: 'center',
color: 'tomato',
backgroundColor: 'papayawhip',
padding: 4,
},
});

View File

@@ -0,0 +1,3 @@
import { BlurView } from 'expo-blur';
export default BlurView;

View File

@@ -0,0 +1,12 @@
import * as React from 'react';
import { View, ViewProps } from 'react-native';
type Props = ViewProps & {
tint: 'light' | 'dark';
intensity: number;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function BlurView({ tint, intensity, ...rest }: Props) {
return <View {...rest} />;
}

View File

@@ -11,10 +11,7 @@ import {
} from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens';
import RNRestart from 'react-native-restart';
import { Updates } from 'expo';
import { Asset } from 'expo-asset';
import { MaterialIcons } from '@expo/vector-icons';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import {
Provider as PaperProvider,
DefaultTheme as PaperLightTheme,
@@ -29,6 +26,7 @@ import {
NavigationContainer,
DefaultTheme,
DarkTheme,
PathConfig,
} from '@react-navigation/native';
import {
createDrawerNavigator,
@@ -36,11 +34,11 @@ import {
} from '@react-navigation/drawer';
import {
createStackNavigator,
Assets as StackAssets,
StackNavigationProp,
HeaderStyleInterpolators,
} from '@react-navigation/stack';
import { restartApp } from './Restart';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
@@ -51,6 +49,7 @@ import StackHeaderCustomization from './Screens/StackHeaderCustomization';
import BottomTabs from './Screens/BottomTabs';
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import NotFound from './Screens/NotFound';
import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI';
@@ -71,6 +70,7 @@ type RootDrawerParamList = {
type RootStackParamList = {
Home: undefined;
NotFound: undefined;
} & {
[P in keyof typeof SCREENS]: undefined;
};
@@ -126,12 +126,10 @@ const Stack = createStackNavigator<RootStackParamList>();
const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets);
export default function App() {
const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false);
const [isReady, setIsReady] = React.useState(Platform.OS === 'web');
const [initialState, setInitialState] = React.useState<
InitialState | undefined
>();
@@ -226,35 +224,45 @@ export default function App() {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
screens: Object.keys(SCREENS).reduce<PathConfig>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
acc[name] = name
const path = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
acc[name] = {
path,
screens: {
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
Dialog: 'dialog',
},
};
return acc;
},
{ Home: '' }
{
Home: '',
NotFound: '*',
}
),
},
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
},
}}
fallback={<Text>Loading</Text>}
@@ -307,12 +315,7 @@ export default function App() {
value={I18nManager.isRTL}
onValueChange={() => {
I18nManager.forceRTL(!I18nManager.isRTL);
// @ts-ignore
if (global.Expo) {
Updates.reloadFromCache();
} else {
RNRestart.Restart();
}
restartApp();
}}
/>
<Divider />
@@ -342,6 +345,11 @@ export default function App() {
</ScrollView>
)}
</Stack.Screen>
<Stack.Screen
name="NotFound"
component={NotFound}
options={{ title: 'Oops!' }}
/>
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
(name) => (
<Stack.Screen

16
example/types/react-native-web.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
declare module 'react-native-web' {
export const AppRegistry: {
registerComponent(
name: string,
callback: () => React.ComponentType<any>
): void;
getApplication(
name: string,
options?: { initialProps: object }
): {
element: React.ReactElement;
getStyleElement(): React.ReactElement;
};
};
}

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.1...@react-navigation/bottom-tabs@5.5.2) (2020-06-06)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.5.0...@react-navigation/bottom-tabs@5.5.1) (2020-05-27)
### Bug Fixes
* fix type of style for various options ([9d822b9](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/9d822b95a6df797e2e63e481573e64ea7d0f9386))
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.7...@react-navigation/bottom-tabs@5.5.0) (2020-05-23)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "5.5.0",
"version": "5.5.2",
"keywords": [
"react-native-component",
"react-component",
@@ -37,7 +37,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/color": "^3.0.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import {
Animated,
TouchableWithoutFeedbackProps,
StyleProp,
TextStyle,
@@ -197,7 +198,7 @@ export type BottomTabBarOptions = {
/**
* Style object for the tab bar container.
*/
style?: StyleProp<ViewStyle>;
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
export type BottomTabBarProps = BottomTabBarOptions & {

View File

@@ -1,8 +1,8 @@
import React from 'react';
import {
View,
Text,
TouchableWithoutFeedback,
Animated,
StyleSheet,
Platform,
StyleProp,
@@ -191,7 +191,7 @@ export default function BottomTabBarItem({
if (typeof label === 'string') {
return (
<Animated.Text
<Text
numberOfLines={1}
style={[
styles.label,
@@ -202,14 +202,10 @@ export default function BottomTabBarItem({
allowFontScaling={allowFontScaling}
>
{label}
</Animated.Text>
</Text>
);
}
if (typeof label === 'string') {
return label;
}
return label({ focused, color });
};

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.26](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.25...@react-navigation/compat@5.1.26) (2020-06-06)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.25](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.24...@react-navigation/compat@5.1.25) (2020-05-27)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.24](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.23...@react-navigation/compat@5.1.24) (2020-05-23)
**Note:** Version bump only for package @react-navigation/compat

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.1.24",
"version": "5.1.26",
"license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": {
@@ -28,7 +28,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"react": "~16.9.0",
"typescript": "^3.8.3"

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.10.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.9.0...@react-navigation/core@5.10.0) (2020-06-06)
### Bug Fixes
* catch missing params when they are required in navigate ([#8389](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8389)) ([8774ca9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/8774ca97e1da91e97677ecd816c85f66af296b93))
* make sure the wildcard pattern catches nested unmatched routes ([c3bd349](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3bd349d77688011c9c55027edd66c6f39de2ade))
* only use the query params for focused route in path ([2d66ef9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2d66ef93ec9923a452415c482c40e7c6b769917c))
* prevent state change being emitted unnecessarily ([ab1f79c](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/ab1f79c096e94475a4da1acf1c850d04fb1bc4cf))
### Features
* add wildcard patterns for paths ([4fe72e3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/4fe72e3ce7bae9120d04e490401f3bad58ebdf5c)), closes [#8019](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8019)
# [5.9.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.2...@react-navigation/core@5.9.0) (2020-05-27)
### Features
* add ref to get current options in `ServerContainer` ([#8333](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8333)) ([0b1a718](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/0b1a718756e208d84b20e45ca56004332308ad54))
## [5.8.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.8.1...@react-navigation/core@5.8.2) (2020-05-23)
**Note:** Version bump only for package @react-navigation/core

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "5.8.2",
"version": "5.10.0",
"keywords": [
"react",
"react-native",

View File

@@ -237,6 +237,12 @@ const BaseNavigationContainer = React.forwardRef(
[getKey, getState, setKey, setState, state, addOptionsGetter]
);
const onStateChangeRef = React.useRef(onStateChange);
React.useEffect(() => {
onStateChangeRef.current = onStateChange;
});
React.useEffect(() => {
if (process.env.NODE_ENV !== 'production') {
if (
@@ -263,12 +269,12 @@ const BaseNavigationContainer = React.forwardRef(
trackState(getRootState);
}
if (!isFirstMountRef.current && onStateChange) {
onStateChange(getRootState());
if (!isFirstMountRef.current && onStateChangeRef.current) {
onStateChangeRef.current(getRootState());
}
isFirstMountRef.current = false;
}, [onStateChange, trackState, getRootState, emitter, state]);
}, [trackState, getRootState, emitter, state]);
return (
<ScheduleUpdateContext.Provider value={scheduleContext}>

View File

@@ -0,0 +1,11 @@
import * as React from 'react';
/**
* Context which holds the values for the current navigation tree.
* Intended for use in SSR. This is not safe to use on the client.
*/
const CurrentRenderContext = React.createContext<
{ options?: object } | undefined
>(undefined);
export default CurrentRenderContext;

View File

@@ -1265,3 +1265,175 @@ it('replaces undefined query params', () => {
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('matches wildcard patterns at root', () => {
const path = '/test/bar/42/whatever';
const config = {
404: '*',
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
},
},
},
};
const state = {
routes: [{ name: '404' }],
};
expect(getPathFromState(state, config)).toBe('/404');
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
});
it('matches wildcard patterns at nested level', () => {
const path = '/bar/42/whatever/baz/initt';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: '*',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { id: '42' },
state: {
routes: [{ name: '404' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe('/bar/42/404');
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(
'/bar/42/404'
);
});
it('matches wildcard patterns at nested level with exact', () => {
const path = '/whatever';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: {
path: '*',
exact: true,
},
},
},
Baz: {},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: '404' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe('/404');
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
});
it('tries to match wildcard patterns at the end', () => {
const path = '/bar/42/test';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: '*',
Test: 'test',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { id: '42' },
state: {
routes: [{ name: 'Test' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('uses nearest parent wildcard match for unmatched paths', () => {
const path = '/bar/42/baz/test';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: 'baz',
},
},
404: '*',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [{ name: '404' }],
},
},
],
};
expect(getPathFromState(state, config)).toBe('/404');
expect(getPathFromState(getStateFromPath(path, config), config)).toBe('/404');
});

View File

@@ -1883,3 +1883,183 @@ it('ignores extra slashes in the pattern', () => {
state
);
});
it('matches wildcard patterns at root', () => {
const path = '/test/bar/42/whatever';
const config = {
404: '*',
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
},
},
},
};
const state = {
routes: [{ name: '404' }],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('matches wildcard patterns at nested level', () => {
const path = '/bar/42/whatever/baz/initt';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: '*',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { id: '42' },
state: {
routes: [{ name: '404' }],
},
},
],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('matches wildcard patterns at nested level with exact', () => {
const path = '/whatever';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: {
path: '*',
exact: true,
},
},
},
Baz: {},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: '404' }],
},
},
],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('tries to match wildcard patterns at the end', () => {
const path = '/bar/42/test';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
404: '*',
Test: 'test',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
params: { id: '42' },
state: {
routes: [{ name: 'Test' }],
},
},
],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('uses nearest parent wildcard match for unmatched paths', () => {
const path = '/bar/42/baz/test';
const config = {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: 'baz',
},
},
404: '*',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [{ name: '404' }],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});

View File

@@ -18,6 +18,19 @@ type ConfigItem = {
screens?: Record<string, ConfigItem>;
};
const getActiveRoute = (state: State): { name: string; params?: object } => {
const route =
typeof state.index === 'number'
? state.routes[state.index]
: state.routes[state.routes.length - 1];
if (route.state) {
return getActiveRoute(route.state);
}
return route;
};
/**
* Utility to serialize a navigation state object to a path string.
*
@@ -69,7 +82,8 @@ export default function getPathFromState(
let pattern: string | undefined;
let currentParams: Record<string, any> = { ...route.params };
let focusedParams: Record<string, any> | undefined;
let focusedRoute = getActiveRoute(state);
let currentOptions = configs;
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
@@ -85,7 +99,7 @@ export default function getPathFromState(
if (route.params) {
const stringify = currentOptions[route.name]?.stringify;
currentParams = fromEntries(
const currentParams = fromEntries(
Object.entries(route.params).map(([key, value]) => [
key,
stringify?.[key] ? stringify[key](value) : String(value),
@@ -95,6 +109,26 @@ export default function getPathFromState(
if (pattern) {
Object.assign(allParams, currentParams);
}
if (focusedRoute === route) {
// If this is the focused route, keep the params for later use
// We save it here since it's been stringified already
focusedParams = { ...currentParams };
pattern
?.split('/')
.filter((p) => p.startsWith(':'))
// eslint-disable-next-line no-loop-func
.forEach((p) => {
const name = getParamName(p);
// Remove the params present in the pattern since we'll only use the rest for query string
if (focusedParams) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete focusedParams[name];
}
});
}
}
// If there is no `screens` property or no nested state, we return pattern
@@ -128,18 +162,19 @@ export default function getPathFromState(
path += pattern
.split('/')
.map((p) => {
const name = p.replace(/^:/, '').replace(/\?$/, '');
const name = getParamName(p);
// We don't know what to show for wildcard patterns
// Showing the route name seems ok, though whatever we show here will be incorrect
// Since the page doesn't actually exist
if (p === '*') {
return route.name;
}
// If the path has a pattern for a param, put the param in the path
if (p.startsWith(':')) {
const value = allParams[name];
// Remove the used value from the params object since we'll use the rest for query string
if (currentParams) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete currentParams[name];
}
if (value === undefined && p.endsWith('?')) {
// Optional params without value assigned in route.params should be ignored
return '';
@@ -155,17 +190,21 @@ export default function getPathFromState(
path += encodeURIComponent(route.name);
}
if (!focusedParams) {
focusedParams = focusedRoute.params;
}
if (route.state) {
path += '/';
} else if (currentParams) {
for (let param in currentParams) {
if (currentParams[param] === 'undefined') {
} else if (focusedParams) {
for (let param in focusedParams) {
if (focusedParams[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete currentParams[param];
delete focusedParams[param];
}
}
const query = queryString.stringify(currentParams);
const query = queryString.stringify(focusedParams);
if (query) {
path += `?${query}`;
@@ -189,6 +228,9 @@ const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
return acc;
}, {} as Record<K, V>);
const getParamName = (pattern: string) =>
pattern.replace(/^:/, '').replace(/\?$/, '');
const joinPaths = (...paths: string[]): string =>
([] as string[])
.concat(...paths.map((p) => p.split('/')))

View File

@@ -59,11 +59,46 @@ export default function getStateFromPath(
createNormalizedConfigs(key, options, [], initialRoutes)
)
)
.sort(
(a, b) =>
// Sort configs so the most exhaustive is always first to be chosen
b.pattern.split('/').length - a.pattern.split('/').length
);
.sort((a, b) => {
// Sort config so that:
// - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end
// If one of the patterns starts with the other, it's more exhaustive
// So move it up
if (a.pattern.startsWith(b.pattern)) {
return 1;
}
if (b.pattern.startsWith(a.pattern)) {
return 1;
}
const aParts = a.pattern.split('/');
const bParts = b.pattern.split('/');
const aWildcardIndex = aParts.indexOf('*');
const bWildcardIndex = bParts.indexOf('*');
// If only one of the patterns has a wildcard, move it down in the list
if (aWildcardIndex === -1 && bWildcardIndex !== -1) {
return -1;
}
if (aWildcardIndex !== -1 && bWildcardIndex === -1) {
return 1;
}
if (aWildcardIndex === bWildcardIndex) {
// If `b` has more `/`, it's more exhaustive
// So we move it up in the list
return bParts.length - aParts.length;
}
// If the wildcard appears later in the pattern (has higher index), it's more specific
// So we move it up in the list
return bWildcardIndex - aWildcardIndex;
});
let remaining = path
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
@@ -104,41 +139,37 @@ export default function getStateFromPath(
let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined;
// We try to match the paths in 2 passes
// In first pass, we match the whole path against the regex instead of segments
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
const { routeNames, allParams, remainingPath } = matchAgainstConfigs(
remaining,
configs.map((c) => ({
...c,
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
}))
);
if (routeNames !== undefined) {
// This will always be empty if full path matched
remaining = remainingPath;
current = createNestedStateObject(
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
result = current;
}
// In second pass, we divide the path into segments and match piece by piece
// This preserves the old behaviour, but we should remove it in next major
while (remaining) {
let routeNames: string[] | undefined;
let allParams: Record<string, any> | undefined;
let { routeNames, allParams, remainingPath } = matchAgainstConfigs(
remaining,
configs
);
// Go through all configs, and see if the next path segment matches our regex
for (const config of configs) {
if (!config.regex) {
continue;
}
const match = remaining.match(config.regex);
// If our regex matches, we need to extract params from the path
if (match) {
routeNames = [...config.routeNames];
const paramPatterns = config.pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
acc[p] = value;
return acc;
}, {});
}
remaining = remaining.replace(match[1], '');
break;
}
}
remaining = remainingPath;
// If we hadn't matched any segments earlier, use the path as route name
if (routeNames === undefined) {
@@ -150,43 +181,7 @@ export default function getStateFromPath(
}
const state = createNestedStateObject(
routeNames.map((name) => {
const config = configs.find((c) => c.screen === name);
let params: object | undefined;
if (allParams && config?.path) {
const pattern = config.path;
if (pattern) {
const paramPatterns = pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
const key = p.replace(/^:/, '').replace(/\?$/, '');
const value = allParams![p];
if (value) {
acc[key] =
config.parse && config.parse[key]
? config.parse[key](value)
: value;
}
return acc;
}, {});
}
}
}
if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
}),
createRouteObjects(configs, routeNames, allParams),
initialRoutes
);
@@ -229,6 +224,46 @@ const joinPaths = (...paths: string[]): string =>
.filter(Boolean)
.join('/');
const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
let routeNames: string[] | undefined;
let allParams: Record<string, any> | undefined;
let remainingPath = remaining;
// Go through all configs, and see if the next path segment matches our regex
for (const config of configs) {
if (!config.regex) {
continue;
}
const match = remainingPath.match(config.regex);
// If our regex matches, we need to extract params from the path
if (match) {
routeNames = [...config.routeNames];
const paramPatterns = config.pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
acc[p] = value;
return acc;
}, {});
}
remainingPath = remainingPath.replace(match[1], '');
break;
}
}
return { routeNames, allParams, remainingPath };
};
const createNormalizedConfigs = (
screen: string,
routeConfig: PathConfig,
@@ -311,7 +346,7 @@ const createConfigItem = (
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}
return `${escape(it)}\\/`;
return `${it === '*' ? '.*' : escape(it)}\\/`;
})
.join('')})`
)
@@ -433,6 +468,49 @@ const createNestedStateObject = (
return state;
};
const createRouteObjects = (
configs: RouteConfig[],
routeNames: string[],
allParams?: Record<string, any>
) =>
routeNames.map((name) => {
const config = configs.find((c) => c.screen === name);
let params: object | undefined;
if (allParams && config?.path) {
const pattern = config.path;
if (pattern) {
const paramPatterns = pattern
.split('/')
.filter((p) => p.startsWith(':'));
if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
const key = p.replace(/^:/, '').replace(/\?$/, '');
const value = allParams![p];
if (value) {
acc[key] =
config.parse && config.parse[key]
? config.parse[key](value)
: value;
}
return acc;
}, {});
}
}
}
if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
});
const findFocusedRoute = (state: InitialState) => {
let current: InitialState | undefined = state;

View File

@@ -7,6 +7,8 @@ export { default as NavigationHelpersContext } from './NavigationHelpersContext'
export { default as NavigationContext } from './NavigationContext';
export { default as NavigationRouteContext } from './NavigationRouteContext';
export { default as CurrentRenderContext } from './CurrentRenderContext';
export { default as useNavigationBuilder } from './useNavigationBuilder';
export { default as useNavigation } from './useNavigation';
export { default as useRoute } from './useRoute';

View File

@@ -0,0 +1,7 @@
/**
* Compare two arrays with primitive values as the content.
* We need to make sure that both values and order match.
*/
export default function isArrayEqual(a: any[], b: any[]) {
return a.length === b.length && a.every((it, index) => it === b[index]);
}

View File

@@ -152,7 +152,7 @@ type NavigationHelpersCommon<
* @param [params] Params object for the route.
*/
navigate<RouteName extends keyof ParamList>(
...args: ParamList[RouteName] extends undefined | any
...args: undefined extends ParamList[RouteName]
? [RouteName] | [RouteName, ParamList[RouteName]]
: [RouteName, ParamList[RouteName]]
): void;

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { NavigationState, ParamListBase } from '@react-navigation/routers';
import CurrentRenderContext from './CurrentRenderContext';
import { Descriptor, NavigationHelpers } from './types';
type Options = {
state: NavigationState;
navigation: NavigationHelpers<ParamListBase>;
descriptors: {
[key: string]: Descriptor<ParamListBase, string, NavigationState, object>;
};
};
/**
* Write the current options, so that server renderer can get current values
* Mutating values like this is not safe in async mode, but it doesn't apply to SSR
*/
export default function useCurrentRender({
state,
navigation,
descriptors,
}: Options) {
const current = React.useContext(CurrentRenderContext);
if (current && navigation.isFocused()) {
current.options = descriptors[state.routes[state.index].key].options;
}
}

View File

@@ -12,14 +12,10 @@ export default function useIsFocused(): boolean {
// eslint-disable-next-line react-hooks/exhaustive-deps
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
const subscribe = React.useCallback(
(callback: (value: boolean) => void) => {
const unsubscribeFocus = navigation.addListener('focus', () =>
callback(true)
);
(callback: () => void) => {
const unsubscribeFocus = navigation.addListener('focus', callback);
const unsubscribeBlur = navigation.addListener('blur', () =>
callback(false)
);
const unsubscribeBlur = navigation.addListener('blur', callback);
return () => {
unsubscribeFocus();

View File

@@ -33,6 +33,8 @@ import {
import useStateGetters from './useStateGetters';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
import isArrayEqual from './isArrayEqual';
// This is to make TypeScript compiler happy
// eslint-disable-next-line babel/no-unused-expressions
@@ -47,13 +49,6 @@ type NavigatorRoute = {
};
};
/**
* Compare two arrays with primitive values as the content.
* We need to make sure that both values and order match.
*/
const isArrayEqual = (a: any[], b: any[]) =>
a.length === b.length && a.every((it, index) => it === b[index]);
/**
* Extract route config object from React children elements.
*
@@ -498,6 +493,12 @@ export default function useNavigationBuilder<
emitter,
});
useCurrentRender({
state,
navigation,
descriptors,
});
return {
state,
navigation,

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import { NavigationState } from '@react-navigation/routers';
import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationRouteContext from './NavigationRouteContext';
import isArrayEqual from './isArrayEqual';
export default function useOnGetState({
getStateForRoute,
@@ -16,13 +17,23 @@ export default function useOnGetState({
const getRehydratedState = React.useCallback(() => {
const state = getState();
return {
...state,
routes: state.routes.map((route) => ({
...route,
state: getStateForRoute(route.key),
})),
};
// Avoid returning new route objects if we don't need to
const routes = state.routes.map((route) => {
const childState = getStateForRoute(route.key);
if (route.state === childState) {
return route;
}
return { ...route, state: childState };
});
if (isArrayEqual(state.routes, routes)) {
return state;
}
return { ...state, routes };
}, [getState, getStateForRoute]);
React.useEffect(() => {

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.8.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.1...@react-navigation/drawer@5.8.2) (2020-06-06)
### Bug Fixes
* typo on drawerPosition default props ([#8357](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8357)) ([762cc44](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/762cc4457842182189eeac84aedbb88169452e1e))
## [5.8.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.8.0...@react-navigation/drawer@5.8.1) (2020-05-27)
**Note:** Version bump only for package @react-navigation/drawer
# [5.8.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.7...@react-navigation/drawer@5.8.0) (2020-05-23)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.8.0",
"version": "5.8.2",
"keywords": [
"react-native-component",
"react-component",
@@ -42,7 +42,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.0",

View File

@@ -100,7 +100,7 @@ type Props = {
export default class DrawerView extends React.Component<Props> {
static defaultProps = {
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front',
gestureEnabled: true,
swipeEnabled: Platform.OS !== 'web',

View File

@@ -238,7 +238,6 @@ export default function DrawerView({
renderDrawerContent={renderNavigationView}
renderSceneContent={renderContent}
keyboardDismissMode={keyboardDismissMode}
drawerPostion={drawerPosition}
dimensions={dimensions}
/>
</DrawerOpenContext.Provider>

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.9...@react-navigation/material-bottom-tabs@5.2.10) (2020-06-06)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.8...@react-navigation/material-bottom-tabs@5.2.9) (2020-05-27)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.7...@react-navigation/material-bottom-tabs@5.2.8) (2020-05-23)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper",
"version": "5.2.8",
"version": "5.2.10",
"keywords": [
"react-native-component",
"react-component",
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"@types/react-native-vector-icons": "^6.4.5",

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.9...@react-navigation/material-top-tabs@5.2.10) (2020-06-06)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.8...@react-navigation/material-top-tabs@5.2.9) (2020-05-27)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.7...@react-navigation/material-top-tabs@5.2.8) (2020-05-23)
**Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.2.8",
"version": "5.2.10",
"keywords": [
"react-native-component",
"react-component",
@@ -41,7 +41,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.0",

View File

@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.5.0...@react-navigation/native@5.5.1) (2020-06-06)
**Note:** Version bump only for package @react-navigation/native
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.4.3...@react-navigation/native@5.5.0) (2020-05-27)
### Bug Fixes
* export types from /native ([af1722d](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/af1722d1e915f3ec234df202f74c4b4c631472c7))
### Features
* add a `ServerContainer` component for SSR ([#8297](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8297)) ([68e750d](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/68e750d5a6d198a2f5bdb86ba631de0a27732943))
* add ref to get current options in `ServerContainer` ([#8333](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8333)) ([0b1a718](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/0b1a718756e208d84b20e45ca56004332308ad54))
## [5.4.3](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.4.2...@react-navigation/native@5.4.3) (2020-05-23)
**Note:** Version bump only for package @react-navigation/native

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/native",
"description": "React Native integration for React Navigation",
"version": "5.4.3",
"version": "5.5.1",
"keywords": [
"react-native",
"react-navigation",
@@ -33,14 +33,17 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^5.8.2"
"@react-navigation/core": "^5.10.0",
"nanoid": "^3.1.9"
},
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.8",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.0",
"react": "~16.9.0",
"react-dom": "^16.13.1",
"react-native": "~0.61.5",
"react-native-testing-library": "^1.13.2",
"typescript": "^3.8.3"

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import { CurrentRenderContext } from '@react-navigation/core';
import ServerContext, { ServerContextType } from './ServerContext';
import { ServerContainerRef } from './types';
type Props = ServerContextType & {
children: React.ReactNode;
};
/**
* Container component for server rendering.
*
* @param props.location Location object to base the initial URL for SSR.
* @param props.children Child elements to render the content.
* @param props.ref Ref object which contains helper methods.
*/
export default React.forwardRef(function ServerContainer(
{ children, location }: Props,
ref: React.Ref<ServerContainerRef>
) {
React.useEffect(() => {
console.error(
"'ServerContainer' should only be used on the server with 'react-dom/server' for SSR."
);
}, []);
const current: { options?: object } = {};
if (ref) {
const value = {
getCurrentOptions() {
return current.options;
},
};
// We write to the `ref` during render instead of `React.useImperativeHandle`
// This is because `useImperativeHandle` will update the ref after 'commit',
// and there's no 'commit' phase during SSR.
// Mutating ref during render is unsafe in concurrent mode, but we don't care about it for SSR.
if (typeof ref === 'function') {
ref(value);
} else {
// @ts-ignore: the TS types are incorrect and say that ref.current is readonly
ref.current = value;
}
}
return (
<ServerContext.Provider value={{ location }}>
<CurrentRenderContext.Provider value={current}>
{children}
</CurrentRenderContext.Provider>
</ServerContext.Provider>
);
});

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
export type ServerContextType = {
location?: {
pathname: string;
search: string;
};
};
const ServerContext = React.createContext<ServerContextType | undefined>(
undefined
);
export default ServerContext;

View File

@@ -0,0 +1,69 @@
const location = new URL('', 'http://example.com');
let listeners: (() => void)[] = [];
let entries = [{ state: null, href: location.href }];
let index = 0;
let currentState: any = null;
const history = {
get state() {
return currentState;
},
pushState(state: any, _: string, path: string) {
Object.assign(location, new URL(path, location.origin));
currentState = state;
entries = entries.slice(0, index + 1);
entries.push({ state, href: location.href });
index = entries.length - 1;
},
replaceState(state: any, _: string, path: string) {
Object.assign(location, new URL(path, location.origin));
currentState = state;
entries[index] = { state, href: location.href };
},
go(n: number) {
setTimeout(() => {
if (
(n > 0 && n < entries.length - index) ||
(n < 0 && Math.abs(n) <= index)
) {
index += n;
Object.assign(location, new URL(entries[index].href));
listeners.forEach((cb) => cb);
}
}, 0);
},
back() {
this.go(-1);
},
forward() {
this.go(1);
},
};
const addEventListener = (type: 'popstate', listener: () => void) => {
if (type === 'popstate') {
listeners.push(listener);
}
};
const removeEventListener = (type: 'popstate', listener: () => void) => {
if (type === 'popstate') {
listeners = listeners.filter((cb) => cb !== listener);
}
};
export default {
location,
history,
addEventListener,
removeEventListener,
};

View File

@@ -0,0 +1,149 @@
import * as React from 'react';
import {
useNavigationBuilder,
createNavigatorFactory,
StackRouter,
TabRouter,
NavigationHelpersContext,
NavigationContainerRef,
} from '@react-navigation/core';
import { act, render } from 'react-native-testing-library';
import NavigationContainer from '../NavigationContainer';
import window from '../__mocks__/window';
// @ts-ignore
global.window = window;
// We want to use the web version of useLinking
jest.mock('../useLinking', () => require('../useLinking.tsx').default);
it('integrates with the history API', () => {
jest.useFakeTimers();
const createStackNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
StackRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
{state.routes.map((route, i) => (
<div key={route.key} aria-current={state.index === i || undefined}>
{descriptors[route.key].render()}
</div>
))}
</NavigationHelpersContext.Provider>
);
});
const createTabNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
TabRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
{state.routes.map((route, i) => (
<div key={route.key} aria-current={state.index === i || undefined}>
{descriptors[route.key].render()}
</div>
))}
</NavigationHelpersContext.Provider>
);
});
const Stack = createStackNavigator();
const Tab = createTabNavigator();
const TestScreen = ({ route }: any): any =>
`${route.name} ${JSON.stringify(route.params)}`;
const linking = {
prefixes: [],
config: {
Home: {
path: '',
initialRouteName: 'Feed',
screens: {
Profile: ':user',
Settings: 'edit',
Updates: 'updates',
Feed: 'feed',
},
},
Chat: 'chat',
},
};
const navigation = React.createRef<NavigationContainerRef>();
render(
<NavigationContainer ref={navigation} linking={linking}>
<Tab.Navigator>
<Tab.Screen name="Home">
{() => (
<Stack.Navigator initialRouteName="Feed">
<Stack.Screen name="Profile" component={TestScreen} />
<Stack.Screen name="Settings" component={TestScreen} />
<Stack.Screen name="Feed" component={TestScreen} />
<Stack.Screen name="Updates" component={TestScreen} />
</Stack.Navigator>
)}
</Tab.Screen>
<Tab.Screen name="Chat" component={TestScreen} />
</Tab.Navigator>
</NavigationContainer>
);
expect(window.location.pathname).toBe('/feed');
act(() => navigation.current?.navigate('Profile', { user: 'jane' }));
expect(window.location.pathname).toBe('/jane');
act(() => navigation.current?.navigate('Updates'));
expect(window.location.pathname).toBe('/updates');
act(() => navigation.current?.goBack());
jest.runAllTimers();
expect(window.location.pathname).toBe('/jane');
act(() => {
window.history.back();
jest.runAllTimers();
});
expect(window.location.pathname).toBe('/feed');
act(() => {
window.history.forward();
jest.runAllTimers();
});
expect(window.location.pathname).toBe('/jane');
act(() => navigation.current?.navigate('Settings'));
expect(window.location.pathname).toBe('/edit');
act(() => {
window.history.go(-2);
jest.runAllTimers();
});
expect(window.location.pathname).toBe('/feed');
act(() => navigation.current?.navigate('Settings'));
act(() => navigation.current?.navigate('Chat'));
expect(window.location.pathname).toBe('/chat');
act(() => navigation.current?.navigate('Home'));
expect(window.location.pathname).toBe('/edit');
});

View File

@@ -0,0 +1,185 @@
import * as React from 'react';
import {
useNavigationBuilder,
createNavigatorFactory,
StackRouter,
TabRouter,
NavigationHelpersContext,
} from '@react-navigation/core';
import { renderToString } from 'react-dom/server';
import NavigationContainer from '../NavigationContainer';
import ServerContainer from '../ServerContainer';
import { ServerContainerRef } from '../types';
// @ts-ignore
global.window = global;
window.addEventListener = () => {};
window.removeEventListener = () => {};
// We want to use the web version of useLinking
jest.mock('../useLinking', () => require('../useLinking.tsx').default);
it('renders correct state with location', () => {
const createStackNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
StackRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
{state.routes.map((route) => (
<div key={route.key}>{descriptors[route.key].render()}</div>
))}
</NavigationHelpersContext.Provider>
);
});
const Stack = createStackNavigator();
const TestScreen = ({ route }: any): any =>
`${route.name} ${JSON.stringify(route.params)}`;
const NestedStack = () => {
return (
<Stack.Navigator initialRouteName="Feed">
<Stack.Screen name="Profile" component={TestScreen} />
<Stack.Screen name="Settings" component={TestScreen} />
<Stack.Screen name="Feed" component={TestScreen} />
<Stack.Screen name="Updates" component={TestScreen} />
</Stack.Navigator>
);
};
const element = (
<NavigationContainer
linking={{
prefixes: [],
config: {
Home: {
initialRouteName: 'Profile',
screens: {
Settings: {
path: ':user/edit',
},
Updates: {
path: ':user/updates',
},
},
},
},
}}
>
<Stack.Navigator>
<Stack.Screen name="Home" component={NestedStack} />
<Stack.Screen name="Chat" component={TestScreen} />
</Stack.Navigator>
</NavigationContainer>
);
// @ts-ignore
window.location = { pathname: '/jane/edit', search: '' };
const client = renderToString(element);
expect(client).toMatchInlineSnapshot(
`"<div><div>Profile undefined</div><div>Settings {&quot;user&quot;:&quot;jane&quot;}</div></div>"`
);
const server = renderToString(
<ServerContainer location={{ pathname: '/john/updates', search: '' }}>
{element}
</ServerContainer>
);
expect(server).toMatchInlineSnapshot(
`"<div><div>Profile undefined</div><div>Updates {&quot;user&quot;:&quot;john&quot;}</div></div>"`
);
});
it('gets the current options', () => {
const createTabNavigator = createNavigatorFactory((props: any) => {
const { navigation, state, descriptors } = useNavigationBuilder(
TabRouter,
props
);
return (
<NavigationHelpersContext.Provider value={navigation}>
{state.routes.map((route) => (
<div key={route.key}>{descriptors[route.key].render()}</div>
))}
</NavigationHelpersContext.Provider>
);
});
const Tab = createTabNavigator();
const TestScreen = ({ route }: any): any =>
`${route.name} ${JSON.stringify(route.params)}`;
const NestedStack = () => {
return (
<Tab.Navigator initialRouteName="Feed">
<Tab.Screen
name="Profile"
component={TestScreen}
options={{ title: 'My profile' }}
/>
<Tab.Screen
name="Settings"
component={TestScreen}
options={{ title: 'Configure' }}
/>
<Tab.Screen
name="Feed"
component={TestScreen}
options={{ title: 'News feed' }}
/>
<Tab.Screen
name="Updates"
component={TestScreen}
options={{ title: 'Updates from cloud', description: 'Woah' }}
/>
</Tab.Navigator>
);
};
const ref = React.createRef<ServerContainerRef>();
renderToString(
<ServerContainer ref={ref}>
<NavigationContainer
initialState={{
routes: [
{
name: 'Others',
state: {
routes: [{ name: 'Updates' }],
},
},
],
}}
>
<Tab.Navigator>
<Tab.Screen
name="Home"
component={TestScreen}
options={{ title: 'My app' }}
/>
<Tab.Screen
name="Others"
component={NestedStack}
options={{ title: 'Other stuff' }}
/>
</Tab.Navigator>
</NavigationContainer>
</ServerContainer>
);
expect(ref.current?.getCurrentOptions()).toEqual({
title: 'Updates from cloud',
description: 'Woah',
});
});

View File

@@ -15,3 +15,7 @@ export { default as useLinking } from './useLinking';
export { default as useLinkTo } from './useLinkTo';
export { default as useLinkProps } from './useLinkProps';
export { default as useLinkBuilder } from './useLinkBuilder';
export { default as ServerContainer } from './ServerContainer';
export * from './types';

View File

@@ -51,3 +51,7 @@ export type LinkingOptions = {
*/
getPathFromState?: typeof getPathFromStateDefault;
};
export type ServerContainerRef = {
getCurrentOptions(): Record<string, any> | undefined;
};

View File

@@ -6,36 +6,226 @@ import {
NavigationState,
getActionFromState,
} from '@react-navigation/core';
import { nanoid } from 'nanoid/non-secure';
import ServerContext from './ServerContext';
import { LinkingOptions } from './types';
type ResultState = ReturnType<typeof getStateFromPathDefault>;
type HistoryState = { index: number };
declare const history: {
state?: HistoryState;
go(delta: number): void;
pushState(state: HistoryState, title: string, url: string): void;
replaceState(state: HistoryState, title: string, url: string): void;
type HistoryRecord = {
// Unique identifier for this record to match it with window.history.state
id: string;
// Navigation state object for the history entry
state: NavigationState;
// Path of the history entry
path: string;
};
const getStateLength = (state: NavigationState) => {
let length = 0;
const createMemoryHistory = () => {
let index = 0;
let items: HistoryRecord[] = [];
if (state.history) {
length = state.history.length;
} else {
length = state.index + 1;
// Whether there's a `history.go(n)` pending
let pending = false;
const history = {
get index(): number {
// We store an id in the state instead of an index
// Index could get out of sync with in-memory values if page reloads
const id = window.history.state?.id;
if (id) {
const index = items.findIndex((item) => item.id === id);
return index > -1 ? index : 0;
}
return 0;
},
get(index: number) {
return items[index]?.state;
},
backIndex({ path }: { path: string }) {
// We need to find the index from the element before current to get closest path to go back to
for (let i = index - 1; i >= 0; i--) {
const item = items[i];
if (item.path === path) {
return i;
}
}
return -1;
},
push({ path, state }: { path: string; state: NavigationState }) {
const id = nanoid();
// When a new entry is pushed, all the existing entries after index will be inaccessible
// So we remove any existing entries after the current index to clean them up
items = items.slice(0, index + 1);
items.push({ path, state, id });
index = items.length - 1;
// We pass empty string for title because it's ignored in all browsers except safari
// We don't store state object in history.state because:
// - browsers have limits on how big it can be, and we don't control the size
// - while not recommended, there could be non-serializable data in state
window.history.pushState({ id }, '', path);
},
replace({ path, state }: { path: string; state: NavigationState }) {
const id = window.history.state?.id ?? nanoid();
if (items.length) {
items[index] = { path, state, id };
} else {
// This is the first time any state modifications are done
// So we need to push the entry as there's nothing to replace
items.push({ path, state, id });
}
window.history.replaceState({ id }, '', path);
},
// `history.go(n)` is asynchronous, there are couple of things to keep in mind:
// - it won't do anything if we can't go `n` steps, the `popstate` event won't fire.
// - each `history.go(n)` call will trigger a separate `popstate` event with correct location.
// - the `popstate` event fires before the next frame after calling `history.go(n)`.
// This method differs from `history.go(n)` in the sense that it'll go back as many steps it can.
go(n: number) {
if (n > 0) {
// We shouldn't go forward more than available index
n = Math.min(n, items.length - 1);
} else if (n < 0) {
// We shouldn't go back more than the index
// Otherwise we'll exit the page
n = Math.max(n, -Math.max(index + 1, 1));
}
if (n === 0) {
return;
}
index += n;
return new Promise((resolve) => {
pending = true;
const done = () => {
pending = false;
window.removeEventListener('popstate', done);
resolve();
};
// Resolve the promise in the next frame
// If `popstate` hasn't fired by then, then it wasn't handled
requestAnimationFrame(() => requestAnimationFrame(done));
window.addEventListener('popstate', done);
window.history.go(n);
});
},
// The `popstate` event is triggered when history changes, except `pushState` and `replaceState`
// If we call `history.go(n)` ourselves, we don't want it to trigger the listener
// Here we normalize it so that only external changes (e.g. user pressing back/forward) trigger the listener
listen(listener: () => void) {
const onPopState = () => {
if (pending) {
// This was triggered by `history.go(n)`, we shouldn't call the listener
return;
}
listener();
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
},
};
return history;
};
/**
* Find the matching navigation state that changed between 2 navigation states
* e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state
*/
const findMatchingState = <T extends NavigationState>(
a: T | undefined,
b: T | undefined
): [T | undefined, T | undefined] => {
if (a === undefined || b === undefined || a.key !== b.key) {
return [undefined, undefined];
}
const focusedState = state.routes[state.index].state;
// Tab and drawer will have `history` property, but stack will have history in `routes`
const aHistoryLength = a.history ? a.history.length : a.routes.length;
const bHistoryLength = b.history ? b.history.length : b.routes.length;
if (focusedState && !focusedState.stale) {
// If the focused route has history entries, we need to count them as well
length += getStateLength(focusedState as NavigationState) - 1;
const aRoute = a.routes[a.index];
const bRoute = b.routes[b.index];
const aChildState = aRoute.state as T | undefined;
const bChildState = bRoute.state as T | undefined;
// Stop here if this is the state object that changed:
// - history length is different
// - focused routes are different
// - one of them doesn't have child state
// - child state keys are different
if (
aHistoryLength !== bHistoryLength ||
aRoute.key !== bRoute.key ||
aChildState === undefined ||
bChildState === undefined ||
aChildState.key !== bChildState.key
) {
return [a, b];
}
return length;
return findMatchingState(aChildState, bChildState);
};
/**
* Run async function in series as it's called.
*/
const series = (cb: () => Promise<void>) => {
// Whether we're currently handling a callback
let handling = false;
let queue: (() => Promise<void>)[] = [];
const callback = async () => {
try {
if (handling) {
// If we're currently handling a previous event, wait before handling this one
// Add the callback to the beginning of the queue
queue.unshift(callback);
return;
}
handling = true;
await cb();
} finally {
handling = false;
if (queue.length) {
// If we have queued items, handle the last one
const last = queue.pop();
last?.();
}
}
};
return callback;
};
let isUsingLinking = false;
@@ -69,6 +259,8 @@ export default function useLinking(
};
});
const [history] = React.useState(createMemoryHistory);
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
@@ -84,11 +276,17 @@ export default function useLinking(
getPathFromStateRef.current = getPathFromState;
}, [config, enabled, getPathFromState, getStateFromPath]);
const server = React.useContext(ServerContext);
const getInitialState = React.useCallback(() => {
let value: ResultState | undefined;
if (enabledRef.current) {
const path = location.pathname + location.search;
const location =
server?.location ??
(typeof window !== 'undefined' ? window.location : undefined);
const path = location ? location.pathname + location.search : undefined;
if (path) {
value = getStateFromPathRef.current(path, configRef.current);
@@ -106,205 +304,146 @@ export default function useLinking(
};
return thenable as PromiseLike<ResultState | undefined>;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
const previousHistoryIndexRef = React.useRef(0);
const pendingIndexChangeRef = React.useRef<number | undefined>();
const pendingStateUpdateRef = React.useRef<boolean>(false);
const pendingStateMultiUpdateRef = React.useRef<boolean>(false);
// If we're navigating ahead >1, we're not restoring whole state,
// but just navigate to the selected route not caring about previous routes
// therefore if we need to go back, we need to pop screen and navigate to the new one
// Possibly, we will need to reuse the same mechanism.
// E.g. if we went ahead+4 (numberOfIndicesAhead = 3), and back-2,
// actually we need to pop the screen we navigated
// and navigate again, setting numberOfIndicesAhead to 1.
const numberOfIndicesAhead = React.useRef(0);
const previousStateRef = React.useRef<NavigationState | undefined>(undefined);
const pendingPopStatePathRef = React.useRef<string | undefined>(undefined);
React.useEffect(() => {
const onPopState = () => {
return history.listen(() => {
const navigation = ref.current;
if (!navigation || !enabled) {
return;
}
const previousHistoryIndex = previousHistoryIndexRef.current;
const historyIndex = history.state?.index ?? 0;
const path = location.pathname + location.search;
previousHistoryIndexRef.current = historyIndex;
pendingPopStatePathRef.current = path;
if (pendingIndexChangeRef.current === historyIndex) {
pendingIndexChangeRef.current = undefined;
// When browser back/forward is clicked, we first need to check if state object for this index exists
// If it does we'll reset to that state object
// Otherwise, we'll handle it like a regular deep link
const recordedState = history.get(history.index);
if (recordedState) {
navigation.resetRoot(recordedState);
return;
}
const state = navigation.getRootState();
const path = getPathFromStateRef.current(state, configRef.current);
const state = getStateFromPathRef.current(path, configRef.current);
let canGoBack = true;
let numberOfBacks = 0;
if (state) {
const action = getActionFromState(state);
if (previousHistoryIndex === historyIndex) {
if (location.pathname + location.search !== path) {
pendingStateUpdateRef.current = true;
history.replaceState({ index: historyIndex }, '', path);
}
} else if (previousHistoryIndex > historyIndex) {
numberOfBacks =
previousHistoryIndex - historyIndex - numberOfIndicesAhead.current;
if (numberOfBacks > 0) {
pendingStateMultiUpdateRef.current = true;
if (numberOfBacks > 1) {
pendingStateMultiUpdateRef.current = true;
}
pendingStateUpdateRef.current = true;
for (let i = 0; i < numberOfBacks; i++) {
navigation.goBack();
}
if (action !== undefined) {
navigation.dispatch(action);
} else {
canGoBack = false;
navigation.resetRoot(state);
}
} else {
// if current path didn't return any state, we should revert to initial state
navigation.resetRoot(state);
}
if (previousHistoryIndex < historyIndex || !canGoBack) {
if (canGoBack) {
numberOfIndicesAhead.current =
historyIndex - previousHistoryIndex - 1;
} else {
navigation.goBack();
numberOfIndicesAhead.current -= previousHistoryIndex - historyIndex;
}
const state = getStateFromPathRef.current(
location.pathname + location.search,
configRef.current
);
pendingStateMultiUpdateRef.current = true;
if (state) {
const action = getActionFromState(state);
pendingStateUpdateRef.current = true;
if (action !== undefined) {
navigation.dispatch(action);
} else {
navigation.resetRoot(state);
}
}
}
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [enabled, ref]);
});
}, [enabled, history, ref]);
React.useEffect(() => {
if (!enabled) {
return;
}
if (ref.current && previousStateLengthRef.current === undefined) {
previousStateLengthRef.current = getStateLength(
ref.current.getRootState()
);
}
if (ref.current && location.pathname + location.search === '/') {
history.replaceState(
{ index: history.state?.index ?? 0 },
'',
getPathFromStateRef.current(
ref.current.getRootState(),
configRef.current
)
);
}
const unsubscribe = ref.current?.addListener('state', () => {
const navigation = ref.current;
if (!navigation) {
return;
}
const state = navigation.getRootState();
if (ref.current) {
// We need to record the current metadata on the first render if they aren't set
// This will allow the initial state to be in the history entry
const state = ref.current.getRootState();
const path = getPathFromStateRef.current(state, configRef.current);
const previousStateLength = previousStateLengthRef.current ?? 1;
const stateLength = getStateLength(state);
if (pendingStateMultiUpdateRef.current) {
if (location.pathname + location.search === path) {
pendingStateMultiUpdateRef.current = false;
} else {
return;
}
if (previousStateRef.current === undefined) {
previousStateRef.current = state;
}
previousStateLengthRef.current = stateLength;
history.replace({ path, state });
}
if (
pendingStateUpdateRef.current &&
location.pathname + location.search === path
) {
pendingStateUpdateRef.current = false;
const onStateChange = async () => {
const navigation = ref.current;
if (!navigation || !enabled) {
return;
}
let index = history.state?.index ?? 0;
const previousState = previousStateRef.current;
const state = navigation.getRootState();
if (previousStateLength === stateLength) {
// If no new entries were added to history in our navigation state, we want to replaceState
if (location.pathname + location.search !== path) {
history.replaceState({ index }, '', path);
previousHistoryIndexRef.current = index;
}
} else if (stateLength > previousStateLength) {
// If new entries were added, pushState until we have same length
// This won't be accurate if multiple entries were added at once, but that's the best we can do
for (let i = 0, l = stateLength - previousStateLength; i < l; i++) {
index++;
history.pushState({ index }, '', path);
}
const pendingPath = pendingPopStatePathRef.current;
const path = getPathFromStateRef.current(state, configRef.current);
previousHistoryIndexRef.current = index;
} else if (previousStateLength > stateLength) {
const delta = Math.min(
previousStateLength - stateLength,
// We need to keep at least one item in the history
// Otherwise we'll exit the page
previousHistoryIndexRef.current - 1
);
previousStateRef.current = state;
pendingPopStatePathRef.current = undefined;
if (delta > 0) {
// We need to set this to ignore the `popstate` event
pendingIndexChangeRef.current = index - delta;
// To detect the kind of state change, we need to:
// - Find the common focused navigation state in previous and current state
// - If only the route keys changed, compare history/routes.length to check if we go back/forward/replace
// - If no common focused navigation state found, it's a replace
const [previousFocusedState, focusedState] = findMatchingState(
previousState,
state
);
// If new entries were removed, go back so that we have same length
history.go(-delta);
} else {
// We're not going back in history, but the navigation state changed
// The URL probably also changed, so we need to re-sync the URL
if (location.pathname + location.search !== path) {
history.replaceState({ index }, '', path);
previousHistoryIndexRef.current = index;
if (
previousFocusedState &&
focusedState &&
// We should only handle push/pop if path changed from what was in last `popstate`
// Otherwise it's likely a change triggered by `popstate`
path !== pendingPath
) {
const historyDelta =
(focusedState.history
? focusedState.history.length
: focusedState.routes.length) -
(previousFocusedState.history
? previousFocusedState.history.length
: previousFocusedState.routes.length);
if (historyDelta > 0) {
// If history length is increased, we should pushState
// Note that path might not actually change here, for example, drawer open should pushState
history.push({ path, state });
} else if (historyDelta < 0) {
// If history length is decreased, i.e. entries were removed, we want to go back
const nextIndex = history.backIndex({ path });
const currentIndex = history.index;
if (nextIndex !== -1 && nextIndex < currentIndex) {
// An existing entry for this path exists and it's less than current index, go back to that
await history.go(nextIndex - currentIndex);
} else {
// We couldn't find an existing entry to go back to, so we'll go back by the delta
// This won't be correct if multiple routes were pushed in one go before
// Usually this shouldn't happen and this is a fallback for that
await history.go(historyDelta);
}
}
}
});
return unsubscribe;
// Store the updated state as well as fix the path if incorrect
history.replace({ path, state });
} else {
// If history length is unchanged, we want to replaceState
history.replace({ path, state });
}
} else {
// If no common navigation state was found, assume it's a replace
// This would happen if the user did a reset/conditionally changed navigators
history.replace({ path, state });
}
};
// We debounce onStateChange coz we don't want multiple state changes to be handled at one time
// This could happen since `history.go(n)` is asynchronous
// If `pushState` or `replaceState` were called before `history.go(n)` completes, it'll mess stuff up
return ref.current?.addListener('state', series(onStateChange));
});
return {

View File

@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.1...@react-navigation/stack@5.4.2) (2020-06-06)
### Bug Fixes
* relatively position float Header if !headerTransparent ([#8285](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8285)) ([78afbff](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/78afbffe976b14bb60666a2b1230127db0dc24f6))
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.4.0...@react-navigation/stack@5.4.1) (2020-05-27)
### Bug Fixes
* allow HeaderBackground's subViews to be touchable ([#8317](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8317)) ([00c23f2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/00c23f2c9ed22fa4d010ffb427f2b52e061d8df4))
* fix type of style for various options ([9d822b9](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/9d822b95a6df797e2e63e481573e64ea7d0f9386))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.9...@react-navigation/stack@5.4.0) (2020-05-23)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "5.4.0",
"version": "5.4.2",
"keywords": [
"react-native-component",
"react-component",
@@ -42,7 +42,7 @@
"devDependencies": {
"@react-native-community/bob": "^0.14.3",
"@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.4.3",
"@react-navigation/native": "^5.5.1",
"@types/color": "^3.0.1",
"@types/react": "^16.9.34",
"@types/react-native": "^0.62.7",

View File

@@ -119,7 +119,7 @@ export type StackHeaderOptions = {
* This may lead to white space or overlap between `headerLeft` and `headerTitle` if a customized `headerLeft` is used.
* It can be solved by adjusting `left` and `right` style in `headerTitleContainerStyle` and `marginHorizontal` in `headerTitleStyle`.
*/
headerTitleContainerStyle?: StyleProp<ViewStyle>;
headerTitleContainerStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
/**
* Tint color for the header.
*/
@@ -157,7 +157,7 @@ export type StackHeaderOptions = {
/**
* Style object for the container of the `headerLeft` component, for example to add padding.
*/
headerLeftContainerStyle?: StyleProp<ViewStyle>;
headerLeftContainerStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
/**
* Function which returns a React Element to display on the right side of the header.
*/
@@ -165,7 +165,7 @@ export type StackHeaderOptions = {
/**
* Style object for the container of the `headerRight` component, for example to add padding.
*/
headerRightContainerStyle?: StyleProp<ViewStyle>;
headerRightContainerStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
/**
* Function which returns a React Element to display custom image in header's back button.
* It receives the `tintColor` in in the options object as an argument. object.
@@ -187,7 +187,7 @@ export type StackHeaderOptions = {
/**
* Style object for the header. You can specify a custom background color here, for example.
*/
headerStyle?: StyleProp<ViewStyle>;
headerStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
/**
* Defaults to `false`. If `true`, the header will not have a background unless you explicitly provide it with `headerBackground`.
* The header will also float over the screen so that it overlaps the content underneath.
@@ -380,7 +380,7 @@ export type StackHeaderLeftButtonProps = {
/**
* Style object for the label.
*/
labelStyle?: React.ComponentProps<typeof Animated.Text>['style'];
labelStyle?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
/**
* Whether label font should scale to respect Text Size accessibility settings.
*/

View File

@@ -1,8 +1,16 @@
import * as React from 'react';
import { Animated, StyleSheet, Platform, ViewProps } from 'react-native';
import {
Animated,
StyleSheet,
Platform,
ViewProps,
StyleProp,
ViewStyle,
} from 'react-native';
import { useTheme } from '@react-navigation/native';
type Props = ViewProps & {
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
children?: React.ReactNode;
};

View File

@@ -312,8 +312,8 @@ export default class HeaderSegment extends React.Component<Props, State> {
return (
<React.Fragment>
<Animated.View
pointerEvents="none"
style={[StyleSheet.absoluteFill, backgroundStyle]}
pointerEvents="box-none"
style={[StyleSheet.absoluteFill, { zIndex: 0 }, backgroundStyle]}
>
{headerBackground ? (
headerBackground({ style: safeStyles })

View File

@@ -46,7 +46,9 @@ type Props = ViewProps & {
onGestureCanceled?: () => void;
onGestureEnd?: () => void;
children: React.ReactNode;
overlay: (props: { style: StyleProp<ViewStyle> }) => React.ReactNode;
overlay: (props: {
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
}) => React.ReactNode;
overlayEnabled: boolean;
shadowEnabled: boolean;
gestureEnabled: boolean;
@@ -83,7 +85,11 @@ export default class Card extends React.Component<Props> {
shadowEnabled: true,
gestureEnabled: true,
gestureVelocityImpact: GESTURE_VELOCITY_IMPACT,
overlay: ({ style }: { style: StyleProp<ViewStyle> }) =>
overlay: ({
style,
}: {
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
}) =>
style ? (
<Animated.View pointerEvents="none" style={[styles.overlay, style]} />
) : null,

View File

@@ -53,8 +53,7 @@ type Props = TransitionPreset & {
gestureVelocityImpact?: number;
mode: StackCardMode;
headerMode: StackHeaderMode;
headerShown?: boolean;
headerTransparent?: boolean;
hasAbsoluteHeader: boolean;
headerHeight: number;
onHeaderHeightChange: (props: {
route: Route<string>;
@@ -82,9 +81,8 @@ function CardContainer({
getFocusedRoute,
mode,
headerMode,
headerShown,
headerStyleInterpolator,
headerTransparent,
hasAbsoluteHeader,
headerHeight,
onHeaderHeightChange,
index,
@@ -187,11 +185,7 @@ function CardContainer({
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
pointerEvents={active ? 'box-none' : pointerEvents}
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
containerStyle={
headerMode === 'float' && !headerTransparent && headerShown !== false
? { marginTop: headerHeight }
: null
}
containerStyle={hasAbsoluteHeader ? { marginTop: headerHeight } : null}
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
style={StyleSheet.absoluteFill}
>

View File

@@ -83,9 +83,10 @@ const getHeaderHeights = (
) => {
return routes.reduce<Record<string, number>>((acc, curr) => {
const { options = {} } = descriptors[curr.key] || {};
const { height = previous[curr.key] } = StyleSheet.flatten(
options.headerStyle || {}
);
const style: any = StyleSheet.flatten(options.headerStyle || {});
const height =
typeof style.height === 'number' ? style.height : previous[curr.key];
const safeAreaInsets = {
...insets,
@@ -334,6 +335,24 @@ export default class CardStack extends React.Component<Props, State> {
return state.routes[state.index];
};
private doesFloatHeaderNeedAbsolutePositioning = () => {
if (this.props.headerMode !== 'float') {
return false;
}
return this.state.scenes.slice(-2).some((scene) => {
const { descriptor } = scene;
const options = descriptor ? descriptor.options : {};
const { headerTransparent, headerShown } = options;
if (headerTransparent || headerShown === false) {
return true;
}
return false;
});
};
render() {
const {
mode,
@@ -362,6 +381,8 @@ export default class CardStack extends React.Component<Props, State> {
const focusedDescriptor = descriptors[focusedRoute.key];
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
const isFloatHeaderAbsolute = this.doesFloatHeaderNeedAbsolutePositioning();
let defaultTransitionPreset =
mode === 'modal' ? ModalTransition : DefaultTransition;
@@ -383,8 +404,36 @@ export default class CardStack extends React.Component<Props, State> {
// For modals, usually we want the screen underneath to be visible, so also disable it there
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
let floatingHeader;
if (headerMode === 'float') {
floatingHeader = (
<React.Fragment key="header">
{renderHeader({
mode: 'float',
layout,
insets: { top, right, bottom, left },
scenes,
getPreviousRoute,
getFocusedRoute: this.getFocusedRoute,
onContentHeightChange: this.handleHeaderLayout,
gestureDirection:
focusedOptions.gestureDirection !== undefined
? focusedOptions.gestureDirection
: defaultTransitionPreset.gestureDirection,
styleInterpolator:
focusedOptions.headerStyleInterpolator !== undefined
? focusedOptions.headerStyleInterpolator
: defaultTransitionPreset.headerStyleInterpolator,
style: isFloatHeaderAbsolute ? styles.floating : undefined,
})}
</React.Fragment>
);
}
return (
<React.Fragment>
{isFloatHeaderAbsolute ? null : floatingHeader}
<MaybeScreenContainer
enabled={isScreensEnabled}
style={styles.container}
@@ -521,8 +570,11 @@ export default class CardStack extends React.Component<Props, State> {
getFocusedRoute={this.getFocusedRoute}
mode={mode}
headerMode={headerMode}
headerShown={headerShown}
headerTransparent={headerTransparent}
hasAbsoluteHeader={
isFloatHeaderAbsolute &&
headerShown !== false &&
!headerTransparent
}
renderHeader={renderHeader}
renderScene={renderScene}
onOpenRoute={onOpenRoute}
@@ -537,26 +589,7 @@ export default class CardStack extends React.Component<Props, State> {
);
})}
</MaybeScreenContainer>
{headerMode === 'float'
? renderHeader({
mode: 'float',
layout,
insets: { top, right, bottom, left },
scenes,
getPreviousRoute,
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,
})
: null}
{isFloatHeaderAbsolute ? floatingHeader : null}
</React.Fragment>
);
}

635
yarn.lock

File diff suppressed because it is too large Load Diff