mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-02-28 22:38:00 +08:00
Compare commits
35 Commits
@react-nav
...
@satya164/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
515e652b37 | ||
|
|
f2291d110f | ||
|
|
942d2be2c7 | ||
|
|
b747e527a4 | ||
|
|
38020de80b | ||
|
|
67404f4999 | ||
|
|
2792f438fe | ||
|
|
2573b5beaa | ||
|
|
2697355ab2 | ||
|
|
a695cf9c05 | ||
|
|
c9c825bee6 | ||
|
|
b172b51f17 | ||
|
|
9c05af50b4 | ||
|
|
24febf6ea9 | ||
|
|
8cbb201f1a | ||
|
|
2467ce4ff7 | ||
|
|
5683bebfd6 | ||
|
|
78485cea69 | ||
|
|
1613915669 | ||
|
|
335a04edc1 | ||
|
|
5e0069a896 | ||
|
|
249248e741 | ||
|
|
821343fed3 | ||
|
|
82edb2581b | ||
|
|
cb67530dc5 | ||
|
|
36689e24c2 | ||
|
|
6e51f596fa | ||
|
|
402df73aa2 | ||
|
|
187aefe9c4 | ||
|
|
2613a62874 | ||
|
|
6bdf6ae4ed | ||
|
|
e2bcf5168c | ||
|
|
dfdba8d741 | ||
|
|
a3f7a5feba | ||
|
|
004c7d7ab1 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
.github/workflows/triage.yml
vendored
4
.github/workflows/triage.yml
vendored
@@ -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
27
.github/workflows/versions.yml
vendored
Normal 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
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"settings": {
|
||||
"import/core-modules": [
|
||||
"detox",
|
||||
"detox/runners/jest/adapter",
|
||||
"detox/runners/jest/specReporter"
|
||||
]
|
||||
},
|
||||
"env": { "jest": true, "jasmine": true }
|
||||
}
|
||||
44
example/e2e/__integration_tests__/Link.test.tsx
Normal file
44
example/e2e/__integration_tests__/Link.test.tsx
Normal 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');
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
13
example/e2e/__integration_tests__/index.test.tsx
Normal file
13
example/e2e/__integration_tests__/index.test.tsx
Normal 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');
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"setupFilesAfterEnv": ["./init.js"],
|
||||
"testEnvironment": "node",
|
||||
"reporters": ["detox/runners/jest/streamlineReporter"],
|
||||
"verbose": true
|
||||
}
|
||||
24
example/e2e/config/setup-playwright.tsx
Normal file
24
example/e2e/config/setup-playwright.tsx
Normal 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 };
|
||||
8
example/e2e/config/setup-server.tsx
Normal file
8
example/e2e/config/setup-server.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
5
example/e2e/config/teardown-server.tsx
Normal file
5
example/e2e/config/teardown-server.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { teardown } from 'jest-dev-server';
|
||||
|
||||
export default async function () {
|
||||
await teardown();
|
||||
}
|
||||
@@ -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
6
example/jest.config.js
Normal 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'],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
148
example/src/Screens/LinkComponent.tsx
Normal file
148
example/src/Screens/LinkComponent.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
127
example/src/Screens/MasterDetail.tsx
Normal file
127
example/src/Screens/MasterDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
example/src/Screens/WebStack.native.tsx
Normal file
3
example/src/Screens/WebStack.native.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function WebStack() {
|
||||
return null;
|
||||
}
|
||||
142
example/src/Screens/WebStack.tsx
Normal file
142
example/src/Screens/WebStack.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
5
netlify.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
[build]
|
||||
base = "/"
|
||||
publish = "example/web-build"
|
||||
command = "yarn example expo build:web"
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
packages/core/src/NavigationHelpersContext.tsx
Normal file
13
packages/core/src/NavigationHelpersContext.tsx
Normal 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;
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
43
packages/native/src/Link.tsx
Normal file
43
packages/native/src/Link.tsx
Normal 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 }),
|
||||
});
|
||||
}
|
||||
8
packages/native/src/LinkingContext.tsx
Normal file
8
packages/native/src/LinkingContext.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export default function () {
|
||||
throw new Error(
|
||||
"'NavigationNativeContainer' has been renamed to 'NavigationContainer"
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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`)
|
||||
|
||||
77
packages/native/src/useLinkBuilder.tsx
Normal file
77
packages/native/src/useLinkBuilder.tsx
Normal 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;
|
||||
}
|
||||
65
packages/native/src/useLinkProps.tsx
Normal file
65
packages/native/src/useLinkProps.tsx
Normal 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 },
|
||||
}),
|
||||
};
|
||||
}
|
||||
55
packages/native/src/useLinkTo.tsx
Normal file
55
packages/native/src/useLinkTo.tsx
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
49
packages/native/src/useThenable.tsx
Normal file
49
packages/native/src/useThenable.tsx
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
71
packages/stack/src/views/Screens.tsx
Normal file
71
packages/stack/src/views/Screens.tsx
Normal 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} />;
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
21
packages/web-stack/LICENSE
Normal file
21
packages/web-stack/LICENSE
Normal 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.
|
||||
5
packages/web-stack/README.md
Normal file
5
packages/web-stack/README.md
Normal 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).
|
||||
62
packages/web-stack/package.json
Normal file
62
packages/web-stack/package.json
Normal 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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
23
packages/web-stack/src/index.tsx
Normal file
23
packages/web-stack/src/index.tsx
Normal 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';
|
||||
@@ -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);
|
||||
166
packages/web-stack/src/types.tsx
Normal file
166
packages/web-stack/src/types.tsx
Normal 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;
|
||||
};
|
||||
17
packages/web-stack/src/utils/debounce.tsx
Normal file
17
packages/web-stack/src/utils/debounce.tsx
Normal 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;
|
||||
}
|
||||
49
packages/web-stack/src/views/Header/Header.tsx
Normal file
49
packages/web-stack/src/views/Header/Header.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
30
packages/web-stack/src/views/Header/HeaderBackButton.tsx
Normal file
30
packages/web-stack/src/views/Header/HeaderBackButton.tsx
Normal 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: {},
|
||||
};
|
||||
74
packages/web-stack/src/views/Header/HeaderContainer.tsx
Normal file
74
packages/web-stack/src/views/Header/HeaderContainer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
72
packages/web-stack/src/views/Header/HeaderSegment.tsx
Normal file
72
packages/web-stack/src/views/Header/HeaderSegment.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
26
packages/web-stack/src/views/Header/HeaderTitle.tsx
Normal file
26
packages/web-stack/src/views/Header/HeaderTitle.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
91
packages/web-stack/src/views/Stack/Card.tsx
Normal file
91
packages/web-stack/src/views/Stack/Card.tsx
Normal 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',
|
||||
},
|
||||
};
|
||||
121
packages/web-stack/src/views/Stack/CardStack.tsx
Executable file
121
packages/web-stack/src/views/Stack/CardStack.tsx
Executable 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
Reference in New Issue
Block a user