Compare commits

..

35 Commits

Author SHA1 Message Date
Satyajit Sahoo
515e652b37 wip 2020-04-27 17:46:42 +02:00
Satyajit Sahoo
f2291d110f feat: add a useLinkProps hook 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
942d2be2c7 feat: add action prop to Link 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
b747e527a4 refactor: remove onLink prop for now 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
38020de80b refactor: simplify API for useLinkBuilder 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
67404f4999 test: configure playwright for e2e tests 2020-04-27 17:45:20 +02:00
Satyajit Sahoo
2792f438fe feat: add useLinkBuilder hook to build links
We need to be able to create links from a navigate action to have accessible links in the built-in components such as drawer and tabs.
2020-04-27 17:45:20 +02:00
satyajit.happy
2573b5beaa feat: add Link component as useLinkTo hook for navigating to links
The `Link` component can be used to navigate to URLs. On web, it'll use an `a` tag for proper accessibility. On React Native, it'll use a `Text`.

Example:

```js
<Link to="/feed/hot">Go to 🔥</Link>
```

Sometimes we might want more complex styling and more control over the behaviour, or navigate to a URL programmatically. The `useLinkTo` hook can be used for that.

Example:

```js
function LinkButton({ to, ...rest }) {
  const linkTo = useLinkTo();

  return (
    <Button
      {...rest}
      href={to}
      onPress={(e) => {
        e.preventDefault();
        linkTo(to);
      }}
    />
  );
}
```
2020-04-27 17:45:20 +02:00
Satyajit Sahoo
2697355ab2 chore: publish
- @react-navigation/bottom-tabs@5.2.8
 - @react-navigation/compat@5.1.10
 - @react-navigation/core@5.3.5
 - @react-navigation/drawer@5.5.1
 - @react-navigation/material-bottom-tabs@5.1.10
 - @react-navigation/material-top-tabs@5.1.10
 - @react-navigation/native@5.1.7
 - @react-navigation/routers@5.4.1
 - @react-navigation/stack@5.2.14
2020-04-27 02:57:03 +02:00
Satyajit Sahoo
a695cf9c05 fix: don't add back the route being replaced 2020-04-27 02:41:46 +02:00
Satyajit Sahoo
c9c825bee6 fix: add config to enable redux devtools integration 2020-04-25 21:46:57 +02:00
Satyajit Sahoo
b172b51f17 fix: fix behaviour of openByDefault in drawer when focus changes 2020-04-23 20:00:47 +02:00
Satyajit Sahoo
9c05af50b4 test: add more tests for TabRouter and history 2020-04-23 18:11:30 +02:00
Satyajit Sahoo
24febf6ea9 fix: spread parent params to children in compat navigator
fixes #6785
2020-04-23 14:10:26 +02:00
Satyajit Sahoo
8cbb201f1a fix: fix typo in navigationOptions 2020-04-23 13:51:40 +02:00
Satyajit Sahoo
2467ce4ff7 chore: publish
- @react-navigation/stack@5.2.13
2020-04-22 17:57:16 +02:00
Satyajit Sahoo
5683bebfd6 chore: publish
- @react-navigation/stack@5.2.12
2020-04-22 16:26:11 +02:00
Satyajit Sahoo
78485cea69 fix: animate card to existing closing state on gesture end
fixes #7938
2020-04-22 15:16:39 +02:00
Satyajit Sahoo
1613915669 chore: mark screens and masked view as optional in stack
Needs e54819c4de to work.
2020-04-22 14:02:21 +02:00
Satyajit Sahoo
335a04edc1 chore: add action to check package versions 2020-04-20 14:35:07 +02:00
Satyajit Sahoo
5e0069a896 chore: publish
- @react-navigation/bottom-tabs@5.2.7
 - @react-navigation/compat@5.1.9
 - @react-navigation/core@5.3.4
 - @react-navigation/drawer@5.5.0
 - @react-navigation/material-bottom-tabs@5.1.9
 - @react-navigation/material-top-tabs@5.1.9
 - @react-navigation/native@5.1.6
 - @react-navigation/routers@5.4.0
 - @react-navigation/stack@5.2.11
2020-04-18 01:28:05 +02:00
Satyajit Sahoo
249248e741 chore: update yarn.lock 2020-04-18 01:24:16 +02:00
Evan Bacon
821343fed3 fix: webkit style error in overlay 2020-04-18 01:14:56 +02:00
Satyajit Sahoo
82edb2581b fix: hide inactive screens for stack on web (#8010) 2020-04-18 01:14:11 +02:00
Satyajit Sahoo
cb67530dc5 chore: tweak album example 2020-04-18 01:13:34 +02:00
Satyajit Sahoo
36689e24c2 feat: add openByDefault option to drawer 2020-04-18 01:13:34 +02:00
Gheorghe Pinzaru
6e51f596fa fix: ios presentation modal cuts the topOffset on the bottom (#7943)
* Add padding bottom to ios presentation modal

Because of the translateY moving the screen out to the bottom of view by 10 pt, these 10pt are hidden under the screen, or steal this size from the safe area. To avoid cutting elements, the size of the screen could be decreased by the `topOffset` using padding on the bottom. Fixes #7856

* Update packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx

Co-Authored-By: Serhii Vecherenko <SDSLeon999@gmail.com>

Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
Co-authored-by: Serhii Vecherenko <SDSLeon999@gmail.com>
2020-04-18 01:13:34 +02:00
Satyajit Sahoo
402df73aa2 chore: add link to how to create minimal repro 2020-04-17 00:24:22 +02:00
Satyajit Sahoo
187aefe9c4 fix: handle initial: false for nested route after first initialization 2020-04-14 17:06:58 +02:00
Satyajit Sahoo
2613a62874 chore: add config for netlify 2020-04-12 22:11:22 +02:00
Satyajit Sahoo
6bdf6ae4ed fix: handle in-page go back when there's no history
fixes #7852
2020-04-10 17:59:40 +02:00
Satyajit Sahoo
e2bcf5168c fix: fix drawer not closing on web
fixes #6759
2020-04-10 17:59:07 +02:00
Satyajit Sahoo
dfdba8d741 fix: disable animation by default on web for stack 2020-04-10 17:02:32 +02:00
Satyajit Sahoo
a3f7a5feba fix: add initial param for actions from deep link 2020-04-10 12:05:16 +02:00
Satyajit Sahoo
004c7d7ab1 fix: add initial option for navigating to nested navigators
By default, params passed to nested navigators is used to initialize the navigator if it's not rendered already. The `initial` option would let the user control this behaviour. By specifying `initial: false`, it'll be possible to acheive the old behaviour of rendering the initial route of the stack before navigating to the new screen.

Example:

```js
navigation.navigate('Account', {
  screen: 'Settings',
  initial: false,
});
```
2020-04-10 11:51:32 +02:00
116 changed files with 4290 additions and 611 deletions

View File

@@ -16,7 +16,9 @@ jobs:
keys:
- v1-dependencies-{{ checksum "yarn.lock" }}
- v1-dependencies-
- run: yarn install --frozen-lockfile
- run:
name: Install project dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v1-dependencies-{{ checksum "yarn.lock" }}
paths: node_modules
@@ -28,28 +30,57 @@ jobs:
steps:
- attach_workspace:
at: ~/project
- run: |
yarn lint
yarn typescript
- run:
name: Lint files
command: yarn lint
- run:
name: Typecheck files
command: yarn typescript
unit-tests:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn test --coverage
cat ./coverage/lcov.info | ./node_modules/.bin/codecov
- run:
name: Run unit tests
command: yarn test --coverage
- run:
name: Upload test coverage
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
- store_artifacts:
path: coverage
destination: coverage
integration-tests:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run:
name: Install Headless Chrome dependencies
command: |
sudo apt-get install -yq \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- run:
name: Build example for web
command: yarn example expo build:web --no-pwa
- run:
name: Run integration tests
command: yarn example test --maxWorkers=2
build-packages:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: |
yarn lerna run prepare
node scripts/check-types-path.js
- run:
name: Build packages in the monorepo
command: yarn lerna run prepare
- run:
name: Verify paths for types
command: node scripts/check-types-path.js
workflows:
version: 2
@@ -62,6 +93,9 @@ workflows:
- unit-tests:
requires:
- install-dependencies
- integration-tests:
requires:
- install-dependencies
- build-packages:
requires:
- install-dependencies

View File

@@ -8,6 +8,7 @@
"@react-navigation/routers",
"@react-navigation/compat",
"@react-navigation/stack",
"@react-navigation/web-stack",
"@react-navigation/drawer",
"@react-navigation/bottom-tabs",
"@react-navigation/material-top-tabs",

View File

@@ -13,7 +13,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to reproduce the issue with minimal code (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
args: comment "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help. Make sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) (link to [snack.expo.io](https://snack.expo.io)) or a repo on GitHub, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.)."
needs-repro:
runs-on: ubuntu-latest
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: comment "Hey! Thanks for opening the issue. Can you provide a minimal repro which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
args: comment "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible. The easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then you can also provide the repro in a GitHub repository."
question:
runs-on: ubuntu-latest

27
.github/workflows/versions.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Check versions
on:
issues:
types: [opened]
jobs:
check-versions:
runs-on: ubuntu-latest
steps:
- uses: react-navigation/check-versions-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
packages: |
@react-navigation/bottom-tabs
@react-navigation/compat
@react-navigation/core
@react-navigation/drawer
@react-navigation/material-bottom-tabs
@react-navigation/material-top-tabs
@react-navigation/native
@react-navigation/routers
@react-navigation/stack
react-navigation-animated-switch
react-navigation-drawer
react-navigation-material-bottom-tabs
react-navigation-stack
react-navigation-tabs

View File

@@ -1,10 +0,0 @@
{
"settings": {
"import/core-modules": [
"detox",
"detox/runners/jest/adapter",
"detox/runners/jest/specReporter"
]
},
"env": { "jest": true, "jasmine": true }
}

View File

@@ -0,0 +1,44 @@
import { page } from '../config/setup-playwright';
beforeEach(async () => {
await page.click('[data-testid=LinkComponent]');
});
it('loads the article page', async () => {
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Article?author=Gandalf'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Article by Gandalf');
});
it('goes to the album page and goes back', async () => {
await page.click('[href="/link-component/Album"]');
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Album'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Album');
await page.click('[aria-label="Article by Gandalf, back"]');
await page.waitForNavigation();
expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Article?author=Gandalf'
);
expect(
((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading'
)?.name
).toBe('Article by Gandalf');
});

View File

@@ -1,9 +0,0 @@
import { by, element, expect, device } from 'detox';
beforeEach(async () => {
await device.reloadReactNative();
});
it('has dark theme toggle', async () => {
await expect(element(by.text('Dark theme'))).toBeVisible();
});

View File

@@ -0,0 +1,13 @@
import { page } from '../config/setup-playwright';
it('loads the example app', async () => {
const snapshot = await page.accessibility.snapshot();
// @ts-ignore
expect(snapshot?.children?.find((it) => it.role === 'heading')?.name).toBe(
'Examples'
);
const title = await page.$eval('[role=heading]', (el) => el.textContent);
expect(title).toBe('Examples');
});

View File

@@ -1,6 +0,0 @@
{
"setupFilesAfterEnv": ["./init.js"],
"testEnvironment": "node",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}

View File

@@ -0,0 +1,24 @@
/* eslint-env jest */
import { chromium, Browser, BrowserContext, Page } from 'playwright';
let browser: Browser;
let context: BrowserContext;
let page: Page;
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
beforeEach(async () => {
context = await browser.newContext();
page = await context.newPage();
await page.goto('http://localhost:3579');
});
export { browser, context, page };

View File

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

View File

@@ -0,0 +1,5 @@
import { teardown } from 'jest-dev-server';
export default async function () {
await teardown();
}

View File

@@ -1,28 +0,0 @@
/* eslint-disable import/no-commonjs */
const detox = require('detox');
const config = require('../../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
// Set the default timeout
jest.setTimeout(120000);
jasmine.getEnv().addReporter(adapter);
// This takes care of generating status logs on a per-spec basis. By default, jest only reports at file-level.
// This is strictly optional.
jasmine.getEnv().addReporter(specReporter);
beforeAll(async () => {
await detox.init(config);
}, 300000);
beforeEach(async () => {
await adapter.beforeEach();
});
afterAll(async () => {
await adapter.afterAll();
await detox.cleanup();
});

6
example/jest.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
testRegex: '/__integration_tests__/.*\\.(test|spec)\\.(js|tsx?)$',
globalSetup: './e2e/config/setup-server.tsx',
globalTeardown: './e2e/config/teardown-server.tsx',
setupFilesAfterEnv: ['./e2e/config/setup-playwright.tsx'],
};

View File

@@ -8,7 +8,8 @@
"web": "expo start:web",
"native": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios"
"ios": "react-native run-ios",
"test": "jest"
},
"dependencies": {
"@expo/vector-icons": "^10.0.0",
@@ -32,10 +33,15 @@
},
"devDependencies": {
"@expo/webpack-config": "^0.11.19",
"@types/jest-dev-server": "^4.2.0",
"@types/react": "^16.9.23",
"@types/react-native": "^0.60.22",
"babel-preset-expo": "^8.1.0",
"expo-cli": "^3.17.18",
"jest": "^25.2.7",
"jest-dev-server": "^4.4.0",
"playwright": "^0.14.0",
"serve": "^11.3.0",
"typescript": "^3.8.3"
}
}

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { Platform } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import TouchableBounce from '../Shared/TouchableBounce';
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
return (
<BottomTabs.Navigator
screenOptions={{
tabBarButton: (props) => <TouchableBounce {...props} />,
tabBarButton:
Platform.OS === 'web'
? undefined
: (props) => <TouchableBounce {...props} />,
}}
>
<BottomTabs.Screen

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { View, ScrollView, StyleSheet } from 'react-native';
import { Button } from 'react-native-paper';
import {
createCompatNavigatorFactory,
@@ -11,25 +11,30 @@ import {
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type CompatStackParams = {
Article: { author: string };
Album: undefined;
Albums: undefined;
Nested: { author: string };
};
const ArticleScreen: CompatScreenType<StackNavigationProp<
CompatStackParams,
'Article'
type NestedStackParams = {
Feed: undefined;
Article: { author: string };
};
const AlbumsScreen: CompatScreenType<StackNavigationProp<
CompatStackParams
>> = ({ navigation }) => {
return (
<React.Fragment>
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Album')}
onPress={() => navigation.push('Nested', { author: 'Babel fish' })}
style={styles.button}
>
Push album
Push nested
</Button>
<Button
mode="outlined"
@@ -39,24 +44,20 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
Go back
</Button>
</View>
<Article author={{ name: navigation.getParam('author') }} />
</React.Fragment>
<Albums scrollEnabled={false} />
</ScrollView>
);
};
ArticleScreen.navigationOptions = ({ navigation }) => ({
title: `Article by ${navigation.getParam('author')}`,
});
const AlbumsScreen: CompatScreenType<StackNavigationProp<
CompatStackParams
>> = ({ navigation }) => {
const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
navigation,
}) => {
return (
<React.Fragment>
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
onPress={() => navigation.push('Article')}
style={styles.button}
>
Push article
@@ -69,22 +70,69 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
Go back
</Button>
</View>
<Albums />
</React.Fragment>
<NewsFeed scrollEnabled={false} />
</ScrollView>
);
};
const CompatStack = createCompatNavigatorFactory(createStackNavigator)<
const ArticleScreen: CompatScreenType<StackNavigationProp<
NestedStackParams,
'Article'
>> = ({ navigation }) => {
navigation.dangerouslyGetParent();
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Albums')}
style={styles.button}
>
Push albums
</Button>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Article
author={{ name: navigation.getParam('author') }}
scrollEnabled={false}
/>
</ScrollView>
);
};
ArticleScreen.navigationOptions = ({ navigation }) => ({
title: `Article by ${navigation.getParam('author')}`,
});
const createCompatStackNavigator = createCompatNavigatorFactory(
createStackNavigator
);
const CompatStack = createCompatStackNavigator<
StackNavigationProp<CompatStackParams>
>(
{
Article: {
screen: ArticleScreen,
Albums: AlbumsScreen,
Nested: {
screen: createCompatStackNavigator<
StackNavigationProp<NestedStackParams>
>(
{
Feed: FeedScreen,
Article: ArticleScreen,
},
{ navigationOptions: { headerShown: false } }
),
params: {
author: 'Gandalf',
},
},
Album: AlbumsScreen,
},
{
mode: 'modal',

View File

@@ -0,0 +1,148 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper';
import {
Link,
StackActions,
RouteProp,
ParamListBase,
useLinkProps,
} from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
type SimpleStackParams = {
Article: { author: string };
Album: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const LinkButton = ({
to,
...rest
}: React.ComponentProps<typeof Button> & { to: string }) => {
const props = useLinkProps({ to });
return <Button {...props} {...rest} />;
};
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Link
to="/link-component/Album"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/Album
</Link>
<Link
to="/link-component/Album"
action={StackActions.replace('Album')}
style={[styles.button, { padding: 8 }]}
>
Replace with /link-component/Album
</Link>
<LinkButton
to="/link-component/Album"
mode="contained"
style={styles.button}
>
Go to /link-component/Album
</LinkButton>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
</ScrollView>
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Link
to="/link-component/Article?author=Babel"
style={[styles.button, { padding: 8 }]}
>
Go to /link-component/Article
</Link>
<LinkButton
to="/link-component/Article?author=Babel"
mode="contained"
style={styles.button}
>
Go to /link-component/Article
</LinkButton>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<Albums scrollEnabled={false} />
</ScrollView>
);
};
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
return (
<SimpleStack.Navigator {...rest}>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params.author}`,
})}
initialParams={{ author: 'Gandalf' }}
/>
<SimpleStack.Screen
name="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</SimpleStack.Navigator>
);
}
const styles = StyleSheet.create({
buttons: {
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -0,0 +1,127 @@
import * as React from 'react';
import { Dimensions, ScaledSize } from 'react-native';
import { Appbar } from 'react-native-paper';
import { ParamListBase } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import {
createDrawerNavigator,
DrawerNavigationProp,
DrawerContent,
} from '@react-navigation/drawer';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type DrawerParams = {
Article: undefined;
NewsFeed: undefined;
Album: undefined;
};
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
const useIsLargeScreen = () => {
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
React.useEffect(() => {
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', onDimensionsChange);
return () => Dimensions.removeEventListener('change', onDimensionsChange);
}, []);
return dimensions.width > 414;
};
const Header = ({
onGoBack,
title,
}: {
onGoBack: () => void;
title: string;
}) => {
const isLargeScreen = useIsLargeScreen();
return (
<Appbar.Header>
{isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />}
<Appbar.Content title={title} />
</Appbar.Header>
);
};
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
<Article />
</>
);
};
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
<NewsFeed />
</>
);
};
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
return (
<>
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
<Albums />
</>
);
};
const Drawer = createDrawerNavigator<DrawerParams>();
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
export default function DrawerScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
gestureEnabled: false,
});
const isLargeScreen = useIsLargeScreen();
return (
<Drawer.Navigator
openByDefault
drawerType={isLargeScreen ? 'permanent' : 'back'}
drawerStyle={isLargeScreen ? null : { width: '100%' }}
overlayColor="transparent"
drawerContent={(props) => (
<>
<Appbar.Header>
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
<Appbar.Content title="Pages" />
</Appbar.Header>
<DrawerContent {...props} />
</>
)}
{...rest}
>
<Drawer.Screen name="Article" component={ArticleScreen} />
<Drawer.Screen
name="NewsFeed"
component={NewsFeedScreen}
options={{ title: 'Feed' }}
/>
<Drawer.Screen
name="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</Drawer.Navigator>
);
}

View File

@@ -0,0 +1,3 @@
export default function WebStack() {
return null;
}

View File

@@ -0,0 +1,142 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native';
import {
createWebStackNavigator,
WebStackNavigationProp,
} from '@react-navigation/web-stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type WebStackParams = {
Article: { author: string };
NewsFeed: undefined;
Album: undefined;
};
type WebStackNavigation = WebStackNavigationProp<WebStackParams>;
const ArticleScreen = ({
navigation,
route,
}: {
navigation: WebStackNavigation;
route: RouteProp<WebStackParams, 'Article'>;
}) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.replace('NewsFeed')}
style={styles.button}
>
Replace with feed
</Button>
<Button
mode="outlined"
onPress={() => navigation.pop()}
style={styles.button}
>
Pop screen
</Button>
</View>
<Article author={{ name: route.params.author }} scrollEnabled={false} />
</ScrollView>
);
};
const NewsFeedScreen = ({ navigation }: { navigation: WebStackNavigation }) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.navigate('Album')}
style={styles.button}
>
Navigate to album
</Button>
<Button
mode="outlined"
onPress={() => navigation.goBack()}
style={styles.button}
>
Go back
</Button>
</View>
<NewsFeed scrollEnabled={false} />
</ScrollView>
);
};
const AlbumsScreen = ({ navigation }: { navigation: WebStackNavigation }) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button}
>
Push article
</Button>
<Button
mode="outlined"
onPress={() => navigation.pop(2)}
style={styles.button}
>
Pop by 2
</Button>
</View>
<Albums scrollEnabled={false} />
</ScrollView>
);
};
const WebStack = createWebStackNavigator<WebStackParams>();
type Props = Partial<React.ComponentProps<typeof WebStack.Navigator>> & {
navigation: WebStackNavigationProp<ParamListBase>;
};
export default function WebStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
return (
<WebStack.Navigator {...rest}>
<WebStack.Screen
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params.author}`,
})}
initialParams={{ author: 'Gandalf' }}
/>
<WebStack.Screen
name="NewsFeed"
component={NewsFeedScreen}
options={{ title: 'Feed' }}
/>
<WebStack.Screen
name="Album"
component={AlbumsScreen}
options={{ title: 'Album' }}
/>
</WebStack.Navigator>
);
}
const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -9,6 +9,7 @@ import {
ScrollViewProps,
Dimensions,
Platform,
ScaledSize,
} from 'react-native';
import { useScrollToTop } from '@react-navigation/native';
@@ -40,15 +41,38 @@ const COVERS = [
];
export default function Albums(props: Partial<ScrollViewProps>) {
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
React.useEffect(() => {
const onDimensionsChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', onDimensionsChange);
return () => Dimensions.removeEventListener('change', onDimensionsChange);
}, []);
const ref = React.useRef<ScrollView>(null);
useScrollToTop(ref);
const itemSize = dimensions.width / Math.floor(dimensions.width / 150);
return (
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
{COVERS.map((source, i) => (
// eslint-disable-next-line react/no-array-index-key
<View key={i} style={styles.item}>
<View
// eslint-disable-next-line react/no-array-index-key
key={i}
style={[
styles.item,
Platform.OS !== 'web' && {
height: itemSize,
width: itemSize,
},
]}
>
<Image source={source} style={styles.photo} />
</View>
))}
@@ -76,10 +100,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
flexWrap: 'wrap',
},
item: {
height: Dimensions.get('window').width / 2,
width: '50%',
},
},
}),
photo: {

View File

@@ -22,10 +22,10 @@ import {
Appbar,
List,
Divider,
Text,
} from 'react-native-paper';
import {
InitialState,
useLinking,
NavigationContainerRef,
NavigationContainer,
DefaultTheme,
@@ -43,6 +43,8 @@ import {
} from '@react-navigation/stack';
import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
import WebStack from './Screens/WebStack';
import SimpleStack from './Screens/SimpleStack';
import ModalPresentationStack from './Screens/ModalPresentationStack';
import StackTransparent from './Screens/StackTransparent';
@@ -53,12 +55,16 @@ import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI';
import SettingsItem from './Shared/SettingsItem';
import MasterDetail from './Screens/MasterDetail';
import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
enableScreens();
// @ts-ignore
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
type RootDrawerParamList = {
Root: undefined;
Another: undefined;
@@ -71,6 +77,11 @@ type RootStackParamList = {
};
const SCREENS = {
...(Platform.OS === 'web'
? {
WebStack: { title: 'Web Stack', component: WebStack },
}
: null),
SimpleStack: { title: 'Simple Stack', component: SimpleStack },
ModalPresentationStack: {
title: 'Modal Presentation Stack',
@@ -97,6 +108,10 @@ const SCREENS = {
title: 'Dynamic Tabs',
component: DynamicTabs,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
@@ -105,6 +120,10 @@ const SCREENS = {
title: 'Compat Layer',
component: CompatAPI,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
},
};
const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -118,34 +137,6 @@ Asset.loadAsync(StackAssets);
export default function App() {
const containerRef = React.useRef<NavigationContainerRef>(null);
// To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
const { getInitialState } = useLinking(containerRef, {
prefixes: LinkingPrefixes,
config: {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
acc[name] = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
return acc;
},
{ Home: '' }
),
},
},
});
const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false);
@@ -156,12 +147,13 @@ export default function App() {
React.useEffect(() => {
const restoreState = async () => {
try {
let state = await getInitialState();
let state;
if (Platform.OS !== 'web' && state === undefined) {
const savedState = await AsyncStorage.getItem(
NAVIGATION_PERSISTENCE_KEY
);
state = savedState ? JSON.parse(savedState) : undefined;
}
@@ -182,7 +174,7 @@ export default function App() {
};
restoreState();
}, [getInitialState]);
}, []);
const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
@@ -214,7 +206,7 @@ export default function App() {
return null;
}
const isLargeScreen = dimensions.width > 900;
const isLargeScreen = dimensions.width >= 1024;
return (
<PaperProvider theme={paperTheme}>
@@ -231,6 +223,34 @@ export default function App() {
)
}
theme={theme}
linking={{
// To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
prefixes: LinkingPrefixes,
config: {
Root: {
path: '',
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
(acc, name) => {
// Convert screen names such as SimpleStack to kebab case (simple-stack)
acc[name] = name
.replace(/([A-Z]+)/g, '-$1')
.replace(/^-/, '')
.toLowerCase();
return acc;
},
{ Home: '' }
),
},
},
}}
fallback={<Text>Loading</Text>}
>
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen
@@ -306,6 +326,7 @@ export default function App() {
(name) => (
<List.Item
key={name}
testID={name}
title={SCREENS[name].title}
onPress={() => navigation.navigate(name)}
/>

5
netlify.toml Normal file
View File

@@ -0,0 +1,5 @@
[build]
base = "/"
publish = "example/web-build"
command = "yarn example expo build:web"

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.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.7](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.6...@react-navigation/bottom-tabs@5.2.7) (2020-04-17)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.5...@react-navigation/bottom-tabs@5.2.6) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "5.2.6",
"version": "5.2.8",
"keywords": [
"react-native-component",
"react-component",
@@ -35,7 +35,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/color": "^3.0.1",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",

View File

@@ -4,6 +4,7 @@ import {
StyleProp,
TextStyle,
ViewStyle,
GestureResponderEvent,
} from 'react-native';
import {
NavigationHelpers,
@@ -196,6 +197,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
};
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & {
export type BottomTabBarButtonProps = Omit<
TouchableWithoutFeedbackProps,
'onPress'
> & {
to?: string;
children: React.ReactNode;
onPress?: (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => void;
};

View File

@@ -14,6 +14,7 @@ import {
NavigationRouteContext,
CommonActions,
useTheme,
useLinkBuilder,
} from '@react-navigation/native';
import { useSafeArea } from 'react-native-safe-area-context';
@@ -50,6 +51,7 @@ export default function BottomTabBar({
tabStyle,
}: Props) {
const { colors } = useTheme();
const buildLink = useLinkBuilder();
const [dimensions, setDimensions] = React.useState(() => {
const { height = 0, width = 0 } = Dimensions.get('window');
@@ -260,6 +262,7 @@ export default function BottomTabBar({
onPress={onPress}
onLongPress={onLongPress}
accessibilityLabel={accessibilityLabel}
to={buildLink(route.name, route.params)}
testID={options.tabBarTestID}
allowFontScaling={allowFontScaling}
activeTintColor={activeTintColor}

View File

@@ -4,11 +4,13 @@ import {
TouchableWithoutFeedback,
Animated,
StyleSheet,
Platform,
StyleProp,
ViewStyle,
TextStyle,
GestureResponderEvent,
} from 'react-native';
import { Route, useTheme } from '@react-navigation/native';
import { Link, Route, useTheme } from '@react-navigation/native';
import Color from 'color';
import TabBarIcon from './TabBarIcon';
@@ -37,6 +39,10 @@ type Props = {
size: number;
color: string;
}) => React.ReactNode;
/**
* URL to use for the link to the tab.
*/
to?: string;
/**
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
*/
@@ -50,13 +56,16 @@ type Props = {
*/
testID?: string;
/**
* Function to execute on press.
* Function to execute on press in React Native.
* On the web, this will use onClick.
*/
onPress: () => void;
onPress: (
e: React.MouseEvent<HTMLElement, MouseEvent> | GestureResponderEvent
) => void;
/**
* Function to execute on long press.
*/
onLongPress: () => void;
onLongPress: (e: GestureResponderEvent) => void;
/**
* Whether the label should be aligned with the icon horizontally.
*/
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
route,
label,
icon,
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => (
<TouchableWithoutFeedback {...rest}>
<View style={style}>{children}</View>
</TouchableWithoutFeedback>
),
to,
button = ({
children,
style,
onPress,
to,
accessibilityRole,
...rest
}: BottomTabBarButtonProps) => {
if (Platform.OS === 'web' && to) {
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
// We need to use `onClick` to be able to prevent default browser handling of links.
return (
<Link
{...rest}
to={to}
style={[styles.button, style]}
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
>
{children}
</Link>
);
} else {
return (
<TouchableWithoutFeedback
{...rest}
accessibilityRole={accessibilityRole}
onPress={onPress}
>
<View style={style}>{children}</View>
</TouchableWithoutFeedback>
);
}
},
accessibilityLabel,
testID,
onPress,
@@ -196,6 +242,7 @@ export default function BottomTabBarItem({
: inactiveBackgroundColor;
return button({
to,
onPress,
onLongPress,
testID,
@@ -248,4 +295,7 @@ const styles = StyleSheet.create({
fontSize: 12,
marginLeft: 20,
},
button: {
display: 'flex',
},
});

View File

@@ -1,7 +1,11 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { TabNavigationState, useTheme } from '@react-navigation/native';
import {
NavigationHelpersContext,
TabNavigationState,
useTheme,
} from '@react-navigation/native';
// eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens';
@@ -91,44 +95,46 @@ export default class BottomTabView extends React.Component<Props, State> {
};
render() {
const { state, descriptors, lazy } = this.props;
const { state, descriptors, navigation, lazy } = this.props;
const { routes } = state;
const { loaded } = this.state;
return (
<SafeAreaProviderCompat>
<View style={styles.container}>
<ScreenContainer style={styles.pages}>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
<NavigationHelpersContext.Provider value={navigation}>
<SafeAreaProviderCompat>
<View style={styles.container}>
<ScreenContainer style={styles.pages}>
{routes.map((route, index) => {
const descriptor = descriptors[route.key];
const { unmountOnBlur } = descriptor.options;
const isFocused = state.index === index;
if (unmountOnBlur && !isFocused) {
return null;
}
if (unmountOnBlur && !isFocused) {
return null;
}
if (lazy && !loaded.includes(index) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
if (lazy && !loaded.includes(index) && !isFocused) {
// Don't render a screen if we've never navigated to it
return null;
}
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
>
<SceneContent isFocused={isFocused}>
{descriptor.render()}
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
{this.renderTabBar()}
</View>
</SafeAreaProviderCompat>
return (
<ResourceSavingScene
key={route.key}
style={StyleSheet.absoluteFill}
isVisible={isFocused}
>
<SceneContent isFocused={isFocused}>
{descriptor.render()}
</SceneContent>
</ResourceSavingScene>
);
})}
</ScreenContainer>
{this.renderTabBar()}
</View>
</SafeAreaProviderCompat>
</NavigationHelpersContext.Provider>
);
}
}

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)
### Bug Fixes
* fix typo in navigationOptions ([8cbb201](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/8cbb201f1a7fb90e45a078df6bc42ce4771cc6a6))
* spread parent params to children in compat navigator ([24febf6](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/24febf6ea99be2e5f22005fdd2a82136d647255c)), closes [#6785](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/6785)
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.8...@react-navigation/compat@5.1.9) (2020-04-17)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.7...@react-navigation/compat@5.1.8) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.1.8",
"version": "5.1.10",
"license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": {
@@ -26,7 +26,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/react": "^16.9.23",
"react": "~16.9.0",
"typescript": "^3.8.3"

View File

@@ -7,6 +7,7 @@ import {
NavigationProp,
RouteProp,
EventMapBase,
NavigationRouteContext,
} from '@react-navigation/native';
import CompatScreen from './CompatScreen';
import ScreenPropsContext from './ScreenPropsContext';
@@ -67,6 +68,9 @@ export default function createCompatNavigatorFactory<
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
function Navigator({ screenProps }: { screenProps?: unknown }) {
const parentRouteParams = React.useContext(NavigationRouteContext)
?.params;
const screens = React.useMemo(
() =>
routeNames.map((name) => {
@@ -135,7 +139,7 @@ export default function createCompatNavigatorFactory<
<Pair.Screen
key={name}
name={name}
initialParams={initialParams}
initialParams={{ ...parentRouteParams, ...initialParams }}
options={screenOptions}
>
{({ navigation, route }) => (
@@ -148,7 +152,7 @@ export default function createCompatNavigatorFactory<
</Pair.Screen>
);
}),
[screenProps]
[parentRouteParams, screenProps]
);
return (
@@ -163,7 +167,7 @@ export default function createCompatNavigatorFactory<
);
}
Navigator.navigationOtions = parentNavigationOptions;
Navigator.navigationOptions = parentNavigationOptions;
return Navigator;
};

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)
### Bug Fixes
* add config to enable redux devtools integration ([c9c825b](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c9c825bee61426635a28ee149eeeff3d628171cd))
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.3...@react-navigation/core@5.3.4) (2020-04-17)
### Bug Fixes
* add initial option for navigating to nested navigators ([004c7d7](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/004c7d7ab1f80faf04b2a1836ec6b79a5419e45f))
* add initial param for actions from deep link ([a3f7a5f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/a3f7a5feba2e6aa2158aeaea6cde73ae1603173e))
* handle initial: false for nested route after first initialization ([187aefe](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/187aefe9c400b499f920c212bf856414e25c5aaf))
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.2...@react-navigation/core@5.3.3) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "5.3.3",
"version": "5.3.5",
"keywords": [
"react",
"react-native",
@@ -29,7 +29,7 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/routers": "^5.3.0",
"@react-navigation/routers": "^5.4.1",
"escape-string-regexp": "^2.0.0",
"nanoid": "^3.0.2",
"query-string": "^6.12.0",

View File

@@ -21,6 +21,9 @@ import { NavigationContainerRef, NavigationContainerProps } from './types';
type State = NavigationState | PartialState<NavigationState> | undefined;
const DEVTOOLS_CONFIG_KEY =
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
const MISSING_CONTEXT_ERROR =
"Couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'? See https://reactnavigation.org/docs/getting-started for setup instructions.";
@@ -143,7 +146,9 @@ const BaseNavigationContainer = React.forwardRef(
);
const { trackState, trackAction } = useDevTools({
enabled: false,
enabled:
// @ts-ignore
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
name: '@react-navigation',
reset,
state,

View File

@@ -0,0 +1,13 @@
import * as React from 'react';
import { ParamListBase } from '@react-navigation/routers';
import { NavigationHelpers } from './types';
/**
* Context which holds the navigation helpers of the parent navigator.
* Navigators should use this context in their view component.
*/
const NavigationHelpersContext = React.createContext<
NavigationHelpers<ParamListBase> | undefined
>(undefined);
export default NavigationHelpersContext;

View File

@@ -35,8 +35,10 @@ it('gets navigate action from state', () => {
author: 'jane',
},
screen: 'qux',
initial: true,
},
screen: 'bar',
initial: true,
},
},
type: 'NAVIGATE',
@@ -70,9 +72,11 @@ it('gets navigate action from state', () => {
payload: {
name: 'foo',
params: {
initial: true,
screen: 'bar',
params: {
screen: 'quz',
initial: false,
},
},
},

View File

@@ -737,6 +737,366 @@ it('navigates to nested child in a navigator', () => {
);
});
it('navigates to nested child in a navigator with initial: false', () => {
const TestRouter: typeof MockRouter = (options) => {
const router = MockRouter(options);
return {
...router,
getStateForAction(state, action, options) {
switch (action.type) {
case 'NAVIGATE': {
if (!options.routeNames.includes(action.payload.name as any)) {
return null;
}
const routes = [
...state.routes,
{
key: String(MockRouterKey.current++),
name: action.payload.name,
params: action.payload.params,
},
];
return {
...state,
index: routes.length - 1,
routes,
};
}
default:
return router.getStateForAction(state, action, options);
}
},
} as typeof router;
};
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(TestRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestComponent = ({ route }: any): any =>
`[${route.name}, ${JSON.stringify(route.params)}]`;
const onStateChange = jest.fn();
const navigation = React.createRef<NavigationContainerRef>();
const first = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(first).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '1',
routeNames: ['foo-a', 'foo-b'],
routes: [
{
key: 'foo-a',
name: 'foo-a',
},
{
key: 'foo-b',
name: 'foo-b',
},
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
screen: 'bar-b',
params: { test: 42 },
})
);
expect(first).toMatchInlineSnapshot(
`"[bar-b, {\\"some\\":\\"stuff\\",\\"test\\":42}]"`
);
expect(navigation.current?.getRootState()).toEqual({
index: 2,
key: '0',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar' },
{
key: '2',
name: 'bar',
params: { params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '3',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b',
name: 'bar-b',
params: { some: 'stuff', test: 42 },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
const second = render(
<BaseNavigationContainer ref={navigation} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">
{() => (
<TestNavigator>
<Screen name="foo-a" component={TestComponent} />
<Screen name="foo-b" component={TestComponent} />
</TestNavigator>
)}
</Screen>
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(second).toMatchInlineSnapshot(`"[foo-a, undefined]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 0,
key: '4',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '5',
routeNames: ['foo-a', 'foo-b'],
routes: [
{ key: 'foo-a', name: 'foo-a' },
{ key: 'foo-b', name: 'foo-b' },
],
stale: false,
type: 'test',
},
},
{ key: 'bar', name: 'bar' },
],
stale: false,
type: 'test',
});
act(
() =>
navigation.current &&
navigation.current.navigate('bar', {
screen: 'bar-b',
params: { test: 42 },
initial: false,
})
);
expect(second).toMatchInlineSnapshot(`"[bar-b, {\\"test\\":42}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 2,
key: '4',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar' },
{
key: '6',
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 2,
key: '7',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b',
name: 'bar-b',
params: { some: 'stuff' },
},
{ key: '8', name: 'bar-b', params: { test: 42 } },
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
const third = render(
<BaseNavigationContainer
ref={navigation}
initialState={{
index: 1,
routes: [
{ name: 'foo' },
{
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '7',
routes: [
{
name: 'bar-a',
params: { lol: 'why' },
},
{
name: 'bar-b',
params: { some: 'stuff' },
},
],
type: 'test',
},
},
],
type: 'test',
}}
>
<TestNavigator>
<Screen name="foo" component={TestComponent} />
<Screen name="bar">
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestComponent}
initialParams={{ lol: 'why' }}
/>
<Screen
name="bar-b"
component={TestComponent}
initialParams={{ some: 'stuff' }}
/>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(third).toMatchInlineSnapshot(`"[bar-b, {\\"some\\":\\"stuff\\"}]"`);
expect(navigation.current?.getRootState()).toEqual({
index: 1,
key: '11',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo-9', name: 'foo' },
{
key: 'bar-10',
name: 'bar',
params: { initial: false, params: { test: 42 }, screen: 'bar-b' },
state: {
index: 1,
key: '14',
routeNames: ['bar-a', 'bar-b'],
routes: [
{
key: 'bar-a-12',
name: 'bar-a',
params: { lol: 'why' },
},
{
key: 'bar-b-13',
name: 'bar-b',
params: { some: 'stuff' },
},
],
stale: false,
type: 'test',
},
},
],
stale: false,
type: 'test',
});
});
it('gives access to internal state', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);

View File

@@ -3,6 +3,7 @@ import { PartialState, NavigationState } from '@react-navigation/routers';
type NavigateParams = {
screen?: string;
params?: NavigateParams;
initial?: boolean;
};
type NavigateAction = {
@@ -35,6 +36,7 @@ export default function getActionFromState(
}
route = current.routes[current.routes.length - 1];
params.initial = current.routes.length === 1;
params.screen = route.name;
if (route.state) {

View File

@@ -3,6 +3,7 @@ export * from '@react-navigation/routers';
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
export { default as createNavigatorFactory } from './createNavigatorFactory';
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
export { default as NavigationContext } from './NavigationContext';
export { default as NavigationRouteContext } from './NavigationRouteContext';

View File

@@ -193,6 +193,20 @@ type NavigationHelpersCommon<
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
canGoBack(): boolean;
/**
* Returns the parent navigator, if any. Reason why the function is called
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
* of parent and other hard-to-follow patterns.
*/
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
/**
* Returns the navigator's state. Reason why the function is called
* dangerouslyGetState is to discourage developers to use internal navigation's state.
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
dangerouslyGetState(): State;
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
export type NavigationHelpers<
@@ -254,20 +268,6 @@ export type NavigationProp<
* @param options Options object for the route.
*/
setOptions(options: Partial<ScreenOptions>): void;
/**
* Returns the parent navigator, if any. Reason why the function is called
* dangerouslyGetParent is to warn developers against overusing it to eg. get parent
* of parent and other hard-to-follow patterns.
*/
dangerouslyGetParent<T = NavigationProp<ParamListBase> | undefined>(): T;
/**
* Returns the navigator's state. Reason why the function is called
* dangerouslyGetState is to discourage developers to use internal navigation's state.
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
*/
dangerouslyGetState(): State;
} & EventConsumer<EventMap & EventMapCore<State>> &
PrivateValueStore<ParamList, RouteName, EventMap>;

View File

@@ -43,6 +43,7 @@ type NavigatorRoute = {
params?: {
screen?: string;
params?: object;
initial?: boolean;
};
};
@@ -176,17 +177,19 @@ export default function useNavigationBuilder<
| NavigatorRoute
| undefined;
const previousRouteRef = React.useRef(route);
const previousNestedParamsRef = React.useRef(route?.params);
React.useEffect(() => {
previousRouteRef.current = route;
previousNestedParamsRef.current = route?.params;
}, [route]);
const { children, ...rest } = options;
const { current: router } = React.useRef<Router<State, any>>(
createRouter({
...((rest as unknown) as RouterOptions),
...(route?.params && typeof route.params.screen === 'string'
...(route?.params &&
route.params.initial !== false &&
typeof route.params.screen === 'string'
? { initialRouteName: route.params.screen }
: null),
})
@@ -219,7 +222,7 @@ export default function useNavigationBuilder<
(acc, curr) => {
const { initialParams } = screens[curr];
const initialParamsFromParams =
route?.params && route.params.screen === curr
route?.params?.initial !== false && route?.params?.screen === curr
? route.params.params
: undefined;
@@ -266,6 +269,8 @@ export default function useNavigationBuilder<
>();
const initializedStateRef = React.useRef<State>();
let isFirstStateInitialization = false;
if (
initializedStateRef.current === undefined ||
currentState !== previousStateRef.current
@@ -274,16 +279,21 @@ export default function useNavigationBuilder<
// We also need to re-initialize it if the state passed from parent was changed (maybe due to reset)
// Otherwise assume that the state was provided as initial state
// So we need to rehydrate it to make it usable
initializedStateRef.current =
currentState === undefined || !isStateValid(currentState)
? router.getInitialState({
routeNames,
routeParamList,
})
: router.getRehydratedState(currentState as PartialState<State>, {
routeNames,
routeParamList,
});
if (currentState === undefined || !isStateValid(currentState)) {
isFirstStateInitialization = true;
initializedStateRef.current = router.getInitialState({
routeNames,
routeParamList,
});
} else {
initializedStateRef.current = router.getRehydratedState(
currentState as PartialState<State>,
{
routeNames,
routeParamList,
}
);
}
}
React.useEffect(() => {
@@ -310,7 +320,8 @@ export default function useNavigationBuilder<
if (
typeof route?.params?.screen === 'string' &&
route.params !== previousRouteRef.current?.params
(route.params !== previousNestedParamsRef.current ||
(route.params.initial === false && isFirstStateInitialization))
) {
// If the route was updated with new name and/or params, we should navigate there
// The update should be limited to current navigator only, so we call the router manually

View File

@@ -7,7 +7,6 @@ import {
Router,
} from '@react-navigation/routers';
import { NavigationEventEmitter } from './useEventEmitter';
import NavigationContext from './NavigationContext';
import { NavigationHelpers, NavigationProp } from './types';
@@ -49,12 +48,10 @@ export default function useNavigationCache<
// Cache object which holds navigation objects for each screen
// We use `React.useMemo` instead of `React.useRef` coz we want to invalidate it when deps change
// In reality, these deps will rarely change, if ever
const parentNavigation = React.useContext(NavigationContext);
const cache = React.useMemo(
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[getState, navigation, setOptions, router, emitter, parentNavigation]
[getState, navigation, setOptions, router, emitter]
);
const actions = {
@@ -99,8 +96,6 @@ export default function useNavigationCache<
...rest,
...helpers,
...emitter.create(route.key),
dangerouslyGetParent: () => parentNavigation as any,
dangerouslyGetState: getState,
dispatch,
setOptions: (options: object) =>
setOptions((o) => ({

View File

@@ -112,6 +112,8 @@ export default function useNavigationHelpers<
false
);
},
dangerouslyGetParent: () => parentNavigationHelpers as any,
dangerouslyGetState: getState,
} as NavigationHelpers<ParamListBase, EventMap> &
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);

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/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27)
**Note:** Version bump only for package @react-navigation/drawer
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.1...@react-navigation/drawer@5.5.0) (2020-04-17)
### Bug Fixes
* fix drawer not closing on web ([e2bcf51](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/e2bcf5168c389833eaaeadb4b8794aaea4a66d17)), closes [#6759](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/6759)
* webkit style error in overlay ([821343f](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/821343fed38577cfdc87a78f13f991d5760bf8f5))
### Features
* add openByDefault option to drawer ([36689e2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/36689e24c21b474692bb7ecd0b901c8afbbe9a20))
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.4.0...@react-navigation/drawer@5.4.1) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.4.1",
"version": "5.5.1",
"keywords": [
"react-native-component",
"react-component",
@@ -40,7 +40,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"del-cli": "^3.0.0",

View File

@@ -21,6 +21,7 @@ type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
function DrawerNavigator({
initialRouteName,
openByDefault,
backBehavior,
children,
screenOptions,
@@ -33,6 +34,7 @@ function DrawerNavigator({
DrawerNavigationEventMap
>(DrawerRouter, {
initialRouteName,
openByDefault,
backBehavior,
children,
screenOptions,

View File

@@ -14,7 +14,8 @@ import {
import {
PanGestureHandler,
TapGestureHandler,
State,
State as GestureState,
TapGestureHandlerStateChangeEvent,
} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
import Overlay from './Overlay';
@@ -95,33 +96,15 @@ type Props = {
renderDrawerContent: Renderer;
renderSceneContent: Renderer;
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
dimensions: { width: number; height: number };
};
/**
* Disables the pan gesture by default on Apple devices in the browser.
* https://stackoverflow.com/a/9039885
*/
function shouldEnableSwipeGesture(): boolean {
if (
Platform.OS === 'web' &&
typeof navigator !== 'undefined' &&
typeof window !== 'undefined'
) {
const isWebAppleDevice =
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return !isWebAppleDevice;
}
return true;
}
export default class DrawerView extends React.Component<Props> {
static defaultProps = {
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front',
gestureEnabled: true,
swipeEnabled: shouldEnableSwipeGesture(),
swipeEnabled: Platform.OS !== 'web',
swipeEdgeWidth: 32,
swipeVelocityThreshold: 500,
keyboardDismissMode: 'on-drag',
@@ -214,6 +197,22 @@ export default class DrawerView extends React.Component<Props> {
}
};
private getDrawerWidth = (): number => {
const { drawerStyle, dimensions } = this.props;
const { width } = StyleSheet.flatten(drawerStyle);
if (typeof width === 'string' && width.endsWith('%')) {
// Try to calculate width if a percentage is given
const percentage = Number(width.replace(/%$/, ''));
if (Number.isFinite(percentage)) {
return dimensions.width * (percentage / 100);
}
}
return typeof width === 'number' ? width : 0;
};
private clock = new Clock();
private interactionHandle: number | undefined;
@@ -225,16 +224,27 @@ export default class DrawerView extends React.Component<Props> {
private nextIsOpen = new Value<Binary | -1>(UNSET);
private isSwiping = new Value<Binary>(FALSE);
private gestureState = new Value<number>(State.UNDETERMINED);
private initialDrawerWidth = this.getDrawerWidth();
private gestureState = new Value<number>(GestureState.UNDETERMINED);
private touchX = new Value<number>(0);
private velocityX = new Value<number>(0);
private gestureX = new Value<number>(0);
private offsetX = new Value<number>(0);
private position = new Value<number>(0);
private position = new Value<number>(
this.props.open
? this.initialDrawerWidth *
(this.props.drawerPosition === 'right'
? DIRECTION_RIGHT
: DIRECTION_LEFT)
: 0
);
private containerWidth = new Value<number>(0);
private drawerWidth = new Value<number>(0);
private drawerOpacity = new Value<number>(0);
private containerWidth = new Value<number>(this.props.dimensions.width);
private drawerWidth = new Value<number>(this.initialDrawerWidth);
private drawerOpacity = new Value<number>(
this.initialDrawerWidth || this.props.drawerType === 'permanent' ? 1 : 0
);
private drawerPosition = new Value<number>(
this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
);
@@ -412,12 +422,12 @@ export default class DrawerView extends React.Component<Props> {
onChange(
this.gestureState,
cond(
eq(this.gestureState, State.ACTIVE),
eq(this.gestureState, GestureState.ACTIVE),
call([], this.handleStartInteraction)
)
),
cond(
eq(this.gestureState, State.ACTIVE),
eq(this.gestureState, GestureState.ACTIVE),
[
cond(this.isSwiping, NOOP, [
// We weren't dragging before, set it to true
@@ -501,14 +511,28 @@ export default class DrawerView extends React.Component<Props> {
},
]);
private handleTapStateChange = event([
{
nativeEvent: {
oldState: (s: Animated.Value<number>) =>
cond(eq(s, State.ACTIVE), set(this.manuallyTriggerSpring, TRUE)),
},
},
]);
private handleTapStateChange =
Platform.OS === 'web'
? // FIXME: Drawer doesn't close on Web with the same code that we use for native
({ nativeEvent }: TapGestureHandlerStateChangeEvent) => {
if (
nativeEvent.state === GestureState.END &&
nativeEvent.oldState === GestureState.ACTIVE
) {
this.toggleDrawer(false);
}
}
: event([
{
nativeEvent: {
oldState: (s: Animated.Value<number>) =>
cond(
eq(s, GestureState.ACTIVE),
set(this.manuallyTriggerSpring, TRUE)
),
},
},
]);
private handleContainerLayout = (e: LayoutChangeEvent) =>
this.containerWidth.setValue(e.nativeEvent.layout.width);
@@ -564,9 +588,15 @@ export default class DrawerView extends React.Component<Props> {
const isOpen = drawerType === 'permanent' ? true : open;
const isRight = drawerPosition === 'right';
const contentTranslateX = drawerType === 'front' ? 0 : this.translateX;
const contentTranslateX =
drawerType === 'front' || drawerType === 'permanent'
? 0
: this.translateX;
const drawerTranslateX =
drawerType === 'back'
drawerType === 'permanent'
? 0
: drawerType === 'back'
? I18nManager.isRTL
? multiply(
sub(this.containerWidth, this.drawerWidth),
@@ -616,9 +646,7 @@ export default class DrawerView extends React.Component<Props> {
<Animated.View
style={[
styles.content,
drawerType !== 'permanent' && {
transform: [{ translateX: contentTranslateX }],
},
{ transform: [{ translateX: contentTranslateX }] },
sceneContainerStyle as any,
]}
>
@@ -645,6 +673,11 @@ export default class DrawerView extends React.Component<Props> {
)
}
</Animated.View>
<Animated.Code
// This is needed to make sure that container width updates with `setValue`
// Without this, it won't update when not used in styles
exec={this.containerWidth}
/>
{drawerType === 'permanent' ? null : (
<Animated.Code
exec={block([
@@ -663,6 +696,10 @@ export default class DrawerView extends React.Component<Props> {
onLayout={this.handleDrawerLayout}
style={[
styles.container,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
drawerType === 'permanent'
? // Without this, the `left`/`right` values don't get reset
isRight
@@ -670,10 +707,6 @@ export default class DrawerView extends React.Component<Props> {
: { left: 0 }
: [
styles.nonPermanent,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
isRight ? { right: offset } : { left: offset },
{ zIndex: drawerType === 'back' ? -1 : 0 },
],

View File

@@ -6,8 +6,10 @@ import {
StyleProp,
ViewStyle,
TextStyle,
Platform,
TouchableWithoutFeedbackProps,
} from 'react-native';
import { useTheme } from '@react-navigation/native';
import { Link, useTheme } from '@react-navigation/native';
import Color from 'color';
import TouchableItem from './TouchableItem';
@@ -26,6 +28,10 @@ type Props = {
size: number;
color: string;
}) => React.ReactNode;
/**
* URL to use for the link to the tab.
*/
to?: string;
/**
* Whether to highlight the drawer item as active.
*/
@@ -60,6 +66,54 @@ type Props = {
style?: StyleProp<ViewStyle>;
};
const Touchable = ({
children,
style,
onPress,
to,
accessibilityRole,
delayPressIn,
...rest
}: TouchableWithoutFeedbackProps & {
to?: string;
children: React.ReactNode;
onPress?: () => void;
}) => {
if (Platform.OS === 'web' && to) {
// React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`.
// We need to use `onClick` to be able to prevent default browser handling of links.
return (
<Link
{...rest}
to={to}
style={[styles.button, style]}
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
>
{children}
</Link>
);
} else {
return (
<TouchableItem
{...rest}
accessibilityRole={accessibilityRole}
delayPressIn={delayPressIn}
onPress={onPress}
>
<View style={style}>{children}</View>
</TouchableItem>
);
}
};
/**
* A component used to show an action item with an icon and a label in a navigation drawer.
*/
@@ -70,6 +124,7 @@ export default function DrawerItem(props: Props) {
icon,
label,
labelStyle,
to,
focused = false,
activeTintColor = colors.primary,
inactiveTintColor = Color(colors.text).alpha(0.68).rgb().string(),
@@ -94,7 +149,7 @@ export default function DrawerItem(props: Props) {
{...rest}
style={[styles.container, { borderRadius, backgroundColor }, style]}
>
<TouchableItem
<Touchable
delayPressIn={0}
onPress={onPress}
style={[styles.wrapper, { borderRadius }]}
@@ -102,6 +157,7 @@ export default function DrawerItem(props: Props) {
accessibilityComponentType="button"
accessibilityRole="button"
accessibilityStates={focused ? ['selected'] : []}
to={to}
>
<React.Fragment>
{iconNode}
@@ -129,7 +185,7 @@ export default function DrawerItem(props: Props) {
)}
</View>
</React.Fragment>
</TouchableItem>
</Touchable>
</View>
);
}
@@ -148,4 +204,7 @@ const styles = StyleSheet.create({
label: {
marginRight: 32,
},
button: {
display: 'flex',
},
});

View File

@@ -3,6 +3,7 @@ import {
CommonActions,
DrawerActions,
DrawerNavigationState,
useLinkBuilder,
} from '@react-navigation/native';
import DrawerItem from './DrawerItem';
import {
@@ -31,6 +32,8 @@ export default function DrawerItemList({
itemStyle,
labelStyle,
}: Props) {
const buildLink = useLinkBuilder();
return (state.routes.map((route, i) => {
const focused = i === state.index;
const { title, drawerLabel, drawerIcon } = descriptors[route.key].options;
@@ -53,6 +56,7 @@ export default function DrawerItemList({
inactiveBackgroundColor={inactiveBackgroundColor}
labelStyle={labelStyle}
style={itemStyle}
to={buildLink(route.name, route.params)}
onPress={() => {
navigation.dispatch({
...(focused

View File

@@ -16,6 +16,7 @@ import {
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import {
NavigationHelpersContext,
DrawerNavigationState,
DrawerActions,
useTheme,
@@ -89,11 +90,9 @@ export default function DrawerView({
sceneContainerStyle,
}: Props) {
const [loaded, setLoaded] = React.useState([state.index]);
const [drawerWidth, setDrawerWidth] = React.useState(() => {
const { height = 0, width = 0 } = Dimensions.get('window');
return getDefaultDrawerWidth({ height, width });
});
const [dimensions, setDimensions] = React.useState(() =>
Dimensions.get('window')
);
const drawerGestureRef = React.useRef<PanGestureHandler>(null);
@@ -141,13 +140,13 @@ export default function DrawerView({
}, [handleDrawerClose, isDrawerOpen, navigation, state.key]);
React.useEffect(() => {
const updateWidth = ({ window }: { window: ScaledSize }) => {
setDrawerWidth(getDefaultDrawerWidth(window));
const updateDimensions = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
Dimensions.addEventListener('change', updateWidth);
Dimensions.addEventListener('change', updateDimensions);
return () => Dimensions.removeEventListener('change', updateWidth);
return () => Dimensions.removeEventListener('change', updateDimensions);
}, []);
if (!loaded.includes(state.index)) {
@@ -203,55 +202,61 @@ export default function DrawerView({
const { gestureEnabled, swipeEnabled } = descriptors[activeKey].options;
return (
<GestureHandlerWrapper style={styles.content}>
<SafeAreaProviderCompat>
<DrawerGestureContext.Provider value={drawerGestureRef}>
<DrawerOpenContext.Provider value={isDrawerOpen}>
<Drawer
open={isDrawerOpen}
gestureEnabled={gestureEnabled}
swipeEnabled={swipeEnabled}
onOpen={handleDrawerOpen}
onClose={handleDrawerClose}
onGestureRef={(ref) => {
// @ts-ignore
drawerGestureRef.current = ref;
}}
gestureHandlerProps={gestureHandlerProps}
drawerType={drawerType}
drawerPosition={drawerPosition}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
drawerStyle={[
{ width: drawerWidth, backgroundColor: colors.card },
drawerType === 'permanent' &&
(drawerPosition === 'left'
? {
borderRightColor: colors.border,
borderRightWidth: StyleSheet.hairlineWidth,
}
: {
borderLeftColor: colors.border,
borderLeftWidth: StyleSheet.hairlineWidth,
}),
drawerStyle,
]}
overlayStyle={{ backgroundColor: overlayColor }}
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}
statusBarAnimation={statusBarAnimation}
renderDrawerContent={renderNavigationView}
renderSceneContent={renderContent}
keyboardDismissMode={keyboardDismissMode}
drawerPostion={drawerPosition}
/>
</DrawerOpenContext.Provider>
</DrawerGestureContext.Provider>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
<NavigationHelpersContext.Provider value={navigation}>
<GestureHandlerWrapper style={styles.content}>
<SafeAreaProviderCompat>
<DrawerGestureContext.Provider value={drawerGestureRef}>
<DrawerOpenContext.Provider value={isDrawerOpen}>
<Drawer
open={isDrawerOpen}
gestureEnabled={gestureEnabled}
swipeEnabled={swipeEnabled}
onOpen={handleDrawerOpen}
onClose={handleDrawerClose}
onGestureRef={(ref) => {
// @ts-ignore
drawerGestureRef.current = ref;
}}
gestureHandlerProps={gestureHandlerProps}
drawerType={drawerType}
drawerPosition={drawerPosition}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
drawerStyle={[
{
width: getDefaultDrawerWidth(dimensions),
backgroundColor: colors.card,
},
drawerType === 'permanent' &&
(drawerPosition === 'left'
? {
borderRightColor: colors.border,
borderRightWidth: StyleSheet.hairlineWidth,
}
: {
borderLeftColor: colors.border,
borderLeftWidth: StyleSheet.hairlineWidth,
}),
drawerStyle,
]}
overlayStyle={{ backgroundColor: overlayColor }}
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}
statusBarAnimation={statusBarAnimation}
renderDrawerContent={renderNavigationView}
renderSceneContent={renderContent}
keyboardDismissMode={keyboardDismissMode}
drawerPostion={drawerPosition}
dimensions={dimensions}
/>
</DrawerOpenContext.Provider>
</DrawerGestureContext.Provider>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
</NavigationHelpersContext.Provider>
);
}

View File

@@ -29,22 +29,24 @@ const Overlay = React.forwardRef(function Overlay(
<Animated.View
{...props}
ref={ref}
style={[styles.overlay, animatedStyle, style]}
style={[styles.overlay, overlayStyle, animatedStyle, style]}
/>
);
});
const overlayStyle = Platform.select<Record<string, string>>({
web: {
// Disable touch highlight on mobile Safari.
// WebkitTapHighlightColor must be used outside of StyleSheet.create because react-native-web will omit the property.
WebkitTapHighlightColor: 'transparent',
},
default: {},
});
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
...Platform.select({
web: {
// Disable touch highlight on mobile Safari.
WebkitTapHighlightColor: 'transparent',
},
default: {},
}),
},
});

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.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.9...@react-navigation/material-bottom-tabs@5.1.10) (2020-04-27)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.8...@react-navigation/material-bottom-tabs@5.1.9) (2020-04-17)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.7...@react-navigation/material-bottom-tabs@5.1.8) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper",
"version": "5.1.8",
"version": "5.1.10",
"keywords": [
"react-native-component",
"react-component",
@@ -36,7 +36,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"@types/react-native-vector-icons": "^6.4.5",

View File

@@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {
NavigationHelpersContext,
Route,
TabNavigationState,
TabActions,
@@ -45,66 +46,68 @@ export default function MaterialBottomTabView({
}, [colors, dark]);
return (
<BottomNavigation
{...rest}
theme={theme}
navigationState={state}
onIndexChange={(index: number) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (typeof options.tabBarIcon === 'string') {
return (
<MaterialCommunityIcons
name={options.tabBarIcon}
color={color}
size={24}
style={styles.icon}
importantForAccessibility="no-hide-descendants"
accessibilityElementsHidden
/>
);
<NavigationHelpersContext.Provider value={navigation}>
<BottomNavigation
{...rest}
theme={theme}
navigationState={state}
onIndexChange={(index: number) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (typeof options.tabBarIcon === 'function') {
return options.tabBarIcon({ focused, color });
if (typeof options.tabBarIcon === 'string') {
return (
<MaterialCommunityIcons
name={options.tabBarIcon}
color={color}
size={24}
style={styles.icon}
importantForAccessibility="no-hide-descendants"
accessibilityElementsHidden
/>
);
}
if (typeof options.tabBarIcon === 'function') {
return options.tabBarIcon({ focused, color });
}
return null;
}}
getLabelText={({ route }: Scene) => {
const { options } = descriptors[route.key];
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;
}}
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
return null;
}}
getLabelText={({ route }: Scene) => {
const { options } = descriptors[route.key];
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;
}}
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (event.defaultPrevented) {
preventDefault();
}
}}
/>
if (event.defaultPrevented) {
preventDefault();
}
}}
/>
</NavigationHelpersContext.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.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.9...@react-navigation/material-top-tabs@5.1.10) (2020-04-27)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.9](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.8...@react-navigation/material-top-tabs@5.1.9) (2020-04-17)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.8](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.7...@react-navigation/material-top-tabs@5.1.8) (2020-04-08)

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.1.8",
"version": "5.1.10",
"keywords": [
"react-native-component",
"react-component",
@@ -39,7 +39,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",
"del-cli": "^3.0.0",

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { TabView, SceneRendererProps } from 'react-native-tab-view';
import {
NavigationHelpersContext,
TabNavigationState,
TabActions,
useTheme,
@@ -45,25 +46,27 @@ export default function MaterialTopTabView({
};
return (
<TabView
{...rest}
onIndexChange={(index) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={renderTabBar}
renderPager={pager}
renderLazyPlaceholder={lazyPlaceholder}
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
/>
<NavigationHelpersContext.Provider value={navigation}>
<TabView
{...rest}
onIndexChange={(index) =>
navigation.dispatch({
...TabActions.jumpTo(state.routes[index].name),
target: state.key,
})
}
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={renderTabBar}
renderPager={pager}
renderLazyPlaceholder={lazyPlaceholder}
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
/>
</NavigationHelpersContext.Provider>
);
}

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.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.6...@react-navigation/native@5.1.7) (2020-04-27)
**Note:** Version bump only for package @react-navigation/native
## [5.1.6](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.5...@react-navigation/native@5.1.6) (2020-04-17)
### Bug Fixes
* handle in-page go back when there's no history ([6bdf6ae](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/6bdf6ae4ed0f83ac1deb3172d9075a6a2adbbe11)), closes [#7852](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/7852)
## [5.1.5](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.4...@react-navigation/native@5.1.5) (2020-04-08)
**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.1.5",
"version": "5.1.7",
"keywords": [
"react-native",
"react-navigation",
@@ -31,7 +31,7 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^5.3.3"
"@react-navigation/core": "^5.3.5"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",

View File

@@ -0,0 +1,43 @@
import * as React from 'react';
import { Text, TextProps, GestureResponderEvent } from 'react-native';
import { NavigationAction } from '@react-navigation/core';
import useLinkProps from './useLinkProps';
type Props = {
to: string;
action?: NavigationAction;
target?: string;
} & (TextProps & { children: React.ReactNode });
/**
* Component to render link to another screen using a path.
* Uses an anchor tag on the web.
*
* @param props.to Absolute path to screen (e.g. `/feeds/hot`).
* @param props.action Optional action to use for in-page navigation. By default, the path is parsed to an action based on linking config.
* @param props.children Child elements to render the content.
*/
export default function Link({ to, action, ...rest }: Props) {
const props = useLinkProps({ to, action });
const onPress = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => {
if ('onPress' in rest) {
// @ts-ignore
rest.onPress?.(e);
}
if (props.onClick) {
props.onClick(e);
} else {
props.onPress(e);
}
};
return React.createElement(Text, {
...props,
...rest,
...(props.onClick ? { onClick: onPress } : { onPress }),
});
}

View File

@@ -0,0 +1,8 @@
import * as React from 'react';
import { LinkingOptions } from './types';
const LinkingContext = React.createContext<{
options: LinkingOptions | undefined;
}>({ options: undefined });
export default LinkingContext;

View File

@@ -6,38 +6,70 @@ import {
} from '@react-navigation/core';
import ThemeProvider from './theming/ThemeProvider';
import DefaultTheme from './theming/DefaultTheme';
import LinkingContext from './LinkingContext';
import useThenable from './useThenable';
import useLinking from './useLinking';
import useBackButton from './useBackButton';
import { Theme } from './types';
import { Theme, LinkingOptions } from './types';
type Props = NavigationContainerProps & {
theme?: Theme;
linking?: LinkingOptions;
fallback?: React.ReactNode;
};
/**
* Container component which holds the navigation state
* designed for mobile apps.
* Container component which holds the navigation state designed for React Native apps.
* This should be rendered at the root wrapping the whole app.
*
* @param props.initialState Initial state object for the navigation tree.
* @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will be ignored if there's an incoming link.
* @param props.onStateChange Callback which is called with the latest navigation state when it changes.
* @param props.theme Theme object for the navigators.
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
* @param props.children Child elements to render the content.
* @param props.ref Ref object which refers to the navigation object containing helper methods.
*/
const NavigationContainer = React.forwardRef(function NavigationContainer(
{ theme = DefaultTheme, ...rest }: Props,
{ theme = DefaultTheme, linking, fallback = null, ...rest }: Props,
ref?: React.Ref<NavigationContainerRef | null>
) {
const isLinkingEnabled = linking ? linking.enabled !== false : false;
const refContainer = React.useRef<NavigationContainerRef>(null);
useBackButton(refContainer);
const { getInitialState } = useLinking(refContainer, {
enabled: isLinkingEnabled,
prefixes: [],
...linking,
});
const [isReady, initialState = rest.initialState] = useThenable(
getInitialState
);
React.useImperativeHandle(ref, () => refContainer.current);
const linkingContext = React.useMemo(() => ({ options: linking }), [linking]);
if (!isReady) {
// This is temporary until we have Suspense for data-fetching
// Then the fallback will be handled by a parent `Suspense` component
return fallback as React.ReactElement;
}
return (
<ThemeProvider value={theme}>
<BaseNavigationContainer {...rest} ref={refContainer} />
</ThemeProvider>
<LinkingContext.Provider value={linkingContext}>
<ThemeProvider value={theme}>
<BaseNavigationContainer
{...rest}
initialState={initialState}
ref={refContainer}
/>
</ThemeProvider>
</LinkingContext.Provider>
);
});

View File

@@ -1,5 +0,0 @@
export default function () {
throw new Error(
"'NavigationNativeContainer' has been renamed to 'NavigationContainer"
);
}

View File

@@ -1,13 +1,17 @@
export * from '@react-navigation/core';
export { default as NavigationContainer } from './NavigationContainer';
export { default as NavigationNativeContainer } from './NavigationNativeContainer';
export { default as useBackButton } from './useBackButton';
export { default as useLinking } from './useLinking';
export { default as useScrollToTop } from './useScrollToTop';
export { default as DefaultTheme } from './theming/DefaultTheme';
export { default as DarkTheme } from './theming/DarkTheme';
export { default as ThemeProvider } from './theming/ThemeProvider';
export { default as useTheme } from './theming/useTheme';
export { default as Link } from './Link';
export { default as useLinking } from './useLinking';
export { default as useLinkTo } from './useLinkTo';
export { default as useLinkProps } from './useLinkProps';
export { default as useLinkBuilder } from './useLinkBuilder';

View File

@@ -15,6 +15,11 @@ export type Theme = {
};
export type LinkingOptions = {
/**
* Whether deep link handling should be enabled.
* Defaults to true.
*/
enabled?: boolean;
/**
* The prefixes are stripped from the URL before parsing them.
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)

View File

@@ -0,0 +1,77 @@
import * as React from 'react';
import {
NavigationHelpers,
NavigationHelpersContext,
NavigationProp,
ParamListBase,
getPathFromState,
} from '@react-navigation/core';
import LinkingContext from './LinkingContext';
type NavigationObject =
| NavigationHelpers<ParamListBase>
| NavigationProp<ParamListBase>;
type MinimalState = {
index: number;
routes: { name: string; params?: object; state?: MinimalState }[];
};
const getRootStateForNavigate = (
navigation: NavigationObject,
state: MinimalState
): MinimalState => {
const parent = navigation.dangerouslyGetParent();
if (parent) {
const parentState = parent.dangerouslyGetState();
return getRootStateForNavigate(parent, {
index: 0,
routes: [
{
...parentState.routes[parentState.index],
state: state,
},
],
});
}
return state;
};
/**
* Build destination link for a navigate action.
* Useful for showing anchor tags on the web for buttons that perform navigation.
*/
export default function useLinkBuilder() {
const navigation = React.useContext(NavigationHelpersContext);
const linking = React.useContext(LinkingContext);
const buildLink = React.useCallback(
(name: string, params?: object) => {
const { options } = linking;
// If we couldn't find a navigation object in context, we're at root
// So we'll construct a basic state object to use
const state = navigation
? getRootStateForNavigate(navigation, {
index: 0,
routes: [{ name, params }],
})
: {
index: 0,
routes: [{ name, params }],
};
const path = options?.getPathFromState
? options.getPathFromState(state, options?.config)
: getPathFromState(state, options?.config);
return path;
},
[linking, navigation]
);
return buildLink;
}

View File

@@ -0,0 +1,65 @@
import * as React from 'react';
import { Platform, GestureResponderEvent } from 'react-native';
import {
NavigationAction,
NavigationHelpersContext,
} from '@react-navigation/core';
import useLinkTo from './useLinkTo';
type Props = {
to: string;
action?: NavigationAction;
};
/**
* Hook to get props for an anchor tag so it can work with in page navigation.
*
* @param props.to Absolute path to screen (e.g. `/feeds/hot`).
* @param props.action Optional action to use for in-page navigation. By default, the path is parsed to an action based on linking config.
*/
export default function useLinkProps({ to, action }: Props) {
const navigation = React.useContext(NavigationHelpersContext);
const linkTo = useLinkTo();
const onPress = (
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
) => {
let shouldHandle = false;
if (Platform.OS !== 'web' || !e) {
shouldHandle = e ? !e.defaultPrevented : true;
} else if (
!e.defaultPrevented && // onPress prevented default
// @ts-ignore
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
// @ts-ignore
(e.button == null || e.button === 0) && // ignore everything but left clicks
// @ts-ignore
[undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc.
) {
e.preventDefault();
shouldHandle = true;
}
if (shouldHandle) {
if (action) {
if (navigation) {
navigation.dispatch(action);
} else {
throw new Error("Couldn't find a navigation object.");
}
} else {
linkTo(to);
}
}
};
return {
href: to,
accessibilityRole: 'link' as const,
...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
}),
};
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import {
getStateFromPath,
getActionFromState,
NavigationContext,
} from '@react-navigation/core';
import LinkingContext from './LinkingContext';
export default function useLinkTo() {
const navigation = React.useContext(NavigationContext);
const linking = React.useContext(LinkingContext);
const linkTo = React.useCallback(
(path: string) => {
if (!path.startsWith('/')) {
throw new Error(`The path must start with '/' (${path}).`);
}
if (navigation === undefined) {
throw new Error(
"Couldn't find a navigation object. Is your component inside a screen in a navigator?"
);
}
const { options } = linking;
const state = options?.getStateFromPath
? options.getStateFromPath(path, options.config)
: getStateFromPath(path, options?.config);
if (state) {
let root = navigation;
let current;
// Traverse up to get the root navigation
while ((current = root.dangerouslyGetParent())) {
root = current;
}
const action = getActionFromState(state);
if (action !== undefined) {
root.dispatch(action);
} else {
root.reset(state);
}
} else {
throw new Error('Failed to parse the path to a navigation state.');
}
},
[linking, navigation]
);
return linkTo;
}

View File

@@ -12,6 +12,7 @@ let isUsingLinking = false;
export default function useLinking(
ref: React.RefObject<NavigationContainerRef>,
{
enabled,
prefixes,
config,
getStateFromPath = getStateFromPathDefault,
@@ -37,15 +38,17 @@ export default function useLinking(
// 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
const enabledRef = React.useRef(enabled);
const prefixesRef = React.useRef(prefixes);
const configRef = React.useRef(config);
const getStateFromPathRef = React.useRef(getStateFromPath);
React.useEffect(() => {
enabledRef.current = enabled;
prefixesRef.current = prefixes;
configRef.current = config;
getStateFromPathRef.current = getStateFromPath;
}, [config, getStateFromPath, prefixes]);
}, [config, enabled, getStateFromPath, prefixes]);
const extractPathFromURL = React.useCallback((url: string) => {
for (const prefix of prefixesRef.current) {
@@ -58,7 +61,19 @@ export default function useLinking(
}, []);
const getInitialState = React.useCallback(async () => {
const url = await Linking.getInitialURL();
if (!enabledRef.current) {
return undefined;
}
const url = await (Promise.race([
Linking.getInitialURL(),
new Promise((resolve) =>
// Timeout in 150ms if `getInitialState` doesn't resolve
// Workaround for https://github.com/facebook/react-native/issues/25675
setTimeout(resolve, 150)
),
]) as Promise<string | null | undefined>);
const path = url ? extractPathFromURL(url) : null;
if (path) {
@@ -70,6 +85,10 @@ export default function useLinking(
React.useEffect(() => {
const listener = ({ url }: { url: string }) => {
if (!enabled) {
return;
}
const path = extractPathFromURL(url);
const navigation = ref.current;
@@ -91,7 +110,7 @@ export default function useLinking(
Linking.addEventListener('url', listener);
return () => Linking.removeEventListener('url', listener);
}, [extractPathFromURL, ref]);
}, [enabled, extractPathFromURL, ref]);
return {
getInitialState,

View File

@@ -8,6 +8,8 @@ import {
} from '@react-navigation/core';
import { LinkingOptions } from './types';
type ResultState = ReturnType<typeof getStateFromPathDefault>;
const getStateLength = (state: NavigationState) => {
let length = 0;
@@ -32,6 +34,7 @@ let isUsingLinking = false;
export default function useLinking(
ref: React.RefObject<NavigationContainerRef>,
{
enabled,
config,
getStateFromPath = getStateFromPathDefault,
getPathFromState = getPathFromStateDefault,
@@ -54,25 +57,34 @@ export default function useLinking(
// 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
const enabledRef = React.useRef(enabled);
const configRef = React.useRef(config);
const getStateFromPathRef = React.useRef(getStateFromPath);
const getPathFromStateRef = React.useRef(getPathFromState);
React.useEffect(() => {
enabledRef.current = enabled;
configRef.current = config;
getStateFromPathRef.current = getStateFromPath;
getPathFromStateRef.current = getPathFromState;
}, [config, getPathFromState, getStateFromPath]);
}, [config, enabled, getPathFromState, getStateFromPath]);
// Make it an async function to keep consistent with the native impl
const getInitialState = React.useCallback(async () => {
const path = location.pathname + location.search;
const getInitialState = React.useCallback(() => {
let value: ResultState | undefined;
if (path) {
return getStateFromPathRef.current(path, configRef.current);
} else {
return undefined;
if (enabledRef.current) {
const path = location.pathname + location.search;
if (path) {
value = getStateFromPathRef.current(path, configRef.current);
}
}
// Make it a thenable to keep consistent with the native impl
return {
then: (callback: (state: ResultState | undefined) => void) =>
callback(value),
};
}, []);
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
@@ -92,10 +104,10 @@ export default function useLinking(
const numberOfIndicesAhead = React.useRef(0);
React.useEffect(() => {
window.addEventListener('popstate', () => {
const onPopState = () => {
const navigation = ref.current;
if (!navigation) {
if (!navigation || !enabled) {
return;
}
@@ -169,10 +181,18 @@ export default function useLinking(
}
}
}
});
}, [ref]);
};
window.addEventListener('popstate', onPopState);
return () => window.removeEventListener('popstate', onPopState);
}, [enabled, ref]);
React.useEffect(() => {
if (!enabled) {
return;
}
if (ref.current && previousStateLengthRef.current === undefined) {
previousStateLengthRef.current = getStateLength(
ref.current.getRootState()
@@ -224,14 +244,14 @@ export default function useLinking(
let index = history.state?.index ?? 0;
if (previousStateLength === stateLength) {
// If no new enrties were added to history in our navigation state, we want to replaceState
// 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 enrties were added, pushState until we have same length
// This won't be accurate if multiple enrties were added at once, but that's the best we can do
// 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);
@@ -239,13 +259,27 @@ export default function useLinking(
previousHistoryIndexRef.current = index;
} else if (previousStateLength > stateLength) {
const delta = 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
);
// We need to set this to ignore the `popstate` event
pendingIndexChangeRef.current = index - delta;
if (delta > 0) {
// We need to set this to ignore the `popstate` event
pendingIndexChangeRef.current = index - delta;
// If new enrties were removed, go back so that we have same length
history.go(-delta);
// 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;
}
}
}
});

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
export default function useThenable<T>(
create: () => {
then(success: (result: T) => void, error?: (error: any) => void): void;
}
) {
const [promise] = React.useState(create);
// Check if our thenable is synchronous
let resolved = false;
let value: T | undefined;
promise.then((result) => {
resolved = true;
value = result;
});
const [state, setState] = React.useState<[boolean, T | undefined]>([
resolved,
value,
]);
React.useEffect(() => {
let cancelled = false;
if (!resolved) {
promise.then(
(result) => {
if (!cancelled) {
setState([true, result]);
}
},
(error) => {
if (!cancelled) {
console.error(error);
setState([true, undefined]);
}
}
);
}
return () => {
cancelled = true;
};
}, [promise, resolved]);
return state;
}

View File

@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.0...@react-navigation/routers@5.4.1) (2020-04-27)
### Bug Fixes
* fix behaviour of openByDefault in drawer when focus changes ([b172b51](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/commit/b172b51f175a9f8044cb2a8e9d74a86480d8f11e))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.3.0...@react-navigation/routers@5.4.0) (2020-04-17)
### Features
* add openByDefault option to drawer ([36689e2](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/commit/36689e24c21b474692bb7ecd0b901c8afbbe9a20))
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.2.1...@react-navigation/routers@5.3.0) (2020-04-08)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/routers",
"description": "Routers to help build custom navigators",
"version": "5.3.0",
"version": "5.4.1",
"keywords": [
"react",
"react-native",

View File

@@ -21,7 +21,9 @@ export type DrawerActionType =
target?: string;
};
export type DrawerRouterOptions = TabRouterOptions;
export type DrawerRouterOptions = TabRouterOptions & {
openByDefault?: boolean;
};
export type DrawerNavigationState = Omit<
TabNavigationState,
@@ -95,10 +97,14 @@ const closeDrawer = (state: DrawerNavigationState): DrawerNavigationState => {
};
};
export default function DrawerRouter(
options: DrawerRouterOptions
): Router<DrawerNavigationState, DrawerActionType | CommonNavigationAction> {
const router = (TabRouter(options) as unknown) as Router<
export default function DrawerRouter({
openByDefault,
...rest
}: DrawerRouterOptions): Router<
DrawerNavigationState,
DrawerActionType | CommonNavigationAction
> {
const router = (TabRouter(rest) as unknown) as Router<
DrawerNavigationState,
TabActionType | CommonNavigationAction
>;
@@ -109,7 +115,11 @@ export default function DrawerRouter(
type: 'drawer',
getInitialState({ routeNames, routeParamList }) {
const state = router.getInitialState({ routeNames, routeParamList });
let state = router.getInitialState({ routeNames, routeParamList });
if (openByDefault) {
state = openDrawer(state);
}
return {
...state,
@@ -143,6 +153,10 @@ export default function DrawerRouter(
getStateForRouteFocus(state, key) {
const result = router.getStateForRouteFocus(state, key);
if (openByDefault) {
return openDrawer(result);
}
return closeDrawer(result);
},
@@ -162,8 +176,14 @@ export default function DrawerRouter(
return openDrawer(state);
case 'GO_BACK':
if (isDrawerOpen(state)) {
return closeDrawer(state);
if (openByDefault) {
if (!isDrawerOpen(state)) {
return openDrawer(state);
}
} else {
if (isDrawerOpen(state)) {
return closeDrawer(state);
}
}
return router.getStateForAction(state, action, options);

View File

@@ -214,6 +214,161 @@ it("doesn't rehydrate state if it's not stale", () => {
).toBe(state);
});
it('restores correct history on rehydrating with backBehavior: order', () => {
const router = TabRouter({ backBehavior: 'order' });
const options = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
};
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
},
options
)
).toEqual({
key: 'tab-test',
index: 2,
routeNames: ['foo', 'bar', 'baz', 'qux'],
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
history: [
{ key: 'foo-0', type: 'route' },
{ key: 'bar-0', type: 'route' },
{ key: 'baz-0', type: 'route' },
],
stale: false,
type: 'tab',
});
});
it('restores correct history on rehydrating with backBehavior: history', () => {
const router = TabRouter({ backBehavior: 'history' });
const options = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
};
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
},
options
)
).toEqual({
key: 'tab-test',
index: 2,
routeNames: ['foo', 'bar', 'baz', 'qux'],
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
history: [{ key: 'baz-0', type: 'route' }],
stale: false,
type: 'tab',
});
});
it('restores correct history on rehydrating with backBehavior: initialRoute', () => {
const router = TabRouter({ backBehavior: 'initialRoute' });
const options = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
};
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
},
options
)
).toEqual({
key: 'tab-test',
index: 2,
routeNames: ['foo', 'bar', 'baz', 'qux'],
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
history: [
{ key: 'foo-0', type: 'route' },
{ key: 'baz-0', type: 'route' },
],
stale: false,
type: 'tab',
});
});
it('restores correct history on rehydrating with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' });
const options = {
routeNames: ['foo', 'bar', 'baz', 'qux'],
routeParamList: {},
};
expect(
router.getRehydratedState(
{
index: 2,
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
},
options
)
).toEqual({
key: 'tab-test',
index: 2,
routeNames: ['foo', 'bar', 'baz', 'qux'],
routes: [
{ key: 'foo-0', name: 'foo' },
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-0', name: 'baz' },
{ key: 'qux-0', name: 'qux' },
],
history: [{ key: 'baz-0', type: 'route' }],
stale: false,
type: 'tab',
});
});
it('gets state on route names change', () => {
const router = TabRouter({});
@@ -254,6 +409,38 @@ it('gets state on route names change', () => {
stale: false,
type: 'tab',
});
expect(
router.getStateForRouteNamesChange(
{
index: 0,
key: 'tab-test',
routeNames: ['bar', 'baz'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
],
history: [{ type: 'route', key: 'bar-test' }],
stale: false,
type: 'tab',
},
{
routeNames: ['foo', 'fiz'],
routeParamList: {},
}
)
).toEqual({
index: 0,
key: 'tab-test',
routeNames: ['foo', 'fiz'],
routes: [
{ key: 'foo-test', name: 'foo' },
{ key: 'fiz-test', name: 'fiz' },
],
history: [{ type: 'route', key: 'foo-test' }],
stale: false,
type: 'tab',
});
});
it('preserves focused route on route names change', () => {

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.14](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.13...@react-navigation/stack@5.2.14) (2020-04-27)
### Bug Fixes
* don't add back the route being replaced ([a695cf9](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/a695cf9c058521ccb4a83eb206dc0da7ce100032))
## [5.2.12](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.11...@react-navigation/stack@5.2.12) (2020-04-22)
### Bug Fixes
* animate card to existing closing state on gesture end ([78485ce](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/78485cea6939b9ffec76e0c4b410bc426ed93402)), closes [#7938](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/7938)
## [5.2.11](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.10...@react-navigation/stack@5.2.11) (2020-04-17)
### Bug Fixes
* disable animation by default on web for stack ([dfdba8d](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/dfdba8d741abb4aa82235688d9f49e26305d2bca))
* hide inactive screens for stack on web ([#8010](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8010)) ([82edb25](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/82edb2581bab960f206fd67368a45ad384955c97))
* ios presentation modal cuts the topOffset on the bottom ([#7943](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/7943)) ([6e51f59](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/6e51f596fa85796c2a3567222f51ff914c1f6c94)), closes [#7856](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/7856)
## [5.2.10](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.9...@react-navigation/stack@5.2.10) (2020-04-08)

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.2.10",
"version": "5.2.14",
"keywords": [
"react-native-component",
"react-component",
@@ -40,7 +40,7 @@
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-native-community/masked-view": "^0.1.7",
"@react-navigation/native": "^5.1.5",
"@react-navigation/native": "^5.1.7",
"@types/color": "^3.0.1",
"@types/react": "^16.9.23",
"@types/react-native": "^0.61.22",

View File

@@ -164,6 +164,7 @@ export function forModalPresentationIOS({
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
marginTop: index === 0 ? 0 : statusBarHeight,
marginBottom: index === 0 ? 0 : topOffset,
transform: [{ translateY }, { scale }],
},
overlayStyle: { opacity: overlayOpacity },

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { Platform } from 'react-native';
import {
useNavigationBuilder,
createNavigatorFactory,
@@ -26,6 +27,11 @@ function StackNavigator({
screenOptions,
...rest
}: Props) {
const defaultOptions = {
gestureEnabled: Platform.OS === 'ios',
animationEnabled: Platform.OS !== 'web',
};
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState,
StackRouterOptions,
@@ -34,7 +40,16 @@ function StackNavigator({
>(StackRouter, {
initialRouteName,
children,
screenOptions,
screenOptions:
typeof screenOptions === 'function'
? (...args) => ({
...defaultOptions,
...screenOptions(...args),
})
: {
...defaultOptions,
...screenOptions,
},
});
React.useEffect(

View File

@@ -1,17 +1,26 @@
import * as React from 'react';
import { UIManager } from 'react-native';
import RNCMaskedView from '@react-native-community/masked-view';
type Props = React.ComponentProps<typeof RNCMaskedView> & {
type MaskedViewType = typeof import('@react-native-community/masked-view').default;
type Props = React.ComponentProps<MaskedViewType> & {
children: React.ReactElement;
};
let RNCMaskedView: MaskedViewType | undefined;
try {
RNCMaskedView = require('@react-native-community/masked-view').default;
} catch (e) {
// Ignore
}
const isMaskedViewAvailable =
// @ts-ignore
UIManager.getViewManagerConfig('RNCMaskedView') != null;
export default function MaskedView({ children, ...rest }: Props) {
if (isMaskedViewAvailable) {
if (isMaskedViewAvailable && RNCMaskedView) {
return <RNCMaskedView {...rest}>{children}</RNCMaskedView>;
}

View File

@@ -0,0 +1,71 @@
import * as React from 'react';
import { Animated, View, Platform, ViewProps } from 'react-native';
let Screens: typeof import('react-native-screens') | undefined;
try {
Screens = require('react-native-screens');
} catch (e) {
// Ignore
}
// The web implementation in react-native-screens seems buggy.
// The view doesn't become visible after coming back in some cases.
// So we use our custom implementation.
class WebScreen extends React.Component<
ViewProps & {
active: number;
children: React.ReactNode;
}
> {
render() {
const { active, style, ...rest } = this.props;
return (
<View
// @ts-ignore
hidden={!active}
style={[style, { display: active ? 'flex' : 'none' }]}
{...rest}
/>
);
}
}
const AnimatedWebScreen = Animated.createAnimatedComponent(WebScreen);
export const MaybeScreenContainer = ({
enabled,
...rest
}: ViewProps & {
enabled: boolean;
children: React.ReactNode;
}) => {
if (enabled && Platform.OS !== 'web' && Screens && Screens.screensEnabled()) {
return <Screens.ScreenContainer {...rest} />;
}
return <View {...rest} />;
};
export const MaybeScreen = ({
enabled,
active,
...rest
}: ViewProps & {
enabled: boolean;
active: number | Animated.AnimatedInterpolation;
children: React.ReactNode;
}) => {
if (enabled && Platform.OS === 'web') {
// @ts-ignore
return <AnimatedWebScreen active={active} {...rest} />;
}
if (enabled && Screens && Screens.screensEnabled()) {
// @ts-ignore
return <Screens.Screen active={active} {...rest} />;
}
return <View {...rest} />;
};

View File

@@ -286,7 +286,7 @@ export default class Card extends React.Component<Props> {
getInvertedMultiplier(gestureDirection) >
distance / 2
? velocity !== 0 || translation !== 0
: false;
: this.props.closing;
this.animate({ closing, velocity });

View File

@@ -1,18 +1,15 @@
import * as React from 'react';
import {
Animated,
View,
StyleSheet,
LayoutChangeEvent,
Dimensions,
Platform,
ViewProps,
} from 'react-native';
import { EdgeInsets } from 'react-native-safe-area-context';
// eslint-disable-next-line import/no-unresolved
import { ScreenContainer, Screen, screensEnabled } from 'react-native-screens'; // Import with * as to prevent getters being called
import { Route, StackNavigationState } from '@react-navigation/native';
import { MaybeScreenContainer, MaybeScreen } from '../Screens';
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import CardContainer from './CardContainer';
@@ -75,37 +72,6 @@ type State = {
const EPSILON = 0.01;
const MaybeScreenContainer = ({
enabled,
...rest
}: ViewProps & {
enabled: boolean;
children: React.ReactNode;
}) => {
if (enabled && screensEnabled()) {
return <ScreenContainer {...rest} />;
}
return <View {...rest} />;
};
const MaybeScreen = ({
enabled,
active,
...rest
}: ViewProps & {
enabled: boolean;
active: number | Animated.AnimatedInterpolation;
children: React.ReactNode;
}) => {
if (enabled && screensEnabled()) {
// @ts-ignore
return <Screen active={active} {...rest} />;
}
return <View {...rest} />;
};
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
const getHeaderHeights = (
@@ -415,7 +381,7 @@ export default class CardStack extends React.Component<Props, State> {
// Screens is buggy on iOS and web, so we only enable it on Android
// For modals, usually we want the screen underneath to be visible, so also disable it there
const isScreensEnabled = Platform.OS === 'android' && mode !== 'modal';
const isScreensEnabled = Platform.OS !== 'ios' && mode !== 'modal';
return (
<React.Fragment>

View File

@@ -3,6 +3,7 @@ import { View, Platform, StyleSheet } from 'react-native';
import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import {
NavigationHelpersContext,
StackActions,
StackNavigationState,
Route,
@@ -147,7 +148,7 @@ export default class StackView extends React.Component<Props, State> {
// We only need to animate routes if the focused route changed
// Animating previous routes won't be visible coz the focused route is on top of everything
if (!previousRoutes.find((r) => r.key === nextFocusedRoute.key)) {
if (!previousRoutes.some((r) => r.key === nextFocusedRoute.key)) {
// A new route has come to the focus, we treat this as a push
// A replace can also trigger this, the animation should look like push
@@ -166,7 +167,7 @@ export default class StackView extends React.Component<Props, State> {
(key) => key !== nextFocusedRoute.key
);
if (!routes.find((r) => r.key === previousFocusedRoute.key)) {
if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
// The previous focused route isn't present in state, we treat this as a replace
openingRouteKeys = openingRouteKeys.filter(
@@ -206,7 +207,7 @@ export default class StackView extends React.Component<Props, State> {
}
}
}
} else if (!routes.find((r) => r.key === previousFocusedRoute.key)) {
} else if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
// The previously focused route was removed, we treat this as a pop
if (
@@ -292,9 +293,7 @@ export default class StackView extends React.Component<Props, State> {
return false;
}
return gestureEnabled !== undefined
? gestureEnabled
: Platform.OS !== 'android';
return gestureEnabled !== false;
}
return false;
@@ -330,24 +329,38 @@ export default class StackView extends React.Component<Props, State> {
};
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
this.setState((state) => ({
routes: state.replacingRouteKeys.length
? state.routes.filter((r) => !state.replacingRouteKeys.includes(r.key))
: state.routes,
openingRouteKeys: state.openingRouteKeys.filter(
(key) => key !== route.key
),
closingRouteKeys: state.closingRouteKeys.filter(
(key) => key !== route.key
),
replacingRouteKeys: [],
}));
const { state, navigation } = this.props;
if (
this.state.replacingRouteKeys.every((key) => key !== route.key) &&
state.routeNames.includes(route.name) &&
!state.routes.some((r) => r.key === route.key)
) {
// If route isn't present in current state, assume that a close animation was cancelled
// So we need to add this route back to the state
navigation.navigate(route);
} else {
this.setState((state) => ({
routes: state.replacingRouteKeys.length
? state.routes.filter(
(r) => !state.replacingRouteKeys.includes(r.key)
)
: state.routes,
openingRouteKeys: state.openingRouteKeys.filter(
(key) => key !== route.key
),
closingRouteKeys: state.closingRouteKeys.filter(
(key) => key !== route.key
),
replacingRouteKeys: [],
}));
}
};
private handleCloseRoute = ({ route }: { route: Route<string> }) => {
const { state, navigation } = this.props;
if (state.routes.find((r) => r.key === route.key)) {
if (state.routes.some((r) => r.key === route.key)) {
// If a route exists in state, trigger a pop
// This will happen in when the route was closed from the card component
// e.g. When the close animation triggered from a gesture ends
@@ -393,7 +406,6 @@ export default class StackView extends React.Component<Props, State> {
render() {
const {
state,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
navigation,
keyboardHandlingEnabled,
mode = 'card',
@@ -411,38 +423,40 @@ export default class StackView extends React.Component<Props, State> {
mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen';
return (
<GestureHandlerWrapper style={styles.container}>
<SafeAreaProviderCompat>
<SafeAreaConsumer>
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<CardStack
mode={mode}
insets={insets as EdgeInsets}
getPreviousRoute={this.getPreviousRoute}
getGesturesEnabled={this.getGesturesEnabled}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
headerMode={headerMode}
state={state}
descriptors={descriptors}
{...rest}
{...props}
/>
)}
</KeyboardManager>
)}
</SafeAreaConsumer>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
<NavigationHelpersContext.Provider value={navigation}>
<GestureHandlerWrapper style={styles.container}>
<SafeAreaProviderCompat>
<SafeAreaConsumer>
{(insets) => (
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
{(props) => (
<CardStack
mode={mode}
insets={insets as EdgeInsets}
getPreviousRoute={this.getPreviousRoute}
getGesturesEnabled={this.getGesturesEnabled}
routes={routes}
openingRouteKeys={openingRouteKeys}
closingRouteKeys={closingRouteKeys}
onOpenRoute={this.handleOpenRoute}
onCloseRoute={this.handleCloseRoute}
onTransitionStart={this.handleTransitionStart}
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
headerMode={headerMode}
state={state}
descriptors={descriptors}
{...rest}
{...props}
/>
)}
</KeyboardManager>
)}
</SafeAreaConsumer>
</SafeAreaProviderCompat>
</GestureHandlerWrapper>
</NavigationHelpersContext.Provider>
);
}
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 React Navigation Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,5 @@
# `@react-navigation/web-stack`
Stack navigator for React Navigation on Web.
Installation instructions and documentation can be found on the [React Navigation website](https://reactnavigation.org/docs/web-stack-navigator.html).

View File

@@ -0,0 +1,62 @@
{
"name": "@react-navigation/webstack",
"description": "Stack navigator component for Web",
"version": "5.0.0",
"keywords": [
"react",
"react-native",
"react-component",
"react-navigation",
"web",
"stack"
],
"license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/web-stack",
"bugs": {
"url": "https://github.com/react-navigation/react-navigation/issues"
},
"homepage": "https://reactnavigation.org/docs/web-stack-navigator.html",
"main": "lib/commonjs/index.js",
"react-native": "src/index.tsx",
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"src",
"lib"
],
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"devDependencies": {
"@react-native-community/bob": "^0.10.0",
"@react-navigation/native": "^5.1.6",
"@types/react": "^16.9.23",
"del-cli": "^3.0.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"typescript": "^3.8.3"
},
"peerDependencies": {
"@react-navigation/native": "^5.0.5",
"react": "*"
},
"@react-native-community/bob": {
"source": "src",
"output": "lib",
"targets": [
"commonjs",
"module",
[
"typescript",
{
"project": "tsconfig.build.json"
}
]
]
}
}

View File

@@ -0,0 +1,23 @@
/**
* Navigators
*/
export { default as createWebStackNavigator } from './navigators/createWebStackNavigator';
/**
* Views
*/
export { default as WebStackView } from './views/Stack/WebStackView';
export { default as Header } from './views/Header/Header';
export { default as HeaderTitle } from './views/Header/HeaderTitle';
export { default as HeaderBackButton } from './views/Header/HeaderBackButton';
/**
* Types
*/
export type {
WebStackNavigationOptions,
WebStackNavigationProp,
WebStackHeaderProps,
WebStackHeaderLeftButtonProps,
WebStackHeaderTitleProps,
} from './types';

View File

@@ -0,0 +1,81 @@
import * as React from 'react';
import {
useNavigationBuilder,
createNavigatorFactory,
DefaultNavigatorOptions,
EventArg,
StackRouter,
StackRouterOptions,
StackNavigationState,
StackActions,
} from '@react-navigation/native';
import WebStackView from '../views/Stack/WebStackView';
import {
WebStackNavigationConfig,
WebStackNavigationOptions,
WebStackNavigationEventMap,
} from '../types';
type Props = DefaultNavigatorOptions<WebStackNavigationOptions> &
StackRouterOptions &
WebStackNavigationConfig;
function StackNavigator({
initialRouteName,
children,
screenOptions,
...rest
}: Props) {
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState,
StackRouterOptions,
WebStackNavigationOptions,
WebStackNavigationEventMap
>(StackRouter, {
initialRouteName,
children,
screenOptions,
});
React.useEffect(
() =>
navigation.addListener &&
navigation.addListener('tabPress', (e) => {
const isFocused = navigation.isFocused();
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (
state.index > 0 &&
isFocused &&
!(e as EventArg<'tabPress', true>).defaultPrevented
) {
// When user taps on already focused tab and we're inside the tab,
// reset the stack to replicate native behaviour
navigation.dispatch({
...StackActions.popToTop(),
target: state.key,
});
}
});
}),
[navigation, state.index, state.key]
);
return (
<WebStackView
{...rest}
state={state}
descriptors={descriptors}
navigation={navigation}
/>
);
}
export default createNavigatorFactory<
StackNavigationState,
WebStackNavigationOptions,
WebStackNavigationEventMap,
typeof StackNavigator
>(StackNavigator);

View File

@@ -0,0 +1,166 @@
import * as React from 'react';
import {
NavigationProp,
ParamListBase,
Descriptor,
RouteProp,
NavigationHelpers,
StackNavigationState,
StackActionHelpers,
} from '@react-navigation/native';
export type WebStackNavigationEventMap = {};
export type WebStackNavigationHelpers = NavigationHelpers<
ParamListBase,
WebStackNavigationEventMap
>;
export type WebStackNavigationProp<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = NavigationProp<
ParamList,
RouteName,
StackNavigationState,
WebStackNavigationOptions,
WebStackNavigationEventMap
> &
StackActionHelpers<ParamList>;
export type WebStackHeaderOptions = {
/**
* String or a function that returns a React Element to be used by the header.
* Defaults to scene `title`.
* It receives `allowFontScaling`, `onLayout`, `style` and `children` in the options object as an argument.
* The title string is passed in `children`.
*/
headerTitle?: string | ((props: WebStackHeaderTitleProps) => React.ReactNode);
/**
* How to align the the header title.
* Defaults to `center` on iOS and `left` on Android.
*/
headerTitleAlign?: 'left' | 'center';
/**
* Style object for the title component.
*/
headerTitleStyle?: React.CSSProperties;
/**
* Tint color for the header.
*/
headerTintColor?: string;
/**
* Function which returns a React Element to display on the left side of the header.
* It receives a number of arguments when rendered (`onPress`, `label`, `labelStyle` and more.
*/
headerLeft?: (props: WebStackHeaderLeftButtonProps) => React.ReactNode;
/**
* Function which returns a React Element to display on the right side of the header.
*/
headerRight?: (props: { tintColor?: string }) => React.ReactNode;
/**
* Style object for the header. You can specify a custom background color here, for example.
*/
headerStyle?: React.CSSProperties;
};
export type WebStackHeaderProps = {
/**
* Navigation prop for the header.
*/
route: RouteProp<ParamListBase, string>;
/**
* Navigation prop for the header.
*/
navigation: WebStackNavigationProp<ParamListBase>;
/**
* Descriptors for the header.
*/
descriptor: WebStackDescriptor;
/**
* If header should display back button
*/
canGoBack: boolean;
};
export type WebStackDescriptor = Descriptor<
ParamListBase,
string,
StackNavigationState,
WebStackNavigationOptions
>;
export type WebStackDescriptorMap = {
[key: string]: WebStackDescriptor;
};
export type WebStackNavigationOptions = WebStackHeaderOptions & {
/**
* String that can be displayed in the header as a fallback for `headerTitle`.
*/
title?: string;
/**
* Function that given `HeaderProps` returns a React Element to display as a header.
*/
header?: (props: WebStackHeaderProps) => React.ReactNode;
/**
* Whether to show the header. The header is shown by default unless `headerMode` was set to `none`.
* Setting this to `false` hides the header.
*/
headerShown?: boolean;
/**
* Style object for the card in stack.
* You can provide a custom background color to use instead of the default background here.
*
* You can also specify `{ backgroundColor: 'transparent' }` to make the previous screen visible underneath.
* This is useful to implement things like modal dialogs..
*/
cardStyle?: React.CSSProperties;
/**
* Whether transition animation should be enabled the screen.
* If you set it to `false`, the screen won't animate when pushing or popping. Defaults to `true`.
*/
animationEnabled?: boolean;
/**
* The type of animation to use when this screen replaces another screen. Defaults to `push`.
* When `pop` is used, the `pop` animation is applied to the screen being replaced.
*/
animationTypeForReplace?: 'push' | 'pop';
};
export type WebStackNavigationConfig = {};
export type WebStackHeaderLeftButtonProps = {
/**
* Whether the button is disabled.
*/
disabled?: boolean;
/**
* Callback to call when the button is clicked.
* By default, this triggers `goBack`.
*/
onClick?: () => void;
/**
* Style object for the button
*/
style?: React.CSSProperties;
/**
* Whether it's possible to navigate back in stack.
*/
canGoBack?: boolean;
};
export type WebStackHeaderTitleProps = {
/**
* Tint color for the header.
*/
tintColor?: string;
/**
* Content of the title element. Usually the title string.
*/
children?: string;
/**
* Style object for the title element.
*/
style?: React.CSSProperties;
};

View File

@@ -0,0 +1,17 @@
export default function debounce<T extends (...args: any[]) => void>(
func: T,
duration: number
): T {
let timeout: NodeJS.Timeout | number | undefined;
return function (this: any, ...args) {
if (!timeout) {
// eslint-disable-next-line babel/no-invalid-this
func.apply(this, args);
timeout = setTimeout(() => {
timeout = undefined;
}, duration);
}
} as T;
}

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import { StackActions } from '@react-navigation/native';
import HeaderSegment from './HeaderSegment';
import HeaderTitle from './HeaderTitle';
import debounce from '../../utils/debounce';
import { WebStackHeaderProps, WebStackHeaderTitleProps } from '../../types';
export default React.memo(function Header({
navigation,
route,
descriptor,
}: WebStackHeaderProps) {
const { options } = descriptor;
const title =
typeof options.headerTitle !== 'function' &&
options.headerTitle !== undefined
? options.headerTitle
: options.title !== undefined
? options.title
: route.name;
const goBack = React.useCallback(
debounce(() => {
if (navigation.isFocused() && navigation.canGoBack()) {
navigation.dispatch({
...StackActions.pop(),
source: route.key,
});
}
}, 50),
[navigation, route.key]
);
return (
<HeaderSegment
{...options}
route={route}
descriptor={descriptor}
title={title}
headerTitle={
typeof options.headerTitle !== 'function'
? (props: WebStackHeaderTitleProps) => <HeaderTitle {...props} />
: options.headerTitle
}
onGoBack={goBack}
/>
);
});

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { useTheme } from '@react-navigation/native';
import { WebStackHeaderLeftButtonProps } from '../../types';
type Props = WebStackHeaderLeftButtonProps;
export default function HeaderBackButton({ disabled, onClick, style }: Props) {
const { colors } = useTheme();
/* TODO: styling */
return (
<button
type="button"
disabled={disabled}
style={{
color: style?.color !== undefined ? style.color : colors.text,
...styles.button,
...style,
}}
onClick={onClick}
>
Back
{/* TODO: SVG image for back button */}
</button>
);
}
const styles = {
button: {},
};

View File

@@ -0,0 +1,74 @@
import * as React from 'react';
import {
NavigationContext,
NavigationRouteContext,
Route,
ParamListBase,
RouteProp,
} from '@react-navigation/native';
import Header from './Header';
import { WebStackNavigationProp, WebStackDescriptor } from '../../types';
export type Props = {
scenes: {
route: RouteProp<ParamListBase, string>;
descriptor: WebStackDescriptor;
}[];
getPreviousRoute: (props: {
route: Route<string>;
}) => Route<string> | undefined;
getFocusedRoute: () => Route<string>;
style?: React.CSSProperties;
};
export default function HeaderContainer({
scenes,
getFocusedRoute,
getPreviousRoute,
style,
}: Props) {
const focusedRoute = getFocusedRoute();
return (
<div style={style}>
{scenes.slice(-3).map((scene, i, self) => {
if (i !== self.length - 1 || !scene) {
return null;
}
const { options } = scene.descriptor;
const isFocused = focusedRoute.key === scene.route.key;
const previousRoute = getPreviousRoute({ route: scene.route });
const props = {
canGoBack: !previousRoute,
route: scene.route,
descriptor: scene.descriptor,
navigation: scene.descriptor.navigation as WebStackNavigationProp<
ParamListBase
>,
};
return (
<NavigationContext.Provider
key={scene.route.key}
value={scene.descriptor.navigation}
>
<NavigationRouteContext.Provider value={scene.route}>
<div aria-hidden={isFocused ? true : false}>
{options.headerShown !== false ? (
options.header !== undefined ? (
options.header(props)
) : (
<Header {...props} />
)
) : null}
</div>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
);
})}
</div>
);
}

View File

@@ -0,0 +1,72 @@
import * as React from 'react';
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
import HeaderBackButton from './HeaderBackButton';
import {
WebStackHeaderLeftButtonProps,
WebStackHeaderTitleProps,
WebStackHeaderOptions,
WebStackDescriptor,
} from '../../types';
type Props = WebStackHeaderOptions & {
headerTitle: (props: WebStackHeaderTitleProps) => React.ReactNode;
onGoBack?: () => void;
title?: string;
route: RouteProp<ParamListBase, string>;
descriptor: WebStackDescriptor;
};
export default function HeaderSegment(props: Props) {
const {
title: currentTitle,
onGoBack,
headerTitle,
headerLeft: left = onGoBack
? (props: WebStackHeaderLeftButtonProps) => (
<HeaderBackButton {...props} />
)
: undefined,
headerTintColor,
headerRight: right,
headerTitleStyle,
headerStyle,
} = props;
const { colors } = useTheme();
const leftButton = left
? left({
onClick: onGoBack,
canGoBack: Boolean(onGoBack),
})
: null;
return (
<div
style={{
backgroundColor: colors.card,
borderBottomColor: colors.border,
...styles.header,
...headerStyle,
}}
>
{leftButton}
{headerTitle({
children: currentTitle,
style: { marginLeft: 18, marginRight: 16, ...headerTitleStyle },
})}
{right ? right({ tintColor: headerTintColor }) : null}
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
header: {
display: 'flex',
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderBottomWidth: 1,
},
};

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
import { useTheme } from '@react-navigation/native';
type Props = JSX.IntrinsicElements['h1'];
export default function HeaderTitle({ style, ...rest }: Props) {
const { colors } = useTheme();
return (
<h1
{...rest}
style={{
color: style?.color === undefined ? colors.text : style?.color,
...styles.title,
...style,
}}
/>
);
}
const styles = {
title: {
fontSize: 18,
fontWeight: 500,
},
};

View File

@@ -0,0 +1,91 @@
import * as React from 'react';
import {
NavigationContext,
NavigationRouteContext,
Route,
RouteProp,
ParamListBase,
useTheme,
} from '@react-navigation/native';
import Header from '../Header/Header';
import { WebStackDescriptor, WebStackNavigationProp } from '../../types';
type Props = {
active: boolean;
focused: boolean;
closing: boolean;
route: RouteProp<ParamListBase, string>;
descriptor: WebStackDescriptor;
canGoBack: boolean;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
onOpenRoute: (props: { route: Route<string> }) => void;
onCloseRoute: (props: { route: Route<string> }) => void;
};
function Card({
active,
closing,
focused,
canGoBack,
onCloseRoute,
onOpenRoute,
renderScene,
route,
descriptor,
}: Props) {
const { colors } = useTheme();
const { options, navigation } = descriptor;
const headerProps = {
canGoBack,
route,
descriptor,
navigation: navigation as WebStackNavigationProp<ParamListBase>,
};
return (
<NavigationContext.Provider key={route.key} value={descriptor.navigation}>
<NavigationRouteContext.Provider value={route}>
<div
aria-hidden={focused ? true : false}
style={{
opacity: active ? 1 : 0,
pointerEvents: focused ? 'auto' : 'none',
backgroundColor: colors.background,
...styles.container,
...options.cardStyle,
}}
onTransitionEnd={() => {
if (closing) {
onCloseRoute({ route });
} else {
onOpenRoute({ route });
}
}}
>
{options.headerShown !== false ? (
options.header !== undefined ? (
options.header(headerProps)
) : (
<Header {...headerProps} />
)
) : null}
{renderScene({ route })}
</div>
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
);
}
export default React.memo(Card);
const styles = {
container: {
position: 'absolute' as const,
top: 0,
left: 0,
right: 0,
bottom: 0,
transitionDuration: '200ms',
},
};

View File

@@ -0,0 +1,121 @@
import * as React from 'react';
import { Route, StackNavigationState } from '@react-navigation/native';
import Card from './Card';
import { WebStackDescriptorMap, WebStackDescriptor } from '../../types';
type Props = {
state: StackNavigationState;
descriptors: WebStackDescriptorMap;
routes: Route<string>[];
openingRouteKeys: string[];
closingRouteKeys: string[];
onOpenRoute: (props: { route: Route<string> }) => void;
onCloseRoute: (props: { route: Route<string> }) => void;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
};
type State = {
routes: Route<string>[];
descriptors: WebStackDescriptorMap;
scenes: { route: Route<string>; descriptor: WebStackDescriptor }[];
};
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
export default class CardStack extends React.Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State) {
if (
props.routes === state.routes &&
props.descriptors === state.descriptors
) {
return null;
}
return {
routes: props.routes,
scenes: props.routes.map((route, index, self) => {
const previousRoute = self[index - 1];
const nextRoute = self[index + 1];
const oldScene = state.scenes[index];
const descriptor =
props.descriptors[route.key] ||
state.descriptors[route.key] ||
(oldScene ? oldScene.descriptor : FALLBACK_DESCRIPTOR);
const nextDescriptor =
props.descriptors[nextRoute?.key] ||
state.descriptors[nextRoute?.key];
const previousDescriptor =
props.descriptors[previousRoute?.key] ||
state.descriptors[previousRoute?.key];
const scene = {
route,
descriptor,
__memo: [route, descriptor, nextDescriptor, previousDescriptor],
};
if (
oldScene &&
scene.__memo.every((it, i) => {
// @ts-ignore
return oldScene.__memo[i] === it;
})
) {
return oldScene;
}
return scene;
}),
descriptors: props.descriptors,
};
}
state: State = {
routes: [],
scenes: [],
descriptors: this.props.descriptors,
};
render() {
const {
state,
routes,
closingRouteKeys,
onOpenRoute,
onCloseRoute,
renderScene,
} = this.props;
const { scenes } = this.state;
const focusedRoute = state.routes[state.index];
return (
<React.Fragment>
{routes.map((route, index, self) => {
const focused = focusedRoute.key === route.key;
const scene = scenes[index];
return (
<Card
key={route.key}
active={index === self.length - 1}
focused={focused}
closing={closingRouteKeys.includes(route.key)}
route={route as any}
descriptor={scene.descriptor}
canGoBack={scenes.length > index}
renderScene={renderScene}
onOpenRoute={onOpenRoute}
onCloseRoute={onCloseRoute}
/>
);
})}
</React.Fragment>
);
}
}

Some files were not shown because too many files have changed in this diff Show More