Compare commits
20 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 |
@@ -16,7 +16,9 @@ jobs:
|
|||||||
keys:
|
keys:
|
||||||
- v1-dependencies-{{ checksum "yarn.lock" }}
|
- v1-dependencies-{{ checksum "yarn.lock" }}
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
- run: yarn install --frozen-lockfile
|
- run:
|
||||||
|
name: Install project dependencies
|
||||||
|
command: yarn install --frozen-lockfile
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: v1-dependencies-{{ checksum "yarn.lock" }}
|
key: v1-dependencies-{{ checksum "yarn.lock" }}
|
||||||
paths: node_modules
|
paths: node_modules
|
||||||
@@ -28,28 +30,57 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/project
|
at: ~/project
|
||||||
- run: |
|
- run:
|
||||||
yarn lint
|
name: Lint files
|
||||||
yarn typescript
|
command: yarn lint
|
||||||
|
- run:
|
||||||
|
name: Typecheck files
|
||||||
|
command: yarn typescript
|
||||||
unit-tests:
|
unit-tests:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/project
|
at: ~/project
|
||||||
- run: |
|
- run:
|
||||||
yarn test --coverage
|
name: Run unit tests
|
||||||
cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
command: yarn test --coverage
|
||||||
|
- run:
|
||||||
|
name: Upload test coverage
|
||||||
|
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: coverage
|
path: coverage
|
||||||
destination: 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:
|
build-packages:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/project
|
at: ~/project
|
||||||
- run: |
|
- run:
|
||||||
yarn lerna run prepare
|
name: Build packages in the monorepo
|
||||||
node scripts/check-types-path.js
|
command: yarn lerna run prepare
|
||||||
|
- run:
|
||||||
|
name: Verify paths for types
|
||||||
|
command: node scripts/check-types-path.js
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
@@ -62,6 +93,9 @@ workflows:
|
|||||||
- unit-tests:
|
- unit-tests:
|
||||||
requires:
|
requires:
|
||||||
- install-dependencies
|
- install-dependencies
|
||||||
|
- integration-tests:
|
||||||
|
requires:
|
||||||
|
- install-dependencies
|
||||||
- build-packages:
|
- build-packages:
|
||||||
requires:
|
requires:
|
||||||
- install-dependencies
|
- install-dependencies
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"@react-navigation/routers",
|
"@react-navigation/routers",
|
||||||
"@react-navigation/compat",
|
"@react-navigation/compat",
|
||||||
"@react-navigation/stack",
|
"@react-navigation/stack",
|
||||||
|
"@react-navigation/web-stack",
|
||||||
"@react-navigation/drawer",
|
"@react-navigation/drawer",
|
||||||
"@react-navigation/bottom-tabs",
|
"@react-navigation/bottom-tabs",
|
||||||
"@react-navigation/material-top-tabs",
|
"@react-navigation/material-top-tabs",
|
||||||
|
|||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
"web": "expo start:web",
|
||||||
"native": "react-native start",
|
"native": "react-native start",
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
"ios": "react-native run-ios"
|
"ios": "react-native run-ios",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^10.0.0",
|
"@expo/vector-icons": "^10.0.0",
|
||||||
@@ -32,10 +33,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/webpack-config": "^0.11.19",
|
"@expo/webpack-config": "^0.11.19",
|
||||||
|
"@types/jest-dev-server": "^4.2.0",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.60.22",
|
"@types/react-native": "^0.60.22",
|
||||||
"babel-preset-expo": "^8.1.0",
|
"babel-preset-expo": "^8.1.0",
|
||||||
"expo-cli": "^3.17.18",
|
"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"
|
"typescript": "^3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import TouchableBounce from '../Shared/TouchableBounce';
|
import TouchableBounce from '../Shared/TouchableBounce';
|
||||||
@@ -28,7 +29,10 @@ export default function BottomTabsScreen() {
|
|||||||
return (
|
return (
|
||||||
<BottomTabs.Navigator
|
<BottomTabs.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarButton: (props) => <TouchableBounce {...props} />,
|
tabBarButton:
|
||||||
|
Platform.OS === 'web'
|
||||||
|
? undefined
|
||||||
|
: (props) => <TouchableBounce {...props} />,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BottomTabs.Screen
|
<BottomTabs.Screen
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
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 { Button } from 'react-native-paper';
|
||||||
import {
|
import {
|
||||||
createCompatNavigatorFactory,
|
createCompatNavigatorFactory,
|
||||||
@@ -11,25 +11,30 @@ import {
|
|||||||
} from '@react-navigation/stack';
|
} from '@react-navigation/stack';
|
||||||
import Article from '../Shared/Article';
|
import Article from '../Shared/Article';
|
||||||
import Albums from '../Shared/Albums';
|
import Albums from '../Shared/Albums';
|
||||||
|
import NewsFeed from '../Shared/NewsFeed';
|
||||||
|
|
||||||
type CompatStackParams = {
|
type CompatStackParams = {
|
||||||
Article: { author: string };
|
Albums: undefined;
|
||||||
Album: undefined;
|
Nested: { author: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArticleScreen: CompatScreenType<StackNavigationProp<
|
type NestedStackParams = {
|
||||||
CompatStackParams,
|
Feed: undefined;
|
||||||
'Article'
|
Article: { author: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
||||||
|
CompatStackParams
|
||||||
>> = ({ navigation }) => {
|
>> = ({ navigation }) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => navigation.push('Album')}
|
onPress={() => navigation.push('Nested', { author: 'Babel fish' })}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
Push album
|
Push nested
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
@@ -39,24 +44,20 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Article author={{ name: navigation.getParam('author') }} />
|
<Albums scrollEnabled={false} />
|
||||||
</React.Fragment>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ArticleScreen.navigationOptions = ({ navigation }) => ({
|
const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
|
||||||
title: `Article by ${navigation.getParam('author')}`,
|
navigation,
|
||||||
});
|
}) => {
|
||||||
|
|
||||||
const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
|
||||||
CompatStackParams
|
|
||||||
>> = ({ navigation }) => {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<ScrollView>
|
||||||
<View style={styles.buttons}>
|
<View style={styles.buttons}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
|
onPress={() => navigation.push('Article')}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
Push article
|
Push article
|
||||||
@@ -69,22 +70,69 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
|
|||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<Albums />
|
<NewsFeed scrollEnabled={false} />
|
||||||
</React.Fragment>
|
</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>
|
StackNavigationProp<CompatStackParams>
|
||||||
>(
|
>(
|
||||||
{
|
{
|
||||||
Article: {
|
Albums: AlbumsScreen,
|
||||||
screen: ArticleScreen,
|
Nested: {
|
||||||
|
screen: createCompatStackNavigator<
|
||||||
|
StackNavigationProp<NestedStackParams>
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
Feed: FeedScreen,
|
||||||
|
Article: ArticleScreen,
|
||||||
|
},
|
||||||
|
{ navigationOptions: { headerShown: false } }
|
||||||
|
),
|
||||||
params: {
|
params: {
|
||||||
author: 'Gandalf',
|
author: 'Gandalf',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Album: AlbumsScreen,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: 'modal',
|
mode: 'modal',
|
||||||
|
|||||||
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
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
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -22,10 +22,10 @@ import {
|
|||||||
Appbar,
|
Appbar,
|
||||||
List,
|
List,
|
||||||
Divider,
|
Divider,
|
||||||
|
Text,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
import {
|
import {
|
||||||
InitialState,
|
InitialState,
|
||||||
useLinking,
|
|
||||||
NavigationContainerRef,
|
NavigationContainerRef,
|
||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
|
|
||||||
import LinkingPrefixes from './LinkingPrefixes';
|
import LinkingPrefixes from './LinkingPrefixes';
|
||||||
import SettingsItem from './Shared/SettingsItem';
|
import SettingsItem from './Shared/SettingsItem';
|
||||||
|
import WebStack from './Screens/WebStack';
|
||||||
import SimpleStack from './Screens/SimpleStack';
|
import SimpleStack from './Screens/SimpleStack';
|
||||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||||
import StackTransparent from './Screens/StackTransparent';
|
import StackTransparent from './Screens/StackTransparent';
|
||||||
@@ -55,11 +56,15 @@ import DynamicTabs from './Screens/DynamicTabs';
|
|||||||
import AuthFlow from './Screens/AuthFlow';
|
import AuthFlow from './Screens/AuthFlow';
|
||||||
import CompatAPI from './Screens/CompatAPI';
|
import CompatAPI from './Screens/CompatAPI';
|
||||||
import MasterDetail from './Screens/MasterDetail';
|
import MasterDetail from './Screens/MasterDetail';
|
||||||
|
import LinkComponent from './Screens/LinkComponent';
|
||||||
|
|
||||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||||
|
|
||||||
enableScreens();
|
enableScreens();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
global.REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED = true;
|
||||||
|
|
||||||
type RootDrawerParamList = {
|
type RootDrawerParamList = {
|
||||||
Root: undefined;
|
Root: undefined;
|
||||||
Another: undefined;
|
Another: undefined;
|
||||||
@@ -72,6 +77,11 @@ type RootStackParamList = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SCREENS = {
|
const SCREENS = {
|
||||||
|
...(Platform.OS === 'web'
|
||||||
|
? {
|
||||||
|
WebStack: { title: 'Web Stack', component: WebStack },
|
||||||
|
}
|
||||||
|
: null),
|
||||||
SimpleStack: { title: 'Simple Stack', component: SimpleStack },
|
SimpleStack: { title: 'Simple Stack', component: SimpleStack },
|
||||||
ModalPresentationStack: {
|
ModalPresentationStack: {
|
||||||
title: 'Modal Presentation Stack',
|
title: 'Modal Presentation Stack',
|
||||||
@@ -110,6 +120,10 @@ const SCREENS = {
|
|||||||
title: 'Compat Layer',
|
title: 'Compat Layer',
|
||||||
component: CompatAPI,
|
component: CompatAPI,
|
||||||
},
|
},
|
||||||
|
LinkComponent: {
|
||||||
|
title: '<Link />',
|
||||||
|
component: LinkComponent,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
const Drawer = createDrawerNavigator<RootDrawerParamList>();
|
||||||
@@ -123,34 +137,6 @@ Asset.loadAsync(StackAssets);
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const containerRef = React.useRef<NavigationContainerRef>(null);
|
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 [theme, setTheme] = React.useState(DefaultTheme);
|
||||||
|
|
||||||
const [isReady, setIsReady] = React.useState(false);
|
const [isReady, setIsReady] = React.useState(false);
|
||||||
@@ -161,12 +147,13 @@ export default function App() {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const restoreState = async () => {
|
const restoreState = async () => {
|
||||||
try {
|
try {
|
||||||
let state = await getInitialState();
|
let state;
|
||||||
|
|
||||||
if (Platform.OS !== 'web' && state === undefined) {
|
if (Platform.OS !== 'web' && state === undefined) {
|
||||||
const savedState = await AsyncStorage.getItem(
|
const savedState = await AsyncStorage.getItem(
|
||||||
NAVIGATION_PERSISTENCE_KEY
|
NAVIGATION_PERSISTENCE_KEY
|
||||||
);
|
);
|
||||||
|
|
||||||
state = savedState ? JSON.parse(savedState) : undefined;
|
state = savedState ? JSON.parse(savedState) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +174,7 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
restoreState();
|
restoreState();
|
||||||
}, [getInitialState]);
|
}, []);
|
||||||
|
|
||||||
const paperTheme = React.useMemo(() => {
|
const paperTheme = React.useMemo(() => {
|
||||||
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
|
||||||
@@ -236,6 +223,34 @@ export default function App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
theme={theme}
|
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.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
@@ -311,6 +326,7 @@ export default function App() {
|
|||||||
(name) => (
|
(name) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
key={name}
|
key={name}
|
||||||
|
testID={name}
|
||||||
title={SCREENS[name].title}
|
title={SCREENS[name].title}
|
||||||
onPress={() => navigation.navigate(name)}
|
onPress={() => navigation.navigate(name)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.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)
|
## [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
|
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/bottom-tabs",
|
"name": "@react-navigation/bottom-tabs",
|
||||||
"description": "Bottom tab navigator following iOS design guidelines",
|
"description": "Bottom tab navigator following iOS design guidelines",
|
||||||
"version": "5.2.7",
|
"version": "5.2.8",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.61.22",
|
"@types/react-native": "^0.61.22",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
StyleProp,
|
StyleProp,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
|
GestureResponderEvent,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {
|
import {
|
||||||
NavigationHelpers,
|
NavigationHelpers,
|
||||||
@@ -196,6 +197,13 @@ export type BottomTabBarProps = BottomTabBarOptions & {
|
|||||||
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BottomTabBarButtonProps = TouchableWithoutFeedbackProps & {
|
export type BottomTabBarButtonProps = Omit<
|
||||||
|
TouchableWithoutFeedbackProps,
|
||||||
|
'onPress'
|
||||||
|
> & {
|
||||||
|
to?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
onPress?: (
|
||||||
|
e: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
NavigationRouteContext,
|
NavigationRouteContext,
|
||||||
CommonActions,
|
CommonActions,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
useLinkBuilder,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import { useSafeArea } from 'react-native-safe-area-context';
|
import { useSafeArea } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ export default function BottomTabBar({
|
|||||||
tabStyle,
|
tabStyle,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const buildLink = useLinkBuilder();
|
||||||
|
|
||||||
const [dimensions, setDimensions] = React.useState(() => {
|
const [dimensions, setDimensions] = React.useState(() => {
|
||||||
const { height = 0, width = 0 } = Dimensions.get('window');
|
const { height = 0, width = 0 } = Dimensions.get('window');
|
||||||
@@ -260,6 +262,7 @@ export default function BottomTabBar({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
to={buildLink(route.name, route.params)}
|
||||||
testID={options.tabBarTestID}
|
testID={options.tabBarTestID}
|
||||||
allowFontScaling={allowFontScaling}
|
allowFontScaling={allowFontScaling}
|
||||||
activeTintColor={activeTintColor}
|
activeTintColor={activeTintColor}
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import {
|
|||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Animated,
|
Animated,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Platform,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
GestureResponderEvent,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Route, useTheme } from '@react-navigation/native';
|
import { Link, Route, useTheme } from '@react-navigation/native';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
import TabBarIcon from './TabBarIcon';
|
import TabBarIcon from './TabBarIcon';
|
||||||
@@ -37,6 +39,10 @@ type Props = {
|
|||||||
size: number;
|
size: number;
|
||||||
color: string;
|
color: string;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* URL to use for the link to the tab.
|
||||||
|
*/
|
||||||
|
to?: string;
|
||||||
/**
|
/**
|
||||||
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
|
* The button for the tab. Uses a `TouchableWithoutFeedback` by default.
|
||||||
*/
|
*/
|
||||||
@@ -50,13 +56,16 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
testID?: string;
|
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.
|
* Function to execute on long press.
|
||||||
*/
|
*/
|
||||||
onLongPress: () => void;
|
onLongPress: (e: GestureResponderEvent) => void;
|
||||||
/**
|
/**
|
||||||
* Whether the label should be aligned with the icon horizontally.
|
* Whether the label should be aligned with the icon horizontally.
|
||||||
*/
|
*/
|
||||||
@@ -104,11 +113,48 @@ export default function BottomTabBarItem({
|
|||||||
route,
|
route,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
button = ({ children, style, ...rest }: BottomTabBarButtonProps) => (
|
to,
|
||||||
<TouchableWithoutFeedback {...rest}>
|
button = ({
|
||||||
<View style={style}>{children}</View>
|
children,
|
||||||
</TouchableWithoutFeedback>
|
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,
|
accessibilityLabel,
|
||||||
testID,
|
testID,
|
||||||
onPress,
|
onPress,
|
||||||
@@ -196,6 +242,7 @@ export default function BottomTabBarItem({
|
|||||||
: inactiveBackgroundColor;
|
: inactiveBackgroundColor;
|
||||||
|
|
||||||
return button({
|
return button({
|
||||||
|
to,
|
||||||
onPress,
|
onPress,
|
||||||
onLongPress,
|
onLongPress,
|
||||||
testID,
|
testID,
|
||||||
@@ -248,4 +295,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
marginLeft: 20,
|
marginLeft: 20,
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { View, StyleSheet } from 'react-native';
|
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
|
// eslint-disable-next-line import/no-unresolved
|
||||||
import { ScreenContainer } from 'react-native-screens';
|
import { ScreenContainer } from 'react-native-screens';
|
||||||
|
|
||||||
@@ -91,44 +95,46 @@ export default class BottomTabView extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { state, descriptors, lazy } = this.props;
|
const { state, descriptors, navigation, lazy } = this.props;
|
||||||
const { routes } = state;
|
const { routes } = state;
|
||||||
const { loaded } = this.state;
|
const { loaded } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProviderCompat>
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
<View style={styles.container}>
|
<SafeAreaProviderCompat>
|
||||||
<ScreenContainer style={styles.pages}>
|
<View style={styles.container}>
|
||||||
{routes.map((route, index) => {
|
<ScreenContainer style={styles.pages}>
|
||||||
const descriptor = descriptors[route.key];
|
{routes.map((route, index) => {
|
||||||
const { unmountOnBlur } = descriptor.options;
|
const descriptor = descriptors[route.key];
|
||||||
const isFocused = state.index === index;
|
const { unmountOnBlur } = descriptor.options;
|
||||||
|
const isFocused = state.index === index;
|
||||||
|
|
||||||
if (unmountOnBlur && !isFocused) {
|
if (unmountOnBlur && !isFocused) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lazy && !loaded.includes(index) && !isFocused) {
|
if (lazy && !loaded.includes(index) && !isFocused) {
|
||||||
// Don't render a screen if we've never navigated to it
|
// Don't render a screen if we've never navigated to it
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourceSavingScene
|
<ResourceSavingScene
|
||||||
key={route.key}
|
key={route.key}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
isVisible={isFocused}
|
isVisible={isFocused}
|
||||||
>
|
>
|
||||||
<SceneContent isFocused={isFocused}>
|
<SceneContent isFocused={isFocused}>
|
||||||
{descriptor.render()}
|
{descriptor.render()}
|
||||||
</SceneContent>
|
</SceneContent>
|
||||||
</ResourceSavingScene>
|
</ResourceSavingScene>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScreenContainer>
|
</ScreenContainer>
|
||||||
{this.renderTabBar()}
|
{this.renderTabBar()}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaProviderCompat>
|
</SafeAreaProviderCompat>
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,18 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.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)
|
## [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
|
**Note:** Version bump only for package @react-navigation/compat
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/compat",
|
"name": "@react-navigation/compat",
|
||||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||||
"version": "5.1.9",
|
"version": "5.1.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"react": "~16.9.0",
|
"react": "~16.9.0",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
NavigationProp,
|
NavigationProp,
|
||||||
RouteProp,
|
RouteProp,
|
||||||
EventMapBase,
|
EventMapBase,
|
||||||
|
NavigationRouteContext,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import CompatScreen from './CompatScreen';
|
import CompatScreen from './CompatScreen';
|
||||||
import ScreenPropsContext from './ScreenPropsContext';
|
import ScreenPropsContext from './ScreenPropsContext';
|
||||||
@@ -67,6 +68,9 @@ export default function createCompatNavigatorFactory<
|
|||||||
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
|
const routeNames = order !== undefined ? order : Object.keys(routeConfig);
|
||||||
|
|
||||||
function Navigator({ screenProps }: { screenProps?: unknown }) {
|
function Navigator({ screenProps }: { screenProps?: unknown }) {
|
||||||
|
const parentRouteParams = React.useContext(NavigationRouteContext)
|
||||||
|
?.params;
|
||||||
|
|
||||||
const screens = React.useMemo(
|
const screens = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
routeNames.map((name) => {
|
routeNames.map((name) => {
|
||||||
@@ -135,7 +139,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
<Pair.Screen
|
<Pair.Screen
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
initialParams={initialParams}
|
initialParams={{ ...parentRouteParams, ...initialParams }}
|
||||||
options={screenOptions}
|
options={screenOptions}
|
||||||
>
|
>
|
||||||
{({ navigation, route }) => (
|
{({ navigation, route }) => (
|
||||||
@@ -148,7 +152,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
</Pair.Screen>
|
</Pair.Screen>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[screenProps]
|
[parentRouteParams, screenProps]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -163,7 +167,7 @@ export default function createCompatNavigatorFactory<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.navigationOtions = parentNavigationOptions;
|
Navigator.navigationOptions = parentNavigationOptions;
|
||||||
|
|
||||||
return Navigator;
|
return Navigator;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.3.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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/core",
|
"name": "@react-navigation/core",
|
||||||
"description": "Core utilities for building navigators",
|
"description": "Core utilities for building navigators",
|
||||||
"version": "5.3.4",
|
"version": "5.3.5",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/routers": "^5.4.0",
|
"@react-navigation/routers": "^5.4.1",
|
||||||
"escape-string-regexp": "^2.0.0",
|
"escape-string-regexp": "^2.0.0",
|
||||||
"nanoid": "^3.0.2",
|
"nanoid": "^3.0.2",
|
||||||
"query-string": "^6.12.0",
|
"query-string": "^6.12.0",
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { NavigationContainerRef, NavigationContainerProps } from './types';
|
|||||||
|
|
||||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||||
|
|
||||||
|
const DEVTOOLS_CONFIG_KEY =
|
||||||
|
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED';
|
||||||
|
|
||||||
const MISSING_CONTEXT_ERROR =
|
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.";
|
"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({
|
const { trackState, trackAction } = useDevTools({
|
||||||
enabled: false,
|
enabled:
|
||||||
|
// @ts-ignore
|
||||||
|
DEVTOOLS_CONFIG_KEY in global ? global[DEVTOOLS_CONFIG_KEY] : false,
|
||||||
name: '@react-navigation',
|
name: '@react-navigation',
|
||||||
reset,
|
reset,
|
||||||
state,
|
state,
|
||||||
|
|||||||
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;
|
||||||
@@ -3,6 +3,7 @@ export * from '@react-navigation/routers';
|
|||||||
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
export { default as BaseNavigationContainer } from './BaseNavigationContainer';
|
||||||
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
export { default as createNavigatorFactory } from './createNavigatorFactory';
|
||||||
|
|
||||||
|
export { default as NavigationHelpersContext } from './NavigationHelpersContext';
|
||||||
export { default as NavigationContext } from './NavigationContext';
|
export { default as NavigationContext } from './NavigationContext';
|
||||||
export { default as NavigationRouteContext } from './NavigationRouteContext';
|
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`.
|
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||||
*/
|
*/
|
||||||
canGoBack(): boolean;
|
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, {}>;
|
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
|
||||||
|
|
||||||
export type NavigationHelpers<
|
export type NavigationHelpers<
|
||||||
@@ -254,20 +268,6 @@ export type NavigationProp<
|
|||||||
* @param options Options object for the route.
|
* @param options Options object for the route.
|
||||||
*/
|
*/
|
||||||
setOptions(options: Partial<ScreenOptions>): void;
|
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>> &
|
} & EventConsumer<EventMap & EventMapCore<State>> &
|
||||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@react-navigation/routers';
|
} from '@react-navigation/routers';
|
||||||
import { NavigationEventEmitter } from './useEventEmitter';
|
import { NavigationEventEmitter } from './useEventEmitter';
|
||||||
import NavigationContext from './NavigationContext';
|
|
||||||
|
|
||||||
import { NavigationHelpers, NavigationProp } from './types';
|
import { NavigationHelpers, NavigationProp } from './types';
|
||||||
|
|
||||||
@@ -49,12 +48,10 @@ export default function useNavigationCache<
|
|||||||
// Cache object which holds navigation objects for each screen
|
// 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
|
// 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
|
// In reality, these deps will rarely change, if ever
|
||||||
const parentNavigation = React.useContext(NavigationContext);
|
|
||||||
|
|
||||||
const cache = React.useMemo(
|
const cache = React.useMemo(
|
||||||
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
|
() => ({ current: {} as NavigationCache<State, ScreenOptions> }),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[getState, navigation, setOptions, router, emitter, parentNavigation]
|
[getState, navigation, setOptions, router, emitter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@@ -99,8 +96,6 @@ export default function useNavigationCache<
|
|||||||
...rest,
|
...rest,
|
||||||
...helpers,
|
...helpers,
|
||||||
...emitter.create(route.key),
|
...emitter.create(route.key),
|
||||||
dangerouslyGetParent: () => parentNavigation as any,
|
|
||||||
dangerouslyGetState: getState,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
setOptions: (options: object) =>
|
setOptions: (options: object) =>
|
||||||
setOptions((o) => ({
|
setOptions((o) => ({
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ export default function useNavigationHelpers<
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
dangerouslyGetParent: () => parentNavigationHelpers as any,
|
||||||
|
dangerouslyGetState: getState,
|
||||||
} as NavigationHelpers<ParamListBase, EventMap> &
|
} as NavigationHelpers<ParamListBase, EventMap> &
|
||||||
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
|
(NavigationProp<ParamListBase, string, any, any, any> | undefined);
|
||||||
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);
|
}, [router, getState, parentNavigationHelpers, emitter.emit, onAction]);
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.5.1](https://github.com/react-navigation/react-navigation/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)
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/drawer",
|
"name": "@react-navigation/drawer",
|
||||||
"description": "Drawer navigator component with animated transitions and gesturess",
|
"description": "Drawer navigator component with animated transitions and gesturess",
|
||||||
"version": "5.5.0",
|
"version": "5.5.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.61.22",
|
"@types/react-native": "^0.61.22",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
StyleProp,
|
StyleProp,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
Platform,
|
||||||
|
TouchableWithoutFeedbackProps,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useTheme } from '@react-navigation/native';
|
import { Link, useTheme } from '@react-navigation/native';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import TouchableItem from './TouchableItem';
|
import TouchableItem from './TouchableItem';
|
||||||
|
|
||||||
@@ -26,6 +28,10 @@ type Props = {
|
|||||||
size: number;
|
size: number;
|
||||||
color: string;
|
color: string;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* URL to use for the link to the tab.
|
||||||
|
*/
|
||||||
|
to?: string;
|
||||||
/**
|
/**
|
||||||
* Whether to highlight the drawer item as active.
|
* Whether to highlight the drawer item as active.
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +66,54 @@ type Props = {
|
|||||||
style?: StyleProp<ViewStyle>;
|
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.
|
* 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,
|
icon,
|
||||||
label,
|
label,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
|
to,
|
||||||
focused = false,
|
focused = false,
|
||||||
activeTintColor = colors.primary,
|
activeTintColor = colors.primary,
|
||||||
inactiveTintColor = Color(colors.text).alpha(0.68).rgb().string(),
|
inactiveTintColor = Color(colors.text).alpha(0.68).rgb().string(),
|
||||||
@@ -94,7 +149,7 @@ export default function DrawerItem(props: Props) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
style={[styles.container, { borderRadius, backgroundColor }, style]}
|
style={[styles.container, { borderRadius, backgroundColor }, style]}
|
||||||
>
|
>
|
||||||
<TouchableItem
|
<Touchable
|
||||||
delayPressIn={0}
|
delayPressIn={0}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[styles.wrapper, { borderRadius }]}
|
style={[styles.wrapper, { borderRadius }]}
|
||||||
@@ -102,6 +157,7 @@ export default function DrawerItem(props: Props) {
|
|||||||
accessibilityComponentType="button"
|
accessibilityComponentType="button"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityStates={focused ? ['selected'] : []}
|
accessibilityStates={focused ? ['selected'] : []}
|
||||||
|
to={to}
|
||||||
>
|
>
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{iconNode}
|
{iconNode}
|
||||||
@@ -129,7 +185,7 @@ export default function DrawerItem(props: Props) {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
</TouchableItem>
|
</Touchable>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,4 +204,7 @@ const styles = StyleSheet.create({
|
|||||||
label: {
|
label: {
|
||||||
marginRight: 32,
|
marginRight: 32,
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CommonActions,
|
CommonActions,
|
||||||
DrawerActions,
|
DrawerActions,
|
||||||
DrawerNavigationState,
|
DrawerNavigationState,
|
||||||
|
useLinkBuilder,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native';
|
||||||
import DrawerItem from './DrawerItem';
|
import DrawerItem from './DrawerItem';
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,8 @@ export default function DrawerItemList({
|
|||||||
itemStyle,
|
itemStyle,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const buildLink = useLinkBuilder();
|
||||||
|
|
||||||
return (state.routes.map((route, i) => {
|
return (state.routes.map((route, i) => {
|
||||||
const focused = i === state.index;
|
const focused = i === state.index;
|
||||||
const { title, drawerLabel, drawerIcon } = descriptors[route.key].options;
|
const { title, drawerLabel, drawerIcon } = descriptors[route.key].options;
|
||||||
@@ -53,6 +56,7 @@ export default function DrawerItemList({
|
|||||||
inactiveBackgroundColor={inactiveBackgroundColor}
|
inactiveBackgroundColor={inactiveBackgroundColor}
|
||||||
labelStyle={labelStyle}
|
labelStyle={labelStyle}
|
||||||
style={itemStyle}
|
style={itemStyle}
|
||||||
|
to={buildLink(route.name, route.params)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.dispatch({
|
navigation.dispatch({
|
||||||
...(focused
|
...(focused
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
GestureHandlerRootView,
|
GestureHandlerRootView,
|
||||||
} from 'react-native-gesture-handler';
|
} from 'react-native-gesture-handler';
|
||||||
import {
|
import {
|
||||||
|
NavigationHelpersContext,
|
||||||
DrawerNavigationState,
|
DrawerNavigationState,
|
||||||
DrawerActions,
|
DrawerActions,
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -201,59 +202,61 @@ export default function DrawerView({
|
|||||||
const { gestureEnabled, swipeEnabled } = descriptors[activeKey].options;
|
const { gestureEnabled, swipeEnabled } = descriptors[activeKey].options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerWrapper style={styles.content}>
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
<SafeAreaProviderCompat>
|
<GestureHandlerWrapper style={styles.content}>
|
||||||
<DrawerGestureContext.Provider value={drawerGestureRef}>
|
<SafeAreaProviderCompat>
|
||||||
<DrawerOpenContext.Provider value={isDrawerOpen}>
|
<DrawerGestureContext.Provider value={drawerGestureRef}>
|
||||||
<Drawer
|
<DrawerOpenContext.Provider value={isDrawerOpen}>
|
||||||
open={isDrawerOpen}
|
<Drawer
|
||||||
gestureEnabled={gestureEnabled}
|
open={isDrawerOpen}
|
||||||
swipeEnabled={swipeEnabled}
|
gestureEnabled={gestureEnabled}
|
||||||
onOpen={handleDrawerOpen}
|
swipeEnabled={swipeEnabled}
|
||||||
onClose={handleDrawerClose}
|
onOpen={handleDrawerOpen}
|
||||||
onGestureRef={(ref) => {
|
onClose={handleDrawerClose}
|
||||||
// @ts-ignore
|
onGestureRef={(ref) => {
|
||||||
drawerGestureRef.current = ref;
|
// @ts-ignore
|
||||||
}}
|
drawerGestureRef.current = ref;
|
||||||
gestureHandlerProps={gestureHandlerProps}
|
}}
|
||||||
drawerType={drawerType}
|
gestureHandlerProps={gestureHandlerProps}
|
||||||
drawerPosition={drawerPosition}
|
drawerType={drawerType}
|
||||||
sceneContainerStyle={[
|
drawerPosition={drawerPosition}
|
||||||
{ backgroundColor: colors.background },
|
sceneContainerStyle={[
|
||||||
sceneContainerStyle,
|
{ backgroundColor: colors.background },
|
||||||
]}
|
sceneContainerStyle,
|
||||||
drawerStyle={[
|
]}
|
||||||
{
|
drawerStyle={[
|
||||||
width: getDefaultDrawerWidth(dimensions),
|
{
|
||||||
backgroundColor: colors.card,
|
width: getDefaultDrawerWidth(dimensions),
|
||||||
},
|
backgroundColor: colors.card,
|
||||||
drawerType === 'permanent' &&
|
},
|
||||||
(drawerPosition === 'left'
|
drawerType === 'permanent' &&
|
||||||
? {
|
(drawerPosition === 'left'
|
||||||
borderRightColor: colors.border,
|
? {
|
||||||
borderRightWidth: StyleSheet.hairlineWidth,
|
borderRightColor: colors.border,
|
||||||
}
|
borderRightWidth: StyleSheet.hairlineWidth,
|
||||||
: {
|
}
|
||||||
borderLeftColor: colors.border,
|
: {
|
||||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
borderLeftColor: colors.border,
|
||||||
}),
|
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||||
drawerStyle,
|
}),
|
||||||
]}
|
drawerStyle,
|
||||||
overlayStyle={{ backgroundColor: overlayColor }}
|
]}
|
||||||
swipeEdgeWidth={edgeWidth}
|
overlayStyle={{ backgroundColor: overlayColor }}
|
||||||
swipeDistanceThreshold={minSwipeDistance}
|
swipeEdgeWidth={edgeWidth}
|
||||||
hideStatusBar={hideStatusBar}
|
swipeDistanceThreshold={minSwipeDistance}
|
||||||
statusBarAnimation={statusBarAnimation}
|
hideStatusBar={hideStatusBar}
|
||||||
renderDrawerContent={renderNavigationView}
|
statusBarAnimation={statusBarAnimation}
|
||||||
renderSceneContent={renderContent}
|
renderDrawerContent={renderNavigationView}
|
||||||
keyboardDismissMode={keyboardDismissMode}
|
renderSceneContent={renderContent}
|
||||||
drawerPostion={drawerPosition}
|
keyboardDismissMode={keyboardDismissMode}
|
||||||
dimensions={dimensions}
|
drawerPostion={drawerPosition}
|
||||||
/>
|
dimensions={dimensions}
|
||||||
</DrawerOpenContext.Provider>
|
/>
|
||||||
</DrawerGestureContext.Provider>
|
</DrawerOpenContext.Provider>
|
||||||
</SafeAreaProviderCompat>
|
</DrawerGestureContext.Provider>
|
||||||
</GestureHandlerWrapper>
|
</SafeAreaProviderCompat>
|
||||||
|
</GestureHandlerWrapper>
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.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)
|
## [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
|
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-bottom-tabs",
|
"name": "@react-navigation/material-bottom-tabs",
|
||||||
"description": "Integration for bottom navigation component from react-native-paper",
|
"description": "Integration for bottom navigation component from react-native-paper",
|
||||||
"version": "5.1.9",
|
"version": "5.1.10",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.61.22",
|
"@types/react-native": "^0.61.22",
|
||||||
"@types/react-native-vector-icons": "^6.4.5",
|
"@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 { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
|
||||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import {
|
import {
|
||||||
|
NavigationHelpersContext,
|
||||||
Route,
|
Route,
|
||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
TabActions,
|
TabActions,
|
||||||
@@ -45,66 +46,68 @@ export default function MaterialBottomTabView({
|
|||||||
}, [colors, dark]);
|
}, [colors, dark]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomNavigation
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
{...rest}
|
<BottomNavigation
|
||||||
theme={theme}
|
{...rest}
|
||||||
navigationState={state}
|
theme={theme}
|
||||||
onIndexChange={(index: number) =>
|
navigationState={state}
|
||||||
navigation.dispatch({
|
onIndexChange={(index: number) =>
|
||||||
...TabActions.jumpTo(state.routes[index].name),
|
navigation.dispatch({
|
||||||
target: state.key,
|
...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
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
renderScene={({ route }) => descriptors[route.key].render()}
|
||||||
|
renderIcon={({ route, focused, color }) => {
|
||||||
|
const { options } = descriptors[route.key];
|
||||||
|
|
||||||
if (typeof options.tabBarIcon === 'function') {
|
if (typeof options.tabBarIcon === 'string') {
|
||||||
return options.tabBarIcon({ focused, color });
|
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;
|
if (event.defaultPrevented) {
|
||||||
}}
|
preventDefault();
|
||||||
getLabelText={({ route }: Scene) => {
|
}
|
||||||
const { options } = descriptors[route.key];
|
}}
|
||||||
|
/>
|
||||||
return options.tabBarLabel !== undefined
|
</NavigationHelpersContext.Provider>
|
||||||
? 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();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.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)
|
## [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
|
**Note:** Version bump only for package @react-navigation/material-top-tabs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/material-top-tabs",
|
"name": "@react-navigation/material-top-tabs",
|
||||||
"description": "Integration for the animated tab view component from react-native-tab-view",
|
"description": "Integration for the animated tab view component from react-native-tab-view",
|
||||||
"version": "5.1.9",
|
"version": "5.1.10",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.61.22",
|
"@types/react-native": "^0.61.22",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { TabView, SceneRendererProps } from 'react-native-tab-view';
|
import { TabView, SceneRendererProps } from 'react-native-tab-view';
|
||||||
import {
|
import {
|
||||||
|
NavigationHelpersContext,
|
||||||
TabNavigationState,
|
TabNavigationState,
|
||||||
TabActions,
|
TabActions,
|
||||||
useTheme,
|
useTheme,
|
||||||
@@ -45,25 +46,27 @@ export default function MaterialTopTabView({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabView
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
{...rest}
|
<TabView
|
||||||
onIndexChange={(index) =>
|
{...rest}
|
||||||
navigation.dispatch({
|
onIndexChange={(index) =>
|
||||||
...TabActions.jumpTo(state.routes[index].name),
|
navigation.dispatch({
|
||||||
target: state.key,
|
...TabActions.jumpTo(state.routes[index].name),
|
||||||
})
|
target: state.key,
|
||||||
}
|
})
|
||||||
renderScene={({ route }) => descriptors[route.key].render()}
|
}
|
||||||
navigationState={state}
|
renderScene={({ route }) => descriptors[route.key].render()}
|
||||||
renderTabBar={renderTabBar}
|
navigationState={state}
|
||||||
renderPager={pager}
|
renderTabBar={renderTabBar}
|
||||||
renderLazyPlaceholder={lazyPlaceholder}
|
renderPager={pager}
|
||||||
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
|
renderLazyPlaceholder={lazyPlaceholder}
|
||||||
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
|
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
|
||||||
sceneContainerStyle={[
|
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}
|
||||||
{ backgroundColor: colors.background },
|
sceneContainerStyle={[
|
||||||
sceneContainerStyle,
|
{ backgroundColor: colors.background },
|
||||||
]}
|
sceneContainerStyle,
|
||||||
/>
|
]}
|
||||||
|
/>
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.1.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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/native",
|
"name": "@react-navigation/native",
|
||||||
"description": "React Native integration for React Navigation",
|
"description": "React Native integration for React Navigation",
|
||||||
"version": "5.1.6",
|
"version": "5.1.7",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native",
|
"react-native",
|
||||||
"react-navigation",
|
"react-navigation",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"clean": "del lib"
|
"clean": "del lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^5.3.4"
|
"@react-navigation/core": "^5.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
|
|||||||
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
@@ -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';
|
} from '@react-navigation/core';
|
||||||
import ThemeProvider from './theming/ThemeProvider';
|
import ThemeProvider from './theming/ThemeProvider';
|
||||||
import DefaultTheme from './theming/DefaultTheme';
|
import DefaultTheme from './theming/DefaultTheme';
|
||||||
|
import LinkingContext from './LinkingContext';
|
||||||
|
import useThenable from './useThenable';
|
||||||
|
import useLinking from './useLinking';
|
||||||
import useBackButton from './useBackButton';
|
import useBackButton from './useBackButton';
|
||||||
import { Theme } from './types';
|
import { Theme, LinkingOptions } from './types';
|
||||||
|
|
||||||
type Props = NavigationContainerProps & {
|
type Props = NavigationContainerProps & {
|
||||||
theme?: Theme;
|
theme?: Theme;
|
||||||
|
linking?: LinkingOptions;
|
||||||
|
fallback?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container component which holds the navigation state
|
* Container component which holds the navigation state designed for React Native apps.
|
||||||
* designed for mobile apps.
|
|
||||||
* This should be rendered at the root wrapping the whole app.
|
* 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.onStateChange Callback which is called with the latest navigation state when it changes.
|
||||||
* @param props.theme Theme object for the navigators.
|
* @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.children Child elements to render the content.
|
||||||
* @param props.ref Ref object which refers to the navigation object containing helper methods.
|
* @param props.ref Ref object which refers to the navigation object containing helper methods.
|
||||||
*/
|
*/
|
||||||
const NavigationContainer = React.forwardRef(function NavigationContainer(
|
const NavigationContainer = React.forwardRef(function NavigationContainer(
|
||||||
{ theme = DefaultTheme, ...rest }: Props,
|
{ theme = DefaultTheme, linking, fallback = null, ...rest }: Props,
|
||||||
ref?: React.Ref<NavigationContainerRef | null>
|
ref?: React.Ref<NavigationContainerRef | null>
|
||||||
) {
|
) {
|
||||||
|
const isLinkingEnabled = linking ? linking.enabled !== false : false;
|
||||||
|
|
||||||
const refContainer = React.useRef<NavigationContainerRef>(null);
|
const refContainer = React.useRef<NavigationContainerRef>(null);
|
||||||
|
|
||||||
useBackButton(refContainer);
|
useBackButton(refContainer);
|
||||||
|
|
||||||
|
const { getInitialState } = useLinking(refContainer, {
|
||||||
|
enabled: isLinkingEnabled,
|
||||||
|
prefixes: [],
|
||||||
|
...linking,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isReady, initialState = rest.initialState] = useThenable(
|
||||||
|
getInitialState
|
||||||
|
);
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => refContainer.current);
|
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 (
|
return (
|
||||||
<ThemeProvider value={theme}>
|
<LinkingContext.Provider value={linkingContext}>
|
||||||
<BaseNavigationContainer {...rest} ref={refContainer} />
|
<ThemeProvider value={theme}>
|
||||||
</ThemeProvider>
|
<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 * from '@react-navigation/core';
|
||||||
|
|
||||||
export { default as NavigationContainer } from './NavigationContainer';
|
export { default as NavigationContainer } from './NavigationContainer';
|
||||||
export { default as NavigationNativeContainer } from './NavigationNativeContainer';
|
|
||||||
|
|
||||||
export { default as useBackButton } from './useBackButton';
|
export { default as useBackButton } from './useBackButton';
|
||||||
export { default as useLinking } from './useLinking';
|
|
||||||
export { default as useScrollToTop } from './useScrollToTop';
|
export { default as useScrollToTop } from './useScrollToTop';
|
||||||
|
|
||||||
export { default as DefaultTheme } from './theming/DefaultTheme';
|
export { default as DefaultTheme } from './theming/DefaultTheme';
|
||||||
export { default as DarkTheme } from './theming/DarkTheme';
|
export { default as DarkTheme } from './theming/DarkTheme';
|
||||||
export { default as ThemeProvider } from './theming/ThemeProvider';
|
export { default as ThemeProvider } from './theming/ThemeProvider';
|
||||||
export { default as useTheme } from './theming/useTheme';
|
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 = {
|
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.
|
* The prefixes are stripped from the URL before parsing them.
|
||||||
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)
|
* Usually they are the `scheme` + `host` (e.g. `myapp://chat?user=jane`)
|
||||||
|
|||||||
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
@@ -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
@@ -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(
|
export default function useLinking(
|
||||||
ref: React.RefObject<NavigationContainerRef>,
|
ref: React.RefObject<NavigationContainerRef>,
|
||||||
{
|
{
|
||||||
|
enabled,
|
||||||
prefixes,
|
prefixes,
|
||||||
config,
|
config,
|
||||||
getStateFromPath = getStateFromPathDefault,
|
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
|
// 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`
|
// 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
|
// 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 prefixesRef = React.useRef(prefixes);
|
||||||
const configRef = React.useRef(config);
|
const configRef = React.useRef(config);
|
||||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
enabledRef.current = enabled;
|
||||||
prefixesRef.current = prefixes;
|
prefixesRef.current = prefixes;
|
||||||
configRef.current = config;
|
configRef.current = config;
|
||||||
getStateFromPathRef.current = getStateFromPath;
|
getStateFromPathRef.current = getStateFromPath;
|
||||||
}, [config, getStateFromPath, prefixes]);
|
}, [config, enabled, getStateFromPath, prefixes]);
|
||||||
|
|
||||||
const extractPathFromURL = React.useCallback((url: string) => {
|
const extractPathFromURL = React.useCallback((url: string) => {
|
||||||
for (const prefix of prefixesRef.current) {
|
for (const prefix of prefixesRef.current) {
|
||||||
@@ -58,7 +61,19 @@ export default function useLinking(
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getInitialState = React.useCallback(async () => {
|
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;
|
const path = url ? extractPathFromURL(url) : null;
|
||||||
|
|
||||||
if (path) {
|
if (path) {
|
||||||
@@ -70,6 +85,10 @@ export default function useLinking(
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const listener = ({ url }: { url: string }) => {
|
const listener = ({ url }: { url: string }) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const path = extractPathFromURL(url);
|
const path = extractPathFromURL(url);
|
||||||
const navigation = ref.current;
|
const navigation = ref.current;
|
||||||
|
|
||||||
@@ -91,7 +110,7 @@ export default function useLinking(
|
|||||||
Linking.addEventListener('url', listener);
|
Linking.addEventListener('url', listener);
|
||||||
|
|
||||||
return () => Linking.removeEventListener('url', listener);
|
return () => Linking.removeEventListener('url', listener);
|
||||||
}, [extractPathFromURL, ref]);
|
}, [enabled, extractPathFromURL, ref]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getInitialState,
|
getInitialState,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
} from '@react-navigation/core';
|
} from '@react-navigation/core';
|
||||||
import { LinkingOptions } from './types';
|
import { LinkingOptions } from './types';
|
||||||
|
|
||||||
|
type ResultState = ReturnType<typeof getStateFromPathDefault>;
|
||||||
|
|
||||||
const getStateLength = (state: NavigationState) => {
|
const getStateLength = (state: NavigationState) => {
|
||||||
let length = 0;
|
let length = 0;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ let isUsingLinking = false;
|
|||||||
export default function useLinking(
|
export default function useLinking(
|
||||||
ref: React.RefObject<NavigationContainerRef>,
|
ref: React.RefObject<NavigationContainerRef>,
|
||||||
{
|
{
|
||||||
|
enabled,
|
||||||
config,
|
config,
|
||||||
getStateFromPath = getStateFromPathDefault,
|
getStateFromPath = getStateFromPathDefault,
|
||||||
getPathFromState = getPathFromStateDefault,
|
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
|
// 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`
|
// 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
|
// 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 configRef = React.useRef(config);
|
||||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||||
const getPathFromStateRef = React.useRef(getPathFromState);
|
const getPathFromStateRef = React.useRef(getPathFromState);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
enabledRef.current = enabled;
|
||||||
configRef.current = config;
|
configRef.current = config;
|
||||||
getStateFromPathRef.current = getStateFromPath;
|
getStateFromPathRef.current = getStateFromPath;
|
||||||
getPathFromStateRef.current = getPathFromState;
|
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(() => {
|
||||||
const getInitialState = React.useCallback(async () => {
|
let value: ResultState | undefined;
|
||||||
const path = location.pathname + location.search;
|
|
||||||
|
|
||||||
if (path) {
|
if (enabledRef.current) {
|
||||||
return getStateFromPathRef.current(path, configRef.current);
|
const path = location.pathname + location.search;
|
||||||
} else {
|
|
||||||
return undefined;
|
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);
|
const previousStateLengthRef = React.useRef<number | undefined>(undefined);
|
||||||
@@ -92,10 +104,10 @@ export default function useLinking(
|
|||||||
const numberOfIndicesAhead = React.useRef(0);
|
const numberOfIndicesAhead = React.useRef(0);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
window.addEventListener('popstate', () => {
|
const onPopState = () => {
|
||||||
const navigation = ref.current;
|
const navigation = ref.current;
|
||||||
|
|
||||||
if (!navigation) {
|
if (!navigation || !enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,10 +181,18 @@ export default function useLinking(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}, [ref]);
|
|
||||||
|
window.addEventListener('popstate', onPopState);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('popstate', onPopState);
|
||||||
|
}, [enabled, ref]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ref.current && previousStateLengthRef.current === undefined) {
|
if (ref.current && previousStateLengthRef.current === undefined) {
|
||||||
previousStateLengthRef.current = getStateLength(
|
previousStateLengthRef.current = getStateLength(
|
||||||
ref.current.getRootState()
|
ref.current.getRootState()
|
||||||
|
|||||||
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,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.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)
|
# [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/routers",
|
"name": "@react-navigation/routers",
|
||||||
"description": "Routers to help build custom navigators",
|
"description": "Routers to help build custom navigators",
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
"react-native",
|
"react-native",
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ export default function DrawerRouter({
|
|||||||
getStateForRouteFocus(state, key) {
|
getStateForRouteFocus(state, key) {
|
||||||
const result = router.getStateForRouteFocus(state, key);
|
const result = router.getStateForRouteFocus(state, key);
|
||||||
|
|
||||||
|
if (openByDefault) {
|
||||||
|
return openDrawer(result);
|
||||||
|
}
|
||||||
|
|
||||||
return closeDrawer(result);
|
return closeDrawer(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,161 @@ it("doesn't rehydrate state if it's not stale", () => {
|
|||||||
).toBe(state);
|
).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', () => {
|
it('gets state on route names change', () => {
|
||||||
const router = TabRouter({});
|
const router = TabRouter({});
|
||||||
|
|
||||||
@@ -254,6 +409,38 @@ it('gets state on route names change', () => {
|
|||||||
stale: false,
|
stale: false,
|
||||||
type: 'tab',
|
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', () => {
|
it('preserves focused route on route names change', () => {
|
||||||
|
|||||||
@@ -3,6 +3,28 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [5.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)
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@react-navigation/stack",
|
"name": "@react-navigation/stack",
|
||||||
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
|
||||||
"version": "5.2.11",
|
"version": "5.2.14",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react-native-component",
|
"react-native-component",
|
||||||
"react-component",
|
"react-component",
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/bob": "^0.10.0",
|
"@react-native-community/bob": "^0.10.0",
|
||||||
"@react-native-community/masked-view": "^0.1.7",
|
"@react-native-community/masked-view": "^0.1.7",
|
||||||
"@react-navigation/native": "^5.1.6",
|
"@react-navigation/native": "^5.1.7",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-native": "^0.61.22",
|
"@types/react-native": "^0.61.22",
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { UIManager } from 'react-native';
|
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;
|
children: React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let RNCMaskedView: MaskedViewType | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
RNCMaskedView = require('@react-native-community/masked-view').default;
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
const isMaskedViewAvailable =
|
const isMaskedViewAvailable =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
UIManager.getViewManagerConfig('RNCMaskedView') != null;
|
UIManager.getViewManagerConfig('RNCMaskedView') != null;
|
||||||
|
|
||||||
export default function MaskedView({ children, ...rest }: Props) {
|
export default function MaskedView({ children, ...rest }: Props) {
|
||||||
if (isMaskedViewAvailable) {
|
if (isMaskedViewAvailable && RNCMaskedView) {
|
||||||
return <RNCMaskedView {...rest}>{children}</RNCMaskedView>;
|
return <RNCMaskedView {...rest}>{children}</RNCMaskedView>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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) >
|
getInvertedMultiplier(gestureDirection) >
|
||||||
distance / 2
|
distance / 2
|
||||||
? velocity !== 0 || translation !== 0
|
? velocity !== 0 || translation !== 0
|
||||||
: false;
|
: this.props.closing;
|
||||||
|
|
||||||
this.animate({ closing, velocity });
|
this.animate({ closing, velocity });
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
View,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
LayoutChangeEvent,
|
LayoutChangeEvent,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Platform,
|
Platform,
|
||||||
ViewProps,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { EdgeInsets } from 'react-native-safe-area-context';
|
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 { Route, StackNavigationState } from '@react-navigation/native';
|
||||||
|
|
||||||
|
import { MaybeScreenContainer, MaybeScreen } from '../Screens';
|
||||||
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
|
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
|
||||||
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||||
import CardContainer from './CardContainer';
|
import CardContainer from './CardContainer';
|
||||||
@@ -75,67 +72,6 @@ type State = {
|
|||||||
|
|
||||||
const EPSILON = 0.01;
|
const EPSILON = 0.01;
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
const MaybeScreenContainer = ({
|
|
||||||
enabled,
|
|
||||||
...rest
|
|
||||||
}: ViewProps & {
|
|
||||||
enabled: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
if (enabled && Platform.OS !== 'web' && screensEnabled()) {
|
|
||||||
return <ScreenContainer {...rest} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <View {...rest} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 && screensEnabled()) {
|
|
||||||
// @ts-ignore
|
|
||||||
return <Screen active={active} {...rest} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <View {...rest} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
|
const FALLBACK_DESCRIPTOR = Object.freeze({ options: {} });
|
||||||
|
|
||||||
const getHeaderHeights = (
|
const getHeaderHeights = (
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { View, Platform, StyleSheet } from 'react-native';
|
|||||||
import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context';
|
import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import {
|
import {
|
||||||
|
NavigationHelpersContext,
|
||||||
StackActions,
|
StackActions,
|
||||||
StackNavigationState,
|
StackNavigationState,
|
||||||
Route,
|
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
|
// 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
|
// 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 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
|
// 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
|
(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
|
// The previous focused route isn't present in state, we treat this as a replace
|
||||||
|
|
||||||
openingRouteKeys = openingRouteKeys.filter(
|
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
|
// The previously focused route was removed, we treat this as a pop
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -328,24 +329,38 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
||||||
this.setState((state) => ({
|
const { state, navigation } = this.props;
|
||||||
routes: state.replacingRouteKeys.length
|
|
||||||
? state.routes.filter((r) => !state.replacingRouteKeys.includes(r.key))
|
if (
|
||||||
: state.routes,
|
this.state.replacingRouteKeys.every((key) => key !== route.key) &&
|
||||||
openingRouteKeys: state.openingRouteKeys.filter(
|
state.routeNames.includes(route.name) &&
|
||||||
(key) => key !== route.key
|
!state.routes.some((r) => r.key === route.key)
|
||||||
),
|
) {
|
||||||
closingRouteKeys: state.closingRouteKeys.filter(
|
// If route isn't present in current state, assume that a close animation was cancelled
|
||||||
(key) => key !== route.key
|
// So we need to add this route back to the state
|
||||||
),
|
navigation.navigate(route);
|
||||||
replacingRouteKeys: [],
|
} 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> }) => {
|
private handleCloseRoute = ({ route }: { route: Route<string> }) => {
|
||||||
const { state, navigation } = this.props;
|
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
|
// If a route exists in state, trigger a pop
|
||||||
// This will happen in when the route was closed from the card component
|
// This will happen in when the route was closed from the card component
|
||||||
// e.g. When the close animation triggered from a gesture ends
|
// e.g. When the close animation triggered from a gesture ends
|
||||||
@@ -391,7 +406,6 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
navigation,
|
navigation,
|
||||||
keyboardHandlingEnabled,
|
keyboardHandlingEnabled,
|
||||||
mode = 'card',
|
mode = 'card',
|
||||||
@@ -409,38 +423,40 @@ export default class StackView extends React.Component<Props, State> {
|
|||||||
mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen';
|
mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerWrapper style={styles.container}>
|
<NavigationHelpersContext.Provider value={navigation}>
|
||||||
<SafeAreaProviderCompat>
|
<GestureHandlerWrapper style={styles.container}>
|
||||||
<SafeAreaConsumer>
|
<SafeAreaProviderCompat>
|
||||||
{(insets) => (
|
<SafeAreaConsumer>
|
||||||
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
|
{(insets) => (
|
||||||
{(props) => (
|
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
|
||||||
<CardStack
|
{(props) => (
|
||||||
mode={mode}
|
<CardStack
|
||||||
insets={insets as EdgeInsets}
|
mode={mode}
|
||||||
getPreviousRoute={this.getPreviousRoute}
|
insets={insets as EdgeInsets}
|
||||||
getGesturesEnabled={this.getGesturesEnabled}
|
getPreviousRoute={this.getPreviousRoute}
|
||||||
routes={routes}
|
getGesturesEnabled={this.getGesturesEnabled}
|
||||||
openingRouteKeys={openingRouteKeys}
|
routes={routes}
|
||||||
closingRouteKeys={closingRouteKeys}
|
openingRouteKeys={openingRouteKeys}
|
||||||
onOpenRoute={this.handleOpenRoute}
|
closingRouteKeys={closingRouteKeys}
|
||||||
onCloseRoute={this.handleCloseRoute}
|
onOpenRoute={this.handleOpenRoute}
|
||||||
onTransitionStart={this.handleTransitionStart}
|
onCloseRoute={this.handleCloseRoute}
|
||||||
onTransitionEnd={this.handleTransitionEnd}
|
onTransitionStart={this.handleTransitionStart}
|
||||||
renderHeader={this.renderHeader}
|
onTransitionEnd={this.handleTransitionEnd}
|
||||||
renderScene={this.renderScene}
|
renderHeader={this.renderHeader}
|
||||||
headerMode={headerMode}
|
renderScene={this.renderScene}
|
||||||
state={state}
|
headerMode={headerMode}
|
||||||
descriptors={descriptors}
|
state={state}
|
||||||
{...rest}
|
descriptors={descriptors}
|
||||||
{...props}
|
{...rest}
|
||||||
/>
|
{...props}
|
||||||
)}
|
/>
|
||||||
</KeyboardManager>
|
)}
|
||||||
)}
|
</KeyboardManager>
|
||||||
</SafeAreaConsumer>
|
)}
|
||||||
</SafeAreaProviderCompat>
|
</SafeAreaConsumer>
|
||||||
</GestureHandlerWrapper>
|
</SafeAreaProviderCompat>
|
||||||
|
</GestureHandlerWrapper>
|
||||||
|
</NavigationHelpersContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
371
packages/web-stack/src/views/Stack/WebStackView.tsx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
StackActions,
|
||||||
|
StackNavigationState,
|
||||||
|
Route,
|
||||||
|
} from '@react-navigation/native';
|
||||||
|
|
||||||
|
import CardStack from './CardStack';
|
||||||
|
import {
|
||||||
|
WebStackNavigationHelpers,
|
||||||
|
WebStackNavigationConfig,
|
||||||
|
WebStackDescriptorMap,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
type Props = WebStackNavigationConfig & {
|
||||||
|
state: StackNavigationState;
|
||||||
|
navigation: WebStackNavigationHelpers;
|
||||||
|
descriptors: WebStackDescriptorMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
// Local copy of the routes which are actually rendered
|
||||||
|
routes: Route<string>[];
|
||||||
|
// Previous routes, to compare whether routes have changed or not
|
||||||
|
previousRoutes: Route<string>[];
|
||||||
|
// Previous descriptors, to compare whether descriptors have changed or not
|
||||||
|
previousDescriptors: WebStackDescriptorMap;
|
||||||
|
// List of routes being opened, we need to animate pushing of these new routes
|
||||||
|
openingRouteKeys: string[];
|
||||||
|
// List of routes being closed, we need to animate popping of these routes
|
||||||
|
closingRouteKeys: string[];
|
||||||
|
// List of routes being replaced, we need to keep a copy until the new route animates in
|
||||||
|
replacingRouteKeys: string[];
|
||||||
|
// Since the local routes can vary from the routes from props, we need to keep the descriptors for old routes
|
||||||
|
// Otherwise we won't be able to access the options for routes that were removed
|
||||||
|
descriptors: WebStackDescriptorMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two arrays with primitive values as the content.
|
||||||
|
* We need to make sure that both values and order match.
|
||||||
|
*/
|
||||||
|
const isArrayEqual = (a: any[], b: any[]) =>
|
||||||
|
a.length === b.length && a.every((it, index) => it === b[index]);
|
||||||
|
|
||||||
|
export default class StackView extends React.Component<Props, State> {
|
||||||
|
static getDerivedStateFromProps(
|
||||||
|
props: Readonly<Props>,
|
||||||
|
state: Readonly<State>
|
||||||
|
) {
|
||||||
|
// If there was no change in routes, we don't need to compute anything
|
||||||
|
if (
|
||||||
|
(props.state.routes === state.previousRoutes ||
|
||||||
|
isArrayEqual(
|
||||||
|
props.state.routes.map((r) => r.key),
|
||||||
|
state.previousRoutes.map((r) => r.key)
|
||||||
|
)) &&
|
||||||
|
state.routes.length
|
||||||
|
) {
|
||||||
|
let routes = state.routes;
|
||||||
|
let previousRoutes = state.previousRoutes;
|
||||||
|
let descriptors = props.descriptors;
|
||||||
|
let previousDescriptors = state.previousDescriptors;
|
||||||
|
|
||||||
|
if (props.descriptors !== state.previousDescriptors) {
|
||||||
|
descriptors = state.routes.reduce<WebStackDescriptorMap>(
|
||||||
|
(acc, route) => {
|
||||||
|
acc[route.key] =
|
||||||
|
props.descriptors[route.key] || state.descriptors[route.key];
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
previousDescriptors = props.descriptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.state.routes !== state.previousRoutes) {
|
||||||
|
// if any route objects have changed, we should update them
|
||||||
|
const map = props.state.routes.reduce<Record<string, Route<string>>>(
|
||||||
|
(acc, route) => {
|
||||||
|
acc[route.key] = route;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
routes = state.routes.map((route) => map[route.key] || route);
|
||||||
|
previousRoutes = props.state.routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
routes,
|
||||||
|
previousRoutes,
|
||||||
|
descriptors,
|
||||||
|
previousDescriptors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we determine which routes were added or removed to animate them
|
||||||
|
// We keep a copy of the route being removed in local state to be able to animate it
|
||||||
|
|
||||||
|
let routes =
|
||||||
|
props.state.index < props.state.routes.length - 1
|
||||||
|
? // Remove any extra routes from the state
|
||||||
|
// The last visible route should be the focused route, i.e. at current index
|
||||||
|
props.state.routes.slice(0, props.state.index + 1)
|
||||||
|
: props.state.routes;
|
||||||
|
|
||||||
|
// Now we need to determine which routes were added and removed
|
||||||
|
let {
|
||||||
|
openingRouteKeys,
|
||||||
|
closingRouteKeys,
|
||||||
|
replacingRouteKeys,
|
||||||
|
previousRoutes,
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const previousFocusedRoute = previousRoutes[previousRoutes.length - 1] as
|
||||||
|
| Route<string>
|
||||||
|
| undefined;
|
||||||
|
const nextFocusedRoute = routes[routes.length - 1];
|
||||||
|
|
||||||
|
const isAnimationEnabled = (key: string) => {
|
||||||
|
const descriptor = props.descriptors[key] || state.descriptors[key];
|
||||||
|
|
||||||
|
return descriptor ? descriptor.options.animationEnabled !== false : true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnimationTypeForReplace = (key: string) => {
|
||||||
|
const descriptor = props.descriptors[key] || state.descriptors[key];
|
||||||
|
|
||||||
|
return descriptor.options.animationTypeForReplace ?? 'push';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousFocusedRoute &&
|
||||||
|
previousFocusedRoute.key !== nextFocusedRoute.key
|
||||||
|
) {
|
||||||
|
// 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.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
|
||||||
|
|
||||||
|
if (
|
||||||
|
isAnimationEnabled(nextFocusedRoute.key) &&
|
||||||
|
!openingRouteKeys.includes(nextFocusedRoute.key)
|
||||||
|
) {
|
||||||
|
// In this case, we need to animate pushing the focused route
|
||||||
|
// We don't care about animating any other added routes because they won't be visible
|
||||||
|
openingRouteKeys = [...openingRouteKeys, nextFocusedRoute.key];
|
||||||
|
|
||||||
|
closingRouteKeys = closingRouteKeys.filter(
|
||||||
|
(key) => key !== nextFocusedRoute.key
|
||||||
|
);
|
||||||
|
replacingRouteKeys = replacingRouteKeys.filter(
|
||||||
|
(key) => key !== nextFocusedRoute.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(
|
||||||
|
(key) => key !== previousFocusedRoute.key
|
||||||
|
);
|
||||||
|
|
||||||
|
if (getAnimationTypeForReplace(nextFocusedRoute.key) === 'pop') {
|
||||||
|
closingRouteKeys = [
|
||||||
|
...closingRouteKeys,
|
||||||
|
previousFocusedRoute.key,
|
||||||
|
];
|
||||||
|
|
||||||
|
// By default, new routes have a push animation, so we add it to `openingRouteKeys` before
|
||||||
|
// But since user configured it to animate the old screen like a pop, we need to add this without animation
|
||||||
|
// So remove it from `openingRouteKeys` which will remove the animation
|
||||||
|
openingRouteKeys = openingRouteKeys.filter(
|
||||||
|
(key) => key !== nextFocusedRoute.key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the route being removed at the end to animate it out
|
||||||
|
routes = [...routes, previousFocusedRoute];
|
||||||
|
} else {
|
||||||
|
replacingRouteKeys = [
|
||||||
|
...replacingRouteKeys,
|
||||||
|
previousFocusedRoute.key,
|
||||||
|
];
|
||||||
|
|
||||||
|
closingRouteKeys = closingRouteKeys.filter(
|
||||||
|
(key) => key !== previousFocusedRoute.key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the old route in the state because it's visible under the new route, and removing it will feel abrupt
|
||||||
|
// We need to insert it just before the focused one (the route being pushed)
|
||||||
|
// After the push animation is completed, routes being replaced will be removed completely
|
||||||
|
routes = routes.slice();
|
||||||
|
routes.splice(routes.length - 1, 0, previousFocusedRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!routes.some((r) => r.key === previousFocusedRoute.key)) {
|
||||||
|
// The previously focused route was removed, we treat this as a pop
|
||||||
|
|
||||||
|
if (
|
||||||
|
isAnimationEnabled(previousFocusedRoute.key) &&
|
||||||
|
!closingRouteKeys.includes(previousFocusedRoute.key)
|
||||||
|
) {
|
||||||
|
closingRouteKeys = [...closingRouteKeys, previousFocusedRoute.key];
|
||||||
|
|
||||||
|
// Sometimes a route can be closed before the opening animation finishes
|
||||||
|
// So we also need to remove it from the opening list
|
||||||
|
openingRouteKeys = openingRouteKeys.filter(
|
||||||
|
(key) => key !== previousFocusedRoute.key
|
||||||
|
);
|
||||||
|
replacingRouteKeys = replacingRouteKeys.filter(
|
||||||
|
(key) => key !== previousFocusedRoute.key
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep a copy of route being removed in the state to be able to animate it
|
||||||
|
routes = [...routes, previousFocusedRoute];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Looks like some routes were re-arranged and no focused routes were added/removed
|
||||||
|
// i.e. the currently focused route already existed and the previously focused route still exists
|
||||||
|
// We don't know how to animate this
|
||||||
|
}
|
||||||
|
} else if (replacingRouteKeys.length || closingRouteKeys.length) {
|
||||||
|
// Keep the routes we are closing or replacing if animation is enabled for them
|
||||||
|
routes = routes.slice();
|
||||||
|
routes.splice(
|
||||||
|
routes.length - 1,
|
||||||
|
0,
|
||||||
|
...state.routes.filter(({ key }) =>
|
||||||
|
isAnimationEnabled(key)
|
||||||
|
? replacingRouteKeys.includes(key) || closingRouteKeys.includes(key)
|
||||||
|
: false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!routes.length) {
|
||||||
|
throw new Error(
|
||||||
|
'There should always be at least one route in the navigation state.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptors = routes.reduce<WebStackDescriptorMap>((acc, route) => {
|
||||||
|
acc[route.key] =
|
||||||
|
props.descriptors[route.key] || state.descriptors[route.key];
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
routes,
|
||||||
|
previousRoutes: props.state.routes,
|
||||||
|
previousDescriptors: props.descriptors,
|
||||||
|
openingRouteKeys,
|
||||||
|
closingRouteKeys,
|
||||||
|
replacingRouteKeys,
|
||||||
|
descriptors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
state: State = {
|
||||||
|
routes: [],
|
||||||
|
previousRoutes: [],
|
||||||
|
previousDescriptors: {},
|
||||||
|
openingRouteKeys: [],
|
||||||
|
closingRouteKeys: [],
|
||||||
|
replacingRouteKeys: [],
|
||||||
|
descriptors: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderScene = ({ route }: { route: Route<string> }) => {
|
||||||
|
const descriptor =
|
||||||
|
this.state.descriptors[route.key] || this.props.descriptors[route.key];
|
||||||
|
|
||||||
|
if (!descriptor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptor.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
||||||
|
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.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
|
||||||
|
navigation.dispatch({
|
||||||
|
...StackActions.pop(),
|
||||||
|
source: route.key,
|
||||||
|
target: state.key,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// We need to clean up any state tracking the route and pop it immediately
|
||||||
|
this.setState((state) => ({
|
||||||
|
routes: state.routes.filter((r) => r.key !== route.key),
|
||||||
|
openingRouteKeys: state.openingRouteKeys.filter(
|
||||||
|
(key) => key !== route.key
|
||||||
|
),
|
||||||
|
closingRouteKeys: state.closingRouteKeys.filter(
|
||||||
|
(key) => key !== route.key
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
state,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
navigation,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
routes,
|
||||||
|
descriptors,
|
||||||
|
openingRouteKeys,
|
||||||
|
closingRouteKeys,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardStack
|
||||||
|
routes={routes}
|
||||||
|
openingRouteKeys={openingRouteKeys}
|
||||||
|
closingRouteKeys={closingRouteKeys}
|
||||||
|
onOpenRoute={this.handleOpenRoute}
|
||||||
|
onCloseRoute={this.handleCloseRoute}
|
||||||
|
renderScene={this.renderScene}
|
||||||
|
state={state}
|
||||||
|
descriptors={descriptors}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/web-stack/src/views/assets/back-icon-mask.png
Normal file
|
After Width: | Height: | Size: 913 B |
BIN
packages/web-stack/src/views/assets/back-icon.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
packages/web-stack/src/views/assets/back-icon@1.5x.android.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
packages/web-stack/src/views/assets/back-icon@1.5x.ios.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
packages/web-stack/src/views/assets/back-icon@1x.android.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
packages/web-stack/src/views/assets/back-icon@1x.ios.png
Normal file
|
After Width: | Height: | Size: 290 B |
BIN
packages/web-stack/src/views/assets/back-icon@2x.android.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
packages/web-stack/src/views/assets/back-icon@2x.ios.png
Normal file
|
After Width: | Height: | Size: 405 B |
BIN
packages/web-stack/src/views/assets/back-icon@3x.android.png
Normal file
|
After Width: | Height: | Size: 167 B |
BIN
packages/web-stack/src/views/assets/back-icon@3x.ios.png
Normal file
|
After Width: | Height: | Size: 761 B |
BIN
packages/web-stack/src/views/assets/back-icon@4x.android.png
Normal file
|
After Width: | Height: | Size: 207 B |
BIN
packages/web-stack/src/views/assets/back-icon@4x.ios.png
Normal file
|
After Width: | Height: | Size: 812 B |