Compare commits
81 Commits
@react-nav
...
@react-nav
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbfb70699 | ||
|
|
abdf9d12b5 | ||
|
|
ba85db28d4 | ||
|
|
e5063b9339 | ||
|
|
ec35bd5821 | ||
|
|
22e306004a | ||
|
|
8a835b3556 | ||
|
|
d9237e9a0d | ||
|
|
0644ac03aa | ||
|
|
41a5e1a385 | ||
|
|
3d937d1e65 | ||
|
|
343320783f | ||
|
|
5411816188 | ||
|
|
d8bda604ee | ||
|
|
c70635b7d7 | ||
|
|
94d7b28c0b | ||
|
|
b19b3b2725 | ||
|
|
3dcec142f7 | ||
|
|
0c159db4c9 | ||
|
|
2479da98ed | ||
|
|
5197ee2a9c | ||
|
|
0ead2662ec | ||
|
|
5af5c29f07 | ||
|
|
d448cdc11f | ||
|
|
0e8fda3196 | ||
|
|
9198597b7f | ||
|
|
9be904d9c4 | ||
|
|
fa4a959549 | ||
|
|
d0510d0220 | ||
|
|
0b4bf1dcc8 | ||
|
|
5a3f8356b0 | ||
|
|
eeae11033a | ||
|
|
b931ae62df | ||
|
|
ea66b1a3b8 | ||
|
|
4bc0c8f66f | ||
|
|
68016de385 | ||
|
|
e55e866af2 | ||
|
|
50b366e734 | ||
|
|
edf96d839f | ||
|
|
141d397bdf | ||
|
|
0f18b91690 | ||
|
|
6262f7298b | ||
|
|
a6f58677dc | ||
|
|
9bfb295620 | ||
|
|
ecd68afb46 | ||
|
|
5fe140e61b | ||
|
|
944fa35ed4 | ||
|
|
2243b45cc1 | ||
|
|
5e7cfc4ac0 | ||
|
|
5751e7f97a | ||
|
|
179e807a64 | ||
|
|
2f1f0af862 | ||
|
|
9976a888a0 | ||
|
|
16c64e7298 | ||
|
|
f1fe951cf9 | ||
|
|
14250851d1 | ||
|
|
42586462fd | ||
|
|
3dede316cc | ||
|
|
63988e0da8 | ||
|
|
67b2ecfcfc | ||
|
|
68ed8a7259 | ||
|
|
6c2acbb304 | ||
|
|
84d75b37e7 | ||
|
|
65e5147910 | ||
|
|
321fa653ad | ||
|
|
2a76dc4d3c | ||
|
|
0a982ee698 | ||
|
|
1da4a6437f | ||
|
|
f1df4a0808 | ||
|
|
14ae3738cf | ||
|
|
32a2206513 | ||
|
|
38520a97ff | ||
|
|
3bf5ddde2a | ||
|
|
43d2c456be | ||
|
|
fe82276b1f | ||
|
|
1e53821d52 | ||
|
|
23ab45aceb | ||
|
|
d9059b56d8 | ||
|
|
ad4eaff1e9 | ||
|
|
da67e134d2 | ||
|
|
ee381a4ba3 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text eol=lf
|
||||
53
.github/workflows/expo-preview.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Expo Preview
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Install and publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Get yarn cache
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Check yarn cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish --release-channel=pr-${{ github.event.number }}
|
||||
|
||||
- name: Get expo link
|
||||
id: expo
|
||||
run: echo "::set-output name=path::@react-navigation/react-navigation-example?release-channel=pr-${{ github.event.number }}"
|
||||
|
||||
- name: Comment on PR
|
||||
uses: unsplash/comment-on-pr@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
msg: The Expo app for the example from this branch is ready!<br><br>[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})<br><br><a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>.
|
||||
44
.github/workflows/expo.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Expo Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Install and publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Setup Expo
|
||||
uses: expo/expo-github-action@v5
|
||||
with:
|
||||
expo-version: 3.x
|
||||
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||
expo-cache: true
|
||||
|
||||
- name: Get yarn cache
|
||||
id: yarn-cache
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Publish Expo app
|
||||
working-directory: ./example
|
||||
run: expo publish
|
||||
27
.github/workflows/rebase.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Automatic Rebase
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
name: Rebase
|
||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# https://github.community/t5/GitHub-Actions/Workflow-is-failing-if-no-job-can-be-ran-due-to-condition/m-p/38186#M3250
|
||||
always_job:
|
||||
name: Always run job
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Always run
|
||||
run: echo "This job is used to prevent the workflow to fail when all other jobs are skipped."
|
||||
@@ -16,5 +16,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/transform-flow-strip-types',
|
||||
'@babel/plugin-proposal-nullish-coalescing-operator',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -5,3 +5,5 @@ If you want to run the example from the repo,
|
||||
- Clone the repository and run `yarn` in the project root
|
||||
- Run `yarn example start` to start the packager
|
||||
- Follow the instructions to open it with the [Expo app](https://expo.io/)
|
||||
|
||||
You can also run the currently published [app on Expo](https://expo.io/@react-navigation/react-navigation-example) on your Android device or iOS simulator or the [web app](https://react-navigation-example.netlify.com/) in your browser.
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.3.0" />
|
||||
<option name="LAST_KNOWN_AGP_VERSION" value="3.3.0" />
|
||||
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.4.2" />
|
||||
<option name="LAST_KNOWN_AGP_VERSION" value="3.4.2" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
@@ -19,8 +19,8 @@
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
|
||||
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
|
||||
<option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
@@ -70,6 +70,7 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/rncli/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
@@ -90,91 +91,103 @@
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 28 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:collections:28.0.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.core:common:1.1.1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-annotations:28.0.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.infer.annotation:infer-annotation:0.11.2@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.code.findbugs:jsr305:3.0.2@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: javax.inject:javax.inject:1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.collection:collection:1.1.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.1.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.1.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp-urlconnection:3.12.1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.squareup.okhttp3:okhttp:3.12.1@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.squareup.okio:okio:1.15.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:disklrucache:4.9.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:annotations:4.9.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib:1.3.41@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: org.jetbrains.kotlin:kotlin-stdlib-common:1.3.41@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: org.jetbrains:annotations:13.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.device.yearclass:yearclass:2.1.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: commons-codec:commons-codec:1.10@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: commons-io:commons-io:1.4@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:disklrucache:4.9.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:annotations:4.9.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.parse.bolts:bolts-tasks:1.4.0@jar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.react:react-native:0.61.5@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.browser:browser:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:glide:4.9.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:gifdecoder:4.9.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-location:16.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-base:16.0.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-places-placereport:16.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-tasks:16.0.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.google.android.gms:play-services-basement:16.0.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-v4:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-fragment:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:customtabs:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-core-ui:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-core-utils:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:loader:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-media-compat:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:viewpager:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:coordinatorlayout:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:drawerlayout:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:slidingpanelayout:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:customview:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:swiperefreshlayout:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:asynclayoutinflater:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:support-compat:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:versionedparcelable:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:cursoradapter:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.1.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:documentfile:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:localbroadcastmanager:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:print:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.github.bumptech.glide:gifdecoder:4.9.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.android.support:interpolator:28.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata:1.1.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core:1.1.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.1@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:drawee:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-base:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.legacy:legacy-support-v4:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.fragment:fragment:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.media:media:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.legacy:legacy-support-core-ui:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.legacy:legacy-support-core-utils:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.appcompat:appcompat-resources:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.drawerlayout:drawerlayout:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.viewpager:viewpager:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.loader:loader:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.activity:activity:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable-animated:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.vectordrawable:vectordrawable:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.coordinatorlayout:coordinatorlayout:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.slidingpanelayout:slidingpanelayout:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.customview:customview:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.swiperefreshlayout:swiperefreshlayout:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.asynclayoutinflater:asynclayoutinflater:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.core:core:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.cursoradapter:cursoradapter:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.interpolator:interpolator:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-viewmodel:2.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.1.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.documentfile:documentfile:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.localbroadcastmanager:localbroadcastmanager:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.print:print:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.savedstate:savedstate:1.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-livedata-core:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: androidx.arch.core:core-runtime:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:fresco:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:fbcore:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:drawee:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-base:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.soloader:soloader:0.6.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:nativeimagefilters:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:nativeimagetranscoder:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:imagepipeline-okhttp3:2.0.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: io.nlopez.smartlocation:library:3.2.11@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: com.facebook.fresco:fbcore:1.10.0@aar" level="project" />
|
||||
<orderEntry type="library" name="Gradle: org.webkit:android-jsc:r245459@aar" level="project" />
|
||||
<orderEntry type="module" module-name="android-expo-permissions" />
|
||||
<orderEntry type="module" module-name="android-expo-constants" />
|
||||
<orderEntry type="module" module-name="android-unimodules-image-loader-interface" />
|
||||
<orderEntry type="module" module-name="android-expo-web-browser" />
|
||||
<orderEntry type="module" module-name="android-unimodules-react-native-adapter" />
|
||||
<orderEntry type="module" module-name="android-expo-file-system" />
|
||||
<orderEntry type="module" module-name="android-expo-location" />
|
||||
<orderEntry type="module" module-name="android-expo-error-recovery" />
|
||||
<orderEntry type="module" module-name="android-unimodules-permissions-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-core" />
|
||||
<orderEntry type="module" module-name="android-expo-app-loader-provider" />
|
||||
<orderEntry type="module" module-name="android-expo-font" />
|
||||
<orderEntry type="module" module-name="android-expo-keep-awake" />
|
||||
<orderEntry type="module" module-name="android-expo-linear-gradient" />
|
||||
<orderEntry type="module" module-name="android-expo-sqlite" />
|
||||
<orderEntry type="module" module-name="android-unimodules-barcode-scanner-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-camera-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-constants-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-face-detector-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-file-system-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-font-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-sensors-interface" />
|
||||
<orderEntry type="module" module-name="android-unimodules-task-manager-interface" />
|
||||
<orderEntry type="module" module-name="android-@react-native-community_masked-view" />
|
||||
<orderEntry type="module" module-name="android-react-native-gesture-handler" />
|
||||
<orderEntry type="module" module-name="android-react-native-reanimated" />
|
||||
<orderEntry type="module" module-name="react-native-safe-area-context" />
|
||||
<orderEntry type="module" module-name="react-native-screens" />
|
||||
<orderEntry type="module" module-name="react-native-reanimated" />
|
||||
<orderEntry type="module" module-name="react-native-gesture-handler" />
|
||||
<orderEntry type="module" module-name="expo-permissions" />
|
||||
<orderEntry type="module" module-name="unimodules-core" />
|
||||
<orderEntry type="module" module-name="unimodules-react-native-adapter" />
|
||||
<orderEntry type="module" module-name="expo-app-loader-provider" />
|
||||
<orderEntry type="module" module-name="expo-constants" />
|
||||
<orderEntry type="module" module-name="expo-file-system" />
|
||||
<orderEntry type="module" module-name="expo-font" />
|
||||
<orderEntry type="module" module-name="expo-keep-awake" />
|
||||
<orderEntry type="module" module-name="expo-linear-gradient" />
|
||||
<orderEntry type="module" module-name="expo-location" />
|
||||
<orderEntry type="module" module-name="expo-sqlite" />
|
||||
<orderEntry type="module" module-name="expo-web-browser" />
|
||||
<orderEntry type="module" module-name="unimodules-barcode-scanner-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-camera-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-constants-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-face-detector-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-file-system-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-font-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-image-loader-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-permissions-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-sensors-interface" />
|
||||
<orderEntry type="module" module-name="unimodules-task-manager-interface" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
@@ -36,12 +36,19 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="rne" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"displayName": "React Navigation Example",
|
||||
"expo": {
|
||||
"name": "@react-navigation/example",
|
||||
"owner": "react-navigation",
|
||||
"slug": "react-navigation-example",
|
||||
"description": "Demo app to showcase various functionality of React Navigation",
|
||||
"privacy": "public",
|
||||
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
BIN
example/assets/album-art-04.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
example/assets/album-art-05.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
example/assets/album-art-06.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
example/assets/album-art-07.jpg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
example/assets/album-art-08.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
example/assets/album-art-09.jpg
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
example/assets/album-art-10.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
example/assets/album-art-11.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
example/assets/album-art-12.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
example/assets/album-art-13.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
example/assets/album-art-14.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
example/assets/album-art-15.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
example/assets/album-art-16.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
example/assets/album-art-17.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
example/assets/album-art-18.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
example/assets/album-art-19.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
example/assets/album-art-20.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
example/assets/album-art-21.jpg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
example/assets/album-art-22.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
example/assets/album-art-23.jpg
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
example/assets/album-art-24.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 683 B |
@@ -2,6 +2,8 @@ PODS:
|
||||
- boost-for-react-native (1.63.0)
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXAppLoaderProvider (8.0.0)
|
||||
- EXBlur (8.0.0):
|
||||
- UMCore
|
||||
- EXConstants (8.0.0):
|
||||
- UMConstantsInterface
|
||||
- UMCore
|
||||
@@ -48,6 +50,8 @@ PODS:
|
||||
- glog
|
||||
- glog (0.3.5)
|
||||
- RCTRequired (0.61.5)
|
||||
- RCTRestart (0.0.13):
|
||||
- React
|
||||
- RCTTypeSafety (0.61.5):
|
||||
- FBLazyVector (= 0.61.5)
|
||||
- Folly (= 2018.10.22.00)
|
||||
@@ -249,11 +253,11 @@ PODS:
|
||||
- ReactCommon/jscallinvoker (= 0.61.5)
|
||||
- RNCMaskedView (0.1.5):
|
||||
- React
|
||||
- RNGestureHandler (1.5.3):
|
||||
- RNGestureHandler (1.5.5):
|
||||
- React
|
||||
- RNReanimated (1.4.0):
|
||||
- React
|
||||
- RNScreens (2.0.0-alpha.22):
|
||||
- RNScreens (2.0.0-alpha.33):
|
||||
- React
|
||||
- UMBarCodeScannerInterface (5.0.0)
|
||||
- UMCameraInterface (5.0.0)
|
||||
@@ -275,6 +279,7 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- DoubleConversion (from `../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXAppLoaderProvider (from `../../node_modules/expo-app-loader-provider/ios`)
|
||||
- EXBlur (from `../../node_modules/expo-blur/ios`)
|
||||
- EXConstants (from `../../node_modules/expo-constants/ios`)
|
||||
- EXErrorRecovery (from `../../node_modules/expo-error-recovery/ios`)
|
||||
- EXFileSystem (from `../../node_modules/expo-file-system/ios`)
|
||||
@@ -290,6 +295,7 @@ DEPENDENCIES:
|
||||
- Folly (from `../../node_modules/react-native/third-party-podspecs/Folly.podspec`)
|
||||
- glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- RCTRequired (from `../../node_modules/react-native/Libraries/RCTRequired`)
|
||||
- RCTRestart (from `../../node_modules/react-native-restart/ios`)
|
||||
- RCTTypeSafety (from `../../node_modules/react-native/Libraries/TypeSafety`)
|
||||
- React (from `../../node_modules/react-native/`)
|
||||
- React-Core (from `../../node_modules/react-native/`)
|
||||
@@ -340,6 +346,9 @@ EXTERNAL SOURCES:
|
||||
EXAppLoaderProvider:
|
||||
:path: !ruby/object:Pathname
|
||||
path: "../../node_modules/expo-app-loader-provider/ios"
|
||||
EXBlur:
|
||||
:path: !ruby/object:Pathname
|
||||
path: "../../node_modules/expo-blur/ios"
|
||||
EXConstants:
|
||||
:path: !ruby/object:Pathname
|
||||
path: "../../node_modules/expo-constants/ios"
|
||||
@@ -380,6 +389,8 @@ EXTERNAL SOURCES:
|
||||
:podspec: "../../node_modules/react-native/third-party-podspecs/glog.podspec"
|
||||
RCTRequired:
|
||||
:path: "../../node_modules/react-native/Libraries/RCTRequired"
|
||||
RCTRestart:
|
||||
:path: "../../node_modules/react-native-restart/ios"
|
||||
RCTTypeSafety:
|
||||
:path: "../../node_modules/react-native/Libraries/TypeSafety"
|
||||
React:
|
||||
@@ -469,6 +480,7 @@ SPEC CHECKSUMS:
|
||||
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
|
||||
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
|
||||
EXAppLoaderProvider: ebdb6bc2632c1ccadbe49f5e4104d8d690969c49
|
||||
EXBlur: d1604f66f89a9414f5ee65dfb23874437c1bb147
|
||||
EXConstants: 4051b16c17ef3defa03c541d42811dc92b249146
|
||||
EXErrorRecovery: d36db99ec6a3808f313f01b0890eb443796dd1c2
|
||||
EXFileSystem: 6e0d9bb6cc4ea404dbb8f583c1a8a2dcdf4b83b6
|
||||
@@ -484,6 +496,7 @@ SPEC CHECKSUMS:
|
||||
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
|
||||
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
|
||||
RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1
|
||||
RCTRestart: dd19aab87fc1118e05b6b5b91b959105647f56b4
|
||||
RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320
|
||||
React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78
|
||||
React-Core: 688b451f7d616cc1134ac95295b593d1b5158a04
|
||||
@@ -504,9 +517,9 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad
|
||||
ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd
|
||||
RNCMaskedView: dd13f9f7b146a9ad82f9b7eb6c9b5548fcf6e990
|
||||
RNGestureHandler: 02905abe54e1f6e59c081a10b4bd689721e17aa6
|
||||
RNGestureHandler: d2270608171c868581b840cfc692f2962c05cd17
|
||||
RNReanimated: b2ab0b693dddd2339bd2f300e770f6302d2e960c
|
||||
RNScreens: 6adf01eb4080f44af6cca551207c6b0505066857
|
||||
RNScreens: 1c7fd499b915c77c21e8e6c327890c5af9b4cf7e
|
||||
UMBarCodeScannerInterface: 3802c8574ef119c150701d679ab386e2266d6a54
|
||||
UMCameraInterface: 985d301f688ed392f815728f0dd906ca34b7ccb1
|
||||
UMConstantsInterface: bda5f8bd3403ad99e663eb3c4da685d063c5653c
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#import <UMCore/UMModuleRegistry.h>
|
||||
#import <UMReactNativeAdapter/UMNativeModulesProxy.h>
|
||||
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>
|
||||
#import <React/RCTLinkingManager.h>
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@@ -52,4 +53,10 @@
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
|
||||
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
|
||||
{
|
||||
return [RCTLinkingManager application:app openURL:url options:options];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,33 +20,27 @@
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>React Navigation Example</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>rne</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
@@ -78,5 +72,19 @@
|
||||
<string>Give React Navigation Example periences permission to access your photos</string>
|
||||
<key>NSRemindersUsageDescription</key>
|
||||
<string>Allow React Navigation Example to access your reminders</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,7 +6,7 @@ const escape = require('escape-string-regexp');
|
||||
const blacklist = require('metro-config/src/defaults/blacklist');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const packages = path.resolve(__dirname, '..', 'packages');
|
||||
const packages = path.resolve(root, 'packages');
|
||||
|
||||
// Get the list of dependencies for all packages in the monorepo
|
||||
const modules = ['@expo/vector-icons']
|
||||
@@ -22,7 +22,7 @@ const modules = ['@expo/vector-icons']
|
||||
);
|
||||
|
||||
// We need to collect list of deps that this package imports
|
||||
// Collecting both dependencies are peerDependencies sould do it
|
||||
// Collecting both dependencies are peerDependencies should do it
|
||||
return Object.keys({
|
||||
...pak.dependencies,
|
||||
...pak.peerDependencies,
|
||||
@@ -59,7 +59,7 @@ module.exports = {
|
||||
// When we import a package from the monorepo, metro won't be able to find their deps
|
||||
// We need to specify them in `extraNodeModules` to tell metro where to find them
|
||||
extraNodeModules: modules.reduce((acc, name) => {
|
||||
acc[name] = path.join(__dirname, '..', 'node_modules', name);
|
||||
acc[name] = path.join(root, 'node_modules', name);
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
@@ -67,11 +67,11 @@ module.exports = {
|
||||
server: {
|
||||
enhanceMiddleware: middleware => {
|
||||
return (req, res, next) => {
|
||||
// When an asset is imported outside the project root, it has wrong path on Android
|
||||
// This happens for the back button in stack, so we fix the path to correct one
|
||||
const assets = '/packages/stack/src/views/assets';
|
||||
|
||||
if (req.url.startsWith(assets)) {
|
||||
// When an asset is imported outside the project root, it has wrong path on Android
|
||||
// This happens for the back button in stack, so we fix the path to correct one
|
||||
req.url = req.url.replace(
|
||||
assets,
|
||||
'/assets/../packages/stack/src/views/assets'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"web": "expo start --web",
|
||||
"web": "expo start:web",
|
||||
"native": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios"
|
||||
@@ -13,18 +13,21 @@
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^10.0.0",
|
||||
"@react-native-community/masked-view": "0.1.5",
|
||||
"@types/react-native-restart": "^0.0.0",
|
||||
"color": "^3.1.2",
|
||||
"expo": "^36.0.2",
|
||||
"expo-asset": "~8.0.0",
|
||||
"expo-blur": "^8.0.0",
|
||||
"react": "~16.9.0",
|
||||
"react-dom": "~16.9.0",
|
||||
"react-native": "~0.61.5",
|
||||
"react-native-gesture-handler": "~1.5.3",
|
||||
"react-native-paper": "^3.4.0",
|
||||
"react-native-gesture-handler": "^1.5.5",
|
||||
"react-native-paper": "^3.5.0",
|
||||
"react-native-reanimated": "^1.4.0",
|
||||
"react-native-restart": "^0.0.13",
|
||||
"react-native-safe-area-context": "^0.6.2",
|
||||
"react-native-screens": "^2.0.0-alpha.22",
|
||||
"react-native-tab-view": "2.11.0",
|
||||
"react-native-screens": "^2.0.0-alpha.33",
|
||||
"react-native-tab-view": "2.13.0",
|
||||
"react-native-unimodules": "^0.7.0",
|
||||
"react-native-web": "^0.11.7"
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default [];
|
||||
export default ['rne://127.0.0.1:19000/--/'];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { View, TextInput, ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { Title, Button } from 'react-native-paper';
|
||||
import { ParamListBase } from '@react-navigation/native';
|
||||
import { useTheme, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
HeaderBackButton,
|
||||
@@ -40,11 +40,25 @@ const SplashScreen = () => {
|
||||
|
||||
const SignInScreen = () => {
|
||||
const { signIn } = React.useContext(AuthContext);
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.content}>
|
||||
<TextInput placeholder="Username" style={styles.input} />
|
||||
<TextInput placeholder="Password" secureTextEntry style={styles.input} />
|
||||
<TextInput
|
||||
placeholder="Username"
|
||||
style={[
|
||||
styles.input,
|
||||
{ backgroundColor: colors.card, color: colors.text },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Password"
|
||||
secureTextEntry
|
||||
style={[
|
||||
styles.input,
|
||||
{ backgroundColor: colors.card, color: colors.text },
|
||||
]}
|
||||
/>
|
||||
<Button mode="contained" onPress={signIn} style={styles.button}>
|
||||
Sign in
|
||||
</Button>
|
||||
@@ -73,6 +87,7 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
isLoading: boolean;
|
||||
isSignout: boolean;
|
||||
userToken: undefined | string;
|
||||
};
|
||||
|
||||
@@ -94,17 +109,20 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
||||
case 'SIGN_IN':
|
||||
return {
|
||||
...prevState,
|
||||
isSignout: false,
|
||||
userToken: action.token,
|
||||
};
|
||||
case 'SIGN_OUT':
|
||||
return {
|
||||
...prevState,
|
||||
isSignout: true,
|
||||
userToken: undefined,
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
isLoading: true,
|
||||
isSignout: false,
|
||||
userToken: undefined,
|
||||
}
|
||||
);
|
||||
@@ -147,7 +165,10 @@ export default function SimpleStackScreen({ navigation }: Props) {
|
||||
) : state.userToken === undefined ? (
|
||||
<SimpleStack.Screen
|
||||
name="SignIn"
|
||||
options={{ title: 'Sign in' }}
|
||||
options={{
|
||||
title: 'Sign in',
|
||||
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
|
||||
}}
|
||||
component={SignInScreen}
|
||||
/>
|
||||
) : (
|
||||
@@ -171,7 +192,6 @@ const styles = StyleSheet.create({
|
||||
input: {
|
||||
margin: 8,
|
||||
padding: 10,
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 3,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: 'rgba(0, 0, 0, 0.08)',
|
||||
|
||||
56
example/src/Screens/DynamicTabs.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { Title, Button } from 'react-native-paper';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
|
||||
type BottomTabParams = {
|
||||
[key: string]: undefined;
|
||||
};
|
||||
|
||||
const BottomTabs = createBottomTabNavigator<BottomTabParams>();
|
||||
|
||||
export default function BottomTabsScreen() {
|
||||
const [tabs, setTabs] = React.useState([0, 1]);
|
||||
|
||||
return (
|
||||
<BottomTabs.Navigator>
|
||||
{tabs.map(i => (
|
||||
<BottomTabs.Screen
|
||||
key={i}
|
||||
name={`tab-${i}`}
|
||||
options={{
|
||||
title: `Tab ${i}`,
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Feather name="octagon" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{() => (
|
||||
<View style={styles.container}>
|
||||
<Title>Tab {i}</Title>
|
||||
<Button onPress={() => setTabs(tabs => [...tabs, tabs.length])}>
|
||||
Add a tab
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() =>
|
||||
setTabs(tabs => (tabs.length > 1 ? tabs.slice(0, -1) : tabs))
|
||||
}
|
||||
>
|
||||
Remove a tab
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</BottomTabs.Screen>
|
||||
))}
|
||||
</BottomTabs.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ const ArticleScreen = ({
|
||||
route: RouteProp<ModalStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -42,14 +42,14 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} />
|
||||
</React.Fragment>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -66,8 +66,8 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const AlbumsScreen = ({
|
||||
}: {
|
||||
navigation: NativeStackNavigation;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -154,8 +154,8 @@ const AlbumsScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
const NativeStack = createNativeStackNavigator<NativeStackParams>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
import Albums from '../Shared/Albums';
|
||||
import NewsFeed from '../Shared/NewsFeed';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
NewsFeed: undefined;
|
||||
Album: undefined;
|
||||
};
|
||||
|
||||
@@ -24,14 +26,42 @@ const ArticleScreen = ({
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Album')}
|
||||
onPress={() => navigation.replace('NewsFeed')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
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: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.navigate('Album')}
|
||||
style={styles.button}
|
||||
>
|
||||
Navigate to album
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
@@ -41,8 +71,8 @@ const ArticleScreen = ({
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} />
|
||||
</React.Fragment>
|
||||
<NewsFeed scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,7 +82,7 @@ const AlbumsScreen = ({
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
@@ -63,14 +93,14 @@ const AlbumsScreen = ({
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
onPress={() => navigation.pop(2)}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
Pop by 2
|
||||
</Button>
|
||||
</View>
|
||||
<Albums />
|
||||
</React.Fragment>
|
||||
<Albums scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -95,6 +125,11 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="NewsFeed"
|
||||
component={NewsFeedScreen}
|
||||
options={{ title: 'Feed' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
|
||||
158
example/src/Screens/StackHeaderCustomization.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Alert, Platform } from 'react-native';
|
||||
import { Button, Appbar } from 'react-native-paper';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { RouteProp, ParamListBase } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
HeaderBackground,
|
||||
useHeaderHeight,
|
||||
} 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 ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Album')}
|
||||
style={styles.button}
|
||||
>
|
||||
Push album
|
||||
</Button>
|
||||
<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;
|
||||
}) => {
|
||||
const headerHeight = useHeaderHeight();
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ paddingTop: headerHeight }}>
|
||||
<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.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}`,
|
||||
headerTintColor: '#fff',
|
||||
headerStyle: { backgroundColor: '#ff005d' },
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleAlign: 'center',
|
||||
headerBackImage: ({ tintColor }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-left-circle-outline"
|
||||
color={tintColor}
|
||||
size={24}
|
||||
style={{ marginHorizontal: Platform.OS === 'ios' ? 8 : 0 }}
|
||||
/>
|
||||
),
|
||||
headerRight: ({ tintColor }) => (
|
||||
<Appbar.Action
|
||||
color={tintColor}
|
||||
icon="dots-horizontal-circle-outline"
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
'Never gonna give you up!',
|
||||
'Never gonna let you down! Never gonna run around and desert you!'
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Album"
|
||||
component={AlbumsScreen}
|
||||
options={{
|
||||
title: 'Album',
|
||||
headerBackTitle: 'Back',
|
||||
headerTransparent: true,
|
||||
headerBackground: () => (
|
||||
<HeaderBackground style={{ backgroundColor: 'transparent' }}>
|
||||
<BlurView
|
||||
tint="light"
|
||||
intensity={75}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
</HeaderBackground>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
});
|
||||
154
example/src/Screens/StackTransparent.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView } from 'react-native';
|
||||
import { Button, Paragraph } from 'react-native-paper';
|
||||
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackNavigationProp,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
|
||||
type SimpleStackParams = {
|
||||
Article: { author: string };
|
||||
Dialog: undefined;
|
||||
};
|
||||
|
||||
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
|
||||
|
||||
const ArticleScreen = ({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
route: RouteProp<SimpleStackParams, 'Article'>;
|
||||
}) => {
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => navigation.push('Dialog')}
|
||||
style={styles.button}
|
||||
>
|
||||
Show Dialog
|
||||
</Button>
|
||||
<Button
|
||||
mode="outlined"
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.button}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
</View>
|
||||
<Article author={{ name: route.params.author }} scrollEnabled={false} />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const DialogScreen = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: SimpleStackNavigation;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.dialog, { backgroundColor: colors.card }]}>
|
||||
<Paragraph>
|
||||
Mise en place is a French term that literally means “put in place.” It
|
||||
also refers to a way cooks in professional kitchens and restaurants
|
||||
set up their work stations—first by gathering all ingredients for a
|
||||
recipes, partially preparing them (like measuring out and chopping),
|
||||
and setting them all near each other. Setting up mise en place before
|
||||
cooking is another top tip for home cooks, as it seriously helps with
|
||||
organization. It’ll pretty much guarantee you never forget to add an
|
||||
ingredient and save you time from running back and forth from the
|
||||
pantry ten times.
|
||||
</Paragraph>
|
||||
<Button style={styles.close} compact onPress={navigation.goBack}>
|
||||
Okay
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 mode="modal" {...rest}>
|
||||
<SimpleStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
initialParams={{ author: 'Gandalf' }}
|
||||
/>
|
||||
<SimpleStack.Screen
|
||||
name="Dialog"
|
||||
component={DialogScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
cardStyle: { backgroundColor: 'transparent' },
|
||||
cardOverlayEnabled: true,
|
||||
cardStyleInterpolator: ({ current: { progress } }) => ({
|
||||
cardStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 0.5, 0.9, 1],
|
||||
outputRange: [0, 0.25, 0.7, 1],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.9, 1],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
overlayStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 0.5],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</SimpleStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
padding: 8,
|
||||
},
|
||||
button: {
|
||||
margin: 8,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dialog: {
|
||||
padding: 16,
|
||||
width: '90%',
|
||||
maxWidth: 400,
|
||||
borderRadius: 3,
|
||||
},
|
||||
close: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
});
|
||||
@@ -1,53 +1,90 @@
|
||||
/* eslint-disable import/no-commonjs */
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, Dimensions, ScrollView, StyleSheet } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop } from '@react-navigation/native';
|
||||
|
||||
const COVERS = [
|
||||
require('../../assets/album-art-1.jpg'),
|
||||
require('../../assets/album-art-2.jpg'),
|
||||
require('../../assets/album-art-3.jpg'),
|
||||
require('../../assets/album-art-4.jpg'),
|
||||
require('../../assets/album-art-5.jpg'),
|
||||
require('../../assets/album-art-6.jpg'),
|
||||
require('../../assets/album-art-7.jpg'),
|
||||
require('../../assets/album-art-8.jpg'),
|
||||
require('../../assets/album-art-01.jpg'),
|
||||
require('../../assets/album-art-02.jpg'),
|
||||
require('../../assets/album-art-03.jpg'),
|
||||
require('../../assets/album-art-04.jpg'),
|
||||
require('../../assets/album-art-05.jpg'),
|
||||
require('../../assets/album-art-06.jpg'),
|
||||
require('../../assets/album-art-07.jpg'),
|
||||
require('../../assets/album-art-08.jpg'),
|
||||
require('../../assets/album-art-09.jpg'),
|
||||
require('../../assets/album-art-10.jpg'),
|
||||
require('../../assets/album-art-11.jpg'),
|
||||
require('../../assets/album-art-12.jpg'),
|
||||
require('../../assets/album-art-13.jpg'),
|
||||
require('../../assets/album-art-14.jpg'),
|
||||
require('../../assets/album-art-15.jpg'),
|
||||
require('../../assets/album-art-16.jpg'),
|
||||
require('../../assets/album-art-17.jpg'),
|
||||
require('../../assets/album-art-18.jpg'),
|
||||
require('../../assets/album-art-19.jpg'),
|
||||
require('../../assets/album-art-20.jpg'),
|
||||
require('../../assets/album-art-21.jpg'),
|
||||
require('../../assets/album-art-22.jpg'),
|
||||
require('../../assets/album-art-23.jpg'),
|
||||
require('../../assets/album-art-24.jpg'),
|
||||
];
|
||||
|
||||
export default function Albums() {
|
||||
export default function Albums(props: Partial<ScrollViewProps>) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={ref}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.content}
|
||||
>
|
||||
<ScrollView ref={ref} contentContainerStyle={styles.content} {...props}>
|
||||
{COVERS.map((source, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Image key={i} source={source} style={styles.cover} />
|
||||
))}
|
||||
{COVERS.map((source, i) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Image key={i + 'F'} source={source} style={styles.cover} />
|
||||
<View key={i} style={styles.item}>
|
||||
<Image source={source} style={styles.photo} />
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
content: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
cover: {
|
||||
width: '50%',
|
||||
height: Dimensions.get('window').width / 2,
|
||||
...Platform.select({
|
||||
web: {
|
||||
content: {
|
||||
display: 'grid' as 'none',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
||||
},
|
||||
item: {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
default: {
|
||||
content: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
item: {
|
||||
height: Dimensions.get('window').width / 2,
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
}),
|
||||
photo: {
|
||||
flex: 1,
|
||||
resizeMode: 'cover',
|
||||
paddingTop: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { View, Text, Image, ScrollView, StyleSheet } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
|
||||
type Props = {
|
||||
type Props = Partial<ScrollViewProps> & {
|
||||
date?: string;
|
||||
author?: {
|
||||
name: string;
|
||||
@@ -14,6 +21,7 @@ export default function Article({
|
||||
author = {
|
||||
name: 'Knowledge Bot',
|
||||
},
|
||||
...rest
|
||||
}: Props) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
@@ -26,6 +34,7 @@ export default function Article({
|
||||
ref={ref}
|
||||
style={{ backgroundColor: colors.card }}
|
||||
contentContainerStyle={styles.content}
|
||||
{...rest}
|
||||
>
|
||||
<View style={styles.author}>
|
||||
<Image
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TextInput,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
import Color from 'color';
|
||||
@@ -17,7 +18,7 @@ const MESSAGES = [
|
||||
'make me a sandwich',
|
||||
];
|
||||
|
||||
export default function Chat() {
|
||||
export default function Chat(props: Partial<ScrollViewProps>) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
@@ -29,6 +30,7 @@ export default function Chat() {
|
||||
<ScrollView
|
||||
style={styles.inverted}
|
||||
contentContainerStyle={styles.content}
|
||||
{...props}
|
||||
>
|
||||
{MESSAGES.map((text, i) => {
|
||||
const odd = i % 2;
|
||||
|
||||
146
example/src/Shared/NewsFeed.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
ScrollViewProps,
|
||||
} from 'react-native';
|
||||
import { useScrollToTop, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Avatar,
|
||||
Subheading,
|
||||
IconButton,
|
||||
Divider,
|
||||
} from 'react-native-paper';
|
||||
import Color from 'color';
|
||||
|
||||
type Props = Partial<ScrollViewProps>;
|
||||
|
||||
const Author = () => {
|
||||
return (
|
||||
<View style={[styles.row, styles.attribution]}>
|
||||
<Avatar.Image source={require('../../assets/avatar-1.png')} size={32} />
|
||||
<Subheading style={styles.author}>Joke bot</Subheading>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<IconButton style={styles.icon} size={16} icon="heart-outline" />
|
||||
<IconButton style={styles.icon} size={16} icon="comment-outline" />
|
||||
<IconButton style={styles.icon} size={16} icon="share-outline" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function NewsFeed(props: Props) {
|
||||
const ref = React.useRef<ScrollView>(null);
|
||||
|
||||
useScrollToTop(ref);
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<ScrollView ref={ref} {...props}>
|
||||
<Card style={styles.card}>
|
||||
<TextInput
|
||||
placeholder="What's on your mind?"
|
||||
placeholderTextColor={Color(colors.text)
|
||||
.alpha(0.5)
|
||||
.rgb()
|
||||
.string()}
|
||||
style={styles.input}
|
||||
/>
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
If you aren't impressed with the picture of the first Black
|
||||
Hole, you clearly don't understand the gravity of the
|
||||
situation.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
I went to the zoo and I saw a baguette in a cage. I asked the
|
||||
zookeeper about it and he said it was bread in captivity.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Image source={require('../../assets/book.jpg')} style={styles.cover} />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>Why didn't 4 ask 5 out? Because he was 2².</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
What did Master Yoda say when he first saw himself in 4k? HDMI.
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
<Card style={styles.card}>
|
||||
<Author />
|
||||
<Card.Content style={styles.content}>
|
||||
<Text>
|
||||
Someone broke into my house and stole 20% of my couch. Ouch!
|
||||
</Text>
|
||||
</Card.Content>
|
||||
<Divider />
|
||||
<Footer />
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
padding: 16,
|
||||
backgroundColor: 'transparent',
|
||||
margin: 0,
|
||||
},
|
||||
card: {
|
||||
marginVertical: 8,
|
||||
borderRadius: 0,
|
||||
},
|
||||
cover: {
|
||||
height: 160,
|
||||
borderRadius: 0,
|
||||
},
|
||||
content: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
attribution: {
|
||||
margin: 12,
|
||||
},
|
||||
author: {
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
26
example/src/Shared/SettingsItem.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Subheading, Switch } from 'react-native-paper';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: () => void;
|
||||
};
|
||||
|
||||
export default function SettingsItem({ label, value, onValueChange }: Props) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
}}
|
||||
>
|
||||
<Subheading>{label}</Subheading>
|
||||
<Switch value={value} onValueChange={onValueChange} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
AsyncStorage,
|
||||
YellowBox,
|
||||
Platform,
|
||||
StatusBar,
|
||||
I18nManager,
|
||||
} from 'react-native';
|
||||
import RNRestart from 'react-native-restart';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import {
|
||||
Provider as PaperProvider,
|
||||
DefaultTheme as PaperLightTheme,
|
||||
DarkTheme as PaperDarkTheme,
|
||||
Subheading,
|
||||
Appbar,
|
||||
List,
|
||||
Switch,
|
||||
Divider,
|
||||
} from 'react-native-paper';
|
||||
import { Asset } from 'expo-asset';
|
||||
@@ -42,11 +41,16 @@ import LinkingPrefixes from './LinkingPrefixes';
|
||||
import SimpleStack from './Screens/SimpleStack';
|
||||
import NativeStack from './Screens/NativeStack';
|
||||
import ModalPresentationStack from './Screens/ModalPresentationStack';
|
||||
import StackTransparent from './Screens/StackTransparent';
|
||||
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
|
||||
import BottomTabs from './Screens/BottomTabs';
|
||||
import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
|
||||
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
|
||||
import DynamicTabs from './Screens/DynamicTabs';
|
||||
import AuthFlow from './Screens/AuthFlow';
|
||||
import CompatAPI from './Screens/CompatAPI';
|
||||
import SettingsItem from './Shared/SettingsItem';
|
||||
import { Updates } from 'expo';
|
||||
|
||||
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
|
||||
|
||||
@@ -68,6 +72,14 @@ const SCREENS = {
|
||||
title: 'Modal Presentation Stack',
|
||||
component: ModalPresentationStack,
|
||||
},
|
||||
StackTransparent: {
|
||||
title: 'Transparent Stack',
|
||||
component: StackTransparent,
|
||||
},
|
||||
StackHeaderCustomization: {
|
||||
title: 'Header Customization in Stack',
|
||||
component: StackHeaderCustomization,
|
||||
},
|
||||
BottomTabs: { title: 'Bottom Tabs', component: BottomTabs },
|
||||
MaterialTopTabs: {
|
||||
title: 'Material Top Tabs',
|
||||
@@ -77,6 +89,10 @@ const SCREENS = {
|
||||
title: 'Material Bottom Tabs',
|
||||
component: MaterialBottomTabs,
|
||||
},
|
||||
DynamicTabs: {
|
||||
title: 'Dynamic Tabs',
|
||||
component: DynamicTabs,
|
||||
},
|
||||
AuthFlow: {
|
||||
title: 'Auth Flow',
|
||||
component: AuthFlow,
|
||||
@@ -101,22 +117,27 @@ export default function App() {
|
||||
// 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: 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();
|
||||
Root: {
|
||||
path: 'root',
|
||||
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;
|
||||
},
|
||||
{}
|
||||
),
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,7 +153,7 @@ export default function App() {
|
||||
try {
|
||||
let state = await getInitialState();
|
||||
|
||||
if (state === undefined) {
|
||||
if (Platform.OS !== 'web' && state === undefined) {
|
||||
const savedState = await AsyncStorage.getItem(
|
||||
NAVIGATION_PERSISTENCE_KEY
|
||||
);
|
||||
@@ -233,34 +254,39 @@ export default function App() {
|
||||
<ScrollView
|
||||
style={{ backgroundColor: theme.colors.background }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 16,
|
||||
<SettingsItem
|
||||
label="Right to left"
|
||||
value={I18nManager.isRTL}
|
||||
onValueChange={() => {
|
||||
I18nManager.forceRTL(!I18nManager.isRTL);
|
||||
// @ts-ignore
|
||||
if (global.Expo) {
|
||||
Updates.reloadFromCache();
|
||||
} else {
|
||||
RNRestart.Restart();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Subheading>Dark theme</Subheading>
|
||||
<Switch
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
/>
|
||||
<Divider />
|
||||
<SettingsItem
|
||||
label="Dark theme"
|
||||
value={theme.dark}
|
||||
onValueChange={() => {
|
||||
AsyncStorage.setItem(
|
||||
THEME_PERSISTENCE_KEY,
|
||||
theme.dark ? 'light' : 'dark'
|
||||
);
|
||||
|
||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
setTheme(t => (t.dark ? DefaultTheme : DarkTheme));
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
{(Object.keys(SCREENS) as (keyof typeof SCREENS)[]).map(
|
||||
name => (
|
||||
<List.Item
|
||||
key={name}
|
||||
title={SCREENS[name].title}
|
||||
onPress={() => navigation.push(name)}
|
||||
onPress={() => navigation.navigate(name)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,8 @@ const packages = path.resolve(__dirname, '..', 'packages');
|
||||
module.exports = async function(env, argv) {
|
||||
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||
|
||||
config.context = path.resolve(__dirname, '..');
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.(js|ts|tsx)$/,
|
||||
include: /(packages|example)\/.+/,
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"@react-navigation/([^/]+)": "<rootDir>/packages/$1/src"
|
||||
}
|
||||
},
|
||||
"preset": "react-native"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
|
||||
@@ -3,6 +3,63 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.43](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.42...@react-navigation/bottom-tabs@5.0.0-alpha.43) (2020-02-03)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.42](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.39...@react-navigation/bottom-tabs@5.0.0-alpha.42) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.40](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.39...@react-navigation/bottom-tabs@5.0.0-alpha.40) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.39](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.38...@react-navigation/bottom-tabs@5.0.0-alpha.39) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use layout instead of dimensions for determining tab bar layout ([f1fe951](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/f1fe951cf9d602e1b6d4932e3c6c77bbeaaec5c0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.38](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.37...@react-navigation/bottom-tabs@5.0.0-alpha.38) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't use native driver on web ([0a982ee](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/0a982ee6984b24c0ba053a30223e255f3835e050))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.37](https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.0.0-alpha.36...@react-navigation/bottom-tabs@5.0.0-alpha.37) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/bottom-tabs
|
||||
|
||||
21
packages/bottom-tabs/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.
|
||||
@@ -10,7 +10,7 @@
|
||||
"android",
|
||||
"tab"
|
||||
],
|
||||
"version": "5.0.0-alpha.37",
|
||||
"version": "5.0.0-alpha.43",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/bottom-tabs",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -21,6 +21,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -29,7 +30,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.25",
|
||||
"@react-navigation/routers": "^5.0.0-alpha.31",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -17,11 +17,11 @@ export type BottomTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
/**
|
||||
* Event which fires on long press on the tab in the tab bar.
|
||||
*/
|
||||
tabLongPress: undefined;
|
||||
tabLongPress: { data: undefined };
|
||||
};
|
||||
|
||||
export type LabelPosition = 'beside-icon' | 'below-icon';
|
||||
@@ -99,6 +99,12 @@ export type BottomTabNavigationOptions = {
|
||||
* Renders `TouchableWithoutFeedback` by default.
|
||||
*/
|
||||
tabBarButton?: (props: BottomTabBarButtonProps) => React.ReactNode;
|
||||
|
||||
/**
|
||||
* Whether this screen should be unmounted when navigating away from it.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
unmountOnBlur?: boolean;
|
||||
};
|
||||
|
||||
export type BottomTabDescriptor = Descriptor<
|
||||
@@ -118,11 +124,6 @@ export type BottomTabNavigationConfig = {
|
||||
* Set it to `false` if you want to render all screens on initial render.
|
||||
*/
|
||||
lazy?: boolean;
|
||||
/**
|
||||
* Whether a screen should be unmounted when navigating away from it.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
unmountInactiveScreens?: boolean;
|
||||
/**
|
||||
* Function that returns a React element to display as the tab bar.
|
||||
*/
|
||||
@@ -176,14 +177,9 @@ export type BottomTabBarOptions = {
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
/**
|
||||
* Whether the label is renderd below the icon or beside the icon.
|
||||
* When a function is passed, it receives the device dimensions to render the label differently.
|
||||
* By default, in `vertical` orinetation, label is rendered below and in `horizontal` orientation, it's renderd beside.
|
||||
*/
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: {
|
||||
dimensions: { height: number; width: number };
|
||||
}) => LabelPosition);
|
||||
labelPosition?: LabelPosition;
|
||||
/**
|
||||
* Whether the label position should adapt to the orientation.
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,8 @@ type Props = BottomTabBarProps & {
|
||||
const DEFAULT_TABBAR_HEIGHT = 50;
|
||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
const useNativeDriver = Platform.OS !== 'web';
|
||||
|
||||
export default function BottomTabBar({
|
||||
state,
|
||||
navigation,
|
||||
@@ -48,7 +50,10 @@ export default function BottomTabBar({
|
||||
const { colors } = useTheme();
|
||||
|
||||
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
|
||||
const [layout, setLayout] = React.useState({ height: 0, width: 0 });
|
||||
const [layout, setLayout] = React.useState({
|
||||
height: 0,
|
||||
width: dimensions.width,
|
||||
});
|
||||
const [keyboardShown, setKeyboardShown] = React.useState(false);
|
||||
|
||||
const [visible] = React.useState(() => new Animated.Value(0));
|
||||
@@ -60,7 +65,7 @@ export default function BottomTabBar({
|
||||
Animated.timing(visible, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
}).start();
|
||||
}
|
||||
}, [keyboardShown, visible]);
|
||||
@@ -76,7 +81,7 @@ export default function BottomTabBar({
|
||||
Animated.timing(visible, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver,
|
||||
}).start(({ finished }) => {
|
||||
if (finished) {
|
||||
setKeyboardShown(false);
|
||||
@@ -122,27 +127,15 @@ export default function BottomTabBar({
|
||||
};
|
||||
|
||||
const shouldUseHorizontalLabels = () => {
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
if (labelPosition) {
|
||||
let position;
|
||||
|
||||
if (typeof labelPosition === 'string') {
|
||||
position = labelPosition;
|
||||
} else {
|
||||
position = labelPosition({ dimensions });
|
||||
}
|
||||
|
||||
if (position) {
|
||||
return position === 'beside-icon';
|
||||
}
|
||||
return labelPosition === 'beside-icon';
|
||||
}
|
||||
|
||||
if (!adaptive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dimensions.width >= 768) {
|
||||
if (layout.width >= 768) {
|
||||
// Screen size matches a tablet
|
||||
let maxTabItemWidth = DEFAULT_MAX_TAB_ITEM_WIDTH;
|
||||
|
||||
@@ -156,8 +149,10 @@ export default function BottomTabBar({
|
||||
}
|
||||
}
|
||||
|
||||
return routes.length * maxTabItemWidth <= dimensions.width;
|
||||
return routes.length * maxTabItemWidth <= layout.width;
|
||||
} else {
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
return isLandscape;
|
||||
}
|
||||
};
|
||||
@@ -205,6 +200,7 @@ export default function BottomTabBar({
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (!focused && !event.defaultPrevented) {
|
||||
|
||||
@@ -92,7 +92,7 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, descriptors, lazy, unmountInactiveScreens } = this.props;
|
||||
const { state, descriptors, lazy } = this.props;
|
||||
const { routes } = state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
@@ -101,17 +101,19 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
if (unmountInactiveScreens && index !== state.index) {
|
||||
const descriptor = descriptors[route.key];
|
||||
const { unmountOnBlur } = descriptor.options;
|
||||
const isFocused = state.index === index;
|
||||
|
||||
if (unmountOnBlur && !isFocused) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
if (lazy && !loaded.includes(index) && !isFocused) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFocused = state.index === index;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
@@ -119,7 +121,7 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
isVisible={isFocused}
|
||||
>
|
||||
<SceneContent isFocused={isFocused}>
|
||||
{descriptors[route.key].render()}
|
||||
{descriptor.render()}
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.32](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.31...@react-navigation/compat@5.0.0-alpha.32) (2020-02-03)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.31](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.28...@react-navigation/compat@5.0.0-alpha.31) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
* throw when assigning or accessing the router property in compat ([944fa35](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/944fa35ed4778ebc7fa7cd50092719cbd5bf3caf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.29](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.28...@react-navigation/compat@5.0.0-alpha.29) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
* throw when assigning or accessing the router property in compat ([944fa35](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/944fa35ed4778ebc7fa7cd50092719cbd5bf3caf))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.28](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.27...@react-navigation/compat@5.0.0-alpha.28) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.27](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.26...@react-navigation/compat@5.0.0-alpha.27) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure re-render on isFirstRouteInParent change in compat layer ([14ae373](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/14ae3738cf46088e082bd1c60b9dcc6dacacd1bf))
|
||||
* improvements to the compat layer ([2a76dc4](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/commit/2a76dc4d3c4cc0365a3afcff6ac321145efed026))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.26](https://github.com/react-navigation/navigation-ex/tree/master/packages/compat/compare/@react-navigation/compat@5.0.0-alpha.25...@react-navigation/compat@5.0.0-alpha.26) (2020-01-14)
|
||||
|
||||
**Note:** Version bump only for package @react-navigation/compat
|
||||
|
||||
21
packages/compat/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.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@react-navigation/compat",
|
||||
"description": "Compatibility layer to write navigator definitions in static configuration format",
|
||||
"version": "5.0.0-alpha.26",
|
||||
"version": "5.0.0-alpha.32",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/compat",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -12,6 +12,7 @@
|
||||
"src",
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -20,7 +21,7 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-navigation/routers": "^5.0.0-alpha.25"
|
||||
"@react-navigation/routers": "^5.0.0-alpha.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.17",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
RouteProp,
|
||||
} from '@react-navigation/native';
|
||||
import ScreenPropsContext from './ScreenPropsContext';
|
||||
import createCompatNavigationProp from './createCompatNavigationProp';
|
||||
import useCompatNavigation from './useCompatNavigation';
|
||||
|
||||
type Props<ParamList extends ParamListBase> = {
|
||||
navigation: NavigationProp<ParamList>;
|
||||
@@ -16,12 +16,7 @@ type Props<ParamList extends ParamListBase> = {
|
||||
function ScreenComponent<ParamList extends ParamListBase>(
|
||||
props: Props<ParamList>
|
||||
) {
|
||||
const navigation = React.useMemo(
|
||||
() =>
|
||||
createCompatNavigationProp(props.navigation as any, props.route as any),
|
||||
[props.navigation, props.route]
|
||||
);
|
||||
|
||||
const navigation = useCompatNavigation();
|
||||
const screenProps = React.useContext(ScreenPropsContext);
|
||||
|
||||
return <props.component navigation={navigation} screenProps={screenProps} />;
|
||||
|
||||
@@ -19,7 +19,7 @@ export function replace({
|
||||
key?: string;
|
||||
newKey?: string;
|
||||
action?: never;
|
||||
}): CommonActions.Action {
|
||||
}): StackActionType {
|
||||
if (action !== undefined) {
|
||||
throw new Error(
|
||||
'Sub-actions are not supported for `replace`. Remove the `action` key from the options.'
|
||||
|
||||
@@ -8,11 +8,17 @@ import {
|
||||
import * as helpers from './helpers';
|
||||
import { CompatNavigationProp } from './types';
|
||||
|
||||
type EventName = 'willFocus' | 'willBlur' | 'didFocus' | 'didBlur' | 'refocus';
|
||||
type EventName =
|
||||
| 'action'
|
||||
| 'willFocus'
|
||||
| 'willBlur'
|
||||
| 'didFocus'
|
||||
| 'didBlur'
|
||||
| 'refocus';
|
||||
|
||||
const focusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
const blurSubscriptions = new WeakMap<() => void, () => void>();
|
||||
const refocusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const focusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const blurSubscriptions = new WeakMap<() => void, () => void>();
|
||||
// const refocusSubscriptions = new WeakMap<() => void, () => void>();
|
||||
|
||||
export default function createCompatNavigationProp<
|
||||
NavigationPropType extends NavigationProp<ParamListBase>,
|
||||
@@ -28,8 +34,17 @@ export default function createCompatNavigationProp<
|
||||
state?: NavigationState | PartialState<NavigationState>;
|
||||
})
|
||||
| NavigationState
|
||||
| PartialState<NavigationState>
|
||||
| PartialState<NavigationState>,
|
||||
context: Record<string, any>,
|
||||
isFirstRouteInParent?: boolean
|
||||
): CompatNavigationProp<NavigationPropType> {
|
||||
context.parent = context.parent || {};
|
||||
context.subscriptions = context.subscriptions || {
|
||||
didFocus: new Map<() => void, () => void>(),
|
||||
didBlur: new Map<() => void, () => void>(),
|
||||
refocus: new Map<() => void, () => void>(),
|
||||
};
|
||||
|
||||
return {
|
||||
...navigation,
|
||||
...Object.entries(helpers).reduce<{
|
||||
@@ -61,7 +76,7 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('transitionEnd', listener);
|
||||
focusSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.didFocus.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'didBlur': {
|
||||
@@ -73,7 +88,7 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('transitionEnd', listener);
|
||||
blurSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.didBlur.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'refocus': {
|
||||
@@ -85,9 +100,11 @@ export default function createCompatNavigationProp<
|
||||
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener('tabPress', listener);
|
||||
refocusSubscriptions.set(callback, unsubscribe);
|
||||
context.subscriptions.refocus.set(callback, unsubscribe);
|
||||
break;
|
||||
}
|
||||
case 'action':
|
||||
throw new Error("Listening to 'action' events is not supported.");
|
||||
default:
|
||||
// @ts-ignore
|
||||
unsubscribe = navigation.addListener(type, callback);
|
||||
@@ -100,6 +117,8 @@ export default function createCompatNavigationProp<
|
||||
return subscription;
|
||||
},
|
||||
removeListener(type: EventName, callback: () => void) {
|
||||
context.subscriptions = context.subscriptions || {};
|
||||
|
||||
switch (type) {
|
||||
case 'willFocus':
|
||||
navigation.removeListener('focus', callback);
|
||||
@@ -108,20 +127,22 @@ export default function createCompatNavigationProp<
|
||||
navigation.removeListener('blur', callback);
|
||||
break;
|
||||
case 'didFocus': {
|
||||
const unsubscribe = focusSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.didFocus.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'didBlur': {
|
||||
const unsubscribe = blurSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.didBlur.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'refocus': {
|
||||
const unsubscribe = refocusSubscriptions.get(callback);
|
||||
const unsubscribe = context.subscriptions.refocus.get(callback);
|
||||
unsubscribe?.();
|
||||
break;
|
||||
}
|
||||
case 'action':
|
||||
throw new Error("Listening to 'action' events is not supported.");
|
||||
default:
|
||||
// @ts-ignore
|
||||
navigation.removeListener(type, callback);
|
||||
@@ -174,6 +195,10 @@ export default function createCompatNavigationProp<
|
||||
return defaultValue;
|
||||
},
|
||||
isFirstRouteInParent(): boolean {
|
||||
if (typeof isFirstRouteInParent === 'boolean') {
|
||||
return isFirstRouteInParent;
|
||||
}
|
||||
|
||||
const { routes } = navigation.dangerouslyGetState();
|
||||
|
||||
// @ts-ignore
|
||||
@@ -185,7 +210,8 @@ export default function createCompatNavigationProp<
|
||||
if (parent) {
|
||||
return createCompatNavigationProp(
|
||||
parent,
|
||||
navigation.dangerouslyGetState()
|
||||
navigation.dangerouslyGetState(),
|
||||
context.parent
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function createCompatNavigatorFactory<
|
||||
? {
|
||||
navigation: createCompatNavigationProp<
|
||||
NavigationPropType
|
||||
>(navigation, route),
|
||||
>(navigation, route, {}),
|
||||
navigationOptions: defaultNavigationOptions || {},
|
||||
screenProps,
|
||||
}
|
||||
@@ -165,7 +165,25 @@ export default function createCompatNavigatorFactory<
|
||||
return Navigator;
|
||||
};
|
||||
|
||||
createCompatNavigator.isCompat = true;
|
||||
Object.defineProperties(createCompatNavigator, {
|
||||
isCompat: {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
router: {
|
||||
get() {
|
||||
throw new Error(
|
||||
"It's no longer possible to access the router with the 'router' property."
|
||||
);
|
||||
},
|
||||
set() {
|
||||
throw new Error(
|
||||
"It's no longer possible to override the router by assigning the 'router' property."
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return createCompatNavigator;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useRoute,
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
useNavigationState,
|
||||
} from '@react-navigation/native';
|
||||
import createCompatNavigationProp from './createCompatNavigationProp';
|
||||
import { CompatNavigationProp } from './types';
|
||||
@@ -14,12 +15,20 @@ export default function useCompatNavigation<
|
||||
const navigation = useNavigation();
|
||||
const route = useRoute();
|
||||
|
||||
const isFirstRouteInParent = useNavigationState(
|
||||
state => state.routes[0].key === route.key
|
||||
);
|
||||
|
||||
const context = React.useRef<Record<string, any>>({});
|
||||
|
||||
return React.useMemo(
|
||||
() =>
|
||||
createCompatNavigationProp(
|
||||
navigation,
|
||||
route as any
|
||||
route as any,
|
||||
context.current,
|
||||
isFirstRouteInParent
|
||||
) as CompatNavigationProp<T>,
|
||||
[navigation, route]
|
||||
[isFirstRouteInParent, navigation, route]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,92 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [5.0.0-alpha.41](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.40...@react-navigation/core@5.0.0-alpha.41) (2020-02-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ignore circular references when checking serializable ([e5063b9](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/e5063b93398350511f3fd2ef48425559f871781f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.40](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.37...@react-navigation/core@5.0.0-alpha.40) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
* add warning when passing inline function to component prop ([fa4a959](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/fa4a959549ccd9dc2f9bd2ea495e99abdedc9f94))
|
||||
* tweak error messages for validation ([2243b45](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/2243b45cc1addf83727166d82736d214f181b1fb))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `screens` prop for nested configs ([#308](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/308)) ([b931ae6](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/b931ae62dfb2c5253c94ea5ace73e9070ec17c4a))
|
||||
* add useIsDrawerOpen hook ([#299](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/299)) ([ecd68af](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/ecd68afb46a4c56200748da5e5fb284fa5a839db))
|
||||
* integrate with history API on web ([5a3f835](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/5a3f8356b05bff7ed20893a5db6804612da3e568))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.38](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.37...@react-navigation/core@5.0.0-alpha.38) (2020-02-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add licenses ([0c159db](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/0c159db4c9bc85e83b5cfe6819ab2562669a4d8f))
|
||||
* add warning when passing inline function to component prop ([fa4a959](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/fa4a959549ccd9dc2f9bd2ea495e99abdedc9f94))
|
||||
* tweak error messages for validation ([2243b45](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/2243b45cc1addf83727166d82736d214f181b1fb))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add `screens` prop for nested configs ([#308](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/308)) ([b931ae6](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/b931ae62dfb2c5253c94ea5ace73e9070ec17c4a))
|
||||
* add useIsDrawerOpen hook ([#299](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/299)) ([ecd68af](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/ecd68afb46a4c56200748da5e5fb284fa5a839db))
|
||||
* integrate with history API on web ([5a3f835](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/5a3f8356b05bff7ed20893a5db6804612da3e568))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.37](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.36...@react-navigation/core@5.0.0-alpha.37) (2020-01-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add error message when trying to use v4 API with v5 ([179e807](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/179e807a64a7d031d671c2c4b12edaee3c3440c5))
|
||||
* validate screen configs ([2f1f0af](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/2f1f0af862ef8625da4c2aaf463d45fe17a4ac88))
|
||||
* warn if non-serializable values found in state ([5751e7f](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/5751e7f97a1731a5c71862174dfd931b6ffe13e2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.36](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.35...@react-navigation/core@5.0.0-alpha.36) (2020-01-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disallow canPreventDefault option if not present in types ([d9059b5](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/d9059b56d8a89b39fec43d38a7b0514d41c0b550))
|
||||
* don't add ?if query params is empty ([3bf5ddd](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/3bf5ddde2ac1ba45f1123752d37532175f18a3d9))
|
||||
* fix types for useFocusEffect ([23ab45a](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/23ab45aceb72cc27ebfacdedfbf60d0c540fecfb)), closes [#270](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/270)
|
||||
* make sure that we return correct value if selector changes ([6c2acbb](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/6c2acbb304a9f11789b45a410b6c41911eca3947)), closes [/github.com/react-navigation/navigation-ex/pull/273#issuecomment-576581225](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/issuecomment-576581225)
|
||||
* use protected for private value store ([ad4eaff](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/ad4eaff1e99e4f9fca3a193764fd0f26efa41341))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add useNavigationState hook ([32a2206](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/32a2206513bc084d8da07187385d11db498f1e2a))
|
||||
* let the navigator specify if default can be prevented ([da67e13](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/da67e134d2157201360427d3c10da24f24cae7aa))
|
||||
* support nested config in getPathFromState ([#266](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/issues/266)) ([1e53821](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/commit/1e53821d52be182369add07a86c72221c5dba53e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [5.0.0-alpha.35](https://github.com/react-navigation/navigation-ex/tree/master/packages/core/compare/@react-navigation/core@5.0.0-alpha.34...@react-navigation/core@5.0.0-alpha.35) (2020-01-14)
|
||||
|
||||
|
||||
|
||||
21
packages/core/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.
|
||||
@@ -6,7 +6,7 @@
|
||||
"react-native",
|
||||
"react-navigation"
|
||||
],
|
||||
"version": "5.0.0-alpha.35",
|
||||
"version": "5.0.0-alpha.41",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/react-navigation/navigation-ex/tree/master/packages/core",
|
||||
"main": "lib/commonjs/index.js",
|
||||
@@ -27,6 +27,7 @@
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"query-string": "^6.9.0",
|
||||
"react-is": "^16.12.0",
|
||||
"shortid": "^2.2.15",
|
||||
"use-subscription": "^1.3.0"
|
||||
},
|
||||
@@ -34,6 +35,7 @@
|
||||
"@babel/core": "^7.7.7",
|
||||
"@react-native-community/bob": "^0.8.0",
|
||||
"@types/react": "^16.9.17",
|
||||
"@types/react-is": "^16.7.1",
|
||||
"@types/shortid": "^0.0.29",
|
||||
"@types/use-subscription": "^1.0.0",
|
||||
"del-cli": "^3.0.0",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import shortid from 'shortid';
|
||||
import { CommonAction, NavigationState, PartialState } from './types';
|
||||
|
||||
/**
|
||||
@@ -11,35 +10,6 @@ const BaseRouter = {
|
||||
action: CommonAction
|
||||
): State | PartialState<State> | null {
|
||||
switch (action.type) {
|
||||
case 'REPLACE': {
|
||||
const index = action.source
|
||||
? state.routes.findIndex(r => r.key === action.source)
|
||||
: state.index;
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name, key, params } = action.payload;
|
||||
|
||||
if (!state.routeNames.includes(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
routes: state.routes.map((route, i) =>
|
||||
i === index
|
||||
? {
|
||||
key: key !== undefined ? key : `${name}-${shortid()}`,
|
||||
name,
|
||||
params,
|
||||
}
|
||||
: route
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET_PARAMS': {
|
||||
const index = action.source
|
||||
? state.routes.findIndex(r => r.key === action.source)
|
||||
|
||||
@@ -14,12 +14,6 @@ export type Action =
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
| {
|
||||
type: 'REPLACE';
|
||||
payload: { name: string; key?: string; params?: object };
|
||||
source?: string;
|
||||
target?: string;
|
||||
}
|
||||
| {
|
||||
type: 'RESET';
|
||||
payload: PartialState<NavigationState>;
|
||||
@@ -59,10 +53,6 @@ export function navigate(...args: any): Action {
|
||||
}
|
||||
}
|
||||
|
||||
export function replace(name: string, params?: object): Action {
|
||||
return { type: 'REPLACE', payload: { name, params } };
|
||||
}
|
||||
|
||||
export function reset(state: PartialState<NavigationState>): Action {
|
||||
return { type: 'RESET', payload: state };
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as React from 'react';
|
||||
import * as CommonActions from './CommonActions';
|
||||
import EnsureSingleNavigator from './EnsureSingleNavigator';
|
||||
import NavigationBuilderContext from './NavigationBuilderContext';
|
||||
import ResetRootContext from './ResetRootContext';
|
||||
import useFocusedListeners from './useFocusedListeners';
|
||||
import useDevTools from './useDevTools';
|
||||
import useStateGetters from './useStateGetters';
|
||||
import isSerializable from './isSerializable';
|
||||
|
||||
import {
|
||||
Route,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
NavigationContainerRef,
|
||||
NavigationContainerProps,
|
||||
} from './types';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
|
||||
type State = NavigationState | PartialState<NavigationState> | undefined;
|
||||
|
||||
@@ -45,6 +46,8 @@ export const NavigationStateContext = React.createContext<{
|
||||
},
|
||||
});
|
||||
|
||||
let hasWarnedForSerialization = false;
|
||||
|
||||
/**
|
||||
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
|
||||
*
|
||||
@@ -203,6 +206,8 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
return getStateForRoute('root');
|
||||
}, [getStateForRoute]);
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce<
|
||||
any
|
||||
@@ -216,6 +221,7 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
);
|
||||
return acc;
|
||||
}, {}),
|
||||
...emitter.create('root'),
|
||||
resetRoot,
|
||||
dispatch,
|
||||
canGoBack,
|
||||
@@ -242,6 +248,25 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (
|
||||
state !== undefined &&
|
||||
!isSerializable(state) &&
|
||||
!hasWarnedForSerialization
|
||||
) {
|
||||
hasWarnedForSerialization = true;
|
||||
|
||||
console.warn(
|
||||
"We found non-serializable values in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use functions in your options, you can use 'navigation.setOptions' instead."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emitter.emit({
|
||||
type: 'state',
|
||||
data: { state },
|
||||
});
|
||||
|
||||
if (skipTrackingRef.current) {
|
||||
skipTrackingRef.current = false;
|
||||
} else {
|
||||
@@ -256,14 +281,12 @@ const Container = React.forwardRef(function NavigationContainer(
|
||||
}
|
||||
|
||||
isFirstMountRef.current = false;
|
||||
}, [state, onStateChange, trackState, getRootState]);
|
||||
}, [state, onStateChange, trackState, getRootState, emitter]);
|
||||
|
||||
return (
|
||||
<NavigationBuilderContext.Provider value={builderContext}>
|
||||
<NavigationStateContext.Provider value={context}>
|
||||
<ResetRootContext.Provider value={resetRoot}>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</ResetRootContext.Provider>
|
||||
<EnsureSingleNavigator>{children}</EnsureSingleNavigator>
|
||||
</NavigationStateContext.Provider>
|
||||
</NavigationBuilderContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationState, PartialState } from './types';
|
||||
|
||||
/**
|
||||
* Context which holds the method to reset root navigator state.
|
||||
*/
|
||||
const ResetRootContext = React.createContext<
|
||||
(state: PartialState<NavigationState> | NavigationState) => void
|
||||
>(() => {
|
||||
throw new Error(
|
||||
"We couldn't find a way to reset root state. Have you wrapped your app with 'NavigationContainer'?"
|
||||
);
|
||||
});
|
||||
|
||||
export default ResetRootContext;
|
||||
@@ -16,64 +16,6 @@ const STATE = {
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
};
|
||||
|
||||
it('replaces focused screen with REPLACE', () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
CommonActions.replace('qux', { answer: 42 })
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces source screen with REPLACE', () => {
|
||||
const result = BaseRouter.getStateForAction(STATE, {
|
||||
...CommonActions.replace('qux', { answer: 42 }),
|
||||
source: 'baz',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
stale: false,
|
||||
type: 'test',
|
||||
key: 'root',
|
||||
index: 1,
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar', params: { fruit: 'orange' } },
|
||||
{ key: 'qux-test', name: 'qux', params: { answer: 42 } },
|
||||
],
|
||||
routeNames: ['foo', 'bar', 'baz', 'qux'],
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't handle REPLACE if source key isn't present", () => {
|
||||
const result = BaseRouter.getStateForAction(STATE, {
|
||||
...CommonActions.replace('qux', { answer: 42 }),
|
||||
source: 'magic',
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it("doesn't handle REPLACE if screen to replace with isn't present", () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
CommonActions.replace('nonexistent', { answer: 42 })
|
||||
);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('sets params for the focused screen with SET_PARAMS', () => {
|
||||
const result = BaseRouter.getStateForAction(
|
||||
STATE,
|
||||
|
||||
@@ -233,8 +233,8 @@ it('handle dispatching with ref', () => {
|
||||
<Screen name="foo2">
|
||||
{() => (
|
||||
<ChildNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</ChildNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -242,8 +242,8 @@ it('handle dispatching with ref', () => {
|
||||
<Screen name="baz">
|
||||
{() => (
|
||||
<ChildNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</ChildNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -254,9 +254,7 @@ it('handle dispatching with ref', () => {
|
||||
render(element).update(element);
|
||||
|
||||
act(() => {
|
||||
if (ref.current != null) {
|
||||
ref.current.dispatch({ type: 'REVERSE' });
|
||||
}
|
||||
ref.current?.dispatch({ type: 'REVERSE' });
|
||||
});
|
||||
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
@@ -309,8 +307,8 @@ it('handle resetting state with ref', () => {
|
||||
<Screen name="foo2">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -318,8 +316,8 @@ it('handle resetting state with ref', () => {
|
||||
<Screen name="baz">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -350,9 +348,7 @@ it('handle resetting state with ref', () => {
|
||||
};
|
||||
|
||||
act(() => {
|
||||
if (ref.current != null) {
|
||||
ref.current.resetRoot(state);
|
||||
}
|
||||
ref.current?.resetRoot(state);
|
||||
});
|
||||
|
||||
expect(onStateChange).toBeCalledTimes(1);
|
||||
@@ -383,7 +379,7 @@ it('handle resetting state with ref', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handle getRootState', () => {
|
||||
it('handles getRootState', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
@@ -398,12 +394,12 @@ it('handle getRootState', () => {
|
||||
<Screen name="foo">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
<Screen name="bar" component={() => null} />
|
||||
<Screen name="bar">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
@@ -440,3 +436,68 @@ it('handle getRootState', () => {
|
||||
type: 'test',
|
||||
});
|
||||
});
|
||||
it('emits state events when the state changes', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{state.routes.map(route => descriptors[route.key].render())}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const ref = React.createRef<NavigationContainerRef>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={ref}>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">{() => null}</Screen>
|
||||
<Screen name="bar">{() => null}</Screen>
|
||||
<Screen name="baz">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element).update(element);
|
||||
|
||||
const listener = jest.fn();
|
||||
|
||||
ref.current?.addListener('state', listener);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
ref.current?.navigate('bar');
|
||||
});
|
||||
|
||||
expect(listener.mock.calls[0][0].data.state).toEqual({
|
||||
type: 'test',
|
||||
stale: false,
|
||||
index: 1,
|
||||
key: '9',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'baz', name: 'baz' },
|
||||
],
|
||||
});
|
||||
|
||||
act(() => {
|
||||
ref.current?.navigate('baz', { answer: 42 });
|
||||
});
|
||||
|
||||
expect(listener.mock.calls[1][0].data.state).toEqual({
|
||||
type: 'test',
|
||||
stale: false,
|
||||
index: 2,
|
||||
key: '9',
|
||||
routeNames: ['foo', 'bar', 'baz'],
|
||||
routes: [
|
||||
{ key: 'foo', name: 'foo' },
|
||||
{ key: 'bar', name: 'bar' },
|
||||
{ key: 'baz', name: 'baz', params: { answer: 42 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
12
packages/core/src/__tests__/createNavigatorFactory.test.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import createNavigatorFactory from '../createNavigatorFactory';
|
||||
|
||||
it('throws descriptive error if an argument is passed', () => {
|
||||
const createDummyNavigator = createNavigatorFactory(() => null);
|
||||
|
||||
expect(() => createDummyNavigator()).not.toThrowError();
|
||||
|
||||
// @ts-ignore
|
||||
expect(() => createDummyNavigator({})).toThrowError(
|
||||
"Creating a navigator doesn't take an argument."
|
||||
);
|
||||
});
|
||||
@@ -1,89 +1,401 @@
|
||||
import getPathFromState from '../getPathFromState';
|
||||
import getStateFromPath from '../getStateFromPath';
|
||||
|
||||
it('converts state to path string', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { fruit: 'apple' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { fruit: 'apple' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
).toMatchInlineSnapshot(`"/foo/bar/baz%20qux?author=jane&valid=true"`);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const path = '/foo/bar/baz%20qux?author=jane&valid=true';
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('converts state to path string with config', () => {
|
||||
expect(
|
||||
getPathFromState(
|
||||
{
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet', avaliable: false },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { author: 'Jane', valid: true, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
const path = '/few/bar/sweet/apple/baz/jane?id=x10&valid=true';
|
||||
const config = {
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
id: (id: string) => Number(id.replace(/^x/, '')),
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
id: (id: number) => `x${id}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
stringify: {
|
||||
author: author => author.toLowerCase(),
|
||||
id: id => `x${id}`,
|
||||
},
|
||||
name: 'Foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet', avaliable: false },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { author: 'Jane', valid: true, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
).toMatchInlineSnapshot(`"/few/bar/sweet/apple/baz/jane?id=x10&valid=true"`);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
routes: [{ name: 'bar' }],
|
||||
const path = '/foo/bar';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
state: {
|
||||
routes: [{ name: 'bar' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it("doesn't add query param for empty params", () => {
|
||||
const path = '/foo';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path))).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens', () => {
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
id: (id: number) => `x${id}`,
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: '10',
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles state with config with nested screens and unused configs', () => {
|
||||
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toLowerCase()),
|
||||
unknown: (_: unknown) => 'x',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object with stringify in it', () => {
|
||||
const path = '/bar/sweet/apple/foe/bis/jane?answer=42&count=10&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe('/foo/bar');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foe',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bis',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
it('handles nested object for second route depth and and path and stringify in roots', () => {
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
state: {
|
||||
routes: [{ name: 'Baz' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getPathFromState(state, config)).toBe(path);
|
||||
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import getStateFromPath from '../getStateFromPath';
|
||||
import getPathFromState from '../getPathFromState';
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
expect(
|
||||
getStateFromPath('foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
|
||||
).toEqual({
|
||||
const path = 'foo/bar/baz%20qux?author=jane%20%26%20co&valid=true';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -24,28 +28,31 @@ it('converts path string to initial state', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(state);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
const path = '/foo/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: 'foo',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -72,7 +79,12 @@ it('converts path string to initial state with config', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles leading slash when converting', () => {
|
||||
@@ -112,7 +124,8 @@ it('handles ending slash when converting', () => {
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
expect(getStateFromPath('foo/bar')).toEqual({
|
||||
const path = 'foo/bar';
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'foo',
|
||||
@@ -121,34 +134,36 @@ it('handles route without param', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
it('returns undefined for invalid path', () => {
|
||||
expect(getStateFromPath('//')).toBe(undefined);
|
||||
expect(getStateFromPath(path)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state))).toEqual(state);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
const path = '/foe/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
stringify: {
|
||||
author: (author: string) => author.toLowerCase(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -182,26 +197,35 @@ it('converts path string to initial state with config with nested screens', () =
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config with nested screens and unused configs', () => {
|
||||
expect(
|
||||
getStateFromPath('/few/baz/jane?count=10&answer=42&valid=true', {
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
it('converts path string to initial state with config with nested screens and unused parse functions', () => {
|
||||
const path = '/foe/baz/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) => author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
id: Boolean,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -227,32 +251,46 @@ it('converts path string to initial state with config with nested screens and un
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object with parse in it', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/bar/sweet/apple/few/bis/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: {
|
||||
Foe: 'few',
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
it('handles nested object with unused configs and with parse in it', () => {
|
||||
const path = '/bar/sweet/apple/foe/bis/jane?count=10&answer=42&valid=true';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
},
|
||||
},
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz',
|
||||
screens: {
|
||||
Bos: 'bos',
|
||||
Bis: {
|
||||
path: 'bis/:author',
|
||||
stringify: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toLowerCase()),
|
||||
},
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
@@ -293,21 +331,32 @@ it('handles parse in nested object with parse in it', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth', () => {
|
||||
expect(
|
||||
getStateFromPath('/baz', {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo',
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
Baz: 'baz',
|
||||
path: 'bar',
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -323,28 +372,44 @@ it('handles parse in nested object for second route depth', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('handles parse in nested object for second route depth and and path and parse in roots', () => {
|
||||
expect(
|
||||
getStateFromPath('/baz', {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
const path = '/baz';
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
Baz: 'baz',
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
@@ -360,5 +425,41 @@ it('handles parse in nested object for second route depth and and path and parse
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(state);
|
||||
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined if path is empty', () => {
|
||||
const config = {
|
||||
Foo: {
|
||||
path: 'foo/:id',
|
||||
starting: true,
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Foe: 'foe',
|
||||
Bar: {
|
||||
path: 'bar/:id',
|
||||
parse: {
|
||||
id: Number,
|
||||
},
|
||||
stringify: {
|
||||
id: (id: number) => `id=${id}`,
|
||||
},
|
||||
screens: {
|
||||
Baz: 'baz',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const path = '';
|
||||
|
||||
expect(getStateFromPath(path, config)).toEqual(undefined);
|
||||
});
|
||||
|
||||
@@ -936,3 +936,138 @@ it('switches rendered navigators', () => {
|
||||
'Another navigator is already registered for this container.'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if no name is passed to Screen', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name={undefined as any} component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
'Got an invalid name (undefined) for the screen. It must be a non-empty string.'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if invalid name is passed to Screen', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name={[] as any} component={jest.fn()} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
'Got an invalid name ([]) for the screen. It must be a non-empty string.'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if both children and component are passed', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={jest.fn()}>
|
||||
{jest.fn()}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"Got both 'component' and 'children' props for the screen 'foo'. You must pass only one of them."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for undefined screen component', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={undefined as any} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"Couldn't find a 'component' or 'children' prop for the screen 'foo'"
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for invalid screen component', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={{} as any} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"Got an invalid value for 'component' prop for the screen 'foo'. It must be a a valid React Component."
|
||||
);
|
||||
});
|
||||
|
||||
it('throws descriptive error for invalid children', () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo">{[] as any}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).toThrowError(
|
||||
"Got an invalid value for 'children' prop for the screen 'foo'. It must be a function returning a React Element."
|
||||
);
|
||||
});
|
||||
|
||||
it("doesn't throw if children is null", () => {
|
||||
const TestNavigator = (props: any) => {
|
||||
useNavigationBuilder(MockRouter, props);
|
||||
return null;
|
||||
};
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator>
|
||||
<Screen name="foo" component={jest.fn()}>
|
||||
{null as any}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
expect(() => render(element).update(element)).not.toThrowError();
|
||||
});
|
||||
|
||||
70
packages/core/src/__tests__/isSerializable.test.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import isSerializable from '../isSerializable';
|
||||
|
||||
it('returns true for serializable object', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 0,
|
||||
key: '8',
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [
|
||||
{ key: 'qux', name: 'qux' },
|
||||
{ key: 'lex', name: 'lex' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-serializable object', () => {
|
||||
expect(
|
||||
isSerializable({
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
state: {
|
||||
index: 0,
|
||||
key: '8',
|
||||
routeNames: ['qux', 'lex'],
|
||||
routes: [
|
||||
{ key: 'qux', name: 'qux', params: () => 42 },
|
||||
{ key: 'lex', name: 'lex' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for circular references', () => {
|
||||
const o = {
|
||||
index: 0,
|
||||
key: '7',
|
||||
routeNames: ['foo', 'bar'],
|
||||
routes: [
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'foo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
o.routes[0].state = o;
|
||||
|
||||
expect(isSerializable(o)).toBe(false);
|
||||
});
|
||||
@@ -592,7 +592,7 @@ it('returns true for canGoBack when parent router handles GO_BACK', () => {
|
||||
<Screen name="qux">
|
||||
{() => (
|
||||
<OverrodeNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
</OverrodeNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
|
||||
@@ -391,6 +391,8 @@ it('fires custom events', () => {
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
@@ -400,3 +402,62 @@ it('fires custom events', () => {
|
||||
expect(secondCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('has option to prevent default', () => {
|
||||
expect.assertions(5);
|
||||
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const callback = (e: any) => {
|
||||
expect(e.type).toBe('someSuperCoolEvent');
|
||||
expect(e.data).toBe(42);
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(e.preventDefault).not.toBe(undefined);
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
};
|
||||
|
||||
const Test = ({ navigation }: any) => {
|
||||
React.useEffect(() => navigation.addListener(eventName, callback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen name="first" component={Test} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
data: 42,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
58
packages/core/src/__tests__/useNavigationCache.test.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-native-testing-library';
|
||||
import useEventEmitter from '../useEventEmitter';
|
||||
import useNavigationCache from '../useNavigationCache';
|
||||
import MockRouter, { MockRouterKey } from './__fixtures__/MockRouter';
|
||||
|
||||
beforeEach(() => (MockRouterKey.current = 0));
|
||||
|
||||
it('preserves reference for navigation objects', () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const state = {
|
||||
type: 'tab',
|
||||
stale: false as const,
|
||||
index: 1,
|
||||
key: 'State',
|
||||
routeNames: ['Foo', 'Bar'],
|
||||
routes: [
|
||||
{ key: 'Foo', name: 'Foo' },
|
||||
{ key: 'Bar', name: 'Bar' },
|
||||
],
|
||||
};
|
||||
|
||||
const getState = () => state;
|
||||
const navigation = {} as any;
|
||||
const setOptions = (() => {}) as any;
|
||||
const router = MockRouter({});
|
||||
|
||||
const Test = () => {
|
||||
const previous = React.useRef<any>();
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
const navigations = useNavigationCache({
|
||||
state,
|
||||
getState,
|
||||
navigation,
|
||||
setOptions,
|
||||
router,
|
||||
emitter,
|
||||
});
|
||||
|
||||
if (previous.current) {
|
||||
Object.keys(navigations).forEach(key => {
|
||||
expect(navigations[key]).toBe(previous.current[key]);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
previous.current = navigations;
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const root = render(<Test />);
|
||||
|
||||
root.update(<Test />);
|
||||
});
|
||||
156
packages/core/src/__tests__/useNavigationState.test.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import useNavigationState from '../useNavigationState';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter from './__fixtures__/MockRouter';
|
||||
import { NavigationState } from '../types';
|
||||
|
||||
it('gets the current navigation state', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const state = useNavigationState(state => state);
|
||||
|
||||
callback(state);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0].index).toBe(0);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0].index).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('third'));
|
||||
|
||||
expect(callback).toBeCalledTimes(3);
|
||||
expect(callback.mock.calls[2][0].index).toBe(2);
|
||||
|
||||
act(() => navigation.current.navigate('second', { answer: 42 }));
|
||||
|
||||
expect(callback).toBeCalledTimes(4);
|
||||
expect(callback.mock.calls[3][0].index).toBe(1);
|
||||
expect(callback.mock.calls[3][0].routes[1].params).toEqual({ answer: 42 });
|
||||
});
|
||||
|
||||
it('gets the current navigation state with selector', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const Test = () => {
|
||||
const index = useNavigationState(state => state.index);
|
||||
|
||||
callback(index);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toBe(0);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0]).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('third'));
|
||||
|
||||
expect(callback).toBeCalledTimes(3);
|
||||
expect(callback.mock.calls[1][0]).toBe(1);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(callback).toBeCalledTimes(4);
|
||||
expect(callback.mock.calls[3][0]).toBe(1);
|
||||
});
|
||||
|
||||
it('gets the correct value if selector changes', () => {
|
||||
const TestNavigator = (props: any): any => {
|
||||
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const SelectorContext = React.createContext<any>(null);
|
||||
|
||||
const Test = () => {
|
||||
const selector = React.useContext(SelectorContext);
|
||||
const result = useNavigationState(selector);
|
||||
|
||||
callback(result);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const App = ({ selector }: { selector: (state: NavigationState) => any }) => {
|
||||
return (
|
||||
<SelectorContext.Provider value={selector}>
|
||||
<NavigationContainer ref={navigation}>
|
||||
<TestNavigator>
|
||||
<Screen name="first" component={Test} />
|
||||
<Screen name="second">{() => null}</Screen>
|
||||
<Screen name="third">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
</SelectorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const root = render(<App selector={state => state.index} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toBe(0);
|
||||
|
||||
root.update(<App selector={state => state.routes[state.index].name} />);
|
||||
|
||||
expect(callback).toBeCalledTimes(2);
|
||||
expect(callback.mock.calls[1][0]).toBe('first');
|
||||
});
|
||||
@@ -181,8 +181,8 @@ it("lets children handle the action if parent didn't", () => {
|
||||
<Screen name="baz">
|
||||
{() => (
|
||||
<ChildNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</ChildNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -291,8 +291,8 @@ it("action doesn't bubble if target is specified", () => {
|
||||
<Screen name="baz">
|
||||
{() => (
|
||||
<ChildNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</ChildNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
@@ -356,8 +356,8 @@ it('logs error if no navigator handled the action', () => {
|
||||
<Screen name="baz">
|
||||
{() => (
|
||||
<TestNavigator>
|
||||
<Screen name="qux" component={() => null} />
|
||||
<Screen name="lex" component={() => null} />
|
||||
<Screen name="qux">{() => null}</Screen>
|
||||
<Screen name="lex">{() => null}</Screen>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
|
||||
@@ -13,11 +13,17 @@ export default function createNavigatorFactory<
|
||||
ScreenOptions extends object,
|
||||
NavigatorComponent extends React.ComponentType<any>
|
||||
>(Navigator: NavigatorComponent) {
|
||||
return <ParamList extends ParamListBase>(): TypedNavigator<
|
||||
return function<ParamList extends ParamListBase>(): TypedNavigator<
|
||||
ParamList,
|
||||
ScreenOptions,
|
||||
typeof Navigator
|
||||
> => {
|
||||
> {
|
||||
if (arguments[0] !== undefined) {
|
||||
throw new Error(
|
||||
"Creating a navigator doesn't take an argument. Maybe you are trying to use React Navigation 4 API with React Navigation 5?"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
Navigator,
|
||||
Screen,
|
||||
|
||||
@@ -6,7 +6,13 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
type StringifyConfig = Record<string, (value: any) => string>;
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; stringify?: StringifyConfig };
|
||||
[routeName: string]:
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
stringify?: StringifyConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,22 +43,50 @@ type Options = {
|
||||
* @returns Path representing the state, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getPathFromState(
|
||||
state: State,
|
||||
state?: State,
|
||||
options: Options = {}
|
||||
): string {
|
||||
if (state === undefined) {
|
||||
throw Error('NavigationState not passed');
|
||||
}
|
||||
let path = '/';
|
||||
|
||||
let current: State | undefined = state;
|
||||
|
||||
while (current) {
|
||||
const index = typeof current.index === 'number' ? current.index : 0;
|
||||
const route = current.routes[index] as Route<string> & {
|
||||
state?: State | undefined;
|
||||
let index = typeof current.index === 'number' ? current.index : 0;
|
||||
let route = current.routes[index] as Route<string> & {
|
||||
state?: State;
|
||||
};
|
||||
let currentOptions = options;
|
||||
let pattern = route.name;
|
||||
|
||||
while (route.name in currentOptions) {
|
||||
if (typeof currentOptions[route.name] === 'string') {
|
||||
pattern = currentOptions[route.name] as string;
|
||||
break;
|
||||
} else if (typeof currentOptions[route.name] === 'object') {
|
||||
if (route.state === undefined) {
|
||||
pattern = (currentOptions[route.name] as { path: string }).path;
|
||||
break;
|
||||
} else {
|
||||
if (!(currentOptions[route.name] as { screens?: Options }).screens) {
|
||||
throw Error('Wrong Options object passed');
|
||||
}
|
||||
currentOptions = (currentOptions[route.name] as { screens: Options })
|
||||
.screens;
|
||||
index = typeof route.state.index === 'number' ? route.state.index : 0;
|
||||
route = route.state.routes[index] as Route<string> & {
|
||||
state?: State;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config =
|
||||
options[route.name] !== undefined
|
||||
? (options[route.name] as { stringify?: StringifyConfig }).stringify
|
||||
currentOptions[route.name] !== undefined
|
||||
? (currentOptions[route.name] as { stringify?: StringifyConfig })
|
||||
.stringify
|
||||
: undefined;
|
||||
|
||||
const params = route.params
|
||||
@@ -65,12 +99,7 @@ export default function getPathFromState(
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
if (options[route.name] !== undefined) {
|
||||
const pattern =
|
||||
typeof options[route.name] === 'string'
|
||||
? (options[route.name] as string)
|
||||
: (options[route.name] as { path: string }).path;
|
||||
|
||||
if (currentOptions[route.name] !== undefined) {
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map(p => {
|
||||
@@ -95,7 +124,11 @@ export default function getPathFromState(
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (params) {
|
||||
path += `?${queryString.stringify(params)}`;
|
||||
const query = queryString.stringify(params);
|
||||
|
||||
if (query) {
|
||||
path += `?${query}`;
|
||||
}
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
|
||||
@@ -5,14 +5,20 @@ import { NavigationState, PartialState, InitialState } from './types';
|
||||
type ParseConfig = Record<string, (value: string) => any>;
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; parse?: ParseConfig } | Options;
|
||||
[routeName: string]:
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
parse?: ParseConfig;
|
||||
screens?: Options;
|
||||
};
|
||||
};
|
||||
|
||||
type RouteConfig = {
|
||||
match: RegExp;
|
||||
pattern: string;
|
||||
routeNames: string[];
|
||||
parse: Record<string, (value: string) => any> | undefined;
|
||||
parse: ParseConfig | undefined;
|
||||
};
|
||||
|
||||
type ResultState = PartialState<NavigationState> & {
|
||||
@@ -42,6 +48,9 @@ export default function getStateFromPath(
|
||||
path: string,
|
||||
options: Options = {}
|
||||
): ResultState | undefined {
|
||||
if (path === '') {
|
||||
return undefined;
|
||||
}
|
||||
// Create a normalized configs array which will be easier to use
|
||||
const configs = ([] as RouteConfig[]).concat(
|
||||
...Object.keys(options).map(key => createNormalizedConfigs(key, options))
|
||||
@@ -65,7 +74,7 @@ export default function getStateFromPath(
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeNames = config.routeNames;
|
||||
routeNames = [...config.routeNames];
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
.split('/')
|
||||
@@ -164,7 +173,7 @@ export default function getStateFromPath(
|
||||
const route = current.routes[0];
|
||||
|
||||
const params = queryString.parse(query);
|
||||
const parseFunction = findParseConfigForRoute(route.name, options);
|
||||
const parseFunction = findParseConfigForRoute(route.name, configs);
|
||||
|
||||
if (parseFunction) {
|
||||
Object.keys(params).forEach(name => {
|
||||
@@ -185,7 +194,7 @@ function createNormalizedConfigs(
|
||||
routeConfig: Options,
|
||||
routeNames: string[] = []
|
||||
): RouteConfig[] {
|
||||
const configs = [];
|
||||
const configs: RouteConfig[] = [];
|
||||
|
||||
routeNames.push(key);
|
||||
|
||||
@@ -196,30 +205,19 @@ function createNormalizedConfigs(
|
||||
configs.push(createConfigItem(routeNames, value));
|
||||
} else if (typeof value === 'object') {
|
||||
// if an object is specified as the value (e.g. Foo: { ... }),
|
||||
// it could have config object and optionally nested config
|
||||
Object.keys(value).forEach(nestedKey => {
|
||||
if (nestedKey === 'path') {
|
||||
configs.push(
|
||||
createConfigItem(
|
||||
routeNames,
|
||||
value[nestedKey] as string,
|
||||
value.parse ? (value.parse as ParseConfig) : undefined
|
||||
)
|
||||
);
|
||||
} else if (nestedKey === 'parse') {
|
||||
// We handle custom parse function when a `path` is specified (in nestedKey === path)
|
||||
} else {
|
||||
// If the name of the key is not `path` or `parse`, it's a nested config for route
|
||||
// So we need to traverse into it and collect the configs
|
||||
// it has `path` property and
|
||||
// it could have `screens` prop which has nested configs
|
||||
configs.push(createConfigItem(routeNames, value.path, value.parse));
|
||||
if (value.screens) {
|
||||
Object.keys(value.screens).forEach(nestedConfig => {
|
||||
const result = createNormalizedConfigs(
|
||||
nestedKey,
|
||||
routeConfig[key] as Options,
|
||||
nestedConfig,
|
||||
value.screens as Options,
|
||||
routeNames
|
||||
);
|
||||
|
||||
configs.push(...result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
routeNames.pop();
|
||||
@@ -247,21 +245,12 @@ function createConfigItem(
|
||||
|
||||
function findParseConfigForRoute(
|
||||
routeName: string,
|
||||
config: Options
|
||||
flatConfig: RouteConfig[]
|
||||
): ParseConfig | undefined {
|
||||
if (config[routeName]) {
|
||||
return (config[routeName] as { parse?: ParseConfig }).parse;
|
||||
}
|
||||
|
||||
for (const name in config) {
|
||||
if (typeof config[name] === 'object') {
|
||||
const parse = findParseConfigForRoute(routeName, config[name] as Options);
|
||||
|
||||
if (parse) {
|
||||
return parse;
|
||||
}
|
||||
for (const config of flatConfig) {
|
||||
if (routeName === config.routeNames[config.routeNames.length - 1]) {
|
||||
return config.parse;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export { default as useNavigation } from './useNavigation';
|
||||
export { default as useRoute } from './useRoute';
|
||||
export { default as useFocusEffect } from './useFocusEffect';
|
||||
export { default as useIsFocused } from './useIsFocused';
|
||||
export { default as useNavigationState } from './useNavigationState';
|
||||
|
||||
export { default as getStateFromPath } from './getStateFromPath';
|
||||
export { default as getPathFromState } from './getPathFromState';
|
||||
|
||||
47
packages/core/src/isSerializable.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
const isSerializableWithoutCircularReference = (
|
||||
o: { [key: string]: any },
|
||||
seen = new Set<any>()
|
||||
): boolean => {
|
||||
if (
|
||||
o === undefined ||
|
||||
o === null ||
|
||||
typeof o === 'boolean' ||
|
||||
typeof o === 'number' ||
|
||||
typeof o === 'string'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.toString.call(o) !== '[object Object]' &&
|
||||
!Array.isArray(o)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seen.has(o)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.add(o);
|
||||
|
||||
if (Array.isArray(o)) {
|
||||
for (const it of o) {
|
||||
if (!isSerializableWithoutCircularReference(it, seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const key in o) {
|
||||
if (!isSerializableWithoutCircularReference(o[key], seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default function isSerializable(o: { [key: string]: any }) {
|
||||
return isSerializableWithoutCircularReference(o);
|
||||
}
|
||||
@@ -16,6 +16,10 @@ export type NavigationState = {
|
||||
* List of valid route names as defined in the screen components.
|
||||
*/
|
||||
routeNames: string[];
|
||||
/**
|
||||
* Alternative entries for history.
|
||||
*/
|
||||
history?: unknown[];
|
||||
/**
|
||||
* List of rendered routes.
|
||||
*/
|
||||
@@ -207,31 +211,52 @@ export type Router<
|
||||
|
||||
export type ParamListBase = Record<string, object | undefined>;
|
||||
|
||||
export type EventMapBase = {
|
||||
focus: undefined;
|
||||
blur: undefined;
|
||||
export type EventMapBase = Record<
|
||||
string,
|
||||
{ data?: any; canPreventDefault?: boolean }
|
||||
>;
|
||||
|
||||
export type EventMapCore<State extends NavigationState> = {
|
||||
focus: { data: undefined };
|
||||
blur: { data: undefined };
|
||||
state: { data: { state: State } };
|
||||
};
|
||||
|
||||
export type EventArg<EventName extends string, Data = undefined> = {
|
||||
export type EventArg<
|
||||
EventName extends string,
|
||||
CanPreventDefault extends boolean | undefined = false,
|
||||
Data = undefined
|
||||
> = {
|
||||
/**
|
||||
* Type of the event (e.g. `focus`, `blur`)
|
||||
*/
|
||||
readonly type: EventName;
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
} & (Data extends undefined ? {} : { readonly data: Data });
|
||||
} & (CanPreventDefault extends true
|
||||
? {
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
}
|
||||
: {}) &
|
||||
(Data extends undefined ? {} : { readonly data: Data });
|
||||
|
||||
export type EventListenerCallback<EventName extends string, Data> = (
|
||||
e: EventArg<EventName, Data>
|
||||
export type EventListenerCallback<
|
||||
EventMap extends EventMapBase,
|
||||
EventName extends keyof EventMap
|
||||
> = (
|
||||
e: EventArg<
|
||||
Extract<EventName, string>,
|
||||
EventMap[EventName]['canPreventDefault'],
|
||||
EventMap[EventName]['data']
|
||||
>
|
||||
) => void;
|
||||
|
||||
export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
export type EventConsumer<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Subscribe to events from the parent navigator.
|
||||
*
|
||||
@@ -240,15 +265,15 @@ export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
*/
|
||||
addListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): () => void;
|
||||
removeListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): void;
|
||||
};
|
||||
|
||||
export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
export type EventEmitter<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Emit an event to child screens.
|
||||
*
|
||||
@@ -261,10 +286,17 @@ export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
options: {
|
||||
type: EventName;
|
||||
target?: string;
|
||||
} & (EventMap[EventName] extends undefined
|
||||
? {}
|
||||
: { data: EventMap[EventName] })
|
||||
): EventArg<EventName, EventMap[EventName]>;
|
||||
} & (EventMap[EventName]['canPreventDefault'] extends true
|
||||
? { canPreventDefault: true }
|
||||
: {}) &
|
||||
(EventMap[EventName]['data'] extends undefined
|
||||
? {}
|
||||
: { data: EventMap[EventName]['data'] })
|
||||
): EventArg<
|
||||
EventName,
|
||||
EventMap[EventName]['canPreventDefault'],
|
||||
EventMap[EventName]['data']
|
||||
>;
|
||||
};
|
||||
|
||||
export class PrivateValueStore<A, B, C> {
|
||||
@@ -277,10 +309,8 @@ export class PrivateValueStore<A, B, C> {
|
||||
* The problem with a normal property is that it shows up in intelliSense.
|
||||
* Adding private keyword works, but the annotation is stripped away in declaration.
|
||||
* Turns out if we use an empty string, it doesn't show up in intelliSense.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
''?: { a: A; b: B; c: C };
|
||||
protected ''?: { a: A; b: B; c: C };
|
||||
}
|
||||
|
||||
type NavigationHelpersCommon<
|
||||
@@ -339,13 +369,6 @@ type NavigationHelpersCommon<
|
||||
*/
|
||||
reset(state: PartialState<State> | State): void;
|
||||
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||
|
||||
/**
|
||||
* Go back to the previous route in history.
|
||||
*/
|
||||
@@ -368,7 +391,7 @@ type NavigationHelpersCommon<
|
||||
|
||||
export type NavigationHelpers<
|
||||
ParamList extends ParamListBase,
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList> &
|
||||
EventEmitter<EventMap> & {
|
||||
/**
|
||||
@@ -408,7 +431,7 @@ export type NavigationProp<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList, State> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
@@ -439,7 +462,7 @@ export type NavigationProp<
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
dangerouslyGetState(): State;
|
||||
} & EventConsumer<EventMap & EventMapBase> &
|
||||
} & EventConsumer<EventMap & EventMapCore<State>> &
|
||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||
|
||||
export type RouteProp<
|
||||
@@ -493,7 +516,7 @@ export type Descriptor<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = {
|
||||
/**
|
||||
* Render the component associated with this route.
|
||||
@@ -546,10 +569,7 @@ export type RouteConfig<
|
||||
/**
|
||||
* React component to render for this screen.
|
||||
*/
|
||||
component: React.ComponentType<{
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: any;
|
||||
}>;
|
||||
component: React.ComponentType<any>;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
@@ -563,15 +583,21 @@ export type RouteConfig<
|
||||
);
|
||||
|
||||
export type NavigationContainerRef =
|
||||
| (NavigationHelpers<ParamListBase> & {
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(state?: PartialState<NavigationState> | NavigationState): void;
|
||||
getRootState(): NavigationState;
|
||||
})
|
||||
| (NavigationHelpers<ParamListBase> &
|
||||
EventConsumer<{ state: { data: { state: NavigationState } } }> & {
|
||||
/**
|
||||
* Reset the navigation state of the root navigator to the provided state.
|
||||
*
|
||||
* @param state Navigation state object.
|
||||
*/
|
||||
resetRoot(
|
||||
state?: PartialState<NavigationState> | NavigationState
|
||||
): void;
|
||||
/**
|
||||
* Get the rehydrated navigation state of the navigation tree.
|
||||
*/
|
||||
getRootState(): NavigationState;
|
||||
})
|
||||
| undefined
|
||||
| null;
|
||||
|
||||
|
||||