diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..ee3eef08f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,34 @@ +{ + "env": { + // For RN example development + "development": { + "presets": ["react-native"], + "plugins": [ + "transform-flow-strip-types" + ] + }, + // For Jest + "test": { + "presets": ["react-native"], + "plugins": [ + "transform-flow-strip-types" + ] + }, + // For publishing to NPM for RN + "publish-rn": { + "presets": ["react-native-syntax"], + "plugins": [ + "flow-react-proptypes", + "transform-flow-strip-types" + ] + }, + // For publishing to NPM for web + "publish-web": { + "presets": ["es2015", "stage-1", "react"], + "plugins": [ + "flow-react-proptypes", + "transform-flow-strip-types" + ] + } + } +} diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..cbf6b65a52 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,2 @@ +comment: + require_changes: yes diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..4cde307091 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.gradle] +indent_size = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..e0665bbf69 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +coverage +flow-typed +node_modules +lib* + +## Temporary +examples diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..415cde435f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,75 @@ +{ + "extends": [ + "plugin:flowtype/recommended", + "plugin:react/recommended", + "plugin:import/errors", + "plugin:import/warnings", + "prettier", + "prettier/flowtype", + "prettier/react" + ], + "parser": "babel-eslint", + "plugins": [ + "react", + "flowtype", + "prettier" + ], + "env": { + "jasmine": true + }, + "globals": { + "ReactClass": true + }, + "rules": { + "prettier/prettier": ["error", { + "trailingComma": "es5", + "singleQuote": true + }], + + "no-underscore-dangle": 0, + "no-use-before-define": 0, + "no-unused-expressions": 0, + "new-cap": 0, + "no-plusplus": 0, + "no-class-assign": 0, + "no-duplicate-imports": 0, + + "import/extensions": 0, + "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, + + "react/jsx-filename-extension": [ + 0, { "extensions": [".js", ".jsx"] } + ], + + "react/sort-comp": 0, + "react/prefer-stateless-function": 0, + + "react/forbid-prop-types": 1, + "react/prop-types": 0, + "react/require-default-props": 0, + "react/no-unused-prop-types": 0, + + "flowtype/boolean-style": [ + 2, + "boolean" + ], + "flowtype/no-weak-types": 1, + "flowtype/require-parameter-type": 2, + "flowtype/require-return-type": [ + 0, + "always", + { + "annotateUndefined": "never" + } + ], + "flowtype/require-valid-file-annotation": 2, + "flowtype/use-flow-type": 1, + "flowtype/valid-syntax": 1 + }, + "settings": { + "flowtype": { + "onlyFilesWithFlowAnnotation": true + } + } +} diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..e808a8edf5 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,60 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore templates for 'react-native init' +.*/local-cli/templates/.* + +; Ignore the website subdir +.*/node_modules/react-native/website/.* + +; Ignore "BUCK" generated dirs +.*/node_modules/react-native/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore website node_modules react and react-native +/website/node_modules/react/.* +/website/node_modules/react-native/.* +/website/node_modules/fbjs/.* + +; Ignore misc packages +.*/node_modules/eslint-.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +.*/node_modules/react-native/Libraries/react-native/React.js +.*/node_modules/react-native/Libraries/react-native/ReactNative.js + +/lib +/lib-rn +/examples + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow/ +flow/ + +[options] +module.system=haste + +experimental.strict_type_args=true + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(4[0-2]\\|[1-3][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +unsafe.enable_getters_and_setters=true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 57266e09d9..0000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: react-navigation diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..6bf289c8c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,22 @@ + + +### Current Behavior + + + +### Expected Behavior + + + +### Your Environment + + +| software | version +| ---------------- | ------- +| react-navigation | +| react-native | +| node | +| npm or yarn | diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml deleted file mode 100644 index 41fc659db5..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Bug report -description: Report an issue with React Navigation -labels: [bug] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - If this is not a bug report, please use other relevant channels: - - - [Post a feature request on Canny](https://react-navigation.canny.io/feature-requests) - - [Ask questions on StackOverflow using the react-navigation label](https://stackoverflow.com/questions/tagged/react-navigation) - - [Chat with others in the #help-react-native channel on Discord](https://www.reactiflux.com/) - - Before you proceed: - - - Make sure you are on latest versions of React Navigation packages and their dependencies. - - If you are having an issue with your machine or build tools, the issue belongs on another repository as that is outside of the scope of React Navigation. - - - type: textarea - attributes: - label: Current behavior - description: | - What code are you running and what is happening? Include a screenshot or video if it's an UI related issue. - placeholder: Current behavior - validations: - required: true - - type: textarea - attributes: - label: Expected behavior - description: | - What do you expect to happen instead? - placeholder: Expected behavior - validations: - required: true - - type: input - attributes: - label: Reproduction - description: | - You must provide a way to reproduce the problem. If you don't provide a repro, the issue will be closed automatically after a specific period. - - - Provide a link to a public GitHub repository under your username that reproduces the bug. - - For TypeScript related issues, use [TypeScript Playground](https://www.typescriptlang.org/play) to reproduce the issue. - - Keep the repro code as simple as possible, with the minimum amount of code required to repro the issue. - placeholder: Link to repro - validations: - required: true - - type: checkboxes - attributes: - label: Platform - description: | - What are the platforms where you see the issue? - options: - - label: Android - - label: iOS - - label: Web - - label: Windows - - label: MacOS - - type: checkboxes - attributes: - label: Packages - description: | - Which packages are affected by the issue? - options: - - label: '@react-navigation/bottom-tabs' - - label: '@react-navigation/drawer' - - label: '@react-navigation/material-top-tabs' - - label: '@react-navigation/stack' - - label: '@react-navigation/native-stack' - - label: 'react-native-drawer-layout' - - label: 'react-native-tab-view' - - type: textarea - attributes: - label: Environment - description: | - What are the exact versions of packages that you are using? - - When filling the table below, **please remove the packages** that you're not using. - value: | - - [] I've removed the packages that I don't use - - | package | version | - | -------------------------------------- | ------- | - | @react-navigation/native | - | @react-navigation/bottom-tabs | - | @react-navigation/drawer | - | @react-navigation/material-top-tabs | - | @react-navigation/stack | - | @react-navigation/native-stack | - | react-native-drawer-layout | - | react-native-tab-view | - | react-native-screens | - | react-native-safe-area-context | - | react-native-gesture-handler | - | react-native-reanimated | - | react-native-pager-view | - | react-native | - | expo | - | node | - | npm or pnpm | - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 5eb9fcb595..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,20 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Troubleshooting - url: https://reactnavigation.org/docs/troubleshooting - about: Read how to troubleshoot and fix common issues and mistakes. - - name: Documentation - url: https://reactnavigation.org - about: Read the official documentation. - - name: Feature request - url: https://react-navigation.canny.io/feature-requests - about: Post a feature request on Canny. - - name: Discussion - url: https://github.com/react-navigation/react-navigation/discussions - about: Discuss questions, ideas etc. and share resources related to the library. - - name: StackOverflow - url: https://stackoverflow.com/questions/tagged/react-navigation - about: Ask and answer questions using the react-navigation label. - - name: Reactiflux - url: https://www.reactiflux.com/ - about: Chat with other community members in the `help-react-native` channel. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9c7555c5e4..67820a3a74 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,17 @@ -Please provide enough information so that others can review your pull request. - -**Motivation** +Please provide enough information so that others can review your pull request: Explain the **motivation** for making this change. What existing problem does the pull request solve? -If this pull request addresses an existing issue, link to the issue. If an issue is not present, describe the issue here. +Prefer **small pull requests**. These are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise split it. + +**Test plan (required)** + +Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. + +Make sure you test on both platforms if your change affects both platforms. -**Test plan** +The code must pass tests and shouldn't add more Flow errors. -Describe the **steps to test this change** so that a reviewer can verify it. Provide screenshots or videos if the change affects UI. +**Code formatting** -The change must pass lint, typescript and tests. +Look around. Match the style of the rest of the codebase. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml deleted file mode 100644 index 708cf66864..0000000000 --- a/.github/actions/setup/action.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Setup -description: Setup Node.js and install dependencies - -runs: - using: composite - steps: - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e - with: - node-version-file: .nvmrc - - - name: Install pnpm - run: npm install --global pnpm@11.8.0 - shell: bash - - - name: Get pnpm store path - id: pnpm-store - run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - shell: bash - - - name: Restore pnpm store - id: pnpm-cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ${{ steps.pnpm-store.outputs.path }} - key: ${{ runner.os }}-pnpm-store-v1-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v1- - - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline - shell: bash - - - name: Cache pnpm store - if: steps.pnpm-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ${{ steps.pnpm-store.outputs.path }} - key: ${{ steps.pnpm-cache.outputs.cache-primary-key }} diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml deleted file mode 100644 index 1a308e7e4e..0000000000 --- a/.github/workflows/autofix.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: autofix.ci - -on: - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - autofix: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Fix lint issues - run: pnpm lint --fix - - - name: Autofix - uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a diff --git a/.github/workflows/check-labels.yml b/.github/workflows/check-labels.yml deleted file mode 100644 index da14bb2c11..0000000000 --- a/.github/workflows/check-labels.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Check for labels -on: - issues: - types: - - opened - - edited - -permissions: - issues: write - -jobs: - check-labels: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const body = context.payload.issue.body; - - const packages = Array.from( - body.matchAll(/- \[x\] (@react-navigation\/([\S]+)|react-native-drawer-layout|react-native-tab-view)/gim) - ) - .map((match) => match[2] ?? match[1]) - .filter((name) => - [ - 'bottom-tabs', - 'drawer', - 'material-top-tabs', - 'stack', - 'native-stack', - 'react-native-drawer-layout', - 'react-native-tab-view', - ].includes(name) - ) - .map((name) => `package:${name}`); - - const platforms = Array.from( - body.matchAll(/- \[x\] (Android|iOS|Web|Windows|MacOS)/gim) - ).map((matches) => `platform:${matches[1].toLowerCase()}`); - - const labels = [...packages, ...platforms]; - - if (labels.length) { - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels, - }); - } diff --git a/.github/workflows/check-repro.yml b/.github/workflows/check-repro.yml deleted file mode 100644 index f5e5af69a2..0000000000 --- a/.github/workflows/check-repro.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Check for repro -on: - issues: - types: - - opened - - edited - - labeled - issue_comment: - types: - - created - - edited - -permissions: - issues: write - -jobs: - check-repro: - runs-on: ubuntu-latest - if: (github.event.action == 'labeled' && github.event.label.name == 'needs repro') || github.event.action != 'labeled' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const user = context.payload.comment - ? context.payload.comment.user.login - : context.payload.issue.user.login; - const body = context.payload.comment - ? context.payload.comment.body - : context.payload.issue.body; - - const regex = new RegExp( - `https?:\\/\\/((github\\.com\\/${user}\\/[^/\\s]+(?:\\/tree\\/[^\\s]+)?\\/?(?:\\s|$))|(www\\.typescriptlang\\.org\\/play\\?.+))`, - 'gm' - ); - - if (context.payload.action != 'labeled' && regex.test(body)) { - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['repro provided'], - }); - - try { - await github.rest.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: 'needs repro', - }); - } catch (error) { - if (!/Label does not exist/.test(error.message)) { - throw error; - } - } - } else { - if (context.eventName !== 'issues') { - return; - } - - const body = `Hey @${user}! Thanks for opening the issue. It seems that the issue doesn't contain a link to a repro, or the provided repro is not valid (e.g. broken link, private repo, code doesn't run etc.). - - **The best way to get attention to your issue is to provide an easy way for a developer to reproduce the issue.** - - You can provide a repro using any of the following: - - - Public GitHub repo under your username - - [TypeScript Playground](https://www.typescriptlang.org/play) - - Try to keep the repro as small as possible by narrowing down the minimal amount of code needed to reproduce the issue. See ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example) for more information. - - **Don't:** - - - Link to your entire project or a project containing code unrelated to the issue. - - Link to a specific file in a GitHub repo, as it won't be detected. - - You can edit your original issue to include a link to the repro, or leave it as a comment. **The issue will be closed automatically after a while** if you don't provide a valid repro.` - - const comments = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }); - - if (comments.data.some((comment) => comment.body === body)) { - return; - } - - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body, - }); - - await github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['needs repro'], - }); - } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 5bbbed14ca..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: CI -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - dependencies: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Verify dependency versions - run: pnpm versions check - - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Lint files - run: pnpm lint - - - name: Typecheck files - run: | - pnpm typecheck - pnpm typecheck:standalone:static - pnpm typecheck:standalone:dynamic - pnpm typecheck:declarations - - unit-test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Run unit tests - run: pnpm test --maxWorkers=2 --coverage - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 - - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Build packages in the monorepo - run: pnpm lerna run prepack - - - name: Verify types are correct - run: pnpm typecheck - - - name: Bundle example JavaScript with Metro - run: pnpm example exec expo export --platform all - - - name: Bundle example JavaScript with Vite - run: pnpm example web build diff --git a/.github/workflows/closed-issue.yml b/.github/workflows/closed-issue.yml deleted file mode 100644 index 396f1ec57c..0000000000 --- a/.github/workflows/closed-issue.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Comment on closed issue -on: - issue_comment: - types: - - created - -permissions: - issues: write - -jobs: - closed-issue: - runs-on: ubuntu-latest - if: ${{ github.event.issue.state == 'closed' && !github.event.issue.pull_request }} - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const body = "Hey! This issue is closed and isn't watched by the core team. You are welcome to discuss the issue with others in this thread, but if you think this issue is still valid and needs to be tracked, please open a new issue with a repro."; - - const comments = await github.paginate(github.rest.issues.listComments, { - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - }); - - if (comments.some((comment) => comment.body === body)) { - return; - } - - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body, - }); diff --git a/.github/workflows/e2e-android.yml b/.github/workflows/e2e-android.yml deleted file mode 100644 index 38460ef959..0000000000 --- a/.github/workflows/e2e-android.yml +++ /dev/null @@ -1,182 +0,0 @@ -name: E2E / Android -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - maestro-android: - runs-on: ubuntu-latest - timeout-minutes: 60 - permissions: - checks: read - contents: read - concurrency: - group: ${{ github.workflow_ref }}-${{ github.ref }} - cancel-in-progress: true - env: - MAESTRO_VERSION: 2.2.0 - MAESTRO_ZIP_SHA256: 6de501d2e8adf2d60f4b6b3174dc4b5e393f2f2617245d350a659627dccb0922 - MAESTRO_CLI_NO_ANALYTICS: 1 - MAESTRO_DRIVER_STARTUP_TIMEOUT: 600000 - JAVA_DISTRIBUTION: 'zulu' - JAVA_VERSION: '17' - ANDROID_HOME: /usr/local/lib/android/sdk - ANDROID_USER_HOME: /home/runner/.android - API_LEVEL: 34 - AVD_NAME: e2e_emulator - EMULATOR_PROFILE: pixel_5 - EMULATOR_OPTIONS: '-no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -partition-size 4096 -cores 2 -memory 4096' - ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Calculate fingerprint - id: calculate-fingerprint - run: | - FINGERPRINT=$(pnpm exec expo-updates fingerprint:generate --platform android | jq -r '.hash') - echo "Fingerprint for Android: $FINGERPRINT" - echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" - working-directory: example - - - name: Restore debug build from cache - id: restore-debug-build - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: android-debug-build/ - key: android-debug-build-${{ steps.calculate-fingerprint.outputs.fingerprint }} - - - name: Maximize build space - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: AdityaGarg8/remove-unwanted-software@90e01b21170618765a73370fcc3abbd1684a7793 - with: - remove-dotnet: 'true' - remove-haskell: 'true' - remove-codeql: 'true' - remove-docker-images: 'true' - - - name: Install Maestro - run: | - maestro_dir="$HOME/.maestro" - maestro_tmp_folder="${RUNNER_TEMP:-/tmp}/maestro" - maestro_zip_file="$maestro_tmp_folder/maestro.zip" - maestro_url="https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" - - mkdir -p "$maestro_dir" "$maestro_tmp_folder" - curl -fsSL "$maestro_url" -o "$maestro_zip_file" - printf '%s %s\n' "$MAESTRO_ZIP_SHA256" "$maestro_zip_file" | shasum -a 256 -c - - - unzip -q "$maestro_zip_file" -d "$maestro_tmp_folder" - rm -rf "${maestro_dir:?}/bin" "${maestro_dir:?}/lib" - cp -R "$maestro_tmp_folder/maestro/." "$maestro_dir" - - echo "$maestro_dir/bin" >> "$GITHUB_PATH" - - - name: Install AVD dependencies - run: | - sudo apt update - sudo apt-get install -y libpulse0 libgl1 libxkbfile1 - - - name: Setup JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 - with: - distribution: ${{ env.JAVA_DISTRIBUTION }} - java-version: ${{ env.JAVA_VERSION }} - - - name: Cache Gradle - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- - - - name: Build Android App - if: steps.restore-debug-build.outputs.cache-hit != 'true' - run: | - pnpm expo prebuild --platform android - cd android - ./gradlew assembleDebug -DtestBuildType=debug -Dorg.gradle.jvmargs=-Xmx4g - working-directory: example - - - name: Prepare cache folder - if: steps.restore-debug-build.outputs.cache-hit != 'true' - run: | - mkdir android-debug-build - mv example/android/app/build/outputs/apk/debug/app-debug.apk android-debug-build/android-debug.apk - - - name: Store debug build in cache - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: android-debug-build/ - key: ${{ steps.restore-debug-build.outputs.cache-primary-key }} - - - name: Setup Android SDK - run: | - { - echo "$ANDROID_HOME/emulator" - echo "$ANDROID_HOME/platform-tools" - echo "$ANDROID_HOME/cmdline-tools/latest/bin" - } >> "$GITHUB_PATH" - - - name: Install Android SDK components - run: | - yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null 2>&1 || true - "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install emulator "platform-tools" "platforms;android-${{ env.API_LEVEL }}" "system-images;android-${{ env.API_LEVEL }};google_apis;x86_64" - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Create AVD - run: | - avdmanager create avd -n "${{ env.AVD_NAME }}" -d "${{ env.EMULATOR_PROFILE }}" -k "system-images;android-${{ env.API_LEVEL }};google_apis;x86_64" - - - name: Start Metro bundler - run: pnpm --dir example start & - - - name: Start emulator - timeout-minutes: 10 - run: | - "$ANDROID_HOME/emulator/emulator" -avd "${{ env.AVD_NAME }}" ${{ env.EMULATOR_OPTIONS }} & - adb wait-for-device - adb shell "while [[ -z \$(getprop sys.boot_completed) ]]; do sleep 1; done" - - - name: Wait for Metro bundler - timeout-minutes: 5 - run: | - until curl -sSf "http://localhost:8081/.expo/.virtual-metro-entry.bundle?platform=android&dev=true&minify=false" > /dev/null 2>&1; do - sleep 2 - done - - - name: Run Maestro tests - run: | - adb install android-debug-build/android-debug.apk - cd example - pnpm e2e:native --platform android - - - name: Kill emulator - if: always() - run: adb emu kill || true - - - name: Store debug output - if: failure() || cancelled() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: maestro-debug-output-android - path: example/maestro-debug-output/**/* - include-hidden-files: true - overwrite: true diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml deleted file mode 100644 index 9856d8e7c7..0000000000 --- a/.github/workflows/e2e-ios.yml +++ /dev/null @@ -1,211 +0,0 @@ -name: E2E / iOS -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - maestro-ios: - runs-on: macos-latest - timeout-minutes: 60 - permissions: - checks: read - contents: read - concurrency: - group: ${{ github.workflow_ref }}-${{ github.ref }} - cancel-in-progress: true - env: - MAESTRO_VERSION: 2.2.0 - MAESTRO_ZIP_SHA256: 6de501d2e8adf2d60f4b6b3174dc4b5e393f2f2617245d350a659627dccb0922 - MAESTRO_CLI_NO_ANALYTICS: 1 - MAESTRO_DRIVER_STARTUP_TIMEOUT: 600000 - JAVA_DISTRIBUTION: 'zulu' - JAVA_VERSION: '17' - SIMULATOR_NAME: iPhone 17 Pro - USE_CCACHE: 1 - CCACHE_COMPILERCHECK: content - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Setup - uses: ./.github/actions/setup - - - name: Calculate fingerprint - id: calculate-fingerprint - run: | - FINGERPRINT=$(pnpm exec expo-updates fingerprint:generate --platform ios | jq -r '.hash') - echo "Fingerprint for iOS: $FINGERPRINT" - echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" - working-directory: example - - - name: Restore debug build from cache - id: restore-debug-build - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ios-debug-build/ - key: ios-debug-build-${{ steps.calculate-fingerprint.outputs.fingerprint }} - - - name: Use appropriate Xcode version - uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 - with: - xcode-version: '26.3' - - - name: Install Maestro - run: | - maestro_dir="$HOME/.maestro" - maestro_tmp_folder="${RUNNER_TEMP:-/tmp}/maestro" - maestro_zip_file="$maestro_tmp_folder/maestro.zip" - maestro_url="https://github.com/mobile-dev-inc/Maestro/releases/download/cli-${MAESTRO_VERSION}/maestro.zip" - - mkdir -p "$maestro_dir" "$maestro_tmp_folder" - curl -fsSL "$maestro_url" -o "$maestro_zip_file" - printf '%s %s\n' "$MAESTRO_ZIP_SHA256" "$maestro_zip_file" | shasum -a 256 -c - - - unzip -q "$maestro_zip_file" -d "$maestro_tmp_folder" - rm -rf "${maestro_dir:?}/bin" "${maestro_dir:?}/lib" - cp -R "$maestro_tmp_folder/maestro/." "$maestro_dir" - - echo "$maestro_dir/bin" >> "$GITHUB_PATH" - - - name: Install macOS dependencies - run: | - brew tap facebook/fb - brew install facebook/fb/idb-companion ccache - - - name: Cache CocoaPods - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: | - example/ios/Pods - ~/.cocoapods - ~/Library/Caches/CocoaPods - key: ios-pods-${{ runner.os }}-${{ steps.calculate-fingerprint.outputs.fingerprint }} - restore-keys: | - ios-pods-${{ runner.os }}- - - - name: Cache DerivedData - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: example/ios/build - key: ios-deriveddata-${{ runner.os }}-${{ steps.calculate-fingerprint.outputs.fingerprint }} - restore-keys: | - ios-deriveddata-${{ runner.os }}- - - - name: Cache ccache - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ~/.ccache - key: ios-ccache-${{ runner.os }}-${{ steps.calculate-fingerprint.outputs.fingerprint }} - restore-keys: | - ios-ccache-${{ runner.os }}- - - - name: Set ccache directory - if: steps.restore-debug-build.outputs.cache-hit != 'true' - run: | - echo "CCACHE_DIR=$HOME/.ccache" >> "$GITHUB_ENV" - - - name: Build iOS App - if: steps.restore-debug-build.outputs.cache-hit != 'true' - run: | - pnpm expo prebuild --platform ios - xcodebuild ONLY_ACTIVE_ARCH=YES \ - -workspace ios/ReactNavigationExample.xcworkspace \ - -UseNewBuildSystem=YES \ - -scheme ReactNavigationExample \ - -configuration Debug \ - -destination 'generic/platform=iOS Simulator' \ - -sdk iphonesimulator \ - -derivedDataPath ios/build \ - -quiet - working-directory: example - - - name: Prepare cache folder - if: steps.restore-debug-build.outputs.cache-hit != 'true' - run: | - mkdir ios-debug-build - mv example/ios/build/Build/Products/Debug-iphonesimulator/ReactNavigationExample.app ios-debug-build/ios-debug.app - - - name: Store debug build in cache - if: steps.restore-debug-build.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ios-debug-build/ - key: ${{ steps.restore-debug-build.outputs.cache-primary-key }} - - - name: Setup JDK - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 - with: - distribution: ${{ env.JAVA_DISTRIBUTION }} - java-version: ${{ env.JAVA_VERSION }} - - - name: Start Metro bundler - run: pnpm --dir example start & - - - name: Boot iOS Simulator - timeout-minutes: 5 - run: | - DEVICE_ID=$(xcrun simctl list devices available | grep "$SIMULATOR_NAME" | head -n 1 | awk -F "[()]" '{print $2}') - - if [ -z "$DEVICE_ID" ]; then - echo "No available simulator found for the criteria." - xcrun simctl list devices available - exit 1 - fi - - echo "Booting $SIMULATOR_NAME simulator with DEVICE_ID=$DEVICE_ID..." - xcrun simctl boot "$DEVICE_ID" || true - - while ! xcrun simctl list devices | grep "$DEVICE_ID" | grep -q "(Booted)"; do - sleep 2 - done - - echo "Simulator is booted!" - - # Add deep link handler so we don't see a prompt when running tests - /usr/libexec/PlistBuddy "$HOME/Library/Developer/CoreSimulator/Devices/${DEVICE_ID}/data/Library/Preferences/com.apple.launchservices.schemeapproval.plist" -c "add com.apple.CoreSimulator.CoreSimulatorBridge-->rne string org.reactnavigation.playground" - - - name: Wait for Metro bundler - timeout-minutes: 5 - run: | - until curl -sSf "http://localhost:8081/.expo/.virtual-metro-entry.bundle?platform=ios&dev=true&minify=false" > /dev/null 2>&1; do - sleep 2 - done - - - name: Run Maestro tests - run: | - xcrun simctl spawn booted log stream --style syslog > ~/maestro-simulator.log 2>&1 & # Start capturing simulator logs in the background - xcrun simctl install booted ../ios-debug-build/ios-debug.app - pnpm e2e:native --platform ios - working-directory: example - - - name: Shutdown simulator - if: always() - run: xcrun simctl shutdown booted || true - - - name: Store debug output - if: failure() || cancelled() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: maestro-debug-output-ios - path: example/maestro-debug-output/**/* - include-hidden-files: true - overwrite: true - - - name: Store device logs - if: failure() || cancelled() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: maestro-simulator-output - path: | - ~/Library/Logs/CoreSimulator/* - ~/Library/Logs/maestro/xctest_runner_logs/* - ~/maestro-simulator.log - include-hidden-files: true - overwrite: true diff --git a/.github/workflows/e2e-web.yml b/.github/workflows/e2e-web.yml deleted file mode 100644 index f9b02b8dab..0000000000 --- a/.github/workflows/e2e-web.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: E2E / Web -on: - push: - branches: - - main - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - playwright: - runs-on: ubuntu-latest - timeout-minutes: 60 - concurrency: - group: ${{ github.workflow_ref }}-${{ github.ref }} - cancel-in-progress: true - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - - name: Setup - uses: ./.github/actions/setup - - - name: Get Playwright version - id: playwright - run: | - VERSION=$(pnpm example exec node --print "require('@playwright/test/package.json').version") - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Restore Playwright - id: playwright-cache - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: | - ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.VERSION }} - restore-keys: | - ${{ runner.os }}-playwright- - - - name: Install Playwright - run: pnpm example exec playwright install --with-deps - - - name: Cache Playwright - if: steps.playwright-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae - with: - path: ~/.cache/ms-playwright - key: ${{ steps.playwright-cache.outputs.cache-primary-key }} - - - name: Run e2e tests - run: pnpm example e2e:web --retries=3 diff --git a/.github/workflows/measure-typescript.yml b/.github/workflows/measure-typescript.yml deleted file mode 100644 index 9d58604419..0000000000 --- a/.github/workflows/measure-typescript.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: TypeScript Benchmark - -on: - pull_request: - branches: - - main - paths: - - '**/*.ts' - - '**/*.tsx' - - '**/tsconfig*.json' - - '**/package.json' - - 'pnpm-lock.yaml' - - 'scripts/measure-typescript.ts' - - '.github/workflows/measure-typescript.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - measure: - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - report: ${{ steps.measure.outputs.report }} - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup - uses: ./.github/actions/setup - - - name: Measure TypeScript performance - id: measure - run: | - { - echo 'report<> "$GITHUB_OUTPUT" - - comment: - needs: measure - runs-on: ubuntu-latest - permissions: - pull-requests: write - steps: - - name: Comment noticeable variations - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - env: - REPORT: ${{ needs.measure.outputs.report }} - with: - script: | - const MARKER = ''; - - const output = process.env.REPORT ?? ''; - const noticeable = output.includes('Detected a noticeable performance change'); - const report = output.split('\n---\n')[1]?.trim(); - - if (!report) { - core.setFailed('Could not find the performance report in the script output.'); - return; - } - - const body = `${MARKER}\n${report}\n`; - - core.info(body); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existing = comments.find((c) => c.body?.startsWith(MARKER)); - - if (!noticeable) { - if (existing) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - }); - } - - core.info('No noticeable change - skipping PR comment.'); - return; - } - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 7dcefa2fc7..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Close stale issues and PRs -on: - schedule: - - cron: '30 1 * * *' - -permissions: - issues: write - pull-requests: write - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 30 - days-before-close: 7 - any-of-labels: 'needs more info,needs repro,needs response' - exempt-issue-labels: 'repro provided,keep open' - exempt-pr-labels: 'keep open' - stale-issue-label: 'stale' - stale-pr-label: 'stale' - stale-issue-message: 'Hello ๐Ÿ‘‹, this issue has been open for more than a month without a repro or any activity. If the issue is still present in the latest version, please provide a repro or leave a comment within 7 days to keep it open, otherwise it will be closed automatically. If you found a solution or workaround for the issue, please comment here for others to find. If this issue is critical for you, please consider sending a pull request to fix it.' - stale-pr-message: 'Hello ๐Ÿ‘‹, this pull request has been open for more than a month with no activity on it. If you think this is still necessary with the latest version, please comment and ping a maintainer to get this reviewed, otherwise it will be closed automatically in 7 days.' diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index 6a885a5e28..0000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,144 +0,0 @@ -name: Triage -on: - issues: - types: - - labeled - -permissions: - issues: write - -jobs: - needs-more-info: - runs-on: ubuntu-latest - if: github.event.label.name == 'needs more info' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Can you provide more information about the issue? Please fill the issue template when opening the issue without deleting any section. We need all the information we can to be able to help.\n\nMake sure to at least provide - Current behaviour, Expected behaviour, A way to [reproduce the issue with minimal code](https://stackoverflow.com/help/minimal-reproducible-example) with a public GitHub repo under your username, and the information about your environment (such as the platform of the device, exact versions of all the packages mentioned in the template etc.). In addition, if you can provide a video or GIF demonstrating the issue, it will also be very helpful." - }) - - question: - runs-on: ubuntu-latest - if: github.event.label.name == 'question' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. This helps us prioritize fixing bugs in the library. Seems you have a usage question or an issue unrelated to this library. Please ask the question on [StackOverflow](https://stackoverflow.com/questions/tagged/react-navigation) instead using the `react-navigation` label. You can also chat with other community members on [Reactiflux Discord server](https://www.reactiflux.com/) in the `#help-react-native` channel.\n\nIf you believe that this is actually a bug in the library, please open a new issue and fill the issue template with relevant information." - }) - - feature-request: - runs-on: ubuntu-latest - if: github.event.label.name == 'feature-request' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. The issue tracker is intended for only tracking bug reports. Seems you have a feature request. Please post the feature request on [Canny](https://react-navigation.canny.io/feature-requests) and mention your use case. This lets other users upvote your feature request and helps us prioritize the most requested features.\n\nYou can also open a detailed proposal in our [discussion forum](https://github.com/react-navigation/react-navigation/discussions)." - }) - - out-of-tree-platforms: - runs-on: ubuntu-latest - if: github.event.label.name == 'platform:windows' || github.event.label.name == 'platform:macos' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Windows and MacOS support in React Navigation is a community effort. We don't have the resources to test and maintain these platforms. Any help with fixing issues on these platforms is appreciated. Please feel free to open a PR to fix the issue." - }) - - react-native-screens: - runs-on: ubuntu-latest - if: github.event.label.name == 'library:react-native-screens' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Seems that this issue is related to `react-native-screens` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-screens) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it." - }) - - react-native-reanimated: - runs-on: ubuntu-latest - if: github.event.label.name == 'library:react-native-reanimated' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Seems that this issue is related to `react-native-reanimated` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-reanimated) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it." - }) - - react-native-gesture-handler: - runs-on: ubuntu-latest - if: github.event.label.name == 'library:react-native-gesture-handler' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Seems that this issue is related to `react-native-gesture-handler` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/software-mansion/react-native-gesture-handler) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it." - }) - - react-native-safe-area-context: - runs-on: ubuntu-latest - if: github.event.label.name == 'library:react-native-safe-area-context' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Seems that this issue is related to `react-native-safe-area-context` library which is a dependency of React Navigation. Can you also post your issue in [this repo](https://github.com/th3rdwave/react-native-safe-area-context) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it." - }) - - react-native-pager-view: - runs-on: ubuntu-latest - if: github.event.label.name == 'library:react-native-pager-view' - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Hey! Thanks for opening the issue. Seems that this issue is related to `react-native-pager-view` library which is a dependency of Material Top Tabs. Can you also post your issue in [this repo](https://github.com/callstack/react-native-pager-view) so that it's notified to the maintainers of that library? This will help us fix the issue faster since it's upto the maintainers of that library to investigate it." - }) diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml deleted file mode 100644 index 0e185b2f3f..0000000000 --- a/.github/workflows/versions.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Check versions -on: - issues: - types: - - opened - - edited - -permissions: - issues: write - -jobs: - check-versions: - runs-on: ubuntu-latest - steps: - - uses: react-navigation/check-versions-action@1f7edac18020a4dc38c4d174bb04311b196f9b75 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - required-packages: | - @react-navigation/native - optional-packages: | - @react-navigation/bottom-tabs - @react-navigation/compat - @react-navigation/core - @react-navigation/devtools - @react-navigation/drawer - @react-navigation/material-top-tabs - @react-navigation/routers - @react-navigation/stack - react-native-drawer-layout - react-native-tab-view - tags: | - latest - next diff --git a/.gitignore b/.gitignore index 91138f2452..8e3a27bcb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,117 +1,22 @@ -.DS_Store -.cache -.vscode -.idea -.expo -.gradle -.classpath -.project -.settings -.history - -local.properties - -/coverage/ -/dist/ -/lib/ - -node_modules/ - -xcuserdata -generated -dist -lib -build - -npm-debug.* - -*.tsbuildinfo -*.log -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision -*.orig.* - -*.iml - -# @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8 -# The following patterns were generated by expo-cli +# VSCode +.vscode/ +jsconfig.json -# OSX -# -.DS_Store - -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -project.xcworkspace -.xcode.env.local - -# Android/IntelliJ -# -build/ +# IntelliJ/Webstorm .idea -.gradle -local.properties -*.iml -*.hprof -# node.js -# -node_modules/ -.pnpm-store/ +# NodeJS npm-debug.log -pnpm-debug.log - -# BUCK -buck-out/ -\.buckd/ -*.keystore -!debug.keystore - -# Bundle artifacts -*.jsbundle - -# CocoaPods -/ios/Pods/ - -# Expo -.expo/ -dist/ - -# @end expo-cli - -# Maestro -maestro-debug-output/ +node_modules +lib-rn +lib +yarn-error.log -# Yarn -.yarn-global -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions +# OS X +.DS_Store -# Local Netlify folder -.netlify +# Exponent +.exponent -# AI -.claude +# Jest +coverage \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..d994f86a07 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +examples +__tests__ diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index e8416a151f..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v24.13.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index db5ace137b..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,83 +0,0 @@ -# Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or - advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email - address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to Brent Vatne ([brentvatne@gmail.com](mailto:brentvatne@gmail.com)), Satyajit Sahoo ([satyajit.happy@gmail.com](mailto:satyajit.happy@gmail.com)) or Michaล‚ Osadnik ([micosa97@gmail.com](mailto:micosa97@gmail.com)). All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at . - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -. Translations are available at . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index a2700aad13..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,153 +0,0 @@ -# Contributing - -This library is a community effort: it can only be great if we all help out in one way or another! If you feel like you aren't experienced enough using React Navigation to contribute, you can still make an impact by: - -- Responding to one of the open [issues](https://github.com/react-navigation/react-navigation/issues). Even if you can't resolve or fully answer a question, asking for more information or clarity on an issue is extremely beneficial for someone to come after you to resolve the issue. -- Creating public example repositories or [Snacks](https://snack.expo.dev/) of navigation problems you have solved and sharing the links. -- Answering questions on [Stack Overflow](https://stackoverflow.com/search?q=react-navigation). -- Answering questions in our [Reactiflux](https://www.reactiflux.com/) channel. -- Providing feedback on the open [PRs](https://github.com/react-navigation/react-navigation/pulls). -- Providing feedback on the open [RFCs](https://github.com/react-navigation/rfcs). -- Improving the [website](https://github.com/react-navigation/react-navigation.github.io). - -If you don't know where to start, check the ones with the label [`good first issue`](https://github.com/react-navigation/react-navigation/labels/good%20first%20issue) - even fixing a typo in the documentation is a worthy contribution! - -## Development workflow - -The project uses a monorepo structure for the packages managed by [pnpm workspaces](https://pnpm.io/workspaces) and [lerna](https://lerna.js.org). To get started with the project, run `pnpm install` in the root directory to install the required dependencies for each package: - -```sh -pnpm install -``` - -While developing, you can run the [example app](/example/) with [Expo](https://expo.dev/) to test your changes: - -```sh -pnpm example start -``` - -Make sure your code passes TypeScript and ESLint. Run the following to verify: - -```sh -pnpm typecheck -pnpm lint -``` - -To fix formatting errors, run the following: - -```sh -pnpm lint --fix -``` - -Remember to add tests for your change if possible. Run the unit tests by: - -```sh -pnpm test -``` - -Before running tests configure Playwright with: - -```sh -pnpm exec playwright install -``` - -Run the e2e tests by: - -```sh -pnpm example e2e:web --ui -``` - -By default, this will use the local dev server for the app. If you want to test a production build, first build the [example app](/example/) for web: - -```sh -pnpm example exec expo export --platform web -``` - -Then run the tests with the `CI` environment variable: - -```sh -CI=1 pnpm example e2e:web -``` - -Before running Maestro tests: - -- Install it following the instructions [here](https://maestro.mobile.dev/getting-started/installing-maestro). -- Start the Emulator or Simulator and install the Expo Go app. -- Start the Metro server with `pnpm example start`. - -Then run the Maestro tests by: - -```sh -pnpm example e2e:native -``` - -### Commit message convention - -We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: - -- `fix`: bug fixes, e.g. fix crash due to deprecated method. -- `feat`: new features, e.g. add new method to the module. -- `refactor`: code refactor, e.g. migrate from class components to hooks. -- `docs`: changes to documentation, e.g. add usage example for the module. -- `test`: adding or updating tests, eg add e2e tests using maestro. -- `chore`: tooling changes, e.g. change CI config. - -Our pre-commit hooks verify that your commit message matches this format when committing. - -### Linting and tests - -[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) - -We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. - -Our pre-commit hooks verify that the linter and tests pass when committing. - -### Scripts - -The `package.json` file contains various scripts for common tasks: - -- `pnpm install`: setup project by installing all dependencies and pods. -- `pnpm typecheck`: type-check files with TypeScript. -- `pnpm lint`: lint files with ESLint. -- `pnpm test`: run unit tests with Jest. -- `pnpm example start`: run the example app with Expo. - -### Sending a pull request - -> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). - -When you're sending a pull request: - -- Prefer small pull requests focused on one change. -- Verify that linters and tests are passing. -- Review the documentation to make sure it looks good. -- Follow the pull request template when opening a pull request. -- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. - -## Publishing - -Maintainers with write access to the GitHub repo and the npm organization can publish new versions. To publish a new version, first, you need to export a `GH_TOKEN` environment variable as mentioned [here](https://github.com/lerna-lite/lerna-lite/blob/main/packages/version/README.md#remote-client-auth-tokens). Then run: - -```sh -pnpm release -``` - -This will automatically bump the version and publish the packages. It'll also publish the changelogs on GitHub for each package. - -When releasing a pre-release version, we need to: - -- Update `lerna.json` to set the `preId` (e.g. `alpha`) and `preDistTag` (e.g. `next`) fields, and potentially the `allowBranch` field. -- Run the following command: - -```sh -pnpm lerna publish --conventional-commits --conventional-prerelease --preid alpha -``` - -When releasing a stable version, we need to: - -- Remove the `preId` and `preDistTag` fields from `lerna.json`. -- Run the following command: - -```sh -pnpm lerna publish --conventional-commits --conventional-graduate -``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..063ab66be4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +BSD License + +For React Navigation software + +Copyright (c) 2016-present, React Navigation Contributors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index ec064e96f3..d6392aa8c3 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,56 @@ -# React Navigation 8 +# React Navigation [![CircleCI](https://circleci.com/gh/react-community/react-navigation/tree/master.svg?style=shield&circle-token=622fcb1d78413084c2f44699ed2104246a177485)](https://circleci.com/gh/react-community/react-navigation/tree/master) [![npm version](https://badge.fury.io/js/react-navigation.svg)](https://badge.fury.io/js/react-navigation) [![codecov](https://codecov.io/gh/react-community/react-navigation/branch/master/graph/badge.svg)](https://codecov.io/gh/react-community/react-navigation) -[![Build Status][build-badge]][build] -[![Code Coverage][coverage-badge]][coverage] -[![MIT License][license-badge]][license] -Routing and navigation for your React Native apps. +*Learn once, navigate anywhere.* -Documentation can be found at [reactnavigation.org](https://reactnavigation.org/). +Browse the docs on [reactnavigation.org](https://reactnavigation.org/) or try it out on [our expo demo](https://exp.host/@react-navigation/NavigationPlayground). -This branch contains the code for the upcoming major version of React Navigation. You can find the code for other versions in the following branches: +## Motivation -- [7.x](https://github.com/react-navigation/react-navigation/tree/7.x) (latest stable) -- [6.x](https://github.com/react-navigation/react-navigation/tree/6.x) -- [5.x](https://github.com/react-navigation/react-navigation/tree/5.x) -- [4.x](https://github.com/react-navigation/react-navigation/tree/4.x) -- [3.x](https://github.com/react-navigation/react-navigation/tree/3.x) -- [2.x](https://github.com/react-navigation/react-navigation/tree/2.x) -- [1.x](https://github.com/react-navigation/react-navigation/tree/1.x) +React Navigation is born from the React Native community's need for an +extensible yet easy-to-use navigation solution. It replaces and improves +upon several navigation libraries in the ecosystem, including Ex-Navigation, +React Native's Navigator and NavigationExperimental components. React +Navigation can also be used across React and React Native projects allowing +for a higher degree of shared code. -## Package Versions +Once stable, NavigationExperimental will be deprecated in favor of React +Navigation. React Navigation is a collaboration between people from +Facebook, Exponent and the React community at large. -| Name | Latest Version | -| ---------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | -| [@react-navigation/bottom-tabs](/packages/bottom-tabs) | [![badge](https://img.shields.io/npm/v/@react-navigation/bottom-tabs.svg)](https://www.npmjs.com/package/@react-navigation/bottom-tabs) | -| [@react-navigation/core](/packages/core) | [![badge](https://img.shields.io/npm/v/@react-navigation/core.svg)](https://www.npmjs.com/package/@react-navigation/core) | -| [@react-navigation/devtools](/packages/devtools) | [![badge](https://img.shields.io/npm/v/@react-navigation/devtools.svg)](https://www.npmjs.com/package/@react-navigation/devtools) | -| [@react-navigation/drawer](/packages/drawer) | [![badge](https://img.shields.io/npm/v/@react-navigation/drawer.svg)](https://www.npmjs.com/package/@react-navigation/drawer) | -| [@react-navigation/elements](/packages/elements) | [![badge](https://img.shields.io/npm/v/@react-navigation/elements.svg)](https://www.npmjs.com/package/@react-navigation/elements) | -| [@react-navigation/material-top-tabs](/packages/material-top-tabs) | [![badge](https://img.shields.io/npm/v/@react-navigation/material-top-tabs.svg)](https://www.npmjs.com/package/@react-navigation/material-top-tabs) | -| [@react-navigation/native-stack](/packages/native-stack) | [![badge](https://img.shields.io/npm/v/@react-navigation/native-stack.svg)](https://www.npmjs.com/package/@react-navigation/native-stack) | -| [@react-navigation/native](/packages/native) | [![badge](https://img.shields.io/npm/v/@react-navigation/native.svg)](https://www.npmjs.com/package/@react-navigation/native) | -| [@react-navigation/routers](/packages/routers) | [![badge](https://img.shields.io/npm/v/@react-navigation/routers.svg)](https://www.npmjs.com/package/@react-navigation/routers) | -| [@react-navigation/stack](/packages/stack) | [![badge](https://img.shields.io/npm/v/@react-navigation/stack.svg)](https://www.npmjs.com/package/@react-navigation/stack) | -| [react-native-drawer-layout](/packages/react-native-drawer-layout) | [![badge](https://img.shields.io/npm/v/react-native-drawer-layout.svg)](https://www.npmjs.com/package/react-native-drawer-layout) | -| [react-native-tab-view](/packages/react-native-tab-view) | [![badge](https://img.shields.io/npm/v/react-native-tab-view.svg)](https://www.npmjs.com/package/react-native-tab-view) | +## [Getting started](https://reactnavigation.org/docs/intro/) -## Contributing +1. Create a new React Native App + ``` + react-native init SimpleApp + cd SimpleApp + ``` -Please read through our [contribution guide](CONTRIBUTING.md) to get started! +2. Install the latest version of react-navigation from npm + ``` + yarn add react-navigation + ``` + or + ``` + npm install --save react-navigation + ``` -## Installing from a fork on GitHub +3. Run the new app + ``` + react-native run-android # or: + react-native run-ios + ``` -Since we use a monorepo, it's not possible to install a package from the repository URL. If you need to install a forked version from Git, you can use [`gitpkg`](https://github.com/ramasilveyra/gitpkg). +## Advanced guide -First install `gitpkg`: +- [Redux integration](https://reactnavigation.org/docs/guides/redux) +- [Web integration](https://reactnavigation.org/docs/guides/web) +- [Deep linking](https://reactnavigation.org/docs/guides/linking) +- [Contributors guide](https://reactnavigation.org/docs/guides/contributors) -```sh -pnpm add --global gitpkg -``` +## React Navigation API -Then follow these steps to publish and install a forked package: +- [Navigators](https://reactnavigation.org/docs/navigators/) +- [Routers](https://reactnavigation.org/docs/routers/) +- [Views](https://reactnavigation.org/docs/views/) -1. Fork this repo to your account and clone the forked repo to your local machine -1. Open a Terminal and `cd` to the location of the cloned repo -1. Run `pnpm install` to install any dependencies -1. If you want to make any changes, make them and commit -1. Run `pnpm lerna run prepack` to perform the build steps -1. Now `cd` to the package directory that you want to use (e.g. `cd packages/stack` for `@react-navigation/stack`) -1. Run `gitpkg publish` to publish the package to your repo - -After publishing, you should see something like this: - -```sh -Package uploaded to git@github.com:/.git with the name -``` - -You can now install the dependency in your project: - -```sh -pnpm add /.git# -``` - -Remember to replace ``, `` and `` with right values. - - - -[build-badge]: https://github.com/react-navigation/react-navigation/actions/workflows/ci.yml/badge.svg -[build]: https://github.com/react-navigation/react-navigation/actions/workflows/ci.yml -[coverage-badge]: https://img.shields.io/codecov/c/github/react-navigation/react-navigation.svg -[coverage]: https://codecov.io/github/react-navigation/react-navigation -[license-badge]: https://img.shields.io/npm/l/@react-navigation/core.svg -[license]: https://opensource.org/licenses/MIT diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 8ba8eb658c..0000000000 --- a/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: ['module:@react-native/babel-preset'], - plugins: ['react-native-worklets/plugin'], -}; diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000000..1c52d56e7f --- /dev/null +++ b/circle.yml @@ -0,0 +1,32 @@ +version: 2 +jobs: + build: + docker: + - image: reactcommunity/node-ci:7.10.0-0 # custom image -- includes ocaml, libelf1, Yarn + parallelism: 3 + working_directory: ~/react-navigation + steps: + - checkout + - restore_cache: + key: v2-react-navigation-{{ .Branch }} # generate cache per branch + - run: yarn # install root deps + - run: ./scripts/test.sh # run tests + - deploy: + command: | + if [ "${CIRCLE_BRANCH}" == "master" ]; then + # deploy website + cd website && yarn && cd ../ + yarn run build-docs + ./scripts/deploy-website.sh + + # deploy expo playground demo + exp login -u "$EXPO_USERNAME" -p "$EXPO_PASSWORD" + cd examples/ExpoNavigationPlayground && yarn && exp publish + fi + - save_cache: + key: v2-react-navigation-{{ .Branch }} # generate cache per branch + paths: + - ~/.cache/yarn + - ~/react-navigation/website/node_modules + - ~/react-navigation/examples/NavigationPlayground/node_modules + - ~/react-navigation/examples/ReduxExample/node_modules diff --git a/commitlint.config.js b/commitlint.config.js deleted file mode 100644 index 84dcb122af..0000000000 --- a/commitlint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], -}; diff --git a/docs/api/navigators/DrawerNavigator.md b/docs/api/navigators/DrawerNavigator.md new file mode 100644 index 0000000000..d25c786730 --- /dev/null +++ b/docs/api/navigators/DrawerNavigator.md @@ -0,0 +1,184 @@ +# DrawerNavigator + +Used to easily set up a screen with a drawer navigation. For a live example please see [our expo demo](https://exp.host/@react-navigation/NavigationPlayground). + +```js +class MyHomeScreen extends React.Component { + static navigationOptions = { + drawerLabel: 'Home', + drawerIcon: ({ tintColor }) => ( + + ), + }; + + render() { + return ( + +); +``` + + +### The static `router` + +A router object may be statically defined on your component. If defined, it must follow this interface: + +```javascript +type BackAction = {type: 'Navigation/BACK'}; +type URIAction = {type: 'Navigation/URI', uri: string}; + +interface Router { + getStateForAction(action: (A | BackAction | URIAction), lastState: ?S): ?S; + getActionForURI(uri: string): ?A; +} +``` + +The state and action types of the static router must match the state and action types associated with the navigation prop passed into the component. + +#### router.getStateForAction(action, lastState) + +This function is defined on the static router and is used to define the expected navigation state. + +```javascript +class ScreenWithEditMode extends React.Component { + static router = { + getStateForAction: (action, prevState) => { + return { isEditing: true }; + }, + }; + render() { + // this.props.navigation.state.isEditing === true + ... + } +} +``` + +`getStateForAction` must **always** return a navigation state that can be rendered by the component when passed in as the `navigation.state` prop. + +If null is returned, we are signaling that the previous navigation state has not changed, but the action is handled. This is usually used in cases where the action is being swallowed. + + +#### router.getActionForURI(uri) + +Return an action if a URI can be handled, otherwise return `null` + + + +### Special Actions + +There are two special actions that can be fired into `navigation.dispatch` and can be handled by your `router.getStateForAction`. + +#### Back Action + +This action means the same thing as an Android back button press. + +``` +type BackAction = { type: 'Navigation/BACK' }; +``` + +#### URI Open Action + +Used to request the enclosing app or OS to open a link at a particular URI. If it is a web URI like `http` or `https`, the app may open a WebView to present the page. Or the app may open the URI in a web browser. In some cases, an app may choose to block a URI action or handle it differently. + +``` +type URIAction = { type: 'Navigation/URI', uri: string }; +``` + + +### Special Navigation State + +The state defined by `router.getStateForAction` can contain special navigation properties that may be relevant to your app. The title and current URI of a component may change over time, and the parent often needs to observe the behavior. + +#### `state.title` + +If the navigation state contains 'title', it will be used as the title for the given component. This is relevant for top-level components on the web to update the browser title, and is relevant in mobile apps where a title is shown in the header. + +#### `state.uri` + +A URI can also be put in `state.uri`, which will signal to the parent how it may be possible to deep link into a similar navigation state. In web apps, this will be used to keep the URI bar in sync with the current navigation state of the app. + + +## Use Cases + +### "Block the Android back button on one screen of my app" + +To block the Android back button: + +``` +class Foo extends React.Component { + static router = { + getStateForAction(action, prevState = {}) { + if (action.type === 'Navigation/BACK') return null; + else return prevState; + }, + }; + render() { + ... +``` + +Because we return null, we signal to our container that the action has been handled but the state does not change. The parent should not handle the back behavior at this point, and nothing should be re-rendered. + +### "Link deeply into one screen of my app" + +``` +class Foo extends React.Component { + static router = { + getStateForAction(action, prevState = {deep: false}) { + if (action.type === 'GoDeep') return { deep: true }; + else return prevState; + }, + getActionForURI(uri) { + if (uri === 'myapp://foo') + return {type: 'Go'}; + else if (uri === 'myapp://foo_deep') + return {type: 'GoDeep'}; + return null; + }, + }; + render() { + // this.props.navigation.state.deep may be true or false + ... +``` + +Based on the state URI we may decide to return an action. If an action is returned, `getStateForAction` is expected to output the correct state for a deep link. + +## Reference Implementations + +A library to that helps easily produce navigation-aware components: https://github.com/react-community/react-navigation . (Also uses a HOC to provide navigation containers when needed.) + +A simple navigation container: https://gist.github.com/ericvicenti/77d190e2ec408012255937400e34bdb1 + +A web implementation of a navigation container: https://gist.github.com/ericvicenti/55bef95fcd8558029a3bae8483baea6c diff --git a/docs/guides/Contributors.md b/docs/guides/Contributors.md new file mode 100644 index 0000000000..416d45acc1 --- /dev/null +++ b/docs/guides/Contributors.md @@ -0,0 +1,90 @@ +# Contributors Guide + +## Environment + +React navigation was initially developed on macOS 10.12, with node 7+, and react-native v0.39+. Please open issues when uncovering problems in different environments. + +## Development + +### Fork the repo + +- Fork [`react-navigation`](https://github.com/react-community/react-navigation) on GitHub + +- Run these commands in the terminal to download locally and install it: + +``` +git clone https://github.com//react-navigation.git +cd react-navigation +git remote add upstream https://github.com/react-community/react-navigation.git +npm install +``` + +### Run the example app + +``` +cd examples/NavigationPlayground +npm install +cd ../.. +npm start + +# In a seperate terminal tab: +npm run run-playground-android +# OR: +npm run run-playground-ios +``` + +You can also simply run e.g. `react-native run-android` from within the example app directory (instead of `npm run run-playground-android` from the root `react-navigation` directory); both do the same thing. + +### Run the website + +For development mode and live-reloading: + +``` +cd website +npm install +npm start +``` + +To run the website in production mode with server rendering: + +``` +npm run prod +``` + +### Run tests and type-checking + +``` +jest +flow +``` + +Tests must pass for your changes to be accepted and merged. + +Flow is not yet passing, but your code should be flow checked and we expect that your changes do not introduce any flow errors. + +### Developing Docs + +The docs are indexed in [App.js](https://github.com/react-community/react-navigation/blob/master/website/src/App.js), where all the pages are declared alongside the titles. To test the docs, follow the above instructions for running the website. Changing existing markdown files should not require any testing. + +The markdown from the `docs` folder gets generated and dumped into a json file as a part of the build step. To see updated docs appear in the website, re-run the build step by running `npm run build-docs` from the `react-navigation` root folder. + +## Submitting Contributions + +### New views or unique features + +Often navigation needs are specific to certain apps. If your changes are unique to your app, you may want to fork the view or router that has changed. You can keep the source code in your app, or publish it on npm as a `react-navigation` compatible view or router. + +This library is intended to include highly standard and generic navigation patterns. + +### Major Changes + +Before embarking on any major changes, please file an issue describing the suggested change and motivation. We may already have thought about it and we want to make sure we all are on the same page before starting on any big changes. + +### Minor Bugfixes + +Simple bug fixes are welcomed in pull requests! Please check for duplicate PRs before posting. + +#### Make sure to sync up with the state of upstream before submitting a PR: + +- `git fetch upstream` +- `git rebase upstream/master` diff --git a/docs/guides/Custom-Navigators.md b/docs/guides/Custom-Navigators.md new file mode 100644 index 0000000000..7eb2201b64 --- /dev/null +++ b/docs/guides/Custom-Navigators.md @@ -0,0 +1,94 @@ +# Custom Navigators + +A navigator is any React component that has a [router](/docs/routers/) on it. Here is a basic one, which uses the [router's API](/docs/routers/api) to get the active component to render: + +```js +class MyNavigator extends React.Component { + static router = MyRouter; + render() { + const { state, dispatch } = this.props.navigation; + const { routes, index } = state; + + // Figure out what to render based on the navigation state and the router: + const Component = MyRouter.getComponentForState(state); + + // The state of the active child screen can be found at routes[index] + let childNavigation = { dispatch, state: routes[index] }; + // If we want, we can also tinker with the dispatch function here, to limit + // or augment our children's actions + + // Assuming our children want the convenience of calling .navigate() and so on, + // we should call addNavigationHelpers to augment our navigation prop: + childNavigation = addNavigationHelpers(childNavigation); + + return ; + } +} +``` + +## Navigation Prop + +The navigation prop passed down to a navigator only includes `state` and `dispatch`. This is the current state of the navigator, and an event channel to send action requests. + +All navigators are controlled components: they always display what is coming in through `props.navigation.state`, and their only way to change the state is to send actions into `props.navigation.dispatch`. + +Navigators can specify custom behavior to parent navigators by [customizing their router](/docs/routers/). For example, a navigator is able to specify when actions should be blocked by returning null from `router.getStateForAction`. Or a navigator can specify custom URI handling by overriding `router.getActionForPathAndParams` to output a relevant navigation action, and handling that action in `router.getStateForAction`. + +### Navigation State + +The navigation state that is passed into a navigator's `props.navigation.state` has the following structure: + +``` +{ + index: 1, // identifies which route in the routes array is active + routes: [ + { + // Each route needs a name, which routers will use to associate each route + // with a react component + routeName: 'MyRouteName', + + // A unique id for this route, used to keep order in the routes array: + key: 'myroute-123', + + // Routes can have any additional data. The included routers have `params` + ...customRouteData, + }, + ...moreRoutes, + ] +} +``` + +### Navigation Dispatchers + +A navigator can dispatch navigation actions, such as 'Go to a URI', 'Go back'. + +The dispatcher will return `true` if the action was successfully handled, otherwise `false`. + +## API for building custom navigators + +To help developers implement custom navigators, the following utilities are provided with React Navigation: + +### `createNavigator` + +This utility combines a [router](/docs/routers/) and a [navigation view](/docs/views/) together in a standard way: + +```js +const MyApp = createNavigator(MyRouter)(MyView); +``` + +All this does behind the scenes is: + +```js +const MyApp = ({ navigation }) => ( + +); +MyApp.router = MyRouter; +``` + +### `addNavigationHelpers` + +Takes in a bare navigator navigation prop with `state` and `dispatch`, and augments it with all the various functions in a screen navigation prop, such as `navigation.navigate()` and `navigation.goBack()`. These functions are simply helpers to create the actions and send them into `dispatch`. + +### `createNavigationContainer` + +If you want your navigator to be usable as a top-level component, (without a navigation prop being passed in), you can use `createNavigationContainer`. This utility will make your navigator act like a top-level navigator when the navigation prop is missing. It will manage the app state, and integrate with app-level nav features, like handling incoming and outgoing links, and Android back button behavior. diff --git a/docs/guides/Customizing-Navigation.md b/docs/guides/Customizing-Navigation.md new file mode 100644 index 0000000000..95a8bb12dd --- /dev/null +++ b/docs/guides/Customizing-Navigation.md @@ -0,0 +1,53 @@ +## Customizing Navigation Views + +Modify the presentation of navigation, including styles, animations and gestures. + +## Customizing Routers + +Building a custom router allows you to change the navigation logic of your component, manage navigation state, and define behavior for URIs. + + +A router can be defined like this: + +``` +class MyNavigationAwareComponent extends React.Component { + + static router = { + + // Defines the navigation state for a component: + getStateForAction: (action: {type: string}, lastState?: any) => { + const state = lastState = { myMode: 'default' }; + if (action.type === 'MyAction') { + return { myMode: 'action' }; + } else if (action.type === NavigationActions.BACK) { + return { myMode: 'blockBackButton' }; + } else { + return state; + } + }, + + // Defines if a component can handle a particular URI. + // If it does, return an action to be passed to `getStateForAction` + + getActionForURI: (uri: string) => { + if (uri === 'myapp://myAction') { + return { type: 'MyAction' }; + } + return null; + }, + + }; + + render() { + // render something based on this.props.navigation.state + ... + } + + onButtonPress = () => { + this.props.navigation.dispatch({ type: 'MyAction' }); + }; + + ... + +} +``` diff --git a/docs/guides/Deep-Linking.md b/docs/guides/Deep-Linking.md new file mode 100644 index 0000000000..286ca457d2 --- /dev/null +++ b/docs/guides/Deep-Linking.md @@ -0,0 +1,111 @@ +# Deep Linking + +In this guide we will set up our app to handle external URIs. Let's start with the SimpleApp that [we created in the getting started guide](/docs/intro). + +In this example, we want a URI like `mychat://chat/Taylor` to open our app and link straight into Taylor's chat page. + +## Configuration + +Previously, we had defined a navigator like this: + +```js +const SimpleApp = StackNavigator({ + Home: { screen: HomeScreen }, + Chat: { screen: ChatScreen }, +}); +``` + +We want paths like `chat/Taylor` to link to a "Chat" screen with the `user` passed as a param. Let's re-configure our chat screen with a `path` that tells the router what relative path to match against, and what params to extract. This path spec would be `chat/:user`. + +```js +const SimpleApp = StackNavigator({ + Home: { screen: HomeScreen }, + Chat: { + screen: ChatScreen, + path: 'chat/:user', + }, +}); +``` + + +### URI Prefix + +Next, let's configure our navigation container to extract the path from the app's incoming URI. + +```js +const SimpleApp = StackNavigator({...}); + +// on Android, the URI prefix typically contains a host in addition to scheme +const prefix = Platform.OS == 'android' ? 'mychat://mychat/' : 'mychat://'; + +const MainApp = () => ; +``` + +## iOS + +Let's configure the native iOS app to open based on the `mychat://` URI scheme. + +In `SimpleApp/ios/SimpleApp/AppDelegate.m`: + +``` +// Add the header at the top of the file: +#import + +// Add this above the `@end`: +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + return [RCTLinkingManager application:application openURL:url + sourceApplication:sourceApplication annotation:annotation]; +} +``` + +In Xcode, open the project at `SimpleApp/ios/SimpleApp.xcodeproj`. Select the project in sidebar and navigate to the info tab. Scroll down to "URL Types" and add one. In the new URL type, set the identifier and the url scheme to your desired url scheme. + +![Xcode project info URL types with mychat added](/assets/xcode-linking.png) + +Now you can press play in Xcode, or re-build on the command line: + +```sh +react-native run-ios +``` + +To test the URI on the simulator, run the following: + +``` +xcrun simctl openurl booted mychat://chat/Taylor +``` + +To test the URI on a real device, open Safari and type `mychat://chat/Taylor`. + +## Android + +To configure the external linking in Android, you can create a new intent in the manifest. + +In `SimpleApp/android/app/src/main/AndroidManifest.xml`, add the new `VIEW` type `intent-filter` inside the `MainActivity` entry: + +``` + + + + + + +``` + +Now, re-install the app: + +```sh +react-native run-android +``` + +To test the intent handling in Android, run the following: + +``` +adb shell am start -W -a android.intent.action.VIEW -d "mychat://mychat/chat/Taylor" com.simpleapp +``` + +```phone-example +linking +``` diff --git a/docs/guides/Guide-Headers.md b/docs/guides/Guide-Headers.md new file mode 100644 index 0000000000..6bff77b66a --- /dev/null +++ b/docs/guides/Guide-Headers.md @@ -0,0 +1,82 @@ +# Configuring the Header + +Header is only available for StackNavigator. + +In the previous example, we created a StackNavigator to display several screens in our app. + + +When navigating to a chat screen, we can specify params for the new route by providing them to the navigate function. In this case, we want to provide the name of the person on the chat screen: + +```js +this.props.navigation.navigate('Chat', { user: 'Lucy' }); +``` + +The `user` param can be accessed from the chat screen: + +```js +class ChatScreen extends React.Component { + render() { + const { params } = this.props.navigation.state; + return Chat with {params.user}; + } +} +``` + +### Setting the Header Title + +Next, the header title can be configured to use the screen param: + +```js +class ChatScreen extends React.Component { + static navigationOptions = ({ navigation }) => ({ + title: `Chat with ${navigation.state.params.user}`, + }); + ... +} +``` + +```phone-example +basic-header +``` + + +### Adding a Right Button + +Then we can add a [`header` navigation option](/docs/navigators/navigation-options#Stack-Navigation-Options) that allows us to add a custom right button: + +```js +static navigationOptions = { + headerRight: + +); +``` + +Inside the infra: + +``` +class InfraScreen extends React.Component { + constructor() { + const {initURI, type} = this.props; + const ScreenView = ScreenRegistry[type].screen; + const router = ScreenView.router; + const deepLinkAction = router.getActionForURI(initURI); + const initAction = deepLinkAction || {type: 'init'} + const nav = router.getStateForAction(initAction); + this.state = { + nav, + }; + HybridNavigationModule.setNavOptions(this.state.nav); + } + componentWillUpdate() { + HybridNavigationModule.setNavOptions(this.state.nav); + } + dispatch = (action) => { + const {type} = this.props; + const ScreenView = ScreenRegistry[type].screen; + const {getStateForAction} = ScreenView.router; + const newNavState = getStateForAction(action, this.state.nav); + if (newNavState !== this.state.nav) { + this.setState({ nav: newNavState }); + return true; + } + if (action.type === 'URI') { + HybridNavigationModule.openURI(action.uri); + return true; + } + if (action.type === NavigationActions.BACK) { + HybridNavigationModule.goBack(); + return true; + } + HybridNavigationModule.openAction(action); + return true; + } + render() { + const {type} = this.props; + const ScreenView = ScreenRegistry[type].screen; + const navigation = { + dispatch: this.dispatch, + state: this.state.nav, + }; + return ; + } +} +``` + +## Setting title + +``` +MarketplaceScreen.router = { + getStateForAction(action, lastState) { + return lastState || {title: 'Marketplace Home'}; + }, +}; +``` +A HOC could be used to make this feel more elegant. + + +## Disabling/Enabling the right button + +``` +const TestScreen = ({ navigation }) => ( + + + Pressed {navigation.state} times + +); +TestScreen.router = { + getStateForAction(action, lastState = {}) { + let state = lastState || { + rightButtonEnabled: true, + rightButtonTitle: 'Tap Me', + pressCount: 0, + }; + if (action.type === 'ToggleMyButtonPressability') { + state = { + ...state, + rightButtonEnabled: !state.rightButtonEnabled, + }; + } else if (action.type === 'RightButtonPress') { + state = { + ...state, + pressCount: state.pressCount + 1, + }; + } + return state; + }, +}; +``` + + +## Before JS starts + +A JSON file could be defined for native to consume before JS spins up: + +``` +{ + "screens": [ + { + "type": "Profile", + "path": "/users/:id?name=:name", + "params": { + "name": "string", + "id": "number" + }, + "title": "%name%' s Profile", + "rightButtonTitle": "Message %name%" + }, + { + ... + } + ] +} +``` + +This seems like a pain to set up, so we can statically analyze our JS and autogenerate this JSON! If the JS in an app changes, there could be a way for JS to report the new routing configuration to native for use on the next cold start. diff --git a/docs/guides/Navigation-Actions.md b/docs/guides/Navigation-Actions.md new file mode 100644 index 0000000000..13dd6da93b --- /dev/null +++ b/docs/guides/Navigation-Actions.md @@ -0,0 +1,108 @@ +# Navigation Actions + +All Navigation Actions return an object that can be sent to the router using `navigation.dispatch()` method. + +Note that if you want to dispatch react-navigation actions you should use the action creators provided in this library. + +The following actions are supported: +* [Navigate](#Navigate) - Navigate to another route +* [Reset](#Reset) - Replace current state with a new state +* [Back](#Back) - Go back to previous state +* [Set Params](#SetParams) - Set Params for given route +* [Init](#Init) - Used to initialize first state if state is undefined + +### Navigate +The `Navigate` action will update the current state with the result of a `Navigate` action. + +- `routeName` - *String* - Required - A destination routeName that has been registered somewhere in the app's router +- `params` - *Object* - Optional - Params to merge into the destination route +- `action` - *Object* - Optional - (advanced) The sub-action to run in the child router, if the screen is a navigator. Any one of the actions described in this doc can be set as a sub-action. + +```js +import { NavigationActions } from 'react-navigation' + +const navigateAction = NavigationActions.navigate({ + + routeName: 'Profile', + + params: {}, + + action: NavigationActions.navigate({ routeName: 'SubProfileRoute'}) +}) + +this.props.navigation.dispatch(navigateAction) + +``` + + +### Reset + +The `Reset` action wipes the whole navigation state and replaces it with the result of several actions. + +- `index` - *number* - required - Index of the active route on `routes` array in navigation `state`. +- `actions` - *array* - required - Array of Navigation Actions that will replace the navigation state. + +```js +import { NavigationActions } from 'react-navigation' + +const resetAction = NavigationActions.reset({ + index: 0, + actions: [ + NavigationActions.navigate({ routeName: 'Profile'}) + ] +}) +this.props.navigation.dispatch(resetAction) + +``` +#### How to use the `index` parameter +The `index` param is used to specify the current active route. + +eg: given a basic stack navigation with two routes `Profile` and `Settings`. +To reset the state to a point where the active screen was `Settings` but have it stacked on top of a `Profile` screen, you would do the following: + +```js +import { NavigationActions } from 'react-navigation' + +const resetAction = NavigationActions.reset({ + index: 1, + actions: [ + NavigationActions.navigate({ routeName: 'Profile'}), + NavigationActions.navigate({ routeName: 'Settings'}) + ] +}) +this.props.navigation.dispatch(resetAction) + +``` + +### Back + +Go back to previous screen and close current screen. `back` action creator takes in one optional parameter: +- `key` - *string or null* - optional - If set, navigation will go back from the given key. If null, navigation will go back anywhere. + +```js +import { NavigationActions } from 'react-navigation' + +const backAction = NavigationActions.back({ + key: 'Profile' +}) +this.props.navigation.dispatch(backAction) + +``` + +### SetParams + +When dispatching `SetParams`, the router will produce a new state that has changed the params of a particular route, as identified by the key + +- `params` - *object* - required - New params to be merged into existing route params +- `key` - *string* - required - Route key that should get the new params + +```js +import { NavigationActions } from 'react-navigation' + +const setParamsAction = NavigationActions.setParams({ + params: { title: 'Hello' }, + key: 'screen-123', +}) +this.props.navigation.dispatch(setParamsAction) + +``` diff --git a/docs/guides/Redux-Integration.md b/docs/guides/Redux-Integration.md new file mode 100644 index 0000000000..8fe4edcf80 --- /dev/null +++ b/docs/guides/Redux-Integration.md @@ -0,0 +1,87 @@ +# Redux Integration + +To handle your app's navigation state in redux, you can pass your own `navigation` prop to a navigator. Your navigation prop must provide the current state, as well as access to a dispatcher to handle navigation options. + +With redux, your app's state is defined by a reducer. Each navigation router effectively has a reducer, called `getStateForAction`. The following is a minimal example of how you might use navigators within a redux application: + +``` +import { addNavigationHelpers } from 'react-navigation'; + +const AppNavigator = StackNavigator(AppRouteConfigs); + +const initialState = AppNavigator.router.getStateForAction(AppNavigator.router.getActionForPathAndParams('Login')); + +const navReducer = (state = initialState, action) => { + const nextState = AppNavigator.router.getStateForAction(action, state); + + // Simply return the original `state` if `nextState` is null or undefined. + return nextState || state; +}; + +const appReducer = combineReducers({ + nav: navReducer, + ... +}); + +class App extends React.Component { + render() { + return ( + + ); + } +} + +const mapStateToProps = (state) => ({ + nav: state.nav +}); + +const AppWithNavigationState = connect(mapStateToProps)(App); + +const store = createStore(appReducer); + +class Root extends React.Component { + render() { + return ( + + + + ); + } +} +``` + +Once you do this, your navigation state is stored within your redux store, at which point you can fire navigation actions using your redux dispatch function. + +Keep in mind that when a navigator is given a `navigation` prop, it relinquishes control of its internal state. That means you are now responsible for persisting its state, handling any deep linking, integrating the back button, etc. + +Navigation state is automatically passed down from one navigator to another when you nest them. Note that in order for a child navigator to receive the state from a parent navigator, it should be defined as a `screen`. + +Applying this to the example above, you could instead define `AppNavigator` to contain a nested `TabNavigator` as follows: + +```js +const AppNavigator = StackNavigator({ + Home: { screen: MyTabNavigator }, +}); +``` + +In this case, once you `connect` `AppNavigator` to Redux as is done in `AppWithNavigationState`, `MyTabNavigator` will automatically have access to navigation state as a `navigation` prop. + +## Full example + +There's a working example app with redux [here](https://github.com/react-community/react-navigation/tree/master/examples/ReduxExample) if you want to try it out yourself. + +## Mocking tests + +To make jest tests work with your react-navigation app, you need to change the jest preset in the `package.json`, see [here](https://facebook.github.io/jest/docs/tutorial-react-native.html#transformignorepatterns-customization): + +``` +"jest": { + "preset": "react-native", + "transformIgnorePatterns": [ + "node_modules/(?!(jest-)?react-native|react-navigation)" + ] +} +``` diff --git a/docs/guides/Screen-Nav-Options.md b/docs/guides/Screen-Nav-Options.md new file mode 100644 index 0000000000..5ec9d4b712 --- /dev/null +++ b/docs/guides/Screen-Nav-Options.md @@ -0,0 +1,104 @@ + +# Screen Navigation Options + +Each screen can configure several aspects about how it gets presented in parent navigators. + +#### Two Ways to specify each option + +**Static configuration:** Each navigation option can either be directly assigned: + +```js +class MyScreen extends React.Component { + static navigationOptions = { + title: 'Great', + }; + ... +``` + +**Dynamic Configuration** + +Or, the options can be a function that takes the following arguments, and returns an object of navigation options that will be override the route-defined and navigator-defined navigationOptions. + +- `props` - The same props that are available to the screen component + - `navigation` - The [navigation prop](/docs/navigators/navigation-prop) for the screen, with the screen's route at `navigation.state` + - `screenProps` - The props passing from above the navigator component + - `navigationOptions` - The default or previous options that would be used if new values are not provided + +```js +class ProfileScreen extends React.Component { + static navigationOptions = ({ navigation, screenProps }) => ({ + title: navigation.state.params.name + "'s Profile!", + headerRight: ; -// @ts-expect-error - screen="Albums">Albums; - screen="Albums" params={{ screen: 'Playlist' }}> - Albums -; - screen="Settings">Settings; - screen="Settings" params={{ path: '123' }}> - Settings -; -// @ts-expect-error - screen="PostDetails">PostDetails; - screen="PostDetails" params={{ id: '123' }}> - PostDetails -; - - screen="PostDetails" - params={{ id: '123', section: '123' }} -> - PostDetails -; -// @ts-expect-error - screen="PostDetails" params={{ id: 123 }}> - PostDetails -; - - screen="PostDetails" - // @ts-expect-error - params={{ id: '123', section: 123 }} -> - PostDetails -; -// @ts-expect-error - screen="PostDetails" params={{ ids: '123' }}> - PostDetails -; -// @ts-expect-error - screen="PostDetails" params={{}}> - PostDetails -; - screen="Login">Albums; - screen="Updates">Updates; - screen="Updates" params={{ screen: 'Timeline' }}> - Timeline -; - -// @ts-expect-error -useLinkProps({ screen: 'StackBasic' }); -// @ts-expect-error -useLinkProps({ screen: 'StackBasi' }); -useLinkProps({ - screen: 'StackBasic', - params: { screen: 'Albums' }, -}); -useLinkProps({ screen: 'Home' }); -useLinkProps({ - screen: 'Home', - params: { screen: 'Examples' }, -}); - -// @ts-expect-error -StackBasic; -// @ts-expect-error -StackBasic; - - StackBasic -; -Home; - - Home -; - -// @ts-expect-error -; -// @ts-expect-error -; -; -; -; - -/** - * Check for ParamsForRoute - */ - -/* Invalid route name */ -expectTypeOf< - RouteForName['params'] ->().toBeNever(); - -/* Undefined params */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Valid params */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Optional params */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Nested screen */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Nested screen with optional params */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Nested screen with navigator */ -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf>(); - -/* Multiple screens with same name */ -type MultiParamList = { - MyScreen: { id: string }; - A: NavigatorScreenParams<{ - MyScreen: { user: string }; - }>; - B: NavigatorScreenParams<{ - MyScreen: { group: string }; - }>; - C: { other: number }; -}; - -expectTypeOf< - RouteForName['params'] ->().toEqualTypeOf< - Readonly<{ id: string } | { user: string } | { group: string }> ->(); - -/** - * Check for useRoute return type based on the arguments - */ -expectTypeOf(useRoute()).toEqualTypeOf>(); - -{ - const route = useRoute(); - - if (route.name === 'NotFound') { - expectTypeOf(route).toEqualTypeOf>(); - } - - if (route.name === 'TabChat') { - expectTypeOf(route).toEqualTypeOf< - RouteProp - >(); - } - - // @ts-expect-error - if (route.name === 'Invalid') { - // error - } -} - -expectTypeOf( - useRoute('Settings') -).toEqualTypeOf>(); - -expectTypeOf(useRoute('Artist')).toEqualTypeOf< - Route<'Artist', AlbumTabParamList['Artist']> ->(); - -expectTypeOf( - useRoute('Notifications') -).toEqualTypeOf>(); - -expectTypeOf(useRoute('MyScreen')).toEqualTypeOf< - | Route<'MyScreen', { id: string }> - | Route<'MyScreen', { user: string }> - | Route<'MyScreen', { group: string }> ->(); - -expectTypeOf(useRoute('NotFound')).toEqualTypeOf< - RouteProp ->(); - -expectTypeOf(useRoute('TabChat')).toEqualTypeOf< - RouteProp ->(); - -// @ts-expect-error -useRoute('Invalid'); - -// @ts-expect-error -useRoute('Invalid'); - -/** - * Check for useNavigation return type based on the arguments - */ - -// @ts-expect-error -useNavigation('Invalid'); - -{ - const navigation = useNavigation(); - - expectTypeOf(navigation).toEqualTypeOf>(); - - expectTypeOf(navigation.getParent()).toEqualTypeOf< - NavigationProp | undefined - >(); - - expectTypeOf(navigation.getState()).toEqualTypeOf< - NavigationState | undefined - >(); - - expectTypeOf(navigation.setParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.replaceParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.pushParams).parameter(0).toEqualTypeOf(); -} - -{ - const navigation = useNavigation(); - - expectTypeOf(navigation).toEqualTypeOf< - { - [K in keyof RootStackParamList]: StackNavigationProp< - RootStackParamList, - K - >; - }[keyof RootStackParamList] - >(); - - expectTypeOf(navigation.getParent()).toEqualTypeOf< - NavigationProp | undefined - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - (keyof RootStackParamList)[] - >(); - - // @ts-expect-error - expectTypeOf(navigation.setParams).parameter(0).toBeNever(); - - // @ts-expect-error - expectTypeOf(navigation.replaceParams).parameter(0).toBeNever(); - - // @ts-expect-error - expectTypeOf(navigation.pushParams).parameter(0).toBeNever(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); -} - -{ - const navigation = useNavigation('NotFound'); - - expectTypeOf(navigation).toEqualTypeOf< - StackNavigationProp - >(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf<'NotFound' | undefined>(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - (keyof RootParamList)[] - >(); - - expectTypeOf(navigation.setParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.replaceParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.pushParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); -} - -{ - const navigation = useNavigation('Examples'); - - expectTypeOf(navigation).toEqualTypeOf< - CompositeNavigationProp< - DrawerNavigationProp<{ Examples: undefined }, 'Examples'>, - StackNavigationProp - > - >(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf<'Examples' | 'Home' | undefined>(); - - expectTypeOf(navigation.getParent('Home')).toEqualTypeOf< - StackNavigationProp - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'drawer'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf<'Examples'[]>(); - - expectTypeOf(navigation.setParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.replaceParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.pushParams).parameter(0).toBeUndefined(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); -} - -{ - const navigation = useNavigation('Home'); - - expectTypeOf(navigation).toEqualTypeOf< - StackNavigationProp & - CompositeNavigationProp< - StackNavigationProp, - StackNavigationProp - > - >(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf<'Home' | 'StaticConfigScreen' | undefined>(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - (keyof RootParamList)[] - >(); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf< - Partial | undefined> - >(); - - expectTypeOf(navigation.replaceParams) - .parameter(0) - .toEqualTypeOf< - NavigatorScreenParams<{ Examples: undefined }> | undefined - >(); - - expectTypeOf(navigation.pushParams) - .parameter(0) - .toEqualTypeOf< - NavigatorScreenParams<{ Examples: undefined }> | undefined - >(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); -} - -/** - * Routes from dynamic dynamic navigator should return generic navigation - */ -{ - const navigation = useNavigation('TabChat'); - - expectTypeOf(navigation).toEqualTypeOf< - CompositeNavigationProp< - NavigationProp, - NavigationProp - > - >(); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.replaceParams) - .parameter(0) - .toEqualTypeOf(); - - expectTypeOf(navigation.pushParams) - .parameter(0) - .toEqualTypeOf(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - (keyof BottomTabParamList)[] - >(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf<'TabChat' | 'BottomTabs' | undefined>(); -} - -/** - * Check for useNavigationState return type based on the arguments - */ -{ - const state = useNavigationState((state) => state); - - expectTypeOf(state.routeNames).toEqualTypeOf(); - - /* Selecting specific properties */ - const index = useNavigationState((state) => state.index); - - expectTypeOf(index).toEqualTypeOf(); - - const routes = useNavigationState((state) => state.routes); - - expectTypeOf(routes).toEqualTypeOf[]>( - routes - ); -} - -{ - const state = useNavigationState< - StackNavigationState, - typeof Stack, - 'PostDetails' - >('PostDetails', (state) => state); - - expectTypeOf(state.type).toEqualTypeOf<'stack'>(); - - expectTypeOf(state.routeNames).toEqualTypeOf<(keyof RootStackParamList)[]>(); - - /* Invalid drawer state specified for stack */ - useNavigationState< - DrawerNavigationState, - typeof Stack, - 'PostDetails' - // @ts-expect-error - >('PostDetails', (state) => state); -} - -{ - const type = useNavigationState('Examples', (state) => state.type); - - expectTypeOf(type).toEqualTypeOf<'drawer'>(); - - const routeNames = useNavigationState( - 'Examples', - (state) => state.routeNames - ); - - expectTypeOf(routeNames).toEqualTypeOf<'Examples'[]>(); -} - -{ - const index = useNavigationState('NotFound', (state) => state.index); - - expectTypeOf(index).toEqualTypeOf(); - - const routeNames = useNavigationState( - 'NotFound', - (state) => state.routeNames - ); - - expectTypeOf(routeNames).toEqualTypeOf<(keyof RootParamList)[]>(); -} - -{ - const index = useNavigationState('TabChat', (state) => state.index); - - expectTypeOf(index).toEqualTypeOf(); - - const routeNames = useNavigationState('TabChat', (state) => state.routeNames); - - expectTypeOf(routeNames).toEqualTypeOf<(keyof BottomTabParamList)[]>(); -} - -// @ts-expect-error -useNavigationState('Invalid', (state) => state.index); - -/** - * Check for `linking` prop type validation based on ParamList - */ -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - Home: { - screens: { - Feed: { - screens: { - Popular: 'popular', - Latest: 'latest', - }, - }, - Account: 'account', - }, - }, - PostDetails: 'post/:id', - Settings: 'settings', - Login: 'login', - NotFound: '*', - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - PostDetails: { - path: 'post/:id/:section?', - parse: { - id: (value) => value, - section: (value) => value, - }, - stringify: { - id: (value) => value, - section: (value) => value ?? '', - }, - }, - Albums: { - screens: { - Artist: { - path: 'artist/:id', - parse: { - id: (value) => value, - }, - stringify: { - id: (value) => value, - }, - }, - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - // @ts-expect-error - InvalidScreen: 'invalid', - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - PostDetails: { - path: 'post/:id', - parse: { - // @ts-expect-error - invalidParam: (value) => value, - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - PostDetails: { - path: 'post/:id', - stringify: { - // @ts-expect-error - invalidParam: (value) => String(value), - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - PostDetails: { - path: 'post/:id', - parse: { - id: (value) => { - expectTypeOf(value).toEqualTypeOf(); - return value; - }, - }, - stringify: { - id: (value) => { - expectTypeOf(value).toEqualTypeOf(); - return value; - }, - section: (value) => { - expectTypeOf(value).toEqualTypeOf(); - return value ?? ''; - }, - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - Home: { - screens: { - // @ts-expect-error - InvalidNested: 'invalid', - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - PostDetails: { - path: 'post/:id', - // @ts-expect-error - PostDetails is a leaf route, not a navigator - screens: {}, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - Home: { - initialRouteName: 'Feed', - screens: { - Feed: 'feed', - Account: 'account', - }, - }, - }, - }, - }; - - {null}; -} - -{ - const linking: LinkingOptions = { - prefixes: ['myapp://'], - config: { - screens: { - Home: { - // @ts-expect-error - initialRouteName: 'InvalidRoute', - screens: { - Feed: 'feed', - }, - }, - }, - }, - }; - - {null}; -} - -/** - * Check for required props on the navigator - */ -{ - const MyNavigator = ( - _props: { - variant: 'compact' | 'regular'; - } & DefaultNavigatorOptions< - ParamListBase, - StackNavigationState, - {}, - {}, - unknown - > - ) => null; - - interface MyTypeBag extends NavigatorTypeBagBase { - State: StackNavigationState; - ActionHelpers: StackActionHelpers; - Navigator: typeof MyNavigator; - } - - const createMyNavigator = createNavigatorFactory(MyNavigator); - - const MyStack = createMyNavigator<{ Home: undefined }>(); - - void MyStack; - - expectTypeOf< - Pick, 'variant'> - >().toEqualTypeOf<{ - variant: 'compact' | 'regular'; - }>(); -} diff --git a/example/__typechecks__/standalone/dynamic/assets.d.ts b/example/__typechecks__/standalone/dynamic/assets.d.ts deleted file mode 100644 index e2937d470e..0000000000 --- a/example/__typechecks__/standalone/dynamic/assets.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.png'; diff --git a/example/__typechecks__/standalone/dynamic/index.check.tsx b/example/__typechecks__/standalone/dynamic/index.check.tsx deleted file mode 100644 index 0c73730cee..0000000000 --- a/example/__typechecks__/standalone/dynamic/index.check.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import { - type BottomTabNavigationOptions, - createBottomTabNavigator, -} from '@react-navigation/bottom-tabs'; -import { - createMaterialTopTabNavigator, - type MaterialTopTabNavigationOptions, -} from '@react-navigation/material-top-tabs'; -import { - type NavigatorScreenParams, - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - type NativeStackNavigationOptions, -} from '@react-navigation/native-stack'; -import { expectTypeOf } from 'expect-type'; - -type FeaturedStackParamList = { - ProductList: undefined; - ProductDetails: { productId: number }; - ProductReviews: { productId: number; page: number }; - ProductGallery: { productId: number; index: number }; -}; - -const FeaturedStack = createNativeStackNavigator(); - -type OnSaleStackParamList = { - DealsList: undefined; - DealDetails: { dealId: number }; - FlashSale: { saleId: string }; -}; - -const OnSaleStack = createNativeStackNavigator(); - -type NewArrivalsStackParamList = { - NewArrivalsList: undefined; - NewArrivalDetails: { itemId: number }; -}; - -const NewArrivalsStack = - createNativeStackNavigator(); - -type CategoryTabParamList = { - Featured: NavigatorScreenParams; - OnSale: NavigatorScreenParams; - NewArrivals: NavigatorScreenParams; -}; - -const CategoryTabs = createMaterialTopTabNavigator< - CategoryTabParamList, - { - Featured: typeof FeaturedStack; - OnSale: typeof OnSaleStack; - NewArrivals: typeof NewArrivalsStack; - } ->(); - -type SearchStackParamList = { - SearchHome: undefined; - SearchResults: { query: string; sort: 'price' | 'rating' }; -}; - -const SearchStack = createNativeStackNavigator(); - -// Tabs whose screens are added at runtime, keyed by a category id. -// The id is branded so the keys stay distinct from plain `string`. -// A plain `string` key would widen every route name in the app to `string`. -type CategorySlug = string & { readonly __brand: 'CategorySlug' }; - -type BrandTabParamList = Record; - -const BrandTabs = createMaterialTopTabNavigator(); - -type ShopStackParamList = { - Categories: NavigatorScreenParams; - Search: NavigatorScreenParams; - Brands: NavigatorScreenParams; - Cart: undefined; - Checkout: { cartId: string }; - PromoCode: { code: string }; -}; - -const ShopStack = createNativeStackNavigator(); - -type OrderArchiveParamList = { - ArchivedOrders: undefined; - ArchivedOrderDetails: { orderId: string }; -}; - -type OrderSupportStackParamList = { - OrderSupportHome: undefined; - OrderSupportTicket: { ticketId: string }; -}; - -const OrderSupportStack = - createNativeStackNavigator(); - -type OrdersStackParamList = { - OrderList: undefined; - OrderDetails: { orderId: string }; - TrackShipment: { orderId: string; carrier: string }; - OrderArchive: NavigatorScreenParams; - OrderSupport: NavigatorScreenParams; -}; - -const OrdersStack = createNativeStackNavigator(); - -type InboxStackParamList = { - InboxHome: undefined; - MessageThread: { threadId: string }; -}; - -const InboxStack = createNativeStackNavigator(); - -type MainTabParamList = { - Shop: NavigatorScreenParams; - Orders: NavigatorScreenParams; - Inbox: NavigatorScreenParams; - Account: { userId: string }; -}; - -const MainTabs = createBottomTabNavigator(); - -type RootStackParamList = { - Main: NavigatorScreenParams; - SignIn: { redirectTo: string }; - Paywall: { plan: 'monthly' | 'yearly' }; - EditProfile: { userId: string }; -}; - -const RootStack = createNativeStackNavigator(); - -// The navigators are rendered at runtime; this keeps a value reference to each -// since they're otherwise only referenced as types in the nesting maps above. -export const navigators = { - FeaturedStack, - OnSaleStack, - NewArrivalsStack, - CategoryTabs, - SearchStack, - BrandTabs, - ShopStack, - OrderSupportStack, - OrdersStack, - InboxStack, - MainTabs, - RootStack, -}; - -declare module '@react-navigation/native' { - interface RootNavigator extends RootStackType {} -} - -type RootStackType = typeof RootStack; - -export const ProductListScreen = () => { - const navigation = useNavigation('ProductList'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - ('ProductList' | 'ProductDetails' | 'ProductReviews' | 'ProductGallery')[] - >(); - - // Navigate within the same navigator - navigation.push('ProductDetails', { productId: 1 }); - navigation.push('ProductReviews', { productId: 1, page: 2 }); - navigation.push('ProductGallery', { productId: 1, index: 0 }); - - // @ts-expect-error - productId is required. - navigation.push('ProductDetails'); - - // @ts-expect-error - productId must be a number. - navigation.push('ProductDetails', { productId: '1' }); - - // @ts-expect-error - DealDetails is in a sibling navigator, not reachable by name. - navigation.push('DealDetails', { dealId: 1 }); - - // Navigate to routes in ancestor navigators via the composite - navigation.navigate('Checkout', { cartId: 'c1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/home' }); - - // Navigate into a sibling navigator through the nested params - navigation.navigate('Main', { - screen: 'Shop', - params: { screen: 'Search', params: { screen: 'SearchHome' } }, - }); - - expectTypeOf(navigation.setParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf< - 'ProductList' | 'Featured' | 'Categories' | 'Shop' | 'Main' | undefined - >(); - - return null; -}; - -/** - * Material top tabs nested four navigators deep. - */ -export const FeaturedTabScreen = () => { - const navigation = useNavigation('Featured'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - expectTypeOf(navigation.jumpTo).toBeFunction(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Native stack nested three navigators deep, navigating through leaves. - */ -export const CartScreen = () => { - const navigation = useNavigation('Cart'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.push('Checkout', { cartId: 'c1' }); - navigation.push('PromoCode', { code: 'SALE' }); - - // @ts-expect-error - cartId is required. - navigation.push('Checkout'); - - expectTypeOf(navigation.setParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Plain param-list nested routes expose generic navigation, parent navigation, - * and concrete navigator props when a child is declared with `typeof Navigator`. - */ -export const ParamListOnlyNestedScreen = () => { - /** - * ParamList > ParamList - */ - { - const navigation = useNavigation('ArchivedOrderDetails'); - - navigation.navigate('ArchivedOrders'); - navigation.navigate('ArchivedOrderDetails', { - orderId: 'archived-order-1', - }); - - navigation.navigate('OrderDetails', { orderId: 'order-1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/orders' }); - - // @ts-expect-error - orderId is required. - navigation.navigate('ArchivedOrderDetails'); - - // @ts-expect-error - param-list-only nested routes don't know the navigator type. - navigation.push('ArchivedOrderDetails', { - orderId: 'archived-order-1', - }); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - // @ts-expect-error - param-list-only nested routes don't know the navigator event map. - navigation.addListener('gestureCancel', () => {}); - - expectTypeOf(navigation.getState().type).toEqualTypeOf(); - } - - /** - * Navigator > ParamList - */ - { - const navigation = useNavigation('SearchResults'); - - navigation.navigate('SearchResults', { - query: 'shoes', - sort: 'price', - }); - - navigation.navigate('Cart'); - navigation.navigate('Checkout', { cartId: 'c1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/search' }); - - navigation.navigate('SearchResults', { - query: 'shoes', - // @ts-expect-error - sort must be a valid option. - sort: 'newest', - }); - - // @ts-expect-error - param-list-only nested routes don't know the navigator type. - navigation.push('SearchResults', { - query: 'shoes', - sort: 'price', - }); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - // @ts-expect-error - param-list-only nested routes don't know the navigator event map. - navigation.addListener('gestureCancel', () => {}); - - expectTypeOf(navigation.getState().type).toEqualTypeOf(); - } - - /** - * Navigator > ParamList - */ - { - const navigation = useNavigation('TrackShipment'); - - navigation.navigate('TrackShipment', { - orderId: 'order-1', - carrier: 'ups', - }); - - navigation.navigate('OrderList'); - navigation.navigate('OrderDetails', { orderId: 'order-1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/orders' }); - - // @ts-expect-error - carrier is required. - navigation.navigate('TrackShipment', { orderId: 'order-1' }); - - // @ts-expect-error - param-list-only nested routes don't know the navigator type. - navigation.push('OrderDetails', { orderId: 'order-1' }); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - // @ts-expect-error - param-list-only nested routes don't know the navigator event map. - navigation.addListener('gestureCancel', () => {}); - - expectTypeOf(navigation.getState().type).toEqualTypeOf(); - } - - /** - * Navigator > ParamList - */ - { - const navigation = useNavigation('MessageThread'); - - navigation.navigate('MessageThread', { threadId: 'thread-1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/inbox' }); - - // @ts-expect-error - threadId is required. - navigation.navigate('MessageThread'); - - // @ts-expect-error - param-list-only nested routes don't know the navigator type. - navigation.push('MessageThread', { threadId: 'thread-1' }); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - // @ts-expect-error - param-list-only nested routes don't know the navigator event map. - navigation.addListener('gestureCancel', () => {}); - - expectTypeOf(navigation.getState().type).toEqualTypeOf(); - } - - /** - * ParamList > Navigator - */ - { - const navigation = useNavigation('OrderSupportTicket'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.push('OrderSupportHome'); - navigation.push('OrderSupportTicket', { ticketId: 'ticket-1' }); - - navigation.navigate('OrderDetails', { orderId: 'order-1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/support' }); - - // @ts-expect-error - ticketId is required. - navigation.push('OrderSupportTicket'); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - navigation.addListener('gestureCancel', (e) => { - expectTypeOf(e.data).toEqualTypeOf(); - }); - - navigation.addListener('sheetDetentChange', (e) => { - expectTypeOf(e.data).toEqualTypeOf< - Readonly<{ index: number; stable: boolean }> - >(); - }); - - // @ts-expect-error - native stack doesn't emit tabPress. - navigation.addListener('tabPress', () => {}); - } - - return null; -}; - -/** - * Bottom tabs nested one navigator deep. - */ -export const AccountScreen = () => { - const navigation = useNavigation('Account'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - expectTypeOf(navigation.jumpTo) - .parameter(0) - .toEqualTypeOf<'Shop' | 'Orders' | 'Inbox' | 'Account'>(); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Root navigator. - */ -export const SignInScreen = () => { - const navigation = useNavigation('SignIn'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.navigate('SignIn', { redirectTo: '/home' }); - navigation.navigate('Paywall', { plan: 'monthly' }); - navigation.navigate('EditProfile', { userId: 'u1' }); - - // @ts-expect-error - plan must be a valid option. - navigation.navigate('Paywall', { plan: 'weekly' }); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * `useRoute` recovers params for routes at every nesting level. - */ -export const RouteParamsChecks = () => { - expectTypeOf(useRoute('ProductList').params).toEqualTypeOf(); - - expectTypeOf(useRoute('ProductDetails').params).toEqualTypeOf< - Readonly<{ productId: number }> - >(); - - expectTypeOf(useRoute('SearchResults').params).toEqualTypeOf< - Readonly<{ query: string; sort: 'price' | 'rating' }> - >(); - - expectTypeOf(useRoute('Checkout').params).toEqualTypeOf< - Readonly<{ cartId: string }> - >(); - - expectTypeOf(useRoute('TrackShipment').params).toEqualTypeOf< - Readonly<{ orderId: string; carrier: string }> - >(); - - expectTypeOf(useRoute('ArchivedOrderDetails').params).toEqualTypeOf< - Readonly<{ orderId: string }> - >(); - - expectTypeOf(useRoute('OrderSupportTicket').params).toEqualTypeOf< - Readonly<{ ticketId: string }> - >(); - - expectTypeOf(useRoute('Account').params).toEqualTypeOf< - Readonly<{ userId: string }> - >(); - - expectTypeOf(useRoute('SignIn').params).toEqualTypeOf< - Readonly<{ redirectTo: string }> - >(); - - // @ts-expect-error - not a route in the app. - useRoute('Invalid'); - - return null; -}; - -/** - * `useNavigationState` resolves state for routes at every nesting level. - */ -export const NavigationStateChecks = () => { - expectTypeOf( - useNavigationState('ProductList', (state) => state.type) - ).toEqualTypeOf<'stack'>(); - - expectTypeOf( - useNavigationState('Featured', (state) => state.type) - ).toEqualTypeOf<'tab'>(); - - expectTypeOf( - useNavigationState('Account', (state) => state.type) - ).toEqualTypeOf<'tab'>(); - - expectTypeOf( - useNavigationState('SignIn', (state) => state.index) - ).toEqualTypeOf(); - - expectTypeOf( - useNavigationState('ProductList', (state) => state.routeNames) - ).toEqualTypeOf< - ('ProductList' | 'ProductDetails' | 'ProductReviews' | 'ProductGallery')[] - >(); - - // @ts-expect-error - not a route in the app. - useNavigationState('Invalid', (state) => state.index); - - return null; -}; - -export const InvalidRouteCheck = () => { - // @ts-expect-error - not a route in the app. - useNavigation('Invalid'); - - return null; -}; diff --git a/example/__typechecks__/standalone/dynamic/tsconfig.json b/example/__typechecks__/standalone/dynamic/tsconfig.json deleted file mode 100644 index 018e7c2f81..0000000000 --- a/example/__typechecks__/standalone/dynamic/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../../tsconfig", - "compilerOptions": { - "composite": false, - "declaration": true, - "incremental": false, - "noEmit": true - }, - "include": ["."], - "exclude": [] -} diff --git a/example/__typechecks__/standalone/static/assets.d.ts b/example/__typechecks__/standalone/static/assets.d.ts deleted file mode 100644 index e2937d470e..0000000000 --- a/example/__typechecks__/standalone/static/assets.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.png'; diff --git a/example/__typechecks__/standalone/static/index.check.tsx b/example/__typechecks__/standalone/static/index.check.tsx deleted file mode 100644 index 551b638810..0000000000 --- a/example/__typechecks__/standalone/static/index.check.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import { - type BottomTabNavigationOptions, - createBottomTabNavigator, -} from '@react-navigation/bottom-tabs'; -import { - createMaterialTopTabNavigator, - createMaterialTopTabScreen, - type MaterialTopTabNavigationOptions, -} from '@react-navigation/material-top-tabs'; -import { - createStaticNavigation, - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, - type NativeStackNavigationOptions, -} from '@react-navigation/native-stack'; -import { expectTypeOf } from 'expect-type'; - -const Empty = () => null; - -const FeaturedStack = createNativeStackNavigator({ - screens: { - ProductList: createNativeStackScreen({ - screen: Empty, - linking: { path: 'products' }, - }), - ProductDetails: createNativeStackScreen({ - screen: Empty, - linking: { path: 'products/:productId', parse: { productId: Number } }, - }), - ProductReviews: createNativeStackScreen({ - screen: Empty, - linking: { - path: 'products/:productId/reviews/:page', - parse: { productId: Number, page: Number }, - }, - }), - ProductGallery: createNativeStackScreen({ - screen: Empty, - linking: { - path: 'products/:productId/gallery/:index', - parse: { productId: Number, index: Number }, - }, - }), - }, -}); - -const OnSaleStack = createNativeStackNavigator({ - screens: { - DealsList: createNativeStackScreen({ - screen: Empty, - linking: { path: 'deals' }, - }), - DealDetails: createNativeStackScreen({ - screen: Empty, - linking: { path: 'deals/:dealId', parse: { dealId: Number } }, - }), - FlashSale: createNativeStackScreen({ - screen: Empty, - linking: { path: 'flash/:saleId', parse: { saleId: String } }, - }), - }, -}); - -const NewArrivalsStack = createNativeStackNavigator({ - screens: { - NewArrivalsList: createNativeStackScreen({ - screen: Empty, - linking: { path: 'new' }, - }), - NewArrivalDetails: createNativeStackScreen({ - screen: Empty, - linking: { path: 'new/:itemId', parse: { itemId: Number } }, - }), - }, -}); - -const CategoryTabs = createMaterialTopTabNavigator({ - screens: { - Featured: createMaterialTopTabScreen({ screen: FeaturedStack }), - OnSale: createMaterialTopTabScreen({ screen: OnSaleStack }), - NewArrivals: createMaterialTopTabScreen({ screen: NewArrivalsStack }), - }, -}); - -const SearchStack = createNativeStackNavigator({ - screens: { - SearchHome: createNativeStackScreen({ - screen: Empty, - linking: { path: 'search' }, - }), - SearchResults: createNativeStackScreen({ - screen: Empty, - linking: { - path: 'search/:query/:sort', - parse: { - query: String, - sort: (value: string) => value as 'price' | 'rating', - }, - }, - }), - }, -}); - -// Tabs whose screens are added at runtime, keyed by a category id. -// The id is branded so the keys stay distinct from plain `string`. -// A plain `string` key would widen every route name in the app to `string`. -type CategorySlug = string & { readonly __brand: 'CategorySlug' }; - -const BrandTabs = createMaterialTopTabNavigator({ - screens: {} as Record< - CategorySlug, - ReturnType - >, -}); - -const ShopStack = createNativeStackNavigator({ - screens: { - Categories: createNativeStackScreen({ screen: CategoryTabs }), - Search: createNativeStackScreen({ screen: SearchStack }), - Brands: createNativeStackScreen({ screen: BrandTabs }), - Cart: createNativeStackScreen({ screen: Empty, linking: { path: 'cart' } }), - Checkout: createNativeStackScreen({ - screen: Empty, - linking: { path: 'checkout/:cartId', parse: { cartId: String } }, - }), - PromoCode: createNativeStackScreen({ - screen: Empty, - linking: { path: 'promo/:code', parse: { code: String } }, - }), - }, -}); - -const OrdersStack = createNativeStackNavigator({ - screens: { - OrderList: createNativeStackScreen({ - screen: Empty, - linking: { path: 'orders' }, - }), - OrderDetails: createNativeStackScreen({ - screen: Empty, - linking: { path: 'orders/:orderId', parse: { orderId: String } }, - }), - TrackShipment: createNativeStackScreen({ - screen: Empty, - linking: { - path: 'orders/:orderId/track/:carrier', - parse: { orderId: String, carrier: String }, - }, - }), - }, -}); - -const InboxStack = createNativeStackNavigator({ - screens: { - InboxHome: createNativeStackScreen({ - screen: Empty, - linking: { path: 'inbox' }, - }), - MessageThread: createNativeStackScreen({ - screen: Empty, - linking: { path: 'inbox/:threadId', parse: { threadId: String } }, - }), - }, -}); - -const MainTabs = createBottomTabNavigator({ - screens: { - Shop: { screen: ShopStack }, - Orders: { screen: OrdersStack }, - Inbox: { screen: InboxStack }, - Account: { - screen: Empty, - linking: { path: 'account/:userId', parse: { userId: String } }, - }, - }, -}); - -const RootStack = createNativeStackNavigator({ - screens: { - Main: createNativeStackScreen({ - screen: MainTabs, - options: { headerShown: false }, - }), - SignIn: createNativeStackScreen({ - screen: Empty, - linking: { path: 'sign-in/:redirectTo', parse: { redirectTo: String } }, - }), - }, - groups: { - Modal: { - screens: { - Paywall: createNativeStackScreen({ - screen: Empty, - linking: { - path: 'paywall/:plan', - parse: { plan: (value: string) => value as 'monthly' | 'yearly' }, - }, - }), - EditProfile: createNativeStackScreen({ - screen: Empty, - linking: { path: 'edit-profile/:userId', parse: { userId: String } }, - }), - }, - }, - }, -}); - -createStaticNavigation(RootStack); - -declare module '@react-navigation/native' { - interface RootNavigator extends RootStackType {} -} - -type RootStackType = typeof RootStack; - -export const ProductListScreen = () => { - const navigation = useNavigation('ProductList'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - expectTypeOf(navigation.getState().routeNames).toEqualTypeOf< - ('ProductList' | 'ProductDetails' | 'ProductReviews' | 'ProductGallery')[] - >(); - - // Navigate within the same navigator - navigation.push('ProductDetails', { productId: 1 }); - navigation.push('ProductReviews', { productId: 1, page: 2 }); - navigation.push('ProductGallery', { productId: 1, index: 0 }); - - // @ts-expect-error - productId is required. - navigation.push('ProductDetails'); - - // @ts-expect-error - productId must be a number. - navigation.push('ProductDetails', { productId: '1' }); - - // @ts-expect-error - DealDetails is in a sibling navigator, not reachable by name. - navigation.push('DealDetails', { dealId: 1 }); - - // Navigate to routes in ancestor navigators via the composite - navigation.navigate('Checkout', { cartId: 'c1' }); - navigation.navigate('Account', { userId: 'u1' }); - navigation.navigate('SignIn', { redirectTo: '/home' }); - - // Navigate into a sibling navigator through the nested params - navigation.navigate('Main', { - screen: 'Shop', - params: { screen: 'Search', params: { screen: 'SearchHome' } }, - }); - - expectTypeOf(navigation.setParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.getParent) - .parameter(0) - .toEqualTypeOf< - 'ProductList' | 'Featured' | 'Categories' | 'Shop' | 'Main' | undefined - >(); - - return null; -}; - -/** - * Material top tabs nested four navigators deep. - */ -export const FeaturedTabScreen = () => { - const navigation = useNavigation('Featured'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - expectTypeOf(navigation.jumpTo).toBeFunction(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Native stack nested three navigators deep, navigating through leaves. - */ -export const CartScreen = () => { - const navigation = useNavigation('Cart'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.push('Checkout', { cartId: 'c1' }); - navigation.push('PromoCode', { code: 'SALE' }); - - // @ts-expect-error - cartId is required. - navigation.push('Checkout'); - - expectTypeOf(navigation.setParams).parameter(0).toEqualTypeOf(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Bottom tabs nested one navigator deep. - */ -export const AccountScreen = () => { - const navigation = useNavigation('Account'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - expectTypeOf(navigation.jumpTo) - .parameter(0) - .toEqualTypeOf<'Shop' | 'Orders' | 'Inbox' | 'Account'>(); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * Root navigator. - */ -export const SignInScreen = () => { - const navigation = useNavigation('SignIn'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.navigate('SignIn', { redirectTo: '/home' }); - navigation.navigate('Paywall', { plan: 'monthly' }); - navigation.navigate('EditProfile', { userId: 'u1' }); - - // @ts-expect-error - plan must be a valid option. - navigation.navigate('Paywall', { plan: 'weekly' }); - - expectTypeOf(navigation.setParams) - .parameter(0) - .toEqualTypeOf>(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return null; -}; - -/** - * `useRoute` recovers params for routes at every nesting level. - */ -export const RouteParamsChecks = () => { - expectTypeOf(useRoute('ProductList').params).toEqualTypeOf(); - - expectTypeOf(useRoute('ProductDetails').params).toEqualTypeOf< - Readonly<{ productId: number }> - >(); - - expectTypeOf(useRoute('SearchResults').params).toEqualTypeOf< - Readonly<{ query: string; sort: 'price' | 'rating' }> - >(); - - expectTypeOf(useRoute('Checkout').params).toEqualTypeOf< - Readonly<{ cartId: string }> - >(); - - expectTypeOf(useRoute('TrackShipment').params).toEqualTypeOf< - Readonly<{ orderId: string; carrier: string }> - >(); - - expectTypeOf(useRoute('Account').params).toEqualTypeOf< - Readonly<{ userId: string }> - >(); - - expectTypeOf(useRoute('SignIn').params).toEqualTypeOf< - Readonly<{ redirectTo: string }> - >(); - - // @ts-expect-error - not a route in the app. - useRoute('Invalid'); - - return null; -}; - -/** - * `useNavigationState` resolves state for routes at every nesting level. - */ -export const NavigationStateChecks = () => { - expectTypeOf( - useNavigationState('ProductList', (state) => state.type) - ).toEqualTypeOf<'stack'>(); - - expectTypeOf( - useNavigationState('Featured', (state) => state.type) - ).toEqualTypeOf<'tab'>(); - - expectTypeOf( - useNavigationState('Account', (state) => state.type) - ).toEqualTypeOf<'tab'>(); - - expectTypeOf( - useNavigationState('SignIn', (state) => state.index) - ).toEqualTypeOf(); - - expectTypeOf( - useNavigationState('ProductList', (state) => state.routeNames) - ).toEqualTypeOf< - ('ProductList' | 'ProductDetails' | 'ProductReviews' | 'ProductGallery')[] - >(); - - // @ts-expect-error - not a route in the app. - useNavigationState('Invalid', (state) => state.index); - - return null; -}; - -export const InvalidRouteCheck = () => { - // @ts-expect-error - not a route in the app. - useNavigation('Invalid'); - - return null; -}; diff --git a/example/__typechecks__/standalone/static/tsconfig.json b/example/__typechecks__/standalone/static/tsconfig.json deleted file mode 100644 index 018e7c2f81..0000000000 --- a/example/__typechecks__/standalone/static/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../../../tsconfig", - "compilerOptions": { - "composite": false, - "declaration": true, - "incremental": false, - "noEmit": true - }, - "include": ["."], - "exclude": [] -} diff --git a/example/__typechecks__/static.check.tsx b/example/__typechecks__/static.check.tsx deleted file mode 100644 index 8c878aa8e0..0000000000 --- a/example/__typechecks__/static.check.tsx +++ /dev/null @@ -1,2791 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import { - type BottomTabNavigationOptions, - type BottomTabNavigationProp, - createBottomTabNavigator, - createBottomTabScreen, -} from '@react-navigation/bottom-tabs'; -import { - type CompositeNavigationProp, - createNavigatorFactory, - createStaticNavigation, - type DefaultNavigatorOptions, - type NavigationContainerRef, - type NavigationListForNested, - type NavigationProp, - type NavigatorScreenParams, - type NavigatorTypeBagBase, - type ParamListBase, - type RouteProp, - type ScreenLayoutArgs, - type StackActionHelpers, - type StackNavigationState, - type StaticParamList, - type StaticScreenProps, - type Theme, - useNavigation, - useNavigationState, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { - createStackNavigator, - createStackScreen, - type StackNavigationOptions, - type StackNavigationProp, -} from '@react-navigation/stack'; -import * as arktype from 'arktype'; -import { expectTypeOf } from 'expect-type'; -import * as v from 'valibot'; -import { z } from 'zod'; - -const NativeStack = createNativeStackNavigator({ - groups: { - GroupA: { - screenLayout: ({ navigation, children }) => { - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - expectTypeOf(navigation.push).toExtend<() => void>(); - - return <>{children}; - }, - screens: { - Foo: createNativeStackScreen({ - screen: () => <>, - options: { presentation: 'modal' }, - }), - }, - }, - }, -}); - -createStaticNavigation(NativeStack); - -expectTypeOf>().toEqualTypeOf<{ - Foo: undefined; -}>(); - -/** - * Infer params for a widened screen map without producing a complex union - */ -{ - const S0 = () => null; - const S1 = () => null; - const S2 = () => null; - const S3 = () => null; - const S4 = () => null; - const S5 = () => null; - const S6 = () => null; - const S7 = () => null; - const S8 = () => null; - const S9 = () => null; - const S10 = () => null; - const S11 = () => null; - const S12 = () => null; - const S13 = () => null; - const S14 = () => null; - const S15 = () => null; - const S16 = () => null; - const S17 = () => null; - const S18 = () => null; - const S19 = () => null; - const S20 = () => null; - const S21 = () => null; - const S22 = () => null; - const S23 = () => null; - const S24 = () => null; - const S25 = () => null; - const S26 = () => null; - const S27 = () => null; - const S28 = () => null; - const S29 = () => null; - const S30 = () => null; - const S31 = () => null; - const S32 = () => null; - const S33 = () => null; - const S34 = () => null; - const S35 = () => null; - const S36 = () => null; - const S37 = () => null; - const S38 = () => null; - const S39 = () => null; - const S40 = () => null; - const S41 = () => null; - const S42 = () => null; - const S43 = () => null; - const S44 = () => null; - const S45 = () => null; - const S46 = () => null; - const S47 = () => null; - const S48 = () => null; - const S49 = () => null; - - const screens = { - S0, - S1, - S2, - S3, - S4, - S5, - S6, - S7, - S8, - S9, - S10, - S11, - S12, - S13, - S14, - S15, - S16, - S17, - S18, - S19, - S20, - S21, - S22, - S23, - S24, - S25, - S26, - S27, - S28, - S29, - S30, - S31, - S32, - S33, - S34, - S35, - S36, - S37, - S38, - S39, - S40, - S41, - S42, - S43, - S44, - S45, - S46, - S47, - S48, - S49, - }; - - type ScreenId = keyof typeof screens; - - const fromEntries = (entries: [K, V][]) => - Object.fromEntries(entries) as Record; - - const ManyScreensStack = createNativeStackNavigator({ - screens: { - ...fromEntries( - (Object.keys(screens) as ScreenId[]).map((id) => [ - id, - createNativeStackScreen({ screen: screens[id] }), - ]) - ), - }, - }); - - const ManyScreensNavigation = createStaticNavigation(ManyScreensStack); - - ; - - type ManyScreensParamList = StaticParamList; - - expectTypeOf().toEqualTypeOf< - Record - >(); - - function ManyScreensChild() { - const navigation = useNavigation('S0'); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - navigation.navigate('S1'); - navigation.preload('S2'); - - return null; - } - - ; -} - -const HomeTabs = createBottomTabNavigator({ - screens: { - Groups: () => null, - Chat: (_: StaticScreenProps<{ id: number }>) => null, - }, -}); - -const RootStack = createStackNavigator({ - screens: { - Home: HomeTabs, - Profile: (_: StaticScreenProps<{ user: string }>) => null, - Feed: createStackScreen({ - screen: (_: StaticScreenProps<{ sort: 'hot' | 'recent' }>) => null, - options: { - headerTitle: 'My Feed', - }, - linking: { - path: 'feed/:sort', - parse: { - sort: (value) => value as 'hot' | 'recent', - }, - }, - initialParams: { - sort: 'hot', - }, - listeners: { - transitionEnd: (e) => { - expectTypeOf(e.data).toEqualTypeOf>(); - }, - }, - }), - Settings: () => null, - // TypeScript has a limit of 24 items in mapped types - // So we add more items to make sure we don't hit the issue - A: () => null, - B: () => null, - C: () => null, - D: () => null, - E: () => null, - F: () => null, - G: () => null, - H: () => null, - I: () => null, - J: () => null, - K: () => null, - L: () => null, - M: () => null, - N: () => null, - O: () => null, - P: () => null, - Q: () => null, - R: () => null, - S: () => null, - T: () => null, - U: () => null, - V: () => null, - W: () => null, - X: () => null, - Y: () => null, - Z: () => null, - }, - groups: { - Guest: { - screens: { - Login: () => null, - Register: (_: StaticScreenProps<{ method: 'email' | 'social' }>) => - null, - }, - }, - User: { - screens: { - Account: () => null, - }, - }, - }, - screenOptions: { - animationTypeForReplace: 'pop', - }, -}); - -const Navigation = createStaticNavigation(RootStack); - -; - -type RootParamList = StaticParamList; - -type RooStackRouteName = - | 'Home' - | 'Profile' - | 'Feed' - | 'Settings' - | 'Login' - | 'Register' - | 'Account' - | 'A' - | 'B' - | 'C' - | 'D' - | 'E' - | 'F' - | 'G' - | 'H' - | 'I' - | 'J' - | 'K' - | 'L' - | 'M' - | 'N' - | 'O' - | 'P' - | 'Q' - | 'R' - | 'S' - | 'T' - | 'U' - | 'V' - | 'W' - | 'X' - | 'Y' - | 'Z'; - -expectTypeOf().toEqualTypeOf(); - -declare let navigation: NavigationProp; - -/** - * Infer param list from navigator - */ -expectTypeOf().toEqualTypeOf<{ - Home: NavigatorScreenParams<{ - Groups: undefined; - Chat: { id: number }; - }>; - Profile: { user: string }; - Feed: { sort: 'hot' | 'recent' }; - Settings: undefined; - Login: undefined; - Register: { method: 'email' | 'social' }; - Account: undefined; - A: undefined; - B: undefined; - C: undefined; - D: undefined; - E: undefined; - F: undefined; - G: undefined; - H: undefined; - I: undefined; - J: undefined; - K: undefined; - L: undefined; - M: undefined; - N: undefined; - O: undefined; - P: undefined; - Q: undefined; - R: undefined; - S: undefined; - T: undefined; - U: undefined; - V: undefined; - W: undefined; - X: undefined; - Y: undefined; - Z: undefined; -}>(); - -/** - * Infer navigation props from navigator - */ -expectTypeOf['Home']>().toEqualTypeOf< - StackNavigationProp ->(); - -expectTypeOf['Feed']>().toEqualTypeOf< - StackNavigationProp ->(); - -expectTypeOf< - NavigationListForNested['Settings'] ->().toEqualTypeOf>(); - -expectTypeOf['Chat']>().toEqualTypeOf< - CompositeNavigationProp< - BottomTabNavigationProp, 'Chat'>, - StackNavigationProp - > ->(); - -expectTypeOf< - // @ts-expect-error - NavigationListForNested['Invalid'] ->().toEqualTypeOf(); - -expectTypeOf>().toEqualTypeOf< - RooStackRouteName | 'Groups' | 'Chat' ->(); - -function StaticNavigatorHookTypeChecks() { - // @ts-expect-error - useNavigation('Invalid'); - - // @ts-expect-error - useNavigationState( - 'Invalid', - (state) => state - ); - - return null; -} - -; - -/** - * Infer screen names from config - */ -expectTypeOf(navigation.getState().routes[0]?.name).toEqualTypeOf< - RooStackRouteName | undefined ->(); - -/** - * Infer params from component props - */ -navigation.navigate('Profile', { user: '123' }); - -// @ts-expect-error -navigation.navigate('Profile'); - -// @ts-expect-error -navigation.navigate('Profile', { user: 123 }); - -// @ts-expect-error -navigation.navigate('Profile', { nonexistent: 'test' }); - -/** - * Infer params from component props for config object - */ -navigation.navigate('Feed', { sort: 'hot' }); -navigation.navigate('Feed', { sort: 'recent' }); - -// @ts-expect-error -navigation.navigate('Feed'); - -// @ts-expect-error -navigation.navigate('Feed', { sort: '42' }); - -// @ts-expect-error -navigation.navigate('Feed', { nonexistent: 'test' }); - -/** - * Infer params for component without props - */ -navigation.navigate('Settings'); -navigation.navigate('Settings', undefined); - -// @ts-expect-error -navigation.navigate('Settings', { nonexistent: 'test' }); - -/** - * Infer params from component props for inside group - */ -navigation.navigate('Register', { method: 'email' }); - -// @ts-expect-error -navigation.navigate('Register', { method: 'token' }); - -/** - * Infer params from nested navigator - */ -// @ts-expect-error -navigation.navigate('Home'); -navigation.navigate('Home', { screen: 'Groups' }); -navigation.navigate('Home', { screen: 'Chat', params: { id: 123 } }); - -// @ts-expect-error -navigation.navigate('Home', { screen: 'Groups', params: { id: '123' } }); - -// @ts-expect-error -navigation.navigate('Home', { screen: 'Chat' }); - -/** - * Deeply nested static config (stack > tabs > stack > stack). - * Models a typical media app to exercise composite navigation props across - * several levels of nesting. - */ -{ - const PlayerStack = createStackNavigator({ - screens: { - NowPlaying: () => null, - Queue: () => null, - Lyrics: (_: StaticScreenProps<{ trackId: string }>) => null, - }, - }); - - const HomeStack = createStackNavigator({ - screens: { - Feed: () => null, - Album: (_: StaticScreenProps<{ albumId: string }>) => null, - Player: PlayerStack, - }, - }); - - const LibraryStack = createStackNavigator({ - screens: { - Library: () => null, - Playlist: (_: StaticScreenProps<{ playlistId: string }>) => null, - }, - }); - - const AppTabs = createBottomTabNavigator({ - screens: { - HomeTab: HomeStack, - LibraryTab: LibraryStack, - Account: () => null, - }, - }); - - const AppStack = createStackNavigator({ - screens: { - Main: AppTabs, - Login: () => null, - Settings: (_: StaticScreenProps<{ section: 'general' | 'privacy' }>) => - null, - }, - }); - - createStaticNavigation(AppStack); - - expectTypeOf>().toEqualTypeOf< - | 'Main' - | 'Login' - | 'Settings' - | 'HomeTab' - | 'LibraryTab' - | 'Account' - | 'Feed' - | 'Album' - | 'Player' - | 'Library' - | 'Playlist' - | 'NowPlaying' - | 'Queue' - | 'Lyrics' - >(); - - /** - * A route nested four levels deep resolves to the full composite chain - */ - expectTypeOf< - NavigationListForNested['Lyrics'] - >().toEqualTypeOf< - CompositeNavigationProp< - StackNavigationProp, 'Lyrics'>, - CompositeNavigationProp< - StackNavigationProp, 'Player'>, - CompositeNavigationProp< - BottomTabNavigationProp, 'HomeTab'>, - StackNavigationProp, 'Main'> - > - > - > - >(); - - /** - * A route nested three levels deep also resolves to its composite chain - */ - expectTypeOf< - NavigationListForNested['Playlist'] - >().toEqualTypeOf< - CompositeNavigationProp< - StackNavigationProp, 'Playlist'>, - CompositeNavigationProp< - BottomTabNavigationProp, 'LibraryTab'>, - StackNavigationProp, 'Main'> - > - > - >(); - - // Navigate to nested screens through the full hierarchy - function NestedNavigateChecks( - navigation: NavigationProp> - ) { - navigation.navigate('Login'); - navigation.navigate('Settings', { section: 'general' }); - navigation.navigate('Main', { screen: 'Account' }); - navigation.navigate('Main', { - screen: 'HomeTab', - params: { screen: 'Album', params: { albumId: 'album-1' } }, - }); - navigation.navigate('Main', { - screen: 'HomeTab', - params: { - screen: 'Player', - params: { screen: 'Lyrics', params: { trackId: 'track-1' } }, - }, - }); - navigation.navigate('Main', { - screen: 'HomeTab', - params: { - screen: 'Player', - // @ts-expect-error - trackId is required for the Lyrics screen - params: { screen: 'Lyrics' }, - }, - }); - } - - void NestedNavigateChecks; - - function MediaAppScreen() { - const navigation = useNavigation( - 'NowPlaying' - ); - - expectTypeOf(navigation).toEqualTypeOf< - CompositeNavigationProp< - StackNavigationProp, 'NowPlaying'>, - CompositeNavigationProp< - StackNavigationProp, 'Player'>, - CompositeNavigationProp< - BottomTabNavigationProp, 'HomeTab'>, - StackNavigationProp, 'Main'> - > - > - > - >(); - - expectTypeOf(navigation.getState()).toEqualTypeOf< - StackNavigationState> - >(); - - navigation.navigate('Queue'); - navigation.navigate('Lyrics', { trackId: 'track-1' }); - navigation.navigate('Album', { albumId: 'album-1' }); - navigation.navigate('LibraryTab', { - screen: 'Playlist', - params: { playlistId: 'playlist-1' }, - }); - navigation.navigate('Settings', { section: 'privacy' }); - - // @ts-expect-error: trackId is required for the Lyrics screen. - navigation.navigate('Lyrics'); - - // @ts-expect-error: albumId is required for the Album screen. - navigation.navigate('Album'); - - // @ts-expect-error: section must be a valid Settings section. - navigation.navigate('Settings', { section: 'account' }); - - const lyricsIndex = useNavigationState( - 'Lyrics', - (state) => state.index - ); - - expectTypeOf(lyricsIndex).toEqualTypeOf(); - - const state = useNavigationState< - StackNavigationState>, - typeof AppStack, - 'Lyrics' - >('Lyrics', (state) => state); - - expectTypeOf(state.type).toEqualTypeOf<'stack'>(); - - expectTypeOf(state.routeNames).toEqualTypeOf< - (keyof StaticParamList)[] - >(); - - return null; - } - - ; -} - -/** - * Infer navigator config options - */ -createBottomTabNavigator({ - backBehavior: 'initialRoute', - screenOptions: { - tabBarActiveTintColor: 'tomato', - }, - screens: {}, -}); - -createBottomTabNavigator({ - // @ts-expect-error - backBehavior: 'unsupported', - screens: {}, -}); - -createBottomTabNavigator({ - screenOptions: { - // @ts-expect-error - tabBarActiveTintColor: 42, - }, - screens: {}, -}); - -createBottomTabNavigator({ - screenOptions: () => ({ - tabBarActiveTintColor: 'tomato', - }), - screens: {}, -}); - -createBottomTabNavigator({ - // @ts-expect-error - screenOptions: () => ({ - tabBarActiveTintColor: 42, - }), - screens: {}, -}); - -/** - * Infer screen options - */ -createBottomTabNavigator({ - screens: { - Test: createBottomTabScreen({ - screen: () => null, - options: { - tabBarActiveTintColor: 'tomato', - }, - }), - }, -}); - -createBottomTabNavigator({ - screens: { - Test: createBottomTabScreen({ - screen: () => null, - options: { - // @ts-expect-error - tabBarActiveTintColor: 42, - }, - }), - }, -}); - -createBottomTabNavigator({ - screens: { - Test: createBottomTabScreen({ - screen: () => null, - options: () => ({ - tabBarActiveTintColor: 'tomato', - }), - }), - }, -}); - -createBottomTabNavigator({ - screens: { - Test: createBottomTabScreen({ - screen: () => null, - // @ts-expect-error - options: () => ({ - tabBarActiveTintColor: 42, - }), - }), - }, -}); - -createBottomTabNavigator({ - screens: { - Test: createBottomTabScreen({ - screen: (_: StaticScreenProps<{ foo: number }>) => null, - initialParams: { - // @ts-expect-error - foo: 'test', - }, - }), - }, -}); - -/** - * Have correct type for screen options callback - */ -createBottomTabNavigator({ - screenOptions: ({ route, navigation, theme }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - expectTypeOf(navigation.jumpTo).toExtend<() => void>(); - expectTypeOf(theme).toEqualTypeOf(); - - return {}; - }, - screens: {}, -}); - -createBottomTabNavigator({ - screens: { - Test: { - screen: () => null, - options: ({ route, navigation, theme }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - expectTypeOf(navigation.jumpTo).toExtend<() => void>(); - expectTypeOf(theme).toEqualTypeOf(); - - return {}; - }, - }, - }, -}); - -/** - * Have correct type for listeners callback - */ -createBottomTabNavigator({ - screenListeners: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - expectTypeOf(navigation.jumpTo).toExtend<() => void>(); - - return {}; - }, - screens: {}, -}); - -createBottomTabNavigator({ - screens: { - Test: { - screen: () => null, - listeners: ({ navigation, route }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - expectTypeOf(navigation.jumpTo).toExtend<() => void>(); - - return {}; - }, - }, - }, -}); - -/** - * Requires `screens` or `groups` to be defined - */ -// @ts-expect-error -createStackNavigator({}); - -createStackNavigator({ - screens: {}, -}); - -createStackNavigator({ - groups: {}, -}); - -/** - * Rejects unknown properties on the navigator config - */ -createStackNavigator({ - screens: {}, - // @ts-expect-error - bogusOption: 'oops', -}); - -createStackNavigator({ - groups: {}, - // @ts-expect-error - unknownKey: true, - // @ts-expect-error - anotherOne: 123, -}); - -createBottomTabNavigator({ - screens: {}, - // @ts-expect-error - notARealKey: 1, -}); - -createNativeStackNavigator({ - screens: {}, - // @ts-expect-error - screenOption: {}, -}); - -/** - * Infer types from group without screens - */ -const MyTabs = createBottomTabNavigator({ - groups: { - Test: { - screens: { - Test: (_: StaticScreenProps<{ foo: string }>) => null, - }, - }, - }, -}); - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const MyStack = createStackNavigator({ - groups: { - Guest: { - screens: { - Login: () => null, - }, - }, - User: { - screens: { - Home: () => null, - Profile: (_: StaticScreenProps<{ id: number }>) => null, - Forum: MyTabs, - }, - }, - }, -}); - -type MyParamList = StaticParamList; - -expectTypeOf().toEqualTypeOf<{ - Login: undefined; - Home: undefined; - Profile: { id: number }; - Forum: NavigatorScreenParams<{ - Test: { foo: string }; - }>; -}>(); - -/** - * Preserves all five generics across 3+ levels of nesting in `CompositeNavigationProp` - */ -{ - const NestedInner = createBottomTabNavigator({ - screens: { - NestedLeaf: (_: StaticScreenProps<{ leafId: string } | undefined>) => - null, - }, - }); - - const NestedMiddle = createStackNavigator({ - screens: { - NestedInner: NestedInner, - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const NestedOuter = createStackNavigator({ - screens: { - NestedMiddle: NestedMiddle, - }, - }); - - type NestedLeafNav = NavigationListForNested< - typeof NestedOuter - >['NestedLeaf']; - - expectTypeOf().toEqualTypeOf< - CompositeNavigationProp< - CompositeNavigationProp< - BottomTabNavigationProp< - StaticParamList, - 'NestedLeaf' - >, - StackNavigationProp, 'NestedInner'> - >, - StackNavigationProp, 'NestedMiddle'> - > - >(); - - // State: `getState()` refers to the tab navigator's state - expectTypeOf< - ReturnType['type'] - >().toEqualTypeOf<'tab'>(); - - // ScreenOptions: `setOptions` refers to the tab navigator's options - expectTypeOf[0]>().toEqualTypeOf< - Partial - >(); - - // RouteName: `setParams` refers to `NestedLeaf`'s params - expectTypeOf[0]>().toEqualTypeOf< - Partial<{ leafId: string } | undefined> - >(); - - const navigation: NestedLeafNav = {} as any; - - // EventMap: `addListener` accepts tab-specific and core events - navigation.addListener('tabPress', () => {}); - navigation.addListener('focus', () => {}); - - // ParamList: `navigate` accepts route names from all three navigators - navigation.navigate('NestedLeaf'); - navigation.navigate('NestedLeaf', { leafId: 'abc' }); - navigation.navigate('NestedInner'); - navigation.navigate('NestedMiddle'); - - // Parent `RouteName`: `getParent` covers all ancestor screens - navigation.getParent('NestedInner'); - navigation.getParent('NestedMiddle'); -} - -/** - * Check for errors on getCurrentRoute - */ -declare const navigationRef: NavigationContainerRef; -const route = navigationRef.getCurrentRoute()!; - -switch (route.name) { - case 'Profile': - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - user: string; - }> - >(); - break; - case 'Feed': - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - sort: 'hot' | 'recent'; - }> - >(); - break; - case 'Settings': - expectTypeOf(route.params).toEqualTypeOf(); - break; - case 'Login': - expectTypeOf(route.params).toEqualTypeOf(); - break; - case 'Register': - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - method: 'email' | 'social'; - }> - >(); - break; - case 'Account': - expectTypeOf(route.params).toEqualTypeOf(); - break; - // Checks for nested routes - case 'Groups': - expectTypeOf(route.params).toEqualTypeOf(); - break; - case 'Chat': - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - }> - >(); - break; -} - -/** - * Infer types from typed screen component - */ -createStackNavigator({ - screens: { - Profile: createStackScreen({ - screen: ( - _: StaticScreenProps<{ userId: string; filter?: 'recent' | 'popular' }> - ) => null, - options: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: string; - filter?: 'recent' | 'popular'; - }> - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return { - headerTitle: route.params.userId, - }; - }, - listeners: ({ route, navigation }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: string; - filter?: 'recent' | 'popular'; - }> - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - layout: ({ route, navigation, children }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: string; - filter?: 'recent' | 'popular'; - }> - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return <>{children}; - }, - getId: ({ params }) => { - expectTypeOf(params).toEqualTypeOf< - Readonly<{ - userId: string; - filter?: 'recent' | 'popular'; - }> - >(); - - return params.userId; - }, - initialParams: { - filter: 'recent', - // @ts-expect-error - userId: 3, - }, - }), - }, -}); - -/** - * Handle screen component without optional params - */ -createStackNavigator({ - screens: { - Details: createStackScreen({ - screen: ( - _: StaticScreenProps<{ itemId: number; info: string } | undefined> - ) => null, - options: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - itemId: number; - info: string; - }> - | undefined - >(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - listeners: ({ route, navigation }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - itemId: number; - info: string; - }> - | undefined - >(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - layout: ({ route, navigation, children }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - itemId: number; - info: string; - }> - | undefined - >(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return <>{children}; - }, - getId: ({ params }) => { - expectTypeOf(params).toEqualTypeOf< - | Readonly<{ - itemId: number; - info: string; - }> - | undefined - >(); - - return params?.itemId.toString(); - }, - initialParams: { - itemId: 1, - info: 'Item 1', - }, - }), - }, -}); - -/** - * Handle screen component without typed route prop - */ -createStackNavigator({ - screens: { - Settings: createStackScreen({ - screen: () => null, - options: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(route.params).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - listeners: ({ route, navigation }) => { - expectTypeOf(route.params).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - layout: ({ route, navigation, children }) => { - expectTypeOf(route.params).toEqualTypeOf(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return <>{children}; - }, - getId: ({ params }) => { - expectTypeOf(params).toEqualTypeOf(); - - return 'static-id'; - }, - }), - }, -}); - -/** - * Handle screen which is a nested navigator - */ -createBottomTabNavigator({ - screens: { - User: createBottomTabScreen({ - screen: createStackNavigator({ - screens: { - Profile: (_: StaticScreenProps<{ userId: string }>) => null, - }, - }), - options: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - NavigatorScreenParams<{ - Profile: { - userId: string; - }; - }> - > - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - return {}; - }, - listeners: ({ route, navigation }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - NavigatorScreenParams<{ - Profile: { - userId: string; - }; - }> - > - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - return {}; - }, - layout: ({ route, navigation, children }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - NavigatorScreenParams<{ - Profile: { - userId: string; - }; - }> - > - >(); - - expectTypeOf(navigation.getState().type).toEqualTypeOf<'tab'>(); - - return <>{children}; - }, - getId: ({ params }) => { - expectTypeOf(params).toEqualTypeOf< - Readonly< - NavigatorScreenParams<{ - Profile: { - userId: string; - }; - }> - > - >(); - - return 'static-id'; - }, - }), - }, -}); - -/** - * Mark params as optional based on the nested screens' params - */ -{ - const ExactUndefinedStack = createStackNavigator({ - screens: { - Home: () => null, - Profile: () => null, - }, - }); - - type ExactUndefinedStackParamList = StaticParamList< - typeof ExactUndefinedStack - >; - - expectTypeOf().toEqualTypeOf<{ - Home: undefined; - Profile: undefined; - }>(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const ExactUndefinedTabs = createBottomTabNavigator({ - screens: { - User: ExactUndefinedStack, - }, - }); - - expectTypeOf>().toEqualTypeOf<{ - User: NavigatorScreenParams | undefined; - }>(); - - const IncludedUndefinedStack = createStackNavigator({ - screens: { - Home: (_: StaticScreenProps<{ homeId: string } | undefined>) => null, - Profile: (_: StaticScreenProps<{ userId: string } | undefined>) => null, - }, - }); - - type IncludedUndefinedStackParamList = StaticParamList< - typeof IncludedUndefinedStack - >; - - expectTypeOf().toEqualTypeOf<{ - Home: { homeId: string } | undefined; - Profile: { userId: string } | undefined; - }>(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const IncludedUndefinedTabs = createBottomTabNavigator({ - screens: { - User: IncludedUndefinedStack, - }, - }); - - expectTypeOf>().toEqualTypeOf<{ - User: NavigatorScreenParams | undefined; - }>(); - - const OneExactUndefinedStack = createStackNavigator({ - screens: { - Home: () => null, - Profile: (_: StaticScreenProps<{ userId: string }>) => null, - }, - }); - - type OneExactUndefinedStackParamList = StaticParamList< - typeof OneExactUndefinedStack - >; - - expectTypeOf().toEqualTypeOf<{ - Home: undefined; - Profile: { userId: string }; - }>(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const OneExactUndefinedTabs = createBottomTabNavigator({ - screens: { - User: OneExactUndefinedStack, - }, - }); - - expectTypeOf>().toEqualTypeOf<{ - User: NavigatorScreenParams; - }>(); - - const OneIncludedUndefinedStack = createStackNavigator({ - screens: { - Home: (_: StaticScreenProps<{ homeId: string } | undefined>) => null, - Profile: (_: StaticScreenProps<{ userId: string }>) => null, - }, - }); - - type OneIncludedUndefinedStackParamList = StaticParamList< - typeof OneIncludedUndefinedStack - >; - - expectTypeOf().toEqualTypeOf<{ - Home: { homeId: string } | undefined; - Profile: { userId: string }; - }>(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const OneIncludedUndefinedTabs = createBottomTabNavigator({ - screens: { - User: OneIncludedUndefinedStack, - }, - }); - - expectTypeOf< - StaticParamList - >().toEqualTypeOf<{ - User: NavigatorScreenParams; - }>(); - - const NoUndefinedStack = createStackNavigator({ - screens: { - Home: (_: StaticScreenProps<{ homeId: string }>) => null, - Profile: (_: StaticScreenProps<{ userId: string }>) => null, - }, - }); - - type NoUndefinedStackParamList = StaticParamList; - - expectTypeOf().toEqualTypeOf<{ - Home: { homeId: string }; - Profile: { userId: string }; - }>(); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const NoUndefinedTabs = createBottomTabNavigator({ - screens: { - User: NoUndefinedStack, - }, - }); - - expectTypeOf>().toEqualTypeOf<{ - User: NavigatorScreenParams; - }>(); -} - -/** - * Params type is `unknown` when `createXScreen` isn't used - */ -createStackNavigator({ - screens: { - Profile: { - screen: (_: StaticScreenProps<{ userId: string }>) => null, - options: ({ route, navigation }) => { - expectTypeOf(route.params).not.toBeAny(); - expectTypeOf(route.params).toBeUnknown(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - listeners: ({ route, navigation }) => { - expectTypeOf(route.params).not.toBeAny(); - expectTypeOf(route.params).toBeUnknown(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return {}; - }, - layout: ({ route, navigation, children }) => { - expectTypeOf(route.params).not.toBeAny(); - expectTypeOf(route.params).toBeUnknown(); - expectTypeOf(navigation.getState().type).toEqualTypeOf<'stack'>(); - - return <>{children}; - }, - getId: ({ params }) => { - expectTypeOf(params).not.toBeAny(); - expectTypeOf(params).toBeUnknown(); - - return 'static-id'; - }, - }, - }, -}); - -/** - * Linking config validates parse/stringify keys against params - */ -createStackNavigator({ - screens: { - Profile: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - userId: string; - postId: number; - commentId: number; - }> - ) => null, - linking: { - path: 'profile/:userId/:postId', - parse: { - userId: String, - postId: Number, - }, - stringify: { - userId: String, - postId: String, - }, - alias: [ - ':userId/:postId', - { - path: 'user/:userId/:postId', - parse: { - userId: String, - postId: Number, - }, - }, - ], - }, - }), - Details: createStackScreen({ - screen: (_: StaticScreenProps<{ id: string }>) => null, - linking: 'details/:id', - }), - Item: createStackScreen({ - screen: (_: StaticScreenProps<{ itemId: string; category: string }>) => - null, - linking: { - path: 'item/:itemId/:category', - parse: { - itemId: String, - }, - }, - }), - }, -}); - -/** - * Linking config infers params from Standard Schema-compatible parsers - */ -createStackNavigator({ - screens: { - ZodCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'zod/:id/:coords', - parse: { - id: Number, - coords: z - .string() - .transform((s) => s.split(',').map(Number)) - .pipe(z.tuple([z.number(), z.number()])), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - ValibotCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'valibot/:id/:coords', - parse: { - id: Number, - coords: v.pipe( - v.string(), - v.transform((s) => s.split(',').map(Number)), - v.tuple([ - v.pipe(v.number(), v.finite()), - v.pipe(v.number(), v.finite()), - ]) - ), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - ArkTypeCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'arktype/:id/:coords', - parse: { - id: Number, - coords: arktype - .type('string') - .pipe( - (s) => s.split(','), - arktype.type(['string.numeric.parse', 'string.numeric.parse']) - ), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - QueryParamZodCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-zod/:id', - parse: { - id: Number, - coords: z - .union([z.string(), z.array(z.string()), z.null(), z.undefined()]) - .transform((value) => { - if (value == null) { - return [0, 0] as const; - } - - if (Array.isArray(value)) { - const [a = 0, b = 0] = value.slice(0, 2).map(Number); - - return [a, b] as const; - } - - const [a = 0, b = 0] = value.split(',').map(Number); - - return [a, b] as const; - }) - .pipe(z.tuple([z.number(), z.number()])), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - QueryParamValibotCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-valibot/:id', - parse: { - id: Number, - coords: v.pipe( - v.union([v.string(), v.array(v.string()), v.null(), v.undefined()]), - v.transform((value) => { - if (value == null) { - return [0, 0] as const; - } - - if (Array.isArray(value)) { - const [a = 0, b = 0] = value.slice(0, 2).map(Number); - - return [a, b] as const; - } - - const [a = 0, b = 0] = value.split(',').map(Number); - - return [a, b] as const; - }), - v.tuple([v.number(), v.number()]) - ), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - QueryParamArktypeCoords: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-arktype/:id', - parse: { - id: Number, - coords: arktype.type('string | string[] | null | undefined').pipe( - (value) => { - if (value == null) { - return [0, 0]; - } - - if (Array.isArray(value)) { - const [a = 0, b = 0] = value.slice(0, 2).map(Number); - - return [a, b]; - } - - const [a = 0, b = 0] = value.split(',').map(Number); - - return [a, b]; - }, - arktype.type(['number', 'number']) - ), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - coords: [number, number]; - }> - >(); - - return {}; - }, - }), - QueryParamFunctionParser: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-function/:id', - parse: { - id: Number, - query: (value: string) => (value === 'new' ? 'new' : 'top'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - query?: 'new' | 'top'; - }> - >(); - - return {}; - }, - }), - QueryParamOnlyFunctionParser: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-function-only', - parse: { - query: (value: string) => (value === 'new' ? 'new' : 'top'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - query?: 'new' | 'top'; - }> - | undefined - >(); - - return {}; - }, - }), - QueryParamOptionalSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-optional/:id', - parse: { - id: Number, - query: z.string().optional(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - query?: string | undefined; - }> - >(); - - return {}; - }, - }), - QueryParamDefaultSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-default/:id', - parse: { - id: Number, - query: z.string().optional().default('fallback'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: number; - query: string; - }> - >(); - - return {}; - }, - }), - QueryParamOnlySchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-only', - parse: { - query: z.string(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - query: string; - }> - >(); - - return {}; - }, - }), - QueryParamOnlyOptionalSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-only-optional', - parse: { - query: z.string().optional(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - query?: string | undefined; - }> - | undefined - >(); - - return {}; - }, - }), - OptionalPathAndQueryFunctionParser: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-function-optional/:id?', - parse: { - query: (value: string) => (value === 'new' ? 'new' : 'top'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - id?: string; - query?: 'new' | 'top'; - }> - | undefined - >(); - - return {}; - }, - }), - OptionalPathAndQueryOptionalSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-schema-optional/:id?', - parse: { - query: z.string().optional(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - id?: string; - query?: string | undefined; - }> - | undefined - >(); - - return {}; - }, - }), - QueryParamOnlyDefaultSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'query-param-default-only', - parse: { - query: z.string().optional().default('fallback'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - query: string; - }> - >(); - - return {}; - }, - }), - }, -}); - -/** - * Path optionality should come from `?` in the pattern, not schema `.optional()` - */ -createStackNavigator({ - screens: { - RequiredPathWithOptionalSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'required/:id', - parse: { - id: z.string().optional(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: string | undefined; - }> - >(); - - return {}; - }, - }), - OptionalPathWithRequiredSchema: createStackScreen({ - screen: () => null, - linking: { - path: 'optional/:id?', - parse: { - id: z.string(), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - id?: string; - }> - | undefined - >(); - - return {}; - }, - }), - }, -}); - -/** - * Type tests for param inference from linking config - */ -{ - const SupportTabs = createBottomTabNavigator({ - screens: { - Faq: () => null, - Contact: createBottomTabScreen({ - screen: () => null, - }), - }, - }); - - type SupportTabsParamList = StaticParamList; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const TestNavigator = createStackNavigator({ - screens: { - /** - * Infer from path and parse - */ - UserProfile: createStackScreen({ - screen: (_: StaticScreenProps<{ userId: number; postId: string }>) => - null, - linking: { - path: 'user/:userId/:postId', - parse: { - userId: (value: string) => Number(value), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: number; - postId: string; - }> - >(); - - return {}; - }, - }), - /** - * Infer from screen props only - */ - Settings: createStackScreen({ - screen: (_: StaticScreenProps<{ name: string; age: number }>) => null, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - name: string; - age: number; - }> - >(); - - return {}; - }, - }), - /** - * Infer from screen props and path without params - */ - UserDashboard: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - userId: string; - section: 'posts' | 'comments'; - }> - ) => null, - linking: { - path: 'user/dashboard', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: string; - section: 'posts' | 'comments'; - }> - >(); - - return {}; - }, - }), - /** - * Infer from path with empty params - */ - UserSettings: createStackScreen({ - screen: (_: StaticScreenProps<{}>) => null, - linking: { - path: 'settings/:userId', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId: string; - }> - >(); - - return {}; - }, - }), - /** - * Infer from path with no props - */ - Help: createStackScreen({ - screen: () => null, - linking: { - path: 'help/:topic', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - topic: string; - }> - >(); - - return {}; - }, - }), - /** - * Handle optional param in path - */ - UserList: createStackScreen({ - screen: (_: StaticScreenProps<{ userId?: number; filter: string }>) => - null, - linking: { - path: 'users/:userId?/:filter', - parse: { - userId: (value: string) => Number(value), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - userId?: number; - filter: string; - }> - >(); - - return {}; - }, - }), - /** - * Handle regex in path - */ - Item: createStackScreen({ - screen: (_: StaticScreenProps<{ category: string }>) => null, - linking: { - path: 'item/:id([0-9]+)/:slug([a-z-]+)/:category', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - id: string; - slug: string; - category: string; - }> - >(); - - return {}; - }, - }), - /** - * Merge params from path and screen props - */ - ItemDetails: createStackScreen({ - screen: (_: StaticScreenProps<{ itemId: string }>) => null, - linking: { - path: 'item/:itemId/:infoType', - parse: { - infoType: (value: string) => - value === 'full' ? 'full' : 'summary', - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - itemId: string; - infoType: 'summary' | 'full'; - }> - >(); - - return {}; - }, - }), - /** - * Merge params from path and screen props with optionality - */ - ItemPrice: createStackScreen({ - screen: ( - _: StaticScreenProps<{ itemId: string; currency?: 'USD' | 'EUR' }> - ) => null, - linking: { - path: 'item/:itemId/:currency?', - parse: { - currency: (value: string) => (value === 'USD' ? 'USD' : 'EUR'), - }, - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - itemId: string; - currency?: 'USD' | 'EUR'; - }> - >(); - - return {}; - }, - }), - /** - * Handle string in pattern - */ - Details: createStackScreen({ - screen: (_: StaticScreenProps<{ detailId: string }>) => null, - linking: 'details/:detailId', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - detailId: string; - }> - >(); - - return {}; - }, - }), - /** - * Handle component and path without params - */ - About: createStackScreen({ - screen: () => null, - linking: { - path: 'about', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf(); - - return {}; - }, - }), - /** - * Nested navigator with params inferred from children - */ - Dashboard: createStackScreen({ - screen: createBottomTabNavigator({ - screens: { - Overview: (_: StaticScreenProps<{ period: string }>) => null, - Stats: createBottomTabScreen({ - screen: (_: StaticScreenProps<{ chartType: 'bar' | 'line' }>) => - null, - }), - }, - }), - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - NavigatorScreenParams<{ - Overview: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }> - > - >(); - - return {}; - }, - }), - /** - * Merge params from nested navigator and path - */ - Feed: createStackScreen({ - screen: createBottomTabNavigator({ - screens: { - Overview: (_: StaticScreenProps<{ period: string }>) => null, - Stats: createBottomTabScreen({ - screen: (_: StaticScreenProps<{ chartType: 'bar' | 'line' }>) => - null, - }), - }, - }), - linking: { - path: 'feed/:section?', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - { section?: string | undefined } & NavigatorScreenParams<{ - Overview: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }> - > - >(); - - return {}; - }, - }), - /** - * Merge required and optional path params from nested navigator and path - */ - ProductReviews: createStackScreen({ - screen: createBottomTabNavigator({ - screens: { - Summary: (_: StaticScreenProps<{ period: string }>) => null, - Stats: createBottomTabScreen({ - screen: (_: StaticScreenProps<{ chartType: 'bar' | 'line' }>) => - null, - }), - }, - }), - linking: { - path: 'products/:id/reviews/:section?', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly< - { id: string; section?: string } & NavigatorScreenParams<{ - Summary: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }> - > - >(); - - return {}; - }, - }), - /** - * Preserve route optionality from optional nested navigator and path - */ - SupportCenter: createStackScreen({ - screen: SupportTabs, - linking: { - path: 'support/:section?', - }, - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly< - { - section?: string; - } & NavigatorScreenParams - > - | undefined - >(); - - return {}; - }, - }), - }, - }); - - type TestParamList = StaticParamList; - - expectTypeOf().toEqualTypeOf<{ - Feed: { - section?: string | undefined; - } & NavigatorScreenParams<{ - Overview: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }>; - Settings: { - name: string; - age: number; - }; - UserProfile: { - userId: number; - postId: string; - }; - UserDashboard: { - userId: string; - section: 'posts' | 'comments'; - }; - UserSettings: { - userId: string; - }; - Help: { - topic: string; - }; - UserList: { - userId?: number; - filter: string; - }; - Item: { - id: string; - slug: string; - category: string; - }; - ItemDetails: { - itemId: string; - infoType: 'full' | 'summary'; - }; - ItemPrice: { - itemId: string; - currency?: 'USD' | 'EUR'; - }; - Details: { - detailId: string; - }; - About: undefined; - Dashboard: NavigatorScreenParams<{ - Overview: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }>; - ProductReviews: { - id: string; - section?: string; - } & NavigatorScreenParams<{ - Summary: { - period: string; - }; - Stats: { - chartType: 'bar' | 'line'; - }; - }>; - SupportCenter: - | ({ section?: string } & NavigatorScreenParams) - | undefined; - }>(); -} - -/** - * Only infers params for valid path patterns - */ -createStackScreen({ - screen: () => null, - linking: 'static/path', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'end:', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'middle:end', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: ':start', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - start: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: '/:start', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - start: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: '/:start?', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - start?: string; - }> - | undefined - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'middle/:part/end', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - part: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'end/:finish', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - finish: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'static/:optional?/path', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - optional?: string; - }> - | undefined - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: ':first/:second?/static/:third?/path', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - first: string; - second?: string; - third?: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'static/path/:withRegex([0-9]+)', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - Readonly<{ - withRegex: string; - }> - >(); - - return {}; - }, -}); - -createStackScreen({ - screen: () => null, - linking: 'static/:optionalRegex([a-z]+)?/path', - options: ({ route }) => { - expectTypeOf(route.params).toEqualTypeOf< - | Readonly<{ - optionalRegex?: string; - }> - | undefined - >(); - - return {}; - }, -}); - -/** - * Preserves types with getComponent - */ -{ - const Stack = createStackNavigator({ - screens: { - Profile: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - userId: string; - filter?: 'recent' | 'popular'; - }> - ) => null, - options: ({ route }) => { - return { - headerTitle: route.params.userId, - }; - }, - }), - Settings: createStackScreen({ - screen: () => null, - }), - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const StackComponent = Stack.getComponent(); - - expectTypeOf>().toExtend<{ - initialRouteName?: 'Profile' | 'Settings' | undefined; - }>(); -} - -/** - * Infers type for nested navigator with getComponent - */ -{ - const Stack = createStackNavigator({ - screens: { - Home: createStackScreen({ - screen: () => null, - }), - Profile: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - userId: string; - }> - ) => null, - }), - }, - }); - - type FooParamList = { - Home: undefined; - Profile: { - userId: string; - }; - }; - - type FooNavigation = - | StackNavigationProp - | StackNavigationProp; - - const StackComponent = Stack.getComponent(); - - expectTypeOf>().toExtend<{ - initialRouteName?: 'Home' | 'Profile' | undefined; - screenLayout?: - | (( - props: ScreenLayoutArgs< - FooParamList, - keyof FooParamList, - StackNavigationOptions, - FooNavigation - > - ) => React.ReactElement) - | undefined; - screenOptions?: - | StackNavigationOptions - | ((props: { - route: RouteProp; - navigation: FooNavigation; - theme: Theme; - }) => StackNavigationOptions) - | undefined; - }>(); - - const Dashboard = () => { - return ; - }; - - Dashboard.config = Stack.config; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const Tabs = createBottomTabNavigator({ - screens: { - Dashboard: Dashboard, - Settings: createBottomTabScreen({ - screen: (_: StaticScreenProps<{ category: string }>) => null, - }), - }, - }); - - type TabsParamList = StaticParamList; - - expectTypeOf().toEqualTypeOf<{ - Dashboard: NavigatorScreenParams<{ - Home: undefined; - Profile: { - userId: string; - }; - }>; - Settings: { - category: string; - }; - }>(); -} - -/** - * Supports with for decorating the static navigator component - */ -{ - type FooParamList = { - Profile: { - userId: string; - }; - Feed: { - sort: 'hot' | 'recent'; - }; - }; - - type FooNavigation = - | StackNavigationProp - | StackNavigationProp; - - const config = { - screens: { - Profile: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - userId: string; - }> - ) => null, - }), - Feed: createStackScreen({ - screen: ( - _: StaticScreenProps<{ - sort: 'hot' | 'recent'; - }> - ) => null, - }), - }, - } as const; - - const Stack = createStackNavigator(config).with(({ Navigator }) => { - expectTypeOf>().toExtend<{ - initialRouteName?: keyof FooParamList | undefined; - screenLayout?: - | (( - props: ScreenLayoutArgs< - FooParamList, - keyof FooParamList, - StackNavigationOptions, - FooNavigation - > - ) => React.ReactElement) - | undefined; - screenOptions?: - | StackNavigationOptions - | ((props: { - route: RouteProp; - navigation: FooNavigation; - theme: Theme; - }) => StackNavigationOptions) - | undefined; - }>(); - - return ; - }); - - expectTypeOf(Stack).not.toHaveProperty('with'); - - expectTypeOf(Stack.config).toEqualTypeOf(); -} - -/** - * Check for required props on the navigator - */ -{ - const MyNavigator = ( - _props: { - variant: 'compact' | 'regular'; - } & DefaultNavigatorOptions< - ParamListBase, - StackNavigationState, - {}, - {}, - unknown - > - ) => null; - - interface MyTypeBag extends NavigatorTypeBagBase { - State: StackNavigationState; - ActionHelpers: StackActionHelpers; - Navigator: typeof MyNavigator; - } - - const createMyNavigator = createNavigatorFactory(MyNavigator); - - expectTypeOf< - Pick[0], 'variant'> - >().toEqualTypeOf<{ - variant: 'compact' | 'regular'; - }>(); - - const Stack = createMyNavigator({ - variant: 'compact', - screens: { - Home: () => null, - }, - }); - - const StackComponent = Stack.getComponent(); - - expectTypeOf< - Pick, 'variant'> - >().toEqualTypeOf<{ - variant?: 'compact' | 'regular'; - }>(); - - void StackComponent; - - const DecoratedStack = createMyNavigator({ - variant: 'compact', - screens: { - Home: () => null, - }, - }).with(({ Navigator }) => { - expectTypeOf< - Pick, 'variant'> - >().toEqualTypeOf<{ - variant?: 'compact' | 'regular'; - }>(); - - return ; - }); - - expectTypeOf(DecoratedStack).not.toHaveProperty('with'); -} diff --git a/example/app.json b/example/app.json deleted file mode 100644 index b41d12bef1..0000000000 --- a/example/app.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "expo": { - "name": "React Navigation Example", - "owner": "react-navigation", - "slug": "react-navigation-example", - "description": "Demonstrates the functionality and various capabilities of React Navigation.", - "version": "1.0.0", - "icon": "./assets/icon.png", - "platforms": [ - "ios", - "android", - "web" - ], - "scheme": "rne", - "android": { - "package": "org.reactnavigation.playground", - "adaptiveIcon": { - "backgroundColor": "#7b61c1", - "foregroundImage": "./assets/icon-foreground.png" - } - }, - "ios": { - "supportsTablet": true, - "bundleIdentifier": "org.reactnavigation.playground", - "icon": "./assets/icon.icon" - }, - "web": { - "bundler": "metro" - }, - "updates": { - "fallbackToCacheTimeout": 0, - "url": "https://u.expo.dev/a7070fc4-41f3-403d-826d-292b5d868327" - }, - "assetBundlePatterns": [ - "**/*" - ], - "runtimeVersion": { - "policy": "sdkVersion" - }, - "plugins": [ - "expo-asset", - "expo-font", - [ - "expo-splash-screen", - { - "image": "./assets/splash.png", - "backgroundColor": "#7b61c1" - } - ] - ], - "buildCacheProvider": { - "plugin": "expo-build-disk-cache", - "options": { - "cacheDir": ".expo/build-cache", - "cacheGcTimeDays": 30 - } - }, - "extra": { - "supportsRTL": true, - "forcesRTL": false, - "eas": { - "projectId": "a7070fc4-41f3-403d-826d-292b5d868327" - } - } - } -} diff --git a/example/assets/album-art/01.jpg b/example/assets/album-art/01.jpg deleted file mode 100644 index 74325b777c..0000000000 Binary files a/example/assets/album-art/01.jpg and /dev/null differ diff --git a/example/assets/album-art/02.jpg b/example/assets/album-art/02.jpg deleted file mode 100644 index 14d3f4ab53..0000000000 Binary files a/example/assets/album-art/02.jpg and /dev/null differ diff --git a/example/assets/album-art/03.jpg b/example/assets/album-art/03.jpg deleted file mode 100644 index b7e87ccb57..0000000000 Binary files a/example/assets/album-art/03.jpg and /dev/null differ diff --git a/example/assets/album-art/04.jpg b/example/assets/album-art/04.jpg deleted file mode 100644 index fcf9d645c9..0000000000 Binary files a/example/assets/album-art/04.jpg and /dev/null differ diff --git a/example/assets/album-art/05.jpg b/example/assets/album-art/05.jpg deleted file mode 100644 index 36a26aa63b..0000000000 Binary files a/example/assets/album-art/05.jpg and /dev/null differ diff --git a/example/assets/album-art/06.jpg b/example/assets/album-art/06.jpg deleted file mode 100644 index efbbcb8104..0000000000 Binary files a/example/assets/album-art/06.jpg and /dev/null differ diff --git a/example/assets/album-art/07.jpg b/example/assets/album-art/07.jpg deleted file mode 100644 index 122a069e30..0000000000 Binary files a/example/assets/album-art/07.jpg and /dev/null differ diff --git a/example/assets/album-art/08.jpg b/example/assets/album-art/08.jpg deleted file mode 100644 index 8180910e0a..0000000000 Binary files a/example/assets/album-art/08.jpg and /dev/null differ diff --git a/example/assets/album-art/09.jpg b/example/assets/album-art/09.jpg deleted file mode 100644 index a8083f1c5e..0000000000 Binary files a/example/assets/album-art/09.jpg and /dev/null differ diff --git a/example/assets/album-art/10.jpg b/example/assets/album-art/10.jpg deleted file mode 100644 index 1903248992..0000000000 Binary files a/example/assets/album-art/10.jpg and /dev/null differ diff --git a/example/assets/album-art/11.jpg b/example/assets/album-art/11.jpg deleted file mode 100644 index ce67e77d90..0000000000 Binary files a/example/assets/album-art/11.jpg and /dev/null differ diff --git a/example/assets/album-art/12.jpg b/example/assets/album-art/12.jpg deleted file mode 100644 index 589d1829b9..0000000000 Binary files a/example/assets/album-art/12.jpg and /dev/null differ diff --git a/example/assets/album-art/13.jpg b/example/assets/album-art/13.jpg deleted file mode 100644 index bc8f18b151..0000000000 Binary files a/example/assets/album-art/13.jpg and /dev/null differ diff --git a/example/assets/album-art/14.jpg b/example/assets/album-art/14.jpg deleted file mode 100644 index bba1e600bb..0000000000 Binary files a/example/assets/album-art/14.jpg and /dev/null differ diff --git a/example/assets/album-art/15.jpg b/example/assets/album-art/15.jpg deleted file mode 100644 index f21ce4c90a..0000000000 Binary files a/example/assets/album-art/15.jpg and /dev/null differ diff --git a/example/assets/album-art/16.jpg b/example/assets/album-art/16.jpg deleted file mode 100644 index 1a3c0a0d48..0000000000 Binary files a/example/assets/album-art/16.jpg and /dev/null differ diff --git a/example/assets/album-art/17.jpg b/example/assets/album-art/17.jpg deleted file mode 100644 index ac832beed5..0000000000 Binary files a/example/assets/album-art/17.jpg and /dev/null differ diff --git a/example/assets/album-art/18.jpg b/example/assets/album-art/18.jpg deleted file mode 100644 index 90bedafff3..0000000000 Binary files a/example/assets/album-art/18.jpg and /dev/null differ diff --git a/example/assets/album-art/19.jpg b/example/assets/album-art/19.jpg deleted file mode 100644 index e471bc25c2..0000000000 Binary files a/example/assets/album-art/19.jpg and /dev/null differ diff --git a/example/assets/album-art/20.jpg b/example/assets/album-art/20.jpg deleted file mode 100644 index 131938e530..0000000000 Binary files a/example/assets/album-art/20.jpg and /dev/null differ diff --git a/example/assets/album-art/21.jpg b/example/assets/album-art/21.jpg deleted file mode 100644 index 962ec3dcc1..0000000000 Binary files a/example/assets/album-art/21.jpg and /dev/null differ diff --git a/example/assets/album-art/22.jpg b/example/assets/album-art/22.jpg deleted file mode 100644 index 10018e1557..0000000000 Binary files a/example/assets/album-art/22.jpg and /dev/null differ diff --git a/example/assets/album-art/23.jpg b/example/assets/album-art/23.jpg deleted file mode 100644 index 6f35a038e4..0000000000 Binary files a/example/assets/album-art/23.jpg and /dev/null differ diff --git a/example/assets/album-art/24.jpg b/example/assets/album-art/24.jpg deleted file mode 100644 index 5c68c37d2e..0000000000 Binary files a/example/assets/album-art/24.jpg and /dev/null differ diff --git a/example/assets/icon-foreground.png b/example/assets/icon-foreground.png deleted file mode 100644 index 91f42f3eca..0000000000 Binary files a/example/assets/icon-foreground.png and /dev/null differ diff --git a/example/assets/icon-foreground.svg b/example/assets/icon-foreground.svg deleted file mode 100644 index 62e73d6c53..0000000000 --- a/example/assets/icon-foreground.svg +++ /dev/null @@ -1 +0,0 @@ -8 \ No newline at end of file diff --git a/example/assets/icon.icon/Assets/badge-background.png b/example/assets/icon.icon/Assets/badge-background.png deleted file mode 100644 index 88581d6060..0000000000 Binary files a/example/assets/icon.icon/Assets/badge-background.png and /dev/null differ diff --git a/example/assets/icon.icon/Assets/badge-text.png b/example/assets/icon.icon/Assets/badge-text.png deleted file mode 100644 index e39fdc2606..0000000000 Binary files a/example/assets/icon.icon/Assets/badge-text.png and /dev/null differ diff --git a/example/assets/icon.icon/Assets/spiro.png b/example/assets/icon.icon/Assets/spiro.png deleted file mode 100644 index 2f07c36c41..0000000000 Binary files a/example/assets/icon.icon/Assets/spiro.png and /dev/null differ diff --git a/example/assets/icon.icon/icon.json b/example/assets/icon.icon/icon.json deleted file mode 100644 index 5e83a06ac6..0000000000 --- a/example/assets/icon.icon/icon.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "fill" : { - "solid" : "display-p3:0.40381,0.32544,0.65869,1.00000" - }, - "groups" : [ - { - "blend-mode-specializations" : [ - { - "value" : "normal" - }, - { - "appearance" : "dark", - "value" : "normal" - }, - { - "appearance" : "tinted", - "value" : "normal" - } - ], - "blur-material" : null, - "layers" : [ - { - "glass" : false, - "image-name" : "badge-text.png", - "name" : "badge-text" - }, - { - "image-name" : "badge-background.png", - "name" : "badge-background" - } - ], - "name" : "badge", - "position" : { - "scale" : 1, - "translation-in-points" : [ - 200, - -200 - ] - }, - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "specular" : true, - "translucency" : { - "enabled" : false, - "value" : 0.5 - } - }, - { - "layers" : [ - { - "glass" : false, - "image-name" : "spiro.png", - "name" : "spiro" - } - ], - "name" : "logo", - "shadow" : { - "kind" : "neutral", - "opacity" : 0.5 - }, - "translucency" : { - "enabled" : true, - "value" : 0.5 - } - } - ], - "supported-platforms" : { - "circles" : [ - "watchOS" - ], - "squares" : "shared" - } -} \ No newline at end of file diff --git a/example/assets/icon.png b/example/assets/icon.png deleted file mode 100644 index e1d89c119e..0000000000 Binary files a/example/assets/icon.png and /dev/null differ diff --git a/example/assets/icon.svg b/example/assets/icon.svg deleted file mode 100644 index cfb04def06..0000000000 --- a/example/assets/icon.svg +++ /dev/null @@ -1 +0,0 @@ -8 \ No newline at end of file diff --git a/example/assets/icons/README.md b/example/assets/icons/README.md deleted file mode 100644 index 75c7a10c78..0000000000 --- a/example/assets/icons/README.md +++ /dev/null @@ -1 +0,0 @@ -Icon credits to [lucide.dev](https://lucide.dev) diff --git a/example/assets/icons/book-open.png b/example/assets/icons/book-open.png deleted file mode 100644 index 8beb80da69..0000000000 Binary files a/example/assets/icons/book-open.png and /dev/null differ diff --git a/example/assets/icons/book-open@2x.png b/example/assets/icons/book-open@2x.png deleted file mode 100644 index 004ed0782f..0000000000 Binary files a/example/assets/icons/book-open@2x.png and /dev/null differ diff --git a/example/assets/icons/book-open@3x.png b/example/assets/icons/book-open@3x.png deleted file mode 100644 index 8beb80da69..0000000000 Binary files a/example/assets/icons/book-open@3x.png and /dev/null differ diff --git a/example/assets/icons/book-user.png b/example/assets/icons/book-user.png deleted file mode 100644 index 2a97060cdd..0000000000 Binary files a/example/assets/icons/book-user.png and /dev/null differ diff --git a/example/assets/icons/book-user@1x.png b/example/assets/icons/book-user@1x.png deleted file mode 100644 index 4c96329b42..0000000000 Binary files a/example/assets/icons/book-user@1x.png and /dev/null differ diff --git a/example/assets/icons/book-user@2x.png b/example/assets/icons/book-user@2x.png deleted file mode 100644 index 8692970c9e..0000000000 Binary files a/example/assets/icons/book-user@2x.png and /dev/null differ diff --git a/example/assets/icons/book-user@3x.png b/example/assets/icons/book-user@3x.png deleted file mode 100644 index 2a97060cdd..0000000000 Binary files a/example/assets/icons/book-user@3x.png and /dev/null differ diff --git a/example/assets/icons/chevron-down.png b/example/assets/icons/chevron-down.png deleted file mode 100644 index f89d4a609e..0000000000 Binary files a/example/assets/icons/chevron-down.png and /dev/null differ diff --git a/example/assets/icons/chevron-down@1x.png b/example/assets/icons/chevron-down@1x.png deleted file mode 100644 index 8001a99d29..0000000000 Binary files a/example/assets/icons/chevron-down@1x.png and /dev/null differ diff --git a/example/assets/icons/chevron-down@2x.png b/example/assets/icons/chevron-down@2x.png deleted file mode 100644 index bad19c92fd..0000000000 Binary files a/example/assets/icons/chevron-down@2x.png and /dev/null differ diff --git a/example/assets/icons/chevron-down@3x.png b/example/assets/icons/chevron-down@3x.png deleted file mode 100644 index f89d4a609e..0000000000 Binary files a/example/assets/icons/chevron-down@3x.png and /dev/null differ diff --git a/example/assets/icons/chevron-right.png b/example/assets/icons/chevron-right.png deleted file mode 100644 index bfa85ee4ee..0000000000 Binary files a/example/assets/icons/chevron-right.png and /dev/null differ diff --git a/example/assets/icons/chevron-right@1x.png b/example/assets/icons/chevron-right@1x.png deleted file mode 100644 index 0eb1999eaa..0000000000 Binary files a/example/assets/icons/chevron-right@1x.png and /dev/null differ diff --git a/example/assets/icons/chevron-right@2x.png b/example/assets/icons/chevron-right@2x.png deleted file mode 100644 index 7cd72ad281..0000000000 Binary files a/example/assets/icons/chevron-right@2x.png and /dev/null differ diff --git a/example/assets/icons/chevron-right@3x.png b/example/assets/icons/chevron-right@3x.png deleted file mode 100644 index bfa85ee4ee..0000000000 Binary files a/example/assets/icons/chevron-right@3x.png and /dev/null differ diff --git a/example/assets/icons/circle-play.png b/example/assets/icons/circle-play.png deleted file mode 100644 index e7acc2bef4..0000000000 Binary files a/example/assets/icons/circle-play.png and /dev/null differ diff --git a/example/assets/icons/circle-play@1x.png b/example/assets/icons/circle-play@1x.png deleted file mode 100644 index b7cd76694e..0000000000 Binary files a/example/assets/icons/circle-play@1x.png and /dev/null differ diff --git a/example/assets/icons/circle-play@2x.png b/example/assets/icons/circle-play@2x.png deleted file mode 100644 index 8f2f8c1f37..0000000000 Binary files a/example/assets/icons/circle-play@2x.png and /dev/null differ diff --git a/example/assets/icons/circle-play@3x.png b/example/assets/icons/circle-play@3x.png deleted file mode 100644 index e7acc2bef4..0000000000 Binary files a/example/assets/icons/circle-play@3x.png and /dev/null differ diff --git a/example/assets/icons/disc-3.png b/example/assets/icons/disc-3.png deleted file mode 100644 index 2bed1baa4f..0000000000 Binary files a/example/assets/icons/disc-3.png and /dev/null differ diff --git a/example/assets/icons/disc-3@1x.png b/example/assets/icons/disc-3@1x.png deleted file mode 100644 index ea1629716e..0000000000 Binary files a/example/assets/icons/disc-3@1x.png and /dev/null differ diff --git a/example/assets/icons/disc-3@2x.png b/example/assets/icons/disc-3@2x.png deleted file mode 100644 index 90883bd6b8..0000000000 Binary files a/example/assets/icons/disc-3@2x.png and /dev/null differ diff --git a/example/assets/icons/disc-3@3x.png b/example/assets/icons/disc-3@3x.png deleted file mode 100644 index 2bed1baa4f..0000000000 Binary files a/example/assets/icons/disc-3@3x.png and /dev/null differ diff --git a/example/assets/icons/heart.png b/example/assets/icons/heart.png deleted file mode 100644 index bc81093e47..0000000000 Binary files a/example/assets/icons/heart.png and /dev/null differ diff --git a/example/assets/icons/heart@1x.png b/example/assets/icons/heart@1x.png deleted file mode 100644 index 80d391ec4d..0000000000 Binary files a/example/assets/icons/heart@1x.png and /dev/null differ diff --git a/example/assets/icons/heart@2x.png b/example/assets/icons/heart@2x.png deleted file mode 100644 index 2b230514cb..0000000000 Binary files a/example/assets/icons/heart@2x.png and /dev/null differ diff --git a/example/assets/icons/heart@3x.png b/example/assets/icons/heart@3x.png deleted file mode 100644 index bc81093e47..0000000000 Binary files a/example/assets/icons/heart@3x.png and /dev/null differ diff --git a/example/assets/icons/landmark.png b/example/assets/icons/landmark.png deleted file mode 100644 index 8278e8e591..0000000000 Binary files a/example/assets/icons/landmark.png and /dev/null differ diff --git a/example/assets/icons/landmark@1x.png b/example/assets/icons/landmark@1x.png deleted file mode 100644 index 103616346a..0000000000 Binary files a/example/assets/icons/landmark@1x.png and /dev/null differ diff --git a/example/assets/icons/landmark@2x.png b/example/assets/icons/landmark@2x.png deleted file mode 100644 index 1bf6bce9b3..0000000000 Binary files a/example/assets/icons/landmark@2x.png and /dev/null differ diff --git a/example/assets/icons/landmark@3x.png b/example/assets/icons/landmark@3x.png deleted file mode 100644 index 8278e8e591..0000000000 Binary files a/example/assets/icons/landmark@3x.png and /dev/null differ diff --git a/example/assets/icons/layout-dashboard.png b/example/assets/icons/layout-dashboard.png deleted file mode 100644 index 6a4bafc194..0000000000 Binary files a/example/assets/icons/layout-dashboard.png and /dev/null differ diff --git a/example/assets/icons/layout-dashboard@1x.png b/example/assets/icons/layout-dashboard@1x.png deleted file mode 100644 index 79d9ce3cae..0000000000 Binary files a/example/assets/icons/layout-dashboard@1x.png and /dev/null differ diff --git a/example/assets/icons/layout-dashboard@2x.png b/example/assets/icons/layout-dashboard@2x.png deleted file mode 100644 index 10f2a74fd9..0000000000 Binary files a/example/assets/icons/layout-dashboard@2x.png and /dev/null differ diff --git a/example/assets/icons/layout-dashboard@3x.png b/example/assets/icons/layout-dashboard@3x.png deleted file mode 100644 index 6a4bafc194..0000000000 Binary files a/example/assets/icons/layout-dashboard@3x.png and /dev/null differ diff --git a/example/assets/icons/library.png b/example/assets/icons/library.png deleted file mode 100644 index b3fe05e2d4..0000000000 Binary files a/example/assets/icons/library.png and /dev/null differ diff --git a/example/assets/icons/library@1x.png b/example/assets/icons/library@1x.png deleted file mode 100644 index d2fb94dd08..0000000000 Binary files a/example/assets/icons/library@1x.png and /dev/null differ diff --git a/example/assets/icons/library@2x.png b/example/assets/icons/library@2x.png deleted file mode 100644 index e8b6e6b16e..0000000000 Binary files a/example/assets/icons/library@2x.png and /dev/null differ diff --git a/example/assets/icons/library@3x.png b/example/assets/icons/library@3x.png deleted file mode 100644 index b3fe05e2d4..0000000000 Binary files a/example/assets/icons/library@3x.png and /dev/null differ diff --git a/example/assets/icons/list-music.png b/example/assets/icons/list-music.png deleted file mode 100644 index 4d5ee18deb..0000000000 Binary files a/example/assets/icons/list-music.png and /dev/null differ diff --git a/example/assets/icons/list-music@1x.png b/example/assets/icons/list-music@1x.png deleted file mode 100644 index f5efb22e21..0000000000 Binary files a/example/assets/icons/list-music@1x.png and /dev/null differ diff --git a/example/assets/icons/list-music@2x.png b/example/assets/icons/list-music@2x.png deleted file mode 100644 index 65089c5c87..0000000000 Binary files a/example/assets/icons/list-music@2x.png and /dev/null differ diff --git a/example/assets/icons/list-music@3x.png b/example/assets/icons/list-music@3x.png deleted file mode 100644 index 4d5ee18deb..0000000000 Binary files a/example/assets/icons/list-music@3x.png and /dev/null differ diff --git a/example/assets/icons/message-circle.png b/example/assets/icons/message-circle.png deleted file mode 100644 index d4c2286a00..0000000000 Binary files a/example/assets/icons/message-circle.png and /dev/null differ diff --git a/example/assets/icons/message-circle@1x.png b/example/assets/icons/message-circle@1x.png deleted file mode 100644 index b3da9e713a..0000000000 Binary files a/example/assets/icons/message-circle@1x.png and /dev/null differ diff --git a/example/assets/icons/message-circle@2x.png b/example/assets/icons/message-circle@2x.png deleted file mode 100644 index d426c34a98..0000000000 Binary files a/example/assets/icons/message-circle@2x.png and /dev/null differ diff --git a/example/assets/icons/message-circle@3x.png b/example/assets/icons/message-circle@3x.png deleted file mode 100644 index d4c2286a00..0000000000 Binary files a/example/assets/icons/message-circle@3x.png and /dev/null differ diff --git a/example/assets/icons/music.png b/example/assets/icons/music.png deleted file mode 100644 index 0b994c26a3..0000000000 Binary files a/example/assets/icons/music.png and /dev/null differ diff --git a/example/assets/icons/music@1x.png b/example/assets/icons/music@1x.png deleted file mode 100644 index 5264ff2163..0000000000 Binary files a/example/assets/icons/music@1x.png and /dev/null differ diff --git a/example/assets/icons/music@2x.png b/example/assets/icons/music@2x.png deleted file mode 100644 index d4bdbdcf88..0000000000 Binary files a/example/assets/icons/music@2x.png and /dev/null differ diff --git a/example/assets/icons/music@3x.png b/example/assets/icons/music@3x.png deleted file mode 100644 index 0b994c26a3..0000000000 Binary files a/example/assets/icons/music@3x.png and /dev/null differ diff --git a/example/assets/icons/newspaper.png b/example/assets/icons/newspaper.png deleted file mode 100644 index b2ff627555..0000000000 Binary files a/example/assets/icons/newspaper.png and /dev/null differ diff --git a/example/assets/icons/newspaper@1x.png b/example/assets/icons/newspaper@1x.png deleted file mode 100644 index 0ce265e7b5..0000000000 Binary files a/example/assets/icons/newspaper@1x.png and /dev/null differ diff --git a/example/assets/icons/newspaper@2x.png b/example/assets/icons/newspaper@2x.png deleted file mode 100644 index 9ea0069c76..0000000000 Binary files a/example/assets/icons/newspaper@2x.png and /dev/null differ diff --git a/example/assets/icons/newspaper@3x.png b/example/assets/icons/newspaper@3x.png deleted file mode 100644 index b2ff627555..0000000000 Binary files a/example/assets/icons/newspaper@3x.png and /dev/null differ diff --git a/example/assets/icons/paw-print.png b/example/assets/icons/paw-print.png deleted file mode 100644 index 5651adb5d9..0000000000 Binary files a/example/assets/icons/paw-print.png and /dev/null differ diff --git a/example/assets/icons/paw-print@2x.png b/example/assets/icons/paw-print@2x.png deleted file mode 100644 index 073571d2af..0000000000 Binary files a/example/assets/icons/paw-print@2x.png and /dev/null differ diff --git a/example/assets/icons/paw-print@3x.png b/example/assets/icons/paw-print@3x.png deleted file mode 100644 index 5651adb5d9..0000000000 Binary files a/example/assets/icons/paw-print@3x.png and /dev/null differ diff --git a/example/assets/icons/pie-chart.png b/example/assets/icons/pie-chart.png deleted file mode 100644 index d2ca16807d..0000000000 Binary files a/example/assets/icons/pie-chart.png and /dev/null differ diff --git a/example/assets/icons/pie-chart@1x.png b/example/assets/icons/pie-chart@1x.png deleted file mode 100644 index e6f1428732..0000000000 Binary files a/example/assets/icons/pie-chart@1x.png and /dev/null differ diff --git a/example/assets/icons/pie-chart@2x.png b/example/assets/icons/pie-chart@2x.png deleted file mode 100644 index f6eea84542..0000000000 Binary files a/example/assets/icons/pie-chart@2x.png and /dev/null differ diff --git a/example/assets/icons/pie-chart@3x.png b/example/assets/icons/pie-chart@3x.png deleted file mode 100644 index d2ca16807d..0000000000 Binary files a/example/assets/icons/pie-chart@3x.png and /dev/null differ diff --git a/example/assets/icons/radio-tower.png b/example/assets/icons/radio-tower.png deleted file mode 100644 index 628bdb60d8..0000000000 Binary files a/example/assets/icons/radio-tower.png and /dev/null differ diff --git a/example/assets/icons/radio-tower@1x.png b/example/assets/icons/radio-tower@1x.png deleted file mode 100644 index 91cfc4a6e3..0000000000 Binary files a/example/assets/icons/radio-tower@1x.png and /dev/null differ diff --git a/example/assets/icons/radio-tower@2x.png b/example/assets/icons/radio-tower@2x.png deleted file mode 100644 index a14f36376c..0000000000 Binary files a/example/assets/icons/radio-tower@2x.png and /dev/null differ diff --git a/example/assets/icons/radio-tower@3x.png b/example/assets/icons/radio-tower@3x.png deleted file mode 100644 index 628bdb60d8..0000000000 Binary files a/example/assets/icons/radio-tower@3x.png and /dev/null differ diff --git a/example/assets/icons/radio.png b/example/assets/icons/radio.png deleted file mode 100644 index f31adff59b..0000000000 Binary files a/example/assets/icons/radio.png and /dev/null differ diff --git a/example/assets/icons/radio@1x.png b/example/assets/icons/radio@1x.png deleted file mode 100644 index 0ea5ecc621..0000000000 Binary files a/example/assets/icons/radio@1x.png and /dev/null differ diff --git a/example/assets/icons/radio@2x.png b/example/assets/icons/radio@2x.png deleted file mode 100644 index 131b605735..0000000000 Binary files a/example/assets/icons/radio@2x.png and /dev/null differ diff --git a/example/assets/icons/radio@3x.png b/example/assets/icons/radio@3x.png deleted file mode 100644 index f31adff59b..0000000000 Binary files a/example/assets/icons/radio@3x.png and /dev/null differ diff --git a/example/assets/icons/search.png b/example/assets/icons/search.png deleted file mode 100644 index ea5ef29825..0000000000 Binary files a/example/assets/icons/search.png and /dev/null differ diff --git a/example/assets/icons/search@1x.png b/example/assets/icons/search@1x.png deleted file mode 100644 index 8ef5b5b92d..0000000000 Binary files a/example/assets/icons/search@1x.png and /dev/null differ diff --git a/example/assets/icons/search@2x.png b/example/assets/icons/search@2x.png deleted file mode 100644 index 704e2f456f..0000000000 Binary files a/example/assets/icons/search@2x.png and /dev/null differ diff --git a/example/assets/icons/search@3x.png b/example/assets/icons/search@3x.png deleted file mode 100644 index ea5ef29825..0000000000 Binary files a/example/assets/icons/search@3x.png and /dev/null differ diff --git a/example/assets/icons/sparkles.png b/example/assets/icons/sparkles.png deleted file mode 100644 index ac93ff707a..0000000000 Binary files a/example/assets/icons/sparkles.png and /dev/null differ diff --git a/example/assets/icons/sparkles@1x.png b/example/assets/icons/sparkles@1x.png deleted file mode 100644 index 6cf8cbfffb..0000000000 Binary files a/example/assets/icons/sparkles@1x.png and /dev/null differ diff --git a/example/assets/icons/sparkles@2x.png b/example/assets/icons/sparkles@2x.png deleted file mode 100644 index 6c12146203..0000000000 Binary files a/example/assets/icons/sparkles@2x.png and /dev/null differ diff --git a/example/assets/icons/sparkles@3x.png b/example/assets/icons/sparkles@3x.png deleted file mode 100644 index ac93ff707a..0000000000 Binary files a/example/assets/icons/sparkles@3x.png and /dev/null differ diff --git a/example/assets/icons/swords.png b/example/assets/icons/swords.png deleted file mode 100644 index 1a2008192f..0000000000 Binary files a/example/assets/icons/swords.png and /dev/null differ diff --git a/example/assets/icons/swords@2x.png b/example/assets/icons/swords@2x.png deleted file mode 100644 index 6f0ef0ea4c..0000000000 Binary files a/example/assets/icons/swords@2x.png and /dev/null differ diff --git a/example/assets/icons/swords@3x.png b/example/assets/icons/swords@3x.png deleted file mode 100644 index 1a2008192f..0000000000 Binary files a/example/assets/icons/swords@3x.png and /dev/null differ diff --git a/example/assets/icons/user-round-plus.png b/example/assets/icons/user-round-plus.png deleted file mode 100644 index f595e14f8d..0000000000 Binary files a/example/assets/icons/user-round-plus.png and /dev/null differ diff --git a/example/assets/icons/user-round-plus@1x.png b/example/assets/icons/user-round-plus@1x.png deleted file mode 100644 index b5359f1c1d..0000000000 Binary files a/example/assets/icons/user-round-plus@1x.png and /dev/null differ diff --git a/example/assets/icons/user-round-plus@2x.png b/example/assets/icons/user-round-plus@2x.png deleted file mode 100644 index c162327c15..0000000000 Binary files a/example/assets/icons/user-round-plus@2x.png and /dev/null differ diff --git a/example/assets/icons/user-round-plus@3x.png b/example/assets/icons/user-round-plus@3x.png deleted file mode 100644 index f595e14f8d..0000000000 Binary files a/example/assets/icons/user-round-plus@3x.png and /dev/null differ diff --git a/example/assets/misc/avatar-1.png b/example/assets/misc/avatar-1.png deleted file mode 100644 index be10079fdb..0000000000 Binary files a/example/assets/misc/avatar-1.png and /dev/null differ diff --git a/example/assets/misc/avatar-2.png b/example/assets/misc/avatar-2.png deleted file mode 100644 index c9d4c41ce7..0000000000 Binary files a/example/assets/misc/avatar-2.png and /dev/null differ diff --git a/example/assets/misc/balloons.jpg b/example/assets/misc/balloons.jpg deleted file mode 100644 index b77900d19c..0000000000 Binary files a/example/assets/misc/balloons.jpg and /dev/null differ diff --git a/example/assets/misc/book.jpg b/example/assets/misc/book.jpg deleted file mode 100644 index a3ad6ebfaa..0000000000 Binary files a/example/assets/misc/book.jpg and /dev/null differ diff --git a/example/assets/misc/cpu.jpg b/example/assets/misc/cpu.jpg deleted file mode 100644 index 389ea527d4..0000000000 Binary files a/example/assets/misc/cpu.jpg and /dev/null differ diff --git a/example/assets/misc/water.jpg b/example/assets/misc/water.jpg deleted file mode 100644 index 3bbe770a49..0000000000 Binary files a/example/assets/misc/water.jpg and /dev/null differ diff --git a/example/assets/places/banff.jpg b/example/assets/places/banff.jpg deleted file mode 100644 index adc86ea9e8..0000000000 Binary files a/example/assets/places/banff.jpg and /dev/null differ diff --git a/example/assets/places/crater-lake.jpg b/example/assets/places/crater-lake.jpg deleted file mode 100644 index 45cea83459..0000000000 Binary files a/example/assets/places/crater-lake.jpg and /dev/null differ diff --git a/example/assets/places/grand-canyon.jpg b/example/assets/places/grand-canyon.jpg deleted file mode 100644 index d8794bbfa0..0000000000 Binary files a/example/assets/places/grand-canyon.jpg and /dev/null differ diff --git a/example/assets/places/lake-tahoe.jpg b/example/assets/places/lake-tahoe.jpg deleted file mode 100644 index d3c135165a..0000000000 Binary files a/example/assets/places/lake-tahoe.jpg and /dev/null differ diff --git a/example/assets/places/redwood.jpg b/example/assets/places/redwood.jpg deleted file mode 100644 index e49dd91803..0000000000 Binary files a/example/assets/places/redwood.jpg and /dev/null differ diff --git a/example/assets/places/yellowstone.jpg b/example/assets/places/yellowstone.jpg deleted file mode 100644 index b818cc2ae5..0000000000 Binary files a/example/assets/places/yellowstone.jpg and /dev/null differ diff --git a/example/assets/places/yosemite.jpg b/example/assets/places/yosemite.jpg deleted file mode 100644 index ea62646348..0000000000 Binary files a/example/assets/places/yosemite.jpg and /dev/null differ diff --git a/example/assets/places/zion.jpg b/example/assets/places/zion.jpg deleted file mode 100644 index 0fb915d02a..0000000000 Binary files a/example/assets/places/zion.jpg and /dev/null differ diff --git a/example/assets/showcase/dinos/allosaurus.png b/example/assets/showcase/dinos/allosaurus.png deleted file mode 100644 index aa570a2a42..0000000000 Binary files a/example/assets/showcase/dinos/allosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/ankylosaurus.png b/example/assets/showcase/dinos/ankylosaurus.png deleted file mode 100644 index 0f296d1f0b..0000000000 Binary files a/example/assets/showcase/dinos/ankylosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/apatosaurus.png b/example/assets/showcase/dinos/apatosaurus.png deleted file mode 100644 index e27ebe741b..0000000000 Binary files a/example/assets/showcase/dinos/apatosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/brachiosaurus.png b/example/assets/showcase/dinos/brachiosaurus.png deleted file mode 100644 index e067a62b74..0000000000 Binary files a/example/assets/showcase/dinos/brachiosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/carnotaurus.png b/example/assets/showcase/dinos/carnotaurus.png deleted file mode 100644 index 885d57f0e0..0000000000 Binary files a/example/assets/showcase/dinos/carnotaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/ceratosaurus.png b/example/assets/showcase/dinos/ceratosaurus.png deleted file mode 100644 index 96a3205cc7..0000000000 Binary files a/example/assets/showcase/dinos/ceratosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/coelophysis.png b/example/assets/showcase/dinos/coelophysis.png deleted file mode 100644 index 68865f3a75..0000000000 Binary files a/example/assets/showcase/dinos/coelophysis.png and /dev/null differ diff --git a/example/assets/showcase/dinos/compsognathus.png b/example/assets/showcase/dinos/compsognathus.png deleted file mode 100644 index 46f0fc6fc5..0000000000 Binary files a/example/assets/showcase/dinos/compsognathus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/deinonychus.png b/example/assets/showcase/dinos/deinonychus.png deleted file mode 100644 index d2a9ced49a..0000000000 Binary files a/example/assets/showcase/dinos/deinonychus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/dilophosaurus.png b/example/assets/showcase/dinos/dilophosaurus.png deleted file mode 100644 index aaf7b226c3..0000000000 Binary files a/example/assets/showcase/dinos/dilophosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/diplodocus.png b/example/assets/showcase/dinos/diplodocus.png deleted file mode 100644 index f9f59be38a..0000000000 Binary files a/example/assets/showcase/dinos/diplodocus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/eoraptor.png b/example/assets/showcase/dinos/eoraptor.png deleted file mode 100644 index 571f91236b..0000000000 Binary files a/example/assets/showcase/dinos/eoraptor.png and /dev/null differ diff --git a/example/assets/showcase/dinos/gallimimus.png b/example/assets/showcase/dinos/gallimimus.png deleted file mode 100644 index db1e876b7f..0000000000 Binary files a/example/assets/showcase/dinos/gallimimus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/giganotosaurus.png b/example/assets/showcase/dinos/giganotosaurus.png deleted file mode 100644 index 75867956fc..0000000000 Binary files a/example/assets/showcase/dinos/giganotosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/herrerasaurus.png b/example/assets/showcase/dinos/herrerasaurus.png deleted file mode 100644 index 03b4e53b6a..0000000000 Binary files a/example/assets/showcase/dinos/herrerasaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/iguanodon.png b/example/assets/showcase/dinos/iguanodon.png deleted file mode 100644 index 55ab3fe383..0000000000 Binary files a/example/assets/showcase/dinos/iguanodon.png and /dev/null differ diff --git a/example/assets/showcase/dinos/kentrosaurus.png b/example/assets/showcase/dinos/kentrosaurus.png deleted file mode 100644 index 63c59fd2b0..0000000000 Binary files a/example/assets/showcase/dinos/kentrosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/maiasaura.png b/example/assets/showcase/dinos/maiasaura.png deleted file mode 100644 index 0f971184fa..0000000000 Binary files a/example/assets/showcase/dinos/maiasaura.png and /dev/null differ diff --git a/example/assets/showcase/dinos/oviraptor.png b/example/assets/showcase/dinos/oviraptor.png deleted file mode 100644 index 2715f47b73..0000000000 Binary files a/example/assets/showcase/dinos/oviraptor.png and /dev/null differ diff --git a/example/assets/showcase/dinos/pachycephalosaurus.png b/example/assets/showcase/dinos/pachycephalosaurus.png deleted file mode 100644 index b5b95aa158..0000000000 Binary files a/example/assets/showcase/dinos/pachycephalosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/parasaurolophus.png b/example/assets/showcase/dinos/parasaurolophus.png deleted file mode 100644 index ec63413e12..0000000000 Binary files a/example/assets/showcase/dinos/parasaurolophus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/plateosaurus.png b/example/assets/showcase/dinos/plateosaurus.png deleted file mode 100644 index 7b7acf525f..0000000000 Binary files a/example/assets/showcase/dinos/plateosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/procompsognathus.png b/example/assets/showcase/dinos/procompsognathus.png deleted file mode 100644 index 34d67437e3..0000000000 Binary files a/example/assets/showcase/dinos/procompsognathus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/spinosaurus.png b/example/assets/showcase/dinos/spinosaurus.png deleted file mode 100644 index fdb4d5d535..0000000000 Binary files a/example/assets/showcase/dinos/spinosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/stegosaurus.png b/example/assets/showcase/dinos/stegosaurus.png deleted file mode 100644 index a5b3d1b733..0000000000 Binary files a/example/assets/showcase/dinos/stegosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/styracosaurus.png b/example/assets/showcase/dinos/styracosaurus.png deleted file mode 100644 index e8d55e03bf..0000000000 Binary files a/example/assets/showcase/dinos/styracosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/therizinosaurus.png b/example/assets/showcase/dinos/therizinosaurus.png deleted file mode 100644 index 0a84c3b308..0000000000 Binary files a/example/assets/showcase/dinos/therizinosaurus.png and /dev/null differ diff --git a/example/assets/showcase/dinos/triceratops.png b/example/assets/showcase/dinos/triceratops.png deleted file mode 100644 index b43974d151..0000000000 Binary files a/example/assets/showcase/dinos/triceratops.png and /dev/null differ diff --git a/example/assets/showcase/dinos/tyrannosaurus-rex.png b/example/assets/showcase/dinos/tyrannosaurus-rex.png deleted file mode 100644 index 34b0f1158c..0000000000 Binary files a/example/assets/showcase/dinos/tyrannosaurus-rex.png and /dev/null differ diff --git a/example/assets/showcase/dinos/velociraptor.png b/example/assets/showcase/dinos/velociraptor.png deleted file mode 100644 index bd4fd1f3f8..0000000000 Binary files a/example/assets/showcase/dinos/velociraptor.png and /dev/null differ diff --git a/example/assets/showcase/food/food-01.jpg b/example/assets/showcase/food/food-01.jpg deleted file mode 100644 index 8275715c77..0000000000 Binary files a/example/assets/showcase/food/food-01.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-02.jpg b/example/assets/showcase/food/food-02.jpg deleted file mode 100644 index fbb3ceffff..0000000000 Binary files a/example/assets/showcase/food/food-02.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-03.jpg b/example/assets/showcase/food/food-03.jpg deleted file mode 100644 index f7d4e345e4..0000000000 Binary files a/example/assets/showcase/food/food-03.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-04.jpg b/example/assets/showcase/food/food-04.jpg deleted file mode 100644 index 3813196448..0000000000 Binary files a/example/assets/showcase/food/food-04.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-05.jpg b/example/assets/showcase/food/food-05.jpg deleted file mode 100644 index 53713a9466..0000000000 Binary files a/example/assets/showcase/food/food-05.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-06.jpg b/example/assets/showcase/food/food-06.jpg deleted file mode 100644 index 01c2895951..0000000000 Binary files a/example/assets/showcase/food/food-06.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-07.jpg b/example/assets/showcase/food/food-07.jpg deleted file mode 100644 index 868764900f..0000000000 Binary files a/example/assets/showcase/food/food-07.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-08.jpg b/example/assets/showcase/food/food-08.jpg deleted file mode 100644 index 2cec5e1ca0..0000000000 Binary files a/example/assets/showcase/food/food-08.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-09.jpg b/example/assets/showcase/food/food-09.jpg deleted file mode 100644 index bcd526058c..0000000000 Binary files a/example/assets/showcase/food/food-09.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-10.jpg b/example/assets/showcase/food/food-10.jpg deleted file mode 100644 index 119bf1c00a..0000000000 Binary files a/example/assets/showcase/food/food-10.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-11.jpg b/example/assets/showcase/food/food-11.jpg deleted file mode 100644 index 0b836b3cda..0000000000 Binary files a/example/assets/showcase/food/food-11.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-12.jpg b/example/assets/showcase/food/food-12.jpg deleted file mode 100644 index 610cd5e051..0000000000 Binary files a/example/assets/showcase/food/food-12.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-13.jpg b/example/assets/showcase/food/food-13.jpg deleted file mode 100644 index c4451bc7ae..0000000000 Binary files a/example/assets/showcase/food/food-13.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-14.jpg b/example/assets/showcase/food/food-14.jpg deleted file mode 100644 index 64a5a4d6ca..0000000000 Binary files a/example/assets/showcase/food/food-14.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-15.jpg b/example/assets/showcase/food/food-15.jpg deleted file mode 100644 index 74d9ed5c08..0000000000 Binary files a/example/assets/showcase/food/food-15.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-16.jpg b/example/assets/showcase/food/food-16.jpg deleted file mode 100644 index b3e1cb79ee..0000000000 Binary files a/example/assets/showcase/food/food-16.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-17.jpg b/example/assets/showcase/food/food-17.jpg deleted file mode 100644 index f85cd7cc02..0000000000 Binary files a/example/assets/showcase/food/food-17.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-18.jpg b/example/assets/showcase/food/food-18.jpg deleted file mode 100644 index e4acbcf5ae..0000000000 Binary files a/example/assets/showcase/food/food-18.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-19.jpg b/example/assets/showcase/food/food-19.jpg deleted file mode 100644 index eb570f3e86..0000000000 Binary files a/example/assets/showcase/food/food-19.jpg and /dev/null differ diff --git a/example/assets/showcase/food/food-20.jpg b/example/assets/showcase/food/food-20.jpg deleted file mode 100644 index 93ff9033e5..0000000000 Binary files a/example/assets/showcase/food/food-20.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-01.jpg b/example/assets/showcase/music/cover-01.jpg deleted file mode 100644 index 14ce6399d4..0000000000 Binary files a/example/assets/showcase/music/cover-01.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-02.jpg b/example/assets/showcase/music/cover-02.jpg deleted file mode 100644 index 7ad45d7477..0000000000 Binary files a/example/assets/showcase/music/cover-02.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-03.jpg b/example/assets/showcase/music/cover-03.jpg deleted file mode 100644 index d561f8be8d..0000000000 Binary files a/example/assets/showcase/music/cover-03.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-04.jpg b/example/assets/showcase/music/cover-04.jpg deleted file mode 100644 index 4688c07e52..0000000000 Binary files a/example/assets/showcase/music/cover-04.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-05.jpg b/example/assets/showcase/music/cover-05.jpg deleted file mode 100644 index afbd0e8363..0000000000 Binary files a/example/assets/showcase/music/cover-05.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-06.jpg b/example/assets/showcase/music/cover-06.jpg deleted file mode 100644 index ea97a6ed35..0000000000 Binary files a/example/assets/showcase/music/cover-06.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-07.jpg b/example/assets/showcase/music/cover-07.jpg deleted file mode 100644 index 2258e2f166..0000000000 Binary files a/example/assets/showcase/music/cover-07.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-08.jpg b/example/assets/showcase/music/cover-08.jpg deleted file mode 100644 index 9aa22d5ad3..0000000000 Binary files a/example/assets/showcase/music/cover-08.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-09.jpg b/example/assets/showcase/music/cover-09.jpg deleted file mode 100644 index 8e610a07fe..0000000000 Binary files a/example/assets/showcase/music/cover-09.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-10.jpg b/example/assets/showcase/music/cover-10.jpg deleted file mode 100644 index 8e47ab8c13..0000000000 Binary files a/example/assets/showcase/music/cover-10.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-11.jpg b/example/assets/showcase/music/cover-11.jpg deleted file mode 100644 index 5399c56bac..0000000000 Binary files a/example/assets/showcase/music/cover-11.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-12.jpg b/example/assets/showcase/music/cover-12.jpg deleted file mode 100644 index 8385d0f722..0000000000 Binary files a/example/assets/showcase/music/cover-12.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-13.jpg b/example/assets/showcase/music/cover-13.jpg deleted file mode 100644 index 6ba927ee83..0000000000 Binary files a/example/assets/showcase/music/cover-13.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-14.jpg b/example/assets/showcase/music/cover-14.jpg deleted file mode 100644 index 99d122394b..0000000000 Binary files a/example/assets/showcase/music/cover-14.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-15.jpg b/example/assets/showcase/music/cover-15.jpg deleted file mode 100644 index 675029cb37..0000000000 Binary files a/example/assets/showcase/music/cover-15.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-16.jpg b/example/assets/showcase/music/cover-16.jpg deleted file mode 100644 index f81d5acf28..0000000000 Binary files a/example/assets/showcase/music/cover-16.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-17.jpg b/example/assets/showcase/music/cover-17.jpg deleted file mode 100644 index 41725cc4e0..0000000000 Binary files a/example/assets/showcase/music/cover-17.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-18.jpg b/example/assets/showcase/music/cover-18.jpg deleted file mode 100644 index a118347d9e..0000000000 Binary files a/example/assets/showcase/music/cover-18.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-19.jpg b/example/assets/showcase/music/cover-19.jpg deleted file mode 100644 index 23cbc93fc2..0000000000 Binary files a/example/assets/showcase/music/cover-19.jpg and /dev/null differ diff --git a/example/assets/showcase/music/cover-20.jpg b/example/assets/showcase/music/cover-20.jpg deleted file mode 100644 index 4f9ee80449..0000000000 Binary files a/example/assets/showcase/music/cover-20.jpg and /dev/null differ diff --git a/example/assets/splash.png b/example/assets/splash.png deleted file mode 100644 index 40559997ce..0000000000 Binary files a/example/assets/splash.png and /dev/null differ diff --git a/example/babel.config.js b/example/babel.config.js deleted file mode 100644 index 99c563b36b..0000000000 --- a/example/babel.config.js +++ /dev/null @@ -1,23 +0,0 @@ -/** @type {import('@babel/core').TransformOptions} */ -module.exports = function (api) { - api.cache(true); - - return { - presets: ['babel-preset-expo'], - overrides: [ - { - exclude: /\/node_modules\//, - plugins: ['@babel/plugin-transform-strict-mode'], - }, - { - include: /\/packages\//, - presets: [ - [ - 'module:react-native-builder-bob/babel-preset', - { modules: 'commonjs' }, - ], - ], - }, - ], - }; -}; diff --git a/example/e2e/dialog.mjs b/example/e2e/dialog.mjs deleted file mode 100644 index 0e61aab851..0000000000 --- a/example/e2e/dialog.mjs +++ /dev/null @@ -1,13 +0,0 @@ -export function run(page, env) { - const listener = async (dialog) => { - if (env.action === 'accept') { - await dialog.accept(); - } else { - await dialog.dismiss(); - } - - page.off('dialog', listener); - }; - - page.on('dialog', listener); -} diff --git a/example/e2e/launch.yml b/example/e2e/launch.yml deleted file mode 100644 index c256c4a8f6..0000000000 --- a/example/e2e/launch.yml +++ /dev/null @@ -1,11 +0,0 @@ -appId: ${APP_ID} ---- -- stopApp -- retry: - maxRetries: 3 - commands: - - openLink: - link: ${APP_SCHEME}${LINK} - - extendedWaitUntil: - visible: ${TEXT} - timeout: 60000 diff --git a/example/e2e/maestro/auth-flow.yml b/example/e2e/maestro/auth-flow.yml deleted file mode 100644 index 23b3c50008..0000000000 --- a/example/e2e/maestro/auth-flow.yml +++ /dev/null @@ -1,42 +0,0 @@ -appId: ${APP_ID} -name: Auth Flow ---- -- runFlow: - file: ../launch.yml - env: - LINK: auth-flow - TEXT: 'Welcome' -- tapOn: - text: 'Sign in' -- assertVisible: - text: 'Home' -- tapOn: - text: 'Go to Chat' -- assertVisible: - text: 'Chat' -- tapOn: - text: 'Sign out' -- assertVisible: - text: 'Sign in' -- openLink: - link: ${APP_SCHEME}auth-flow/profile -- assertVisible: - text: 'Welcome' -- tapOn: - text: 'Sign in' -- assertVisible: - text: 'This is your profile' -- tapOn: - text: 'Go to Chat' -- assertVisible: - text: 'Chat' -- tapOn: - text: 'Sign out' -- assertVisible: - text: 'Sign in' -- openLink: - link: ${APP_SCHEME}auth-flow -- tapOn: - text: 'Sign in' -- assertVisible: - text: 'Home' diff --git a/example/e2e/maestro/bottom-tabs-dynamic.yml b/example/e2e/maestro/bottom-tabs-dynamic.yml deleted file mode 100644 index 7668506ec8..0000000000 --- a/example/e2e/maestro/bottom-tabs-dynamic.yml +++ /dev/null @@ -1,28 +0,0 @@ -appId: ${APP_ID} -name: Bottom Tabs - Dynamic ---- -- runFlow: - file: ../launch.yml - env: - LINK: bottom-tabs-dynamic - TEXT: 'Tab 0' -- tapOn: - text: 'Add a tab' -- assertVisible: - id: 'tab-2' -- tapOn: - text: 'Add a tab' -- assertVisible: - id: 'tab-3' -- tapOn: - text: 'Add a tab' -- assertVisible: - id: 'tab-4' -- tapOn: - text: 'Remove a tab' -- assertNotVisible: - id: 'tab-4' -- tapOn: - text: 'Remove a tab' -- assertNotVisible: - id: 'tab-3' diff --git a/example/e2e/maestro/bottom-tabs-full-history.yml b/example/e2e/maestro/bottom-tabs-full-history.yml deleted file mode 100644 index 4b3e44193d..0000000000 --- a/example/e2e/maestro/bottom-tabs-full-history.yml +++ /dev/null @@ -1,46 +0,0 @@ -appId: ${APP_ID} -name: Bottom Tabs - Full History ---- -- runFlow: - file: ../launch.yml - env: - LINK: bottom-tabs-full-history - TEXT: 'Tab First (-)' -- tapOn: - text: 'Navigate to Second' -- assertVisible: - text: 'Tab Second (1)' -- tapOn: - text: 'Navigate to First' -- assertVisible: - text: 'Tab First (2)' -- tapOn: - text: 'Navigate to Third' -- assertVisible: - text: 'Tab Third (3)' -- tapOn: - text: 'Navigate to Second' -- assertVisible: - text: 'Tab Second (4)' -- tapOn: - id: 'first-tab' -- assertVisible: - text: 'Tab First (2)' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Tab Second (4)' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Tab Third (3)' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Tab First (2)' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Tab Second (1)' -- tapOn: - text: 'Go back' diff --git a/example/e2e/maestro/bottom-tabs-preload-flow.yml b/example/e2e/maestro/bottom-tabs-preload-flow.yml deleted file mode 100644 index e17fd2c673..0000000000 --- a/example/e2e/maestro/bottom-tabs-preload-flow.yml +++ /dev/null @@ -1,27 +0,0 @@ -appId: ${APP_ID} -name: Bottom Tabs - Preload Flow ---- -- runFlow: - file: ../launch.yml - env: - LINK: bottom-tabs-preload-flow - TEXT: 'Bottom Tabs Preload Flow' -- assertVisible: - text: 'Details is not preloaded yet.' -- tapOn: - text: 'Preload Details' -- extendedWaitUntil: - timeout: 5000 - visible: 'Details is preloaded!' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Loaded!' -- tapOn: - text: 'Back to home' -- assertVisible: - text: 'Bottom Tabs Preload Flow' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Loaded!' diff --git a/example/e2e/maestro/bottom-tabs.yml b/example/e2e/maestro/bottom-tabs.yml deleted file mode 100644 index 204fff48a9..0000000000 --- a/example/e2e/maestro/bottom-tabs.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Bottom Tabs - Basic ---- -- runFlow: - file: ../launch.yml - env: - LINK: bottom-tabs - TEXT: 'Article by Gandalf' -- tapOn: - id: 'albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go to Chat' -- assertVisible: - text: 'Chat' -- tapOn: - id: 'contacts' -- assertVisible: - text: 'Contacts' -- tapOn: - id: 'article' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/components-link.yml b/example/e2e/maestro/components-link.yml deleted file mode 100644 index 0a8b205192..0000000000 --- a/example/e2e/maestro/components-link.yml +++ /dev/null @@ -1,32 +0,0 @@ -appId: ${APP_ID} -name: Components - Link ---- -- runFlow: - file: ../launch.yml - env: - LINK: components-link - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Go to albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go to Article' -- assertVisible: - text: 'Article by Babel' -- tapOn: - text: 'Replace params' -- assertVisible: - text: 'Article by Gandalf' -- tapOn: - text: 'Push params' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Gandalf' -- tapOn: - text: 'Replace with albums' -- assertVisible: - text: 'Albums' diff --git a/example/e2e/maestro/drawer-master-detail.yml b/example/e2e/maestro/drawer-master-detail.yml deleted file mode 100644 index d8b4ed605d..0000000000 --- a/example/e2e/maestro/drawer-master-detail.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Drawer - Master Detail ---- -- runFlow: - file: ../launch.yml - env: - LINK: drawer-master-detail - TEXT: 'Pages' -- tapOn: - text: 'Feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Go back' -- tapOn: - text: 'Albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go back' -- tapOn: - text: 'Article' -- assertVisible: - text: 'Article' diff --git a/example/e2e/maestro/libraries-drawer-layout.yml b/example/e2e/maestro/libraries-drawer-layout.yml deleted file mode 100644 index 009f3a2f15..0000000000 --- a/example/e2e/maestro/libraries-drawer-layout.yml +++ /dev/null @@ -1,26 +0,0 @@ -appId: ${APP_ID} -name: Libraries - Drawer Layout ---- -- runFlow: - file: ../launch.yml - env: - LINK: drawer-view - TEXT: 'Drawer Layout' -- assertVisible: - text: 'Open drawer' -- tapOn: - text: 'Open drawer' -- extendedWaitUntil: - visible: - text: 'Close' - timeout: 2000 -- tapOn: - text: 'Close' -- extendedWaitUntil: - visible: - text: 'Open drawer' - timeout: 2000 -- tapOn: - text: 'Change position (left)' -- assertVisible: - text: 'Change position (right)' diff --git a/example/e2e/maestro/loaders.yml b/example/e2e/maestro/loaders.yml deleted file mode 100644 index 78718d74b4..0000000000 --- a/example/e2e/maestro/loaders.yml +++ /dev/null @@ -1,132 +0,0 @@ -appId: ${APP_ID} -name: Loaders ---- -- runFlow: - file: ../launch.yml - env: - LINK: loaders - TEXT: 'Velociraptor' -- tapOn: - text: 'Velociraptor' -- assertVisible: - text: 'Loadingโ€ฆ' -- assertNotVisible: - text: 'A small dromaeosaur from late Cretaceous Mongolia.*' -- extendedWaitUntil: - timeout: 15000 - visible: 'Velociraptor' -- assertVisible: - text: 'A small dromaeosaur from late Cretaceous Mongolia.*' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'android'} - commands: - - back -- runFlow: - when: - true: ${maestro.platform == 'ios'} - commands: - - tapOn: - text: 'Dinos' -- assertVisible: - text: 'Velociraptor' -- tapOn: - text: 'Team' -- runFlow: - when: - true: ${maestro.platform == 'web'} - commands: - - assertVisible: - text: 'Catalog' - - assertVisible: - text: 'Battle' - - assertVisible: - text: 'Velociraptor' - - assertNotVisible: - text: 'Current Team' -- runFlow: - when: - true: ${maestro.platform == 'ios' || maestro.platform == 'android'} - commands: - - assertVisible: - text: 'Loadingโ€ฆ' - - assertNotVisible: - text: 'Catalog' - - assertNotVisible: - text: 'Battle' - - assertNotVisible: - text: 'Velociraptor' -- extendedWaitUntil: - timeout: 15000 - visible: 'Current Team' -- assertVisible: - text: 'Tyrannosaurus rex, Triceratops, Velociraptor, Stegosaurus' -- tapOn: - text: 'Battle' -- assertVisible: - text: 'Loadingโ€ฆ' -- assertVisible: - text: 'Catalog' -- assertVisible: - text: 'Team' -- assertVisible: - text: 'Battle' -- assertNotVisible: - text: 'Brachiosaurus vs Spinosaurus' -- extendedWaitUntil: - timeout: 15000 - visible: 'Brachiosaurus vs Spinosaurus' -- assertVisible: - text: 'Battle' -- runFlow: - file: ../launch.yml - env: - LINK: loaders - TEXT: 'Make next load fail' -- tapOn: - text: 'Make next load fail' -- tapOn: - text: 'Battle' -- extendedWaitUntil: - timeout: 5000 - visible: 'Failed to load dinosaur.' -- runFlow: - when: - visible: 'Dismiss' - commands: - - tapOn: 'Dismiss' -- runFlow: - when: - visible: 'Dismiss' - commands: - - tapOn: 'Dismiss' -- assertVisible: - text: 'Try Again' -- assertNotVisible: - text: 'Brachiosaurus vs Spinosaurus' -- runFlow: - file: ../launch.yml - env: - LINK: loaders - TEXT: 'Make next load fail' -- tapOn: - text: 'Make next load fail' -- tapOn: - text: 'Stegosaurus' -- extendedWaitUntil: - timeout: 5000 - visible: 'Failed to load dinosaur.' -- runFlow: - when: - visible: 'Dismiss' - commands: - - tapOn: 'Dismiss' -- runFlow: - when: - visible: 'Dismiss' - commands: - - tapOn: 'Dismiss' -- assertVisible: - text: 'Try Again' -- assertNotVisible: - text: 'Stegosaurus' diff --git a/example/e2e/maestro/material-top-tabs.yml b/example/e2e/maestro/material-top-tabs.yml deleted file mode 100644 index c86543ba69..0000000000 --- a/example/e2e/maestro/material-top-tabs.yml +++ /dev/null @@ -1,49 +0,0 @@ -appId: ${APP_ID} -name: Material Top Tabs - Basic ---- -- runFlow: - file: ../launch.yml - env: - LINK: material-top-tabs-basic - TEXT: 'Chat' -- assertVisible: - text: 'make me a sandwich' -- tapOn: - text: 'Contacts' -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 -- tapOn: - text: 'Albums' -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- tapOn: - text: 'Chat' -- extendedWaitUntil: - visible: - text: 'make me a sandwich' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- swipe: - direction: RIGHT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 diff --git a/example/e2e/maestro/native-bottom-tabs-custom-tab-bar.yml b/example/e2e/maestro/native-bottom-tabs-custom-tab-bar.yml deleted file mode 100644 index 906045ed9a..0000000000 --- a/example/e2e/maestro/native-bottom-tabs-custom-tab-bar.yml +++ /dev/null @@ -1,28 +0,0 @@ -appId: ${APP_ID} -name: Native Bottom Tabs - Custom Tab Bar ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-bottom-tabs-custom-tab-bar - TEXT: 'What is Lorem Ipsum?' -- tapOn: - id: 'albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go to Contacts' -- assertVisible: - text: 'Marissa Castillo' -- tapOn: - id: 'albums' -- assertVisible: - text: 'Albums' -- tapOn: - id: 'contacts' -- assertVisible: - text: 'Marissa Castillo' -- tapOn: - id: 'article' -- assertVisible: - text: 'What is Lorem Ipsum?' diff --git a/example/e2e/maestro/native-bottom-tabs.yml b/example/e2e/maestro/native-bottom-tabs.yml deleted file mode 100644 index 522f5ea6df..0000000000 --- a/example/e2e/maestro/native-bottom-tabs.yml +++ /dev/null @@ -1,32 +0,0 @@ -appId: ${APP_ID} -name: Native Bottom Tabs - Basic ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-bottom-tabs - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Update params' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Navigate to feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go to Contacts' -- assertVisible: - text: 'Marissa Castillo' -- tapOn: - text: 'Albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Article' -- assertVisible: - text: 'Article by Babel fish' diff --git a/example/e2e/maestro/native-stack-card-modal.yml b/example/e2e/maestro/native-stack-card-modal.yml deleted file mode 100644 index 5f762813fa..0000000000 --- a/example/e2e/maestro/native-stack-card-modal.yml +++ /dev/null @@ -1,32 +0,0 @@ -appId: ${APP_ID} -name: Native Stack - Card + Modal ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-stack-card-modal - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by Dalek' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by The Doctor' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Dalek' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/native-stack-header-customization.yml b/example/e2e/maestro/native-stack-header-customization.yml deleted file mode 100644 index 320a54c8f7..0000000000 --- a/example/e2e/maestro/native-stack-header-customization.yml +++ /dev/null @@ -1,29 +0,0 @@ -appId: ${APP_ID} -name: Native Stack - Header Customization ---- -- stopApp -- runFlow: - file: ../launch.yml - env: - LINK: native-stack-header-customization - TEXT: 'What is Lorem Ipsum?' -- tapOn: - text: 'Push feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Navigate to article' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Pop screen' -- tapOn: - text: 'Albums' -- tapOn: - text: 'Pop by 2' -- assertVisible: - text: 'What is Lorem Ipsum?' diff --git a/example/e2e/maestro/native-stack-preload-flow.yml b/example/e2e/maestro/native-stack-preload-flow.yml deleted file mode 100644 index fa504ab2a7..0000000000 --- a/example/e2e/maestro/native-stack-preload-flow.yml +++ /dev/null @@ -1,29 +0,0 @@ -appId: ${APP_ID} -name: Native Stack - Preload Flow ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-stack-preload-flow - TEXT: 'Native Stack Preload Flow' -- assertVisible: - text: 'Details is not preloaded yet.' -- tapOn: - text: 'Preload Details' -- extendedWaitUntil: - timeout: 5000 - visible: 'Details is preloaded!' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Loaded!' -- assertVisible: - text: 'Preloaded' -- tapOn: - text: 'Back to home' -- assertVisible: - text: 'Details is not preloaded yet.' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Fresh' diff --git a/example/e2e/maestro/native-stack-prevent-remove.yml b/example/e2e/maestro/native-stack-prevent-remove.yml deleted file mode 100644 index 241cc5efb9..0000000000 --- a/example/e2e/maestro/native-stack-prevent-remove.yml +++ /dev/null @@ -1,55 +0,0 @@ -appId: ${APP_ID} -name: Native Stack - Prevent Remove ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-stack-prevent-remove - TEXT: 'Input' -- tapOn: - text: 'Push Article' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Push Input' -- assertVisible: - text: 'Discard and go back' -- inputText: 'hello' -- runFlow: - when: - platform: Web - commands: - - runScript: - file: ../dialog.mjs - env: - action: dismiss - - tapOn: - text: 'Discard and go back' - - assertVisible: - text: 'Discard and go back' - - runScript: - file: ../dialog.mjs - env: - action: accept - - tapOn: - text: 'Discard and go back' -- runFlow: - when: - true: ${maestro.platform == 'android' || maestro.platform == 'ios'} - commands: - - tapOn: - text: 'Discard and go back' - - tapOn: - text: "Don't leave" - - assertVisible: - text: 'Discard and go back' - - tapOn: - text: 'Discard and go back' - - tapOn: - text: 'Discard' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Pop to top' -- assertVisible: - text: 'Push Article' diff --git a/example/e2e/maestro/native-stack.yml b/example/e2e/maestro/native-stack.yml deleted file mode 100644 index 75b5764356..0000000000 --- a/example/e2e/maestro/native-stack.yml +++ /dev/null @@ -1,36 +0,0 @@ -appId: ${APP_ID} -name: Native Stack - Basic ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-stack - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Update params' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Navigate to feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Replace with contacts' -- assertVisible: - text: 'Contacts' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Pop to albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Pop by 2' -- assertVisible: - text: 'Article by Babel fish' diff --git a/example/e2e/maestro/navigator-layout.yml b/example/e2e/maestro/navigator-layout.yml deleted file mode 100644 index 43c0714db6..0000000000 --- a/example/e2e/maestro/navigator-layout.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Navigator Layout ---- -- runFlow: - file: ../launch.yml - env: - LINK: navigator-layout - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Navigate to feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Navigate to albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Feed' -- assertVisible: - text: 'Joke bot' -- tapOn: - text: 'Article by Gandalf' -- assertVisible: - text: 'What is Lorem Ipsum?' diff --git a/example/e2e/maestro/screen-layout.yml b/example/e2e/maestro/screen-layout.yml deleted file mode 100644 index d0b61cda8d..0000000000 --- a/example/e2e/maestro/screen-layout.yml +++ /dev/null @@ -1,35 +0,0 @@ -appId: ${APP_ID} -name: Screen Layout ---- -- runFlow: - file: ../launch.yml - env: - LINK: screen-layout - TEXT: 'Loadingโ€ฆ' -- extendedWaitUntil: - timeout: 3500 - visible: 'Suspend' -- tapOn: - text: 'Crash' -- runFlow: - when: - visible: 'Dismiss' - commands: - - tapOn: 'Dismiss' -- assertVisible: - text: 'Something went wrong' -- assertNotVisible: - text: 'Crash' -- tapOn: - text: 'Try again' -- assertVisible: - text: 'Crash' -- tapOn: - text: 'Suspend' -- assertVisible: - text: 'Loadingโ€ฆ' -- assertNotVisible: - text: 'Suspend' -- extendedWaitUntil: - timeout: 3500 - visible: 'Suspend' diff --git a/example/e2e/maestro/showcase-bottom-tabs.yml b/example/e2e/maestro/showcase-bottom-tabs.yml deleted file mode 100644 index 424605bb47..0000000000 --- a/example/e2e/maestro/showcase-bottom-tabs.yml +++ /dev/null @@ -1,94 +0,0 @@ -appId: ${APP_ID} -name: Showcase - Bottom Tabs ---- -- runFlow: - file: ../launch.yml - env: - LINK: bottom-tabs-showcase - TEXT: 'Midnight Waves' -- runFlow: - when: - # Android doesn't expose selected state - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'Albums' - selected: true -- tapOn: - id: 'open-album-albums' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'Albums' - selected: true -- assertVisible: - text: 'Midnight Waves' -- assertVisible: - text: 'Luna Echo' -- tapOn: - text: '.*For You.*' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'For You' - selected: true -- tapOn: - id: 'open-album-for-you' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'For You' - selected: true -- assertVisible: - text: 'Quiet Hours' -- assertVisible: - text: 'Amber Field' -- tapOn: - text: 'Radio' -- assertVisible: - text: 'Velvet FM' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'Radio' - selected: true -- tapOn: - text: '.*For You.*' -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'ios'} - commands: - - assertVisible: - text: 'For You' - selected: true -- assertVisible: - text: 'Quiet Hours' -- openLink: - link: ${APP_SCHEME}bottom-tabs-showcase/album/12 -- runFlow: - when: - # On Web, opening a link results in fresh page load - platform: web - commands: - - assertVisible: - text: 'Albums' - selected: true -- runFlow: - when: - platform: ios - commands: - - assertVisible: - text: 'For You' - selected: true -- assertVisible: - text: 'Signal Lost' -- assertVisible: - text: 'Pixel Drift' diff --git a/example/e2e/maestro/showcase-drawer.yml b/example/e2e/maestro/showcase-drawer.yml deleted file mode 100644 index eb70ce8f73..0000000000 --- a/example/e2e/maestro/showcase-drawer.yml +++ /dev/null @@ -1,20 +0,0 @@ -appId: ${APP_ID} -name: Showcase - Drawer ---- -- runFlow: - file: ../launch.yml - env: - LINK: drawer-showcase - TEXT: 'Total Balance' -- tapOn: - id: 'showcase-drawer-toggle' -- tapOn: - id: 'showcase-drawer-budgets' -- assertVisible: - text: 'Monthly Budget' -- tapOn: - id: 'showcase-drawer-toggle' -- tapOn: - id: 'showcase-drawer-accounts' -- assertVisible: - text: 'Main Checking' diff --git a/example/e2e/maestro/showcase-material-top-tabs.yml b/example/e2e/maestro/showcase-material-top-tabs.yml deleted file mode 100644 index 5b2fde1ca0..0000000000 --- a/example/e2e/maestro/showcase-material-top-tabs.yml +++ /dev/null @@ -1,50 +0,0 @@ -appId: ${APP_ID} -name: Showcase - Material Top Tabs ---- -- runFlow: - file: ../launch.yml - env: - LINK: material-top-tabs-showcase - TEXT: 'Lemon Herb Risotto with Spring Peas' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Morning Favorites' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Light & Fresh' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Crowd Pleasers' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Lemon Tart with Meringue' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Lavender Honey Latte' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'Pan-Seared Salmon with Dill Sauce' -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: "What's in Season" diff --git a/example/e2e/maestro/showcase-native-stack.yml b/example/e2e/maestro/showcase-native-stack.yml deleted file mode 100644 index de2a41d1ca..0000000000 --- a/example/e2e/maestro/showcase-native-stack.yml +++ /dev/null @@ -1,25 +0,0 @@ -appId: ${APP_ID} -name: Showcase - Native Stack ---- -- runFlow: - file: ../launch.yml - env: - LINK: native-stack-showcase - TEXT: 'Lake Tahoe' -- tapOn: - text: 'Places' -- assertVisible: - text: 'Places' -- tapOn: - id: 'showcase-native-stack-place-1' -- assertVisible: - text: 'Lake Tahoe' -- tapOn: - text: 'Lake Tahoe' -# The details sheet content is readable on Android and web, but not reliably on iOS. -- runFlow: - when: - true: ${maestro.platform == 'web' || maestro.platform == 'android'} - commands: - - assertVisible: - text: 'Sierra Nevada.*' diff --git a/example/e2e/maestro/showcase-stack.yml b/example/e2e/maestro/showcase-stack.yml deleted file mode 100644 index d2516c4733..0000000000 --- a/example/e2e/maestro/showcase-stack.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Showcase - Stack ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-showcase - TEXT: 'Specimens' -- tapOn: - id: 'showcase-stack-dino-1' -- assertVisible: - text: 'Quick facts' -- tapOn: - id: 'showcase-stack-open-image-viewer' -- assertVisible: - id: 'showcase-stack-close-image-viewer' -- tapOn: - id: 'showcase-stack-close-image-viewer' -- assertVisible: - text: 'Quick facts' -- tapOn: - id: 'showcase-stack-detail-back' -- assertVisible: - text: 'Specimens' diff --git a/example/e2e/maestro/stack-basic.yml b/example/e2e/maestro/stack-basic.yml deleted file mode 100644 index 89effc3436..0000000000 --- a/example/e2e/maestro/stack-basic.yml +++ /dev/null @@ -1,36 +0,0 @@ -appId: ${APP_ID} -name: Stack - Basic ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-basic - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Update params' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Navigate to feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Replace with contacts' -- assertVisible: - text: 'Contacts' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Pop to albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Pop by 2' -- assertVisible: - text: 'Article by Babel fish' diff --git a/example/e2e/maestro/stack-card-modal.yml b/example/e2e/maestro/stack-card-modal.yml deleted file mode 100644 index 8fd2f44d03..0000000000 --- a/example/e2e/maestro/stack-card-modal.yml +++ /dev/null @@ -1,32 +0,0 @@ -appId: ${APP_ID} -name: Stack - Card + Modal ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-card-modal - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by Dalek' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by The Doctor' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Dalek' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/stack-float-screen-header.yml b/example/e2e/maestro/stack-float-screen-header.yml deleted file mode 100644 index 998c152ff7..0000000000 --- a/example/e2e/maestro/stack-float-screen-header.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Stack - Float + Screen Header ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-float-screen-header - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Push feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Navigate to albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Pop screen' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Pop screen' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/stack-header-customization.yml b/example/e2e/maestro/stack-header-customization.yml deleted file mode 100644 index 85d2bbf174..0000000000 --- a/example/e2e/maestro/stack-header-customization.yml +++ /dev/null @@ -1,28 +0,0 @@ -appId: ${APP_ID} -name: Stack - Header Customization ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-header-customization - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Push feed' -- assertVisible: - text: 'Feed' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Navigate to article' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Pop screen' -- tapOn: - text: 'Albums' -- tapOn: - text: 'Pop by 2' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/stack-modal.yml b/example/e2e/maestro/stack-modal.yml deleted file mode 100644 index ec1e3e73e3..0000000000 --- a/example/e2e/maestro/stack-modal.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Stack - Modal ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-modal - TEXT: 'Article by Gandalf' -- tapOn: - text: 'Push albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Push article' -- assertVisible: - text: 'Article by Babel fish' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Go back' -- assertVisible: - text: 'Article by Gandalf' diff --git a/example/e2e/maestro/stack-preload-flow.yml b/example/e2e/maestro/stack-preload-flow.yml deleted file mode 100644 index 6d01986ee1..0000000000 --- a/example/e2e/maestro/stack-preload-flow.yml +++ /dev/null @@ -1,29 +0,0 @@ -appId: ${APP_ID} -name: Stack - Preload Flow ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-preload-flow - TEXT: 'Stack Preload Flow' -- assertVisible: - text: 'Details is not preloaded yet.' -- tapOn: - text: 'Preload Details' -- extendedWaitUntil: - timeout: 5000 - visible: 'Details is preloaded!' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Loaded!' -- assertVisible: - text: 'Preloaded' -- tapOn: - text: 'Back to home' -- assertVisible: - text: 'Details is not preloaded yet.' -- tapOn: - text: 'Navigate to Details' -- assertVisible: - text: 'Fresh' diff --git a/example/e2e/maestro/stack-prevent-remove.yml b/example/e2e/maestro/stack-prevent-remove.yml deleted file mode 100644 index b82e4292ec..0000000000 --- a/example/e2e/maestro/stack-prevent-remove.yml +++ /dev/null @@ -1,55 +0,0 @@ -appId: ${APP_ID} -name: Stack - Prevent Remove ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-prevent-remove - TEXT: 'Input' -- tapOn: - text: 'Push Article' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Push Input' -- assertVisible: - text: 'Discard and go back' -- inputText: 'hello' -- runFlow: - when: - platform: Web - commands: - - runScript: - file: ../dialog.mjs - env: - action: dismiss - - tapOn: - text: 'Discard and go back' - - assertVisible: - text: 'Discard and go back' - - runScript: - file: ../dialog.mjs - env: - action: accept - - tapOn: - text: 'Discard and go back' -- runFlow: - when: - true: ${maestro.platform == 'android' || maestro.platform == 'ios'} - commands: - - tapOn: - text: 'Discard and go back' - - tapOn: - text: "Don't leave" - - assertVisible: - text: 'Discard and go back' - - tapOn: - text: 'Discard and go back' - - tapOn: - text: 'Discard' -- assertVisible: - text: 'What is Lorem Ipsum?' -- tapOn: - text: 'Pop to top' -- assertVisible: - text: 'Push Article' diff --git a/example/e2e/maestro/stack-retain.yml b/example/e2e/maestro/stack-retain.yml deleted file mode 100644 index 524816e50d..0000000000 --- a/example/e2e/maestro/stack-retain.yml +++ /dev/null @@ -1,40 +0,0 @@ -appId: ${APP_ID} -name: Stack - Retain ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-retain - TEXT: "Let's Go!" -- tapOn: - text: "Let's Go!" -- assertVisible: - text: 'Retain' -- assertVisible: - text: '0' -- tapOn: - text: 'Retain' -- assertVisible: - text: '๐Ÿ“Œ' -- assertVisible: - text: '3' -- tapOn: - id: 'Go back' -- tapOn: - text: "Let's Go!" -- assertVisible: - text: '3' -- assertVisible: - text: '๐Ÿ“Œ' -- tapOn: - text: 'Unretain' -- tapOn: - id: 'Go back' -- tapOn: - text: "Let's Go!" -- assertNotVisible: - text: '๐Ÿ“Œ' -- assertVisible: - text: 'Retain' -- assertVisible: - text: '0' diff --git a/example/e2e/maestro/stack-transparent-modal.yml b/example/e2e/maestro/stack-transparent-modal.yml deleted file mode 100644 index 305d5021e3..0000000000 --- a/example/e2e/maestro/stack-transparent-modal.yml +++ /dev/null @@ -1,28 +0,0 @@ -appId: ${APP_ID} -name: Stack - Transparent Modal ---- -- runFlow: - file: ../launch.yml - env: - LINK: stack-transparent-modal - TEXT: 'Article' -- tapOn: - text: 'Show Dialog' -- assertVisible: - text: 'Trivia' -- tapOn: - text: 'Okay' -- assertNotVisible: - text: 'Trivia' -- tapOn: - text: 'Push NewsFeed' -- assertVisible: - text: 'NewsFeed' -- tapOn: - text: 'Show Dialog' -- assertVisible: - text: 'Trivia' -- tapOn: - text: 'Okay' -- assertNotVisible: - text: 'Trivia' diff --git a/example/e2e/maestro/static-config-screen.yml b/example/e2e/maestro/static-config-screen.yml deleted file mode 100644 index badb7f7a3d..0000000000 --- a/example/e2e/maestro/static-config-screen.yml +++ /dev/null @@ -1,24 +0,0 @@ -appId: ${APP_ID} -name: Static config ---- -- runFlow: - file: ../launch.yml - env: - LINK: static-config-screen - TEXT: 'Albums' -- tapOn: - text: 'Show Chat' -- assertVisible: - id: 'chat' -- tapOn: - id: 'chat' -- assertVisible: - text: 'Chat' -- tapOn: - id: 'albums' -- assertVisible: - text: 'Albums' -- tapOn: - text: 'Hide Chat' -- assertNotVisible: - id: 'chat' diff --git a/example/e2e/maestro/tab-view-auto-width-tab-bar.yml b/example/e2e/maestro/tab-view-auto-width-tab-bar.yml deleted file mode 100644 index 1a4355f50d..0000000000 --- a/example/e2e/maestro/tab-view-auto-width-tab-bar.yml +++ /dev/null @@ -1,32 +0,0 @@ -appId: ${APP_ID} -name: Tab View - Auto Width Tab Bar ---- -- runFlow: - file: ../launch.yml - env: - LINK: libraries-tab-view/auto-width-tab-bar - TEXT: 'Marissa Castillo' -- tapOn: - text: 'Article' -- extendedWaitUntil: - visible: - text: 'What is Lorem Ipsum?' - timeout: 2000 -- tapOn: - text: 'Albums' -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- tapOn: - text: 'Chat' -- extendedWaitUntil: - visible: - text: 'make me a sandwich' - timeout: 2000 -- tapOn: - text: 'Contacts' -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 diff --git a/example/e2e/maestro/tab-view-coverflow.yml b/example/e2e/maestro/tab-view-coverflow.yml deleted file mode 100644 index f7080e7f04..0000000000 --- a/example/e2e/maestro/tab-view-coverflow.yml +++ /dev/null @@ -1,58 +0,0 @@ -appId: ${APP_ID} -name: Tab View - Coverflow ---- -- runFlow: - file: ../launch.yml - env: - LINK: libraries-tab-view/coverflow - TEXT: 'Homogenic' -- assertVisible: - text: 'Homogenic' -- swipe: - duration: 300 - start: 90%, 50% - end: 10%, 50% -- extendedWaitUntil: - visible: - text: 'Number of the Beast' - timeout: 3000 -- swipe: - duration: 300 - start: 90%, 50% - end: 10%, 50% -- extendedWaitUntil: - visible: - text: "It's Blitz" - timeout: 3000 -- swipe: - duration: 300 - start: 10%, 50% - end: 90%, 50% -- extendedWaitUntil: - visible: - text: 'Number of the Beast' - timeout: 3000 -- swipe: - duration: 300 - start: 10%, 50% - end: 90%, 50% -- extendedWaitUntil: - visible: - text: 'Homogenic' - timeout: 3000 -- swipe: - duration: 300 - start: 10%, 50% - end: 90%, 50% -- extendedWaitUntil: - visible: - text: 'Bat Out of Hell' - timeout: 3000 -- swipe: - duration: 300 - start: 90%, 50% - end: 10%, 50% -- extendedWaitUntil: - visible: - text: 'Homogenic' - timeout: 3000 diff --git a/example/e2e/maestro/tab-view-custom-tab-bar.yml b/example/e2e/maestro/tab-view-custom-tab-bar.yml deleted file mode 100644 index db53958b61..0000000000 --- a/example/e2e/maestro/tab-view-custom-tab-bar.yml +++ /dev/null @@ -1,56 +0,0 @@ -appId: ${APP_ID} -name: Tab View - Custom Tab Bar ---- -- runFlow: - file: ../launch.yml - env: - LINK: libraries-tab-view/custom-tab-bar - TEXT: 'Custom tab bar' -- assertVisible: - text: 'Marissa Castillo' -- tapOn: - id: 'albums' -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- tapOn: - id: 'article' -- extendedWaitUntil: - visible: - text: 'What is Lorem Ipsum?' - timeout: 2000 -- tapOn: - id: 'chat' - delay: 500 -- extendedWaitUntil: - visible: - text: 'make me a sandwich' - timeout: 2000 -- tapOn: - id: 'contacts' -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'What is Lorem Ipsum?' - timeout: 2000 -- swipe: - direction: RIGHT - duration: 300 -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 diff --git a/example/e2e/maestro/tab-view-scroll-adapter.yml b/example/e2e/maestro/tab-view-scroll-adapter.yml deleted file mode 100644 index 6a1b7e6cf9..0000000000 --- a/example/e2e/maestro/tab-view-scroll-adapter.yml +++ /dev/null @@ -1,26 +0,0 @@ -appId: ${APP_ID} -name: Tab View - Scroll Adapter ---- -- runFlow: - file: ../launch.yml - env: - LINK: libraries-tab-view/scroll-adapter - TEXT: 'Marissa Castillo' -- tapOn: - text: 'Article' -- extendedWaitUntil: - visible: - text: 'What is Lorem Ipsum?' - timeout: 2000 -- tapOn: - text: 'Albums' -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- tapOn: - text: 'Contacts' -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 diff --git a/example/e2e/maestro/tab-view-scrollable-tab-bar.yml b/example/e2e/maestro/tab-view-scrollable-tab-bar.yml deleted file mode 100644 index 787205449a..0000000000 --- a/example/e2e/maestro/tab-view-scrollable-tab-bar.yml +++ /dev/null @@ -1,55 +0,0 @@ -appId: ${APP_ID} -name: Tab View - Scrollable Tab Bar ---- -- runFlow: - file: ../launch.yml - env: - LINK: libraries-tab-view/scrollable-tab-bar - TEXT: 'Contacts' -- assertVisible: - text: 'Marissa Castillo' -- tapOn: - text: 'Article' -- extendedWaitUntil: - visible: - text: 'What is Lorem Ipsum?' - timeout: 2000 -- tapOn: - text: 'Albums' -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- tapOn: - text: 'Chat' -- extendedWaitUntil: - visible: - text: 'make me a sandwich' - timeout: 2000 -- tapOn: - text: 'Contacts' -- extendedWaitUntil: - visible: - text: 'Marissa Castillo' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 -- swipe: - direction: LEFT - duration: 300 -- extendedWaitUntil: - visible: - text: 'make me a sandwich' - timeout: 2000 -- swipe: - direction: RIGHT - duration: 300 -- extendedWaitUntil: - visible: - id: 'album-0' - timeout: 2000 diff --git a/example/e2e/playwright.config.ts b/example/e2e/playwright.config.ts deleted file mode 100644 index c7f7019cee..0000000000 --- a/example/e2e/playwright.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import process from 'node:process'; - -import { defineConfig } from '@playwright/test'; -import path from 'path'; - -const PORT = 5173; - -export default defineConfig({ - testDir: path.join(__dirname, 'tests'), - timeout: 60 * 1000, - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - ...(process.env.CI ? { workers: 1 } : null), - projects: [ - { - name: 'Chromium', - use: { browserName: 'chromium' }, - }, - { - name: 'Firefox', - use: { browserName: 'firefox' }, - }, - ], - use: { - baseURL: `http://localhost:${PORT}`, - viewport: { width: 390, height: 844 }, - hasTouch: true, - trace: 'on-first-retry', - }, - webServer: [ - { - cwd: path.join(__dirname, '..'), - command: `pnpm web --port ${PORT}`, - url: `http://localhost:${PORT}`, - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, - { - cwd: path.join(__dirname, '..'), - command: 'pnpm server', - url: 'http://localhost:3275', - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, - ], -}); diff --git a/example/e2e/tests/Link.test.ts b/example/e2e/tests/Link.test.ts deleted file mode 100644 index 895a5fcd98..0000000000 --- a/example/e2e/tests/Link.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.getByText('Link').click(); -}); - -test('loads the article screen', async ({ page }) => { - await page.waitForURL('**/components-link/article/gandalf'); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Gandalf' }) - ).toBeVisible(); - - await expect(page.getByRole('link', { name: 'Go to Home' })).toHaveAttribute( - 'href', - '/' - ); -}); - -test('replaces and pushes params', async ({ page }) => { - await page.waitForURL('**/components-link/article/gandalf'); - - await page.getByRole('button', { name: 'Push params' }).click(); - - await page.waitForURL('**/components-link/article/babel-fish'); - - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await page.getByRole('button', { name: 'Push params' }).click(); - - await page.waitForURL('**/components-link/article/gandalf'); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.getByRole('link', { name: 'Go back' }).click(); - - await page.waitForURL('**/components-link/article/babel-fish'); - - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Babel fish' }) - ).toBeVisible(); - - await page.getByRole('link', { name: 'Go back' }).click(); - - await page.waitForURL('**/components-link/article/gandalf'); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Gandalf' }) - ).toBeVisible(); - - await page.getByRole('button', { name: 'Replace params' }).click(); - - await page.waitForURL('**/components-link/article/babel-fish'); - - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Babel fish' }) - ).toBeVisible(); -}); - -test('goes to the album screen and goes back', async ({ page }) => { - await page.waitForURL('**/components-link/article/gandalf'); - - const link = page.getByRole('link', { - name: 'Go to albums', - }); - - await expect(link).toHaveAttribute('href', '/components-link/albums'); - - await link.click(); - - await page.waitForURL('**/components-link/albums'); - - await expect(page).toHaveTitle('Albums - React Navigation Example'); - - await expect(page.getByRole('heading', { name: 'Albums' })).toBeVisible(); - - await expect(page.getByLabel('Home, back')).not.toBeVisible(); - - await page.getByLabel('Article by Gandalf, back').click(); - - await page.waitForURL('**/components-link/article/gandalf'); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Gandalf' }) - ).toBeVisible(); -}); - -test('replaces article with the album screen', async ({ page }) => { - await page.waitForURL('**/components-link/article/gandalf'); - - const link = page.getByRole('link', { - name: 'Replace with albums', - }); - - await expect(link).toHaveAttribute('href', '/components-link/albums'); - - await link.click(); - - await page.waitForURL('**/components-link/albums'); - - await expect(page).toHaveTitle('Albums - React Navigation Example'); - - await expect(page.getByRole('heading', { name: 'Albums' })).toBeVisible(); - - await expect(page.getByLabel('Article by Gandalf, back')).not.toBeVisible(); - - // FIXME: workaround for waiting for the transition to finish - await new Promise((resolve) => { - setTimeout(resolve, 300); - }); - - await page.getByLabel('Home, back').click(); - - await page.waitForURL('**/'); -}); - -test('preserves hash for navigation', async ({ page }) => { - await page.waitForURL('**/components-link/article/gandalf'); - - await page.getByText('Add hash to URL').click(); - - await page.waitForURL('**/components-link/article/gandalf#frodo'); - - await page.getByRole('button', { name: 'Replace params' }).click(); - - await page.waitForURL('**/components-link/article/babel-fish#frodo'); - - await page.reload(); - - await page.waitForURL('**/components-link/article/babel-fish#frodo'); - - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await page.getByRole('link', { name: 'Replace with albums' }).click(); - - await page.waitForURL('**/components-link/albums'); -}); diff --git a/example/e2e/tests/history.test.ts b/example/e2e/tests/history.test.ts deleted file mode 100644 index a582144599..0000000000 --- a/example/e2e/tests/history.test.ts +++ /dev/null @@ -1,696 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('pops to the proper screen after going back and forward in history', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - const buttonToFeed = page.getByRole('button', { - name: 'Navigate to feed', - }); - - await buttonToFeed.click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - await expect(page).toHaveTitle('Feed - React Navigation Example'); - - await page.goBack(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - await expect(page).toHaveTitle('Feed - React Navigation Example'); - - const buttonGoBack = page.getByRole('button', { - name: 'Go back', - }); - - await buttonGoBack.click(); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await expect( - page.getByRole('heading', { name: 'Article by Gandalf' }) - ).toBeVisible(); -}); - -test('restores browser forward entry after programmatic go back', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - await expect(page).toHaveTitle('Feed - React Navigation Example'); - - const feedUrl = page.url(); - - await page - .getByRole('button', { - name: 'Go back', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL(feedUrl); - await expect(page).toHaveTitle('Feed - React Navigation Example'); -}); - -test('keeps state in sync when browser back and forward interrupt navigation', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - await page.evaluate(() => { - const goForward = () => { - window.removeEventListener('popstate', goForward); - window.history.forward(); - }; - - window.addEventListener('popstate', goForward); - window.history.back(); - }); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - await expect(page).toHaveTitle('Feed - React Navigation Example'); - - await page - .getByRole('button', { - name: 'Go back', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); -}); - -test('replaces the current browser history entry on replace', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - await page - .getByRole('button', { - name: 'Replace with contacts', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/contacts'); - await expect(page).toHaveTitle('Contacts - React Navigation Example'); - - await page.goBack(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL('/native-stack/contacts'); - await expect(page).toHaveTitle('Contacts - React Navigation Example'); -}); - -test('truncates browser forward history after navigating from a previous entry', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - const buttonToFeed = page.getByRole('button', { - name: 'Navigate to feed', - }); - - await buttonToFeed.click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - const firstFeedUrl = page.url(); - - await page.goBack(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - - await buttonToFeed.click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - await expect(page).not.toHaveURL(firstFeedUrl); - - const secondFeedUrl = page.url(); - - await page.goForward(); - - await expect(page).toHaveURL(secondFeedUrl); -}); - -test('goes back to the proper history entry after popping nested stack', async ({ - page, -}) => { - await page.goto('/not-found'); - - const buttonGoToHome = page.getByRole('button', { - name: 'Go to home', - }); - - // After click, we have [not-found, home] on history stack - await buttonGoToHome.click(); - - const button = page.getByTestId('StackBasic').filter({ visible: true }); - - // Open nested stack - await button.click(); - - const buttonToFeed = page.getByRole('button', { - name: 'Navigate to feed', - }); - - // Push second screen to the nested stack - // After click, we have [not-found, home, article, feed] on history stack - await buttonToFeed.click(); - - const buttonPopToHome = page.getByRole('button', { - name: 'Pop to home', - }); - - // After click, we have [not-found, home] on history stack - await buttonPopToHome.click(); - - await expect(page).toHaveTitle('Examples - React Navigation Example'); - - await expect(page.getByRole('heading', { name: 'Examples' })).toBeVisible(); - - await page.goBack(); - - await expect(page).toHaveTitle('Oops! - React Navigation Example'); - - await expect(page.getByRole('heading', { name: 'Oops!' })).toBeVisible(); -}); - -test('rolls back browser history when browser back is prevented', async ({ - browserName, - page, -}) => { - await page.goto('/'); - - const cdpSession = - browserName === 'chromium' - ? await page.context().newCDPSession(page) - : undefined; - - const initialHistoryLength = await page.evaluate(() => window.history.length); - - await expect(page).toHaveURL('/'); - - expect(await page.evaluate(() => window.history.length)).toBe( - initialHistoryLength - ); - - if (cdpSession) { - expect( - (await cdpSession.send('Page.getNavigationHistory')).currentIndex - ).toBe(1); - } - - await page - .getByTestId('NativeStackPreventRemove') - .filter({ visible: true }) - .click(); - - await expect(page).toHaveURL('/native-stack-prevent-remove/input'); - - const inputHistoryLength = initialHistoryLength + 1; - - expect(await page.evaluate(() => window.history.length)).toBe( - inputHistoryLength - ); - - if (cdpSession) { - expect( - (await cdpSession.send('Page.getNavigationHistory')).currentIndex - ).toBe(2); - } - - await expect(page.getByText('Discard and go back')).toBeVisible(); - - await page.getByPlaceholder('Type somethingโ€ฆ').fill('hello'); - - await expect(page.getByPlaceholder('Type somethingโ€ฆ')).toHaveValue('hello'); - - await Promise.all([ - page.waitForEvent('dialog').then((dialog) => { - expect(dialog.message()).toBe( - 'You have unsaved changes. Discard them and leave the screen?' - ); - - return dialog.dismiss(); - }), - page.goBack(), - ]); - - await expect(page).toHaveURL('/native-stack-prevent-remove/input'); - - expect(await page.evaluate(() => window.history.length)).toBe( - inputHistoryLength - ); - - if (cdpSession) { - expect( - (await cdpSession.send('Page.getNavigationHistory')).currentIndex - ).toBe(2); - } -}); - -test('keeps browser history in sync when prevented browser back is continued', async ({ - browserName, - page, -}) => { - await page.goto('/'); - - const cdpSession = - browserName === 'chromium' - ? await page.context().newCDPSession(page) - : undefined; - - const initialHistoryLength = await page.evaluate(() => window.history.length); - - await page - .getByTestId('NativeStackPreventRemove') - .filter({ visible: true }) - .click(); - - await expect(page).toHaveURL('/native-stack-prevent-remove/input'); - - const inputHistoryLength = initialHistoryLength + 1; - - await page.getByPlaceholder('Type somethingโ€ฆ').fill('hello'); - - await expect(page.getByPlaceholder('Type somethingโ€ฆ')).toHaveValue('hello'); - - await Promise.all([ - page.waitForEvent('dialog').then((dialog) => { - expect(dialog.message()).toBe( - 'You have unsaved changes. Discard them and leave the screen?' - ); - - return dialog.accept(); - }), - page.goBack(), - ]); - - await expect(page).toHaveURL('/'); - - expect(await page.evaluate(() => window.history.length)).toBe( - inputHistoryLength - ); - - if (cdpSession) { - expect( - (await cdpSession.send('Page.getNavigationHistory')).currentIndex - ).toBe(1); - } -}); - -test('restores previous params on browser back after pushing params', async ({ - page, -}) => { - await page.goto('/components-link/article/gandalf'); - - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.getByRole('button', { name: 'Push params' }).click(); - - await expect(page).toHaveURL('/components-link/article/babel-fish'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await page.goBack(); - - await expect(page).toHaveURL('/components-link/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL('/components-link/article/babel-fish'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); -}); - -test('goes back through params history when the URL contains a hash', async ({ - page, -}) => { - await page.goto('/components-link/article/gandalf'); - - await page.getByRole('button', { name: 'Push params' }).click(); - - await expect(page).toHaveURL('/components-link/article/babel-fish'); - - await page.getByRole('button', { name: 'Add hash to URL' }).click(); - - await expect(page).toHaveURL('/components-link/article/babel-fish#frodo'); - - await page.getByRole('button', { name: 'Push params' }).click(); - - await expect(page).toHaveURL('/components-link/article/gandalf#frodo'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goBack(); - - await expect(page).toHaveURL('/components-link/article/babel-fish#frodo'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await page.goBack(); - - await expect(page).toHaveURL('/components-link/article/babel-fish'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - await page.goBack(); - - await expect(page).toHaveURL('/components-link/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); -}); - -test('replaces browser history entry when params are updated with setParams', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - const historyLength = await page.evaluate(() => window.history.length); - - await page.getByRole('button', { name: 'Update params' }).click(); - - await expect(page).toHaveURL('/native-stack/article/babel-fish'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); - - expect(await page.evaluate(() => window.history.length)).toBe(historyLength); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - await page.goBack(); - - await expect(page).toHaveURL('/native-stack/article/babel-fish'); - await expect(page).toHaveTitle( - 'Article by Babel fish - React Navigation Example' - ); -}); - -test('syncs browser history when switching tabs with fullHistory back behavior', async ({ - page, -}) => { - await page.goto('/bottom-tabs-full-history'); - - await expect(page).toHaveURL('/bottom-tabs-full-history/first'); - await expect(page.getByText('Tab First (-)')).toBeVisible(); - - await page - .getByRole('button', { name: 'Navigate to Second' }) - .filter({ visible: true }) - .click(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/second/1'); - await expect(page.getByText('Tab Second (1)')).toBeVisible(); - - await page - .getByRole('button', { name: 'Navigate to Third' }) - .filter({ visible: true }) - .click(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/third/2'); - await expect(page.getByText('Tab Third (2)')).toBeVisible(); - - await page.goBack(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/second/1'); - await expect(page.getByText('Tab Second (1)')).toBeVisible(); - - await page.goBack(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/first'); - await expect(page.getByText('Tab First (-)')).toBeVisible(); - - await page.goForward(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/second/1'); - await expect(page.getByText('Tab Second (1)')).toBeVisible(); - - await page - .getByRole('button', { name: 'Go back' }) - .filter({ visible: true }) - .click(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/first'); - await expect(page.getByText('Tab First (-)')).toBeVisible(); - - await page.goForward(); - - await expect(page).toHaveURL('/bottom-tabs-full-history/second/1'); - await expect(page.getByText('Tab Second (1)')).toBeVisible(); -}); - -test('goes back to matching history entry when popping multiple screens', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - await page - .getByRole('button', { - name: 'Replace with contacts', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/contacts'); - - await page - .getByRole('button', { - name: 'Push albums', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/albums'); - await expect(page).toHaveTitle('Albums - React Navigation Example'); - - await page - .getByRole('button', { - name: 'Pop by 2', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL('/native-stack/contacts'); - await expect(page).toHaveTitle('Contacts - React Navigation Example'); - - await page.goForward(); - - await expect(page).toHaveURL('/native-stack/albums'); - await expect(page).toHaveTitle('Albums - React Navigation Example'); -}); - -test('restores state when jumping multiple entries in browser history', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - await page - .getByRole('button', { - name: 'Replace with contacts', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/contacts'); - - await page - .getByRole('button', { - name: 'Push albums', - }) - .click(); - - await expect(page).toHaveURL('/native-stack/albums'); - - await page.evaluate(() => window.history.go(-2)); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.evaluate(() => window.history.go(2)); - - await expect(page).toHaveURL('/native-stack/albums'); - await expect(page).toHaveTitle('Albums - React Navigation Example'); -}); - -test('restores screens on browser back and forward after page reload', async ({ - page, -}) => { - await page.goto('/native-stack/article/gandalf'); - - await page - .getByRole('button', { - name: 'Navigate to feed', - }) - .click(); - - await expect(page).toHaveURL(/\/native-stack\/feed\/\d+$/); - - const feedUrl = page.url(); - - // Avoid `page.reload()` since it adds an extra history entry on Firefox - const loaded = page.waitForEvent('load'); - - await page.evaluate(() => window.location.reload()); - await loaded; - - await expect(page).toHaveURL(feedUrl); - await expect(page).toHaveTitle('Feed - React Navigation Example'); - - await page.goBack(); - - await expect(page).toHaveURL('/native-stack/article/gandalf'); - await expect(page).toHaveTitle( - 'Article by Gandalf - React Navigation Example' - ); - - await page.goForward(); - - await expect(page).toHaveURL(feedUrl); - await expect(page).toHaveTitle('Feed - React Navigation Example'); -}); - -test('preserves the URL for not found screens on browser back', async ({ - page, -}) => { - await page.goto('/foo/bar'); - - await expect(page).toHaveURL('/foo/bar'); - await expect(page).toHaveTitle('Oops! - React Navigation Example'); - - await page - .getByRole('button', { - name: 'Go to home', - }) - .click(); - - await expect(page).toHaveURL('/'); - await expect(page).toHaveTitle('Examples - React Navigation Example'); - - await page.goBack(); - - await expect(page).toHaveURL('/foo/bar'); - await expect(page).toHaveTitle('Oops! - React Navigation Example'); - - await page.goForward(); - - await expect(page).toHaveURL('/'); - await expect(page).toHaveTitle('Examples - React Navigation Example'); -}); - -test('adds and removes browser history entry when drawer is opened and closed', async ({ - page, -}) => { - await page.goto('/'); - - const historyLength = await page.evaluate(() => window.history.length); - - const overlay = page.getByRole('button', { name: 'Close drawer' }); - - await page - .getByLabel('Show navigation menu') - .filter({ visible: true }) - .click(); - - await expect(overlay).toBeVisible(); - - expect(await page.evaluate(() => window.history.length)).toBe( - historyLength + 1 - ); - - await expect(page).toHaveURL('/'); - - await page.goBack(); - - await expect(overlay).toBeHidden(); - await expect(page).toHaveURL('/'); - - await page.goForward(); - - await expect(overlay).toBeVisible(); - await expect(page).toHaveURL('/'); -}); diff --git a/example/e2e/tests/index.test.ts b/example/e2e/tests/index.test.ts deleted file mode 100644 index 9cb1522174..0000000000 --- a/example/e2e/tests/index.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('/'); -}); - -test('loads the example app', async ({ page }) => { - expect(await page.title()).toBe('Examples - React Navigation Example'); - - expect( - await page.getByRole('heading', { name: 'Examples' }).isVisible() - ).toBe(true); -}); diff --git a/example/e2e/tests/maestro.test.ts b/example/e2e/tests/maestro.test.ts deleted file mode 100644 index 0f3c22395d..0000000000 --- a/example/e2e/tests/maestro.test.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { expect, type Locator, type Page, test } from '@playwright/test'; -import fs from 'fs'; -import path from 'path'; -import { parseAllDocuments } from 'yaml'; - -test.describe.configure({ mode: 'parallel' }); - -fs.readdirSync(path.join(__dirname, '../maestro')).forEach((file) => { - const content = fs.readFileSync( - path.join(__dirname, '../maestro', file), - 'utf-8' - ); - - const [metadata, steps] = parseAllDocuments(content).map((doc) => - doc.toJSON() - ); - - test(metadata.name, async ({ page }) => { - for (const step of steps) { - try { - await runStep(page, step); - } catch (error) { - throw new Error(`Failed at step: ${JSON.stringify(step)}`, { - cause: error, - }); - } - } - }); -}); - -async function runStep(page: Page, step: any) { - const command = typeof step === 'object' ? Object.keys(step)[0] : step; - - switch (command) { - case 'runFlow': { - if (step.runFlow.file) { - const flowPath = path.join(__dirname, '../maestro', step.runFlow.file); - - const content = fs.readFileSync(flowPath, 'utf-8'); - const [, flowSteps] = parseAllDocuments(content).map((doc) => - doc.toJSON() - ); - - const env = step.runFlow.env || {}; - - for (const flowStep of flowSteps) { - let resolvedStep = flowStep; - - // Simple variable substitution - for (const [key, value] of Object.entries(env)) { - const strValue = String(value); - - resolvedStep = JSON.parse( - JSON.stringify(resolvedStep).replace( - new RegExp(`\\$\\{${key}\\}`, 'g'), - strValue - ) - ); - } - - await runStep(page, resolvedStep); - } - } else if (step.runFlow.when) { - const condition = step.runFlow.when; - - let conditionMet = null; - - if (condition.true) { - conditionMet = evaluateCondition(condition.true); - } - - if (condition.platform) { - conditionMet = - conditionMet !== false && - condition.platform.toLowerCase() === 'web'; - } - - if (condition.visible) { - const locator = query(page, condition.visible).filter({ - visible: true, - }); - - conditionMet = conditionMet !== false && (await locator.isVisible()); - } - - if (condition.notVisible) { - const locator = query(page, condition.notVisible).filter({ - visible: false, - }); - - conditionMet = conditionMet !== false && (await locator.isHidden()); - } - - if (conditionMet === null) { - throw new Error( - `Invalid runFlow condition: ${JSON.stringify(condition)}` - ); - } - - if (conditionMet) { - for (const cmd of step.runFlow.commands) { - await runStep(page, cmd); - } - } - } else { - throw new Error( - `Invalid runFlow step: ${JSON.stringify(step.runFlow)}` - ); - } - - break; - } - - case 'runScript': { - if (step.runScript.file) { - const result = await import( - path.join(__dirname, '../maestro', step.runScript.file) - ); - - await result.run(page, step.runScript.env || {}); - } else { - throw new Error( - `Invalid runScript step: ${JSON.stringify(step.runScript)}` - ); - } - - break; - } - - case 'openLink': { - // eslint-disable-next-line no-template-curly-in-string - await page.goto(step.openLink.link.replace('${APP_SCHEME}', '')); - - break; - } - - case 'tapOn': { - if (step.tapOn.delay) { - await new Promise((resolve) => { - setTimeout(resolve, step.tapOn.delay); - }); - } - - const locator = query(page, step.tapOn); - - const target = locator.filter({ visible: true }).last(); - - const handle = await target.elementHandle(); - await handle?.waitForElementState('stable'); - - await target.dispatchEvent('click'); - - break; - } - - case 'assertVisible': { - await expect( - query(page, step.assertVisible).filter({ visible: true }).last() - ).toBeVisible(); - - break; - } - - case 'assertNotVisible': { - const locator = query(page, step.assertNotVisible); - - if (await locator.isVisible()) { - await expect(locator).not.toBeInViewport(); - } else { - await expect(locator).toBeHidden(); - } - - break; - } - - case 'extendedWaitUntil': { - await new Promise((resolve) => { - setTimeout(resolve, 0); - }); - - const locator = step.extendedWaitUntil.visible - ? query(page, step.extendedWaitUntil.visible) - .filter({ visible: true }) - .last() - : query(page, step.extendedWaitUntil.notVisible).last(); - - const element = await locator.elementHandle(); - - await element?.waitForElementState('stable', { - timeout: step.extendedWaitUntil.timeout, - }); - - if (step.extendedWaitUntil.visible) { - await expect(locator).toBeVisible(); - } else if (step.extendedWaitUntil.notVisible) { - if (await locator.isVisible()) { - await expect(locator).not.toBeInViewport(); - } else { - await expect(locator).toBeHidden(); - } - } - - break; - } - - case 'retry': { - const maxRetries = step.retry.maxRetries ?? 1; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - for (const cmd of step.retry.commands) { - await runStep(page, cmd); - } - - break; - } catch (error) { - if (attempt === maxRetries) { - throw error; - } - } - } - - break; - } - - case 'swipe': { - const duration = step.swipe.duration || 300; - - const viewport = page.viewportSize(); - - if (!viewport) { - throw new Error('Viewport size is not available'); - } - - let startX: number; - let startY: number; - let endX: number; - let endY: number; - - if (step.swipe.start && step.swipe.end) { - const parsePercent = (value: string, dimension: number) => - (parseFloat(value) / 100) * dimension; - - const [startXStr, startYStr] = step.swipe.start - .split(',') - .map((s: string) => s.trim()); - const [endXStr, endYStr] = step.swipe.end - .split(',') - .map((s: string) => s.trim()); - - startX = parsePercent(startXStr, viewport.width); - startY = parsePercent(startYStr, viewport.height); - endX = parsePercent(endXStr, viewport.width); - endY = parsePercent(endYStr, viewport.height); - } else { - const direction = step.swipe.direction; - - startY = viewport.height / 2; - endY = startY; - startX = - direction === 'LEFT' ? viewport.width * 0.8 : viewport.width * 0.2; - endX = - direction === 'LEFT' ? viewport.width * 0.2 : viewport.width * 0.8; - } - - await swipe(page, { startX, startY, endX, endY, duration }); - - await page.waitForTimeout(duration); - - break; - } - - case 'inputText': { - await page.keyboard.type(step.inputText); - - break; - } - - case 'back': { - await page.goBack(); - - break; - } - - case 'stopApp': { - // No-op on web - - break; - } - - default: { - throw new Error(`Unknown command: ${JSON.stringify(step)}`); - } - } -} - -async function swipe( - page: Page, - { - startX, - startY, - endX, - endY, - duration, - }: { - startX: number; - startY: number; - endX: number; - endY: number; - duration: number; - } -) { - await page.evaluate( - async ({ startX, startY, endX, endY, duration }) => { - const target = document.elementFromPoint(startX, startY) ?? document.body; - const steps = Math.max(10, Math.floor(duration / 20)); - - const dispatchTouchEvent = (type: string, x: number, y: number) => { - const touch = new Touch({ - identifier: 0, - target, - clientX: x, - clientY: y, - screenX: x, - screenY: y, - pageX: x + window.scrollX, - pageY: y + window.scrollY, - radiusX: 1, - radiusY: 1, - force: 0.5, - }); - - const activeTouches = type === 'touchend' ? [] : [touch]; - - target.dispatchEvent( - new TouchEvent(type, { - bubbles: true, - cancelable: true, - composed: true, - touches: activeTouches, - targetTouches: activeTouches, - changedTouches: [touch], - }) - ); - }; - - dispatchTouchEvent('touchstart', startX, startY); - - for (let i = 1; i <= steps; i++) { - const progress = i / steps; - - dispatchTouchEvent( - 'touchmove', - startX + (endX - startX) * progress, - startY + (endY - startY) * progress - ); - - await new Promise((resolve) => { - setTimeout(resolve, duration / steps); - }); - } - - dispatchTouchEvent('touchend', endX, endY); - }, - { startX, startY, endX, endY, duration } - ); -} - -function evaluateCondition(condition: boolean | string) { - if (typeof condition === 'boolean') { - return condition; - } - - const expression = condition.trim(); - - if (!expression.startsWith('${') || !expression.endsWith('}')) { - throw new Error(`Invalid runFlow true condition: ${condition}`); - } - - try { - return Boolean( - // eslint-disable-next-line no-new-func - Function( - 'maestro', - `"use strict"; return (${expression.slice(2, -1)});` - )({ platform: 'web' }) - ); - } catch (error) { - throw new Error(`Failed to evaluate runFlow true condition: ${condition}`, { - cause: error, - }); - } -} - -type QueryBy = - | string - | { text: string; selected?: boolean } - | { id: string; selected?: boolean }; - -function query(page: Page, by: QueryBy) { - let locator: Locator; - - if (typeof by === 'string' || 'text' in by) { - const text = typeof by === 'string' ? by : by.text; - const isRegex = text.includes('.*'); - - const args = [ - isRegex ? new RegExp(`^(?:${text})$`) : text, - isRegex ? undefined : { exact: true }, - ] as const; - - locator = page.getByLabel(...args).or(page.getByText(...args)); - } else if ('id' in by) { - locator = page.getByTestId(by.id); - } else { - throw new Error(`Unknown step format: ${JSON.stringify(by)}`); - } - - if (typeof by !== 'string' && typeof by.selected === 'boolean') { - return locator.locator( - `xpath=ancestor-or-self::*[@aria-selected="${by.selected}"]` - ); - } - - return locator; -} diff --git a/example/e2e/tests/server.test.ts b/example/e2e/tests/server.test.ts deleted file mode 100644 index 69030006b0..0000000000 --- a/example/e2e/tests/server.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, test as it } from '@playwright/test'; - -const server = 'http://localhost:3275'; - -it('renders the home page', async () => { - const res = await fetch(server); - const html = await res.text(); - - expect(html).toContain(''); - expect(html).toContain('id="root"'); -}); - -it('renders a linked route from the request location', async () => { - const res = await fetch(`${server}/stack-basic`); - const html = await res.text(); - - expect(html).toContain('Article by Gandalf'); -}); - -it('streams the shell before suspended content', async () => { - const res = await fetch(`${server}/screen-layout/suspense`); - - expect(res.ok).toBe(true); - expect(res.body).not.toBeNull(); - - const reader = res.body!.getReader(); - const decoder = new TextDecoder(); - - let html = ''; - - while (true) { - const { value, done } = await reader.read(); - - if (done) { - break; - } - - html += decoder.decode(value, { stream: true }); - - if (html.includes('Loading')) { - break; - } - } - - expect(html).toContain('Loading'); - expect(html).not.toContain('Suspend'); - - while (true) { - const { value, done } = await reader.read(); - - if (done) { - break; - } - - html += decoder.decode(value, { stream: true }); - } - - html += decoder.decode(); - - expect(html).toContain('Suspend'); -}); diff --git a/example/gesture-handler.native.tsx b/example/gesture-handler.native.tsx deleted file mode 100644 index d025d51ef5..0000000000 --- a/example/gesture-handler.native.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// Only import react-native-gesture-handler on native platforms -import 'react-native-gesture-handler'; diff --git a/example/gesture-handler.tsx b/example/gesture-handler.tsx deleted file mode 100644 index d76ad9a20f..0000000000 --- a/example/gesture-handler.tsx +++ /dev/null @@ -1 +0,0 @@ -// Don't import react-native-gesture-handler on web diff --git a/example/index.html b/example/index.html deleted file mode 100644 index 205889813a..0000000000 --- a/example/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - -
- diff --git a/example/metro.config.js b/example/metro.config.js deleted file mode 100644 index 0e1b38d299..0000000000 --- a/example/metro.config.js +++ /dev/null @@ -1,57 +0,0 @@ -const path = require('path'); -const { withMetroConfig } = require('react-native-monorepo-config'); - -const { getDefaultConfig } = require('@expo/metro-config'); - -const defaultConfig = withMetroConfig(getDefaultConfig(__dirname), { - root: path.resolve(__dirname, '..'), - dirname: __dirname, - conditions: ['@react-navigation/source'], -}); - -/** @type {import('metro-config').MetroConfig} */ -module.exports = { - ...defaultConfig, - - resolver: { - ...defaultConfig.resolver, - - resolveRequest: (context, realModuleName, platform) => { - // We mock out these native deps on web - // This is an additional measure to ensure they don't get added accidentally - const excludedModules = [ - 'react-native-gesture-handler', - 'react-native-reanimated', - 'react-native-page-view', - ]; - - if (platform === 'web' && excludedModules.includes(realModuleName)) { - throw new Error( - `The module '${realModuleName}' should not be imported on Web.` - ); - } - - return defaultConfig.resolver.resolveRequest( - context, - realModuleName, - platform - ); - }, - }, - - server: { - ...defaultConfig.server, - - enhanceMiddleware: (middleware) => { - return (req, res, next) => { - // When an asset is imported outside the project root, it has wrong path on Android - // So we fix the path to correct one - if (/\/packages\/.+\.png\?.+$/.test(req.url)) { - req.url = `/assets/../${req.url}`; - } - - return middleware(req, res, next); - }; - }, - }, -}; diff --git a/example/package.json b/example/package.json deleted file mode 100644 index d2f5c5a26b..0000000000 --- a/example/package.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "name": "@react-navigation/example", - "description": "Demo app to showcase various functionality of React Navigation", - "private": true, - "main": "./App.tsx", - "scripts": { - "start": "expo start --dev-client", - "android": "expo run:android", - "ios": "expo run:ios", - "web": "vite", - "server": "babel-node -i '/node_modules[/\\\\](?react-native)/' -x '.web.tsx,.web.ts,.web.js,.tsx,.ts,.js' --config-file ./server/babel.config.js server", - "e2e:native": "node ../scripts/maestro.ts", - "e2e:web": "pnpm exec playwright test --config=e2e/playwright.config.ts" - }, - "dependencies": { - "@callstack/liquid-glass": "^0.8.0", - "@expo-google-fonts/lora": "^0.4.2", - "@expo-google-fonts/nunito": "^0.4.2", - "@expo-google-fonts/outfit": "^0.4.3", - "@expo-google-fonts/roboto-slab": "^0.4.2", - "@expo-google-fonts/space-grotesk": "^0.4.1", - "@expo/react-native-action-sheet": "^4.1.1", - "@expo/vector-icons": "^15.1.1", - "@react-native-async-storage/async-storage": "2.2.0", - "@react-navigation/bottom-tabs": "workspace:^", - "@react-navigation/devtools": "workspace:^", - "@react-navigation/drawer": "workspace:^", - "@react-navigation/elements": "workspace:^", - "@react-navigation/material-top-tabs": "workspace:^", - "@react-navigation/native": "workspace:^", - "@react-navigation/native-stack": "workspace:^", - "@react-navigation/stack": "workspace:^", - "@tanstack/react-query": "^5.100.3", - "expo": "^55.0.26", - "expo-asset": "~55.0.17", - "expo-blur": "~55.0.14", - "expo-font": "~55.0.8", - "expo-linking": "~55.0.15", - "expo-splash-screen": "~55.0.21", - "expo-status-bar": "~55.0.6", - "expo-updates": "~55.0.24", - "koa": "^3.2.1", - "react": "19.2.0", - "react-dom": "19.2.0", - "react-native": "0.83.6", - "react-native-drawer-layout": "workspace:^", - "react-native-gesture-handler": "^3.0.0", - "react-native-pager-view": "^8.0.0", - "react-native-reanimated": "~4.2.1", - "react-native-safe-area-context": "~5.6.2", - "react-native-screens": "^4.25.2", - "react-native-showtime": "^0.0.5", - "react-native-tab-view": "workspace:^", - "react-native-web": "~0.21.2", - "react-native-worklets": "0.7.4", - "standard-navigation": "^0.0.7" - }, - "devDependencies": { - "@babel/core": "^7.29.7", - "@babel/node": "^7.29.7", - "@babel/plugin-transform-strict-mode": "^7.29.7", - "@expo/metro-config": "~55.0.23", - "@playwright/test": "^1.60.0", - "@types/koa": "^3.0.3", - "@types/mock-require": "^3.0.0", - "@types/react": "~19.2.17", - "@types/react-dom": "~19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "arktype": "^2.2.0", - "babel-plugin-module-resolver": "^5.0.3", - "babel-preset-expo": "~55.0.22", - "expect-type": "^1.3.0", - "expo-build-disk-cache": "^0.7.4", - "metro-config": "0.83.7", - "mock-require-assets": "^0.0.3", - "npm-run-all2": "^9.0.1", - "react-native-builder-bob": "^0.43.0", - "react-native-monorepo-config": "^0.4.0", - "typescript": "^6.0.3", - "valibot": "^1.4.1", - "vite": "^7.3.5", - "vite-plugin-commonjs": "^0.10.4", - "yaml": "^2.9.0", - "zod": "^4.4.3" - }, - "react-navigation": { - "material-symbols": { - "fonts": [ - { - "variant": "sharp", - "weights": [ - 100, - 200, - 300, - 400, - 500, - 600, - 700 - ] - }, - { - "variant": "outlined", - "weights": [ - 100, - 200, - 300, - 400, - 500, - 600, - 700 - ] - }, - { - "variant": "rounded", - "weights": [ - 100, - 200, - 300, - 400, - 500, - 600, - 700 - ] - } - ] - } - } -} diff --git a/example/server/babel.config.js b/example/server/babel.config.js deleted file mode 100644 index 1c34629a39..0000000000 --- a/example/server/babel.config.js +++ /dev/null @@ -1,38 +0,0 @@ -const path = require('path'); -const fs = require('fs'); - -const condition = '@react-navigation/source'; -const packages = path.resolve(__dirname, '..', '..', 'packages'); - -const alias = Object.fromEntries( - fs - .readdirSync(packages) - .filter((dir) => !dir.startsWith('.')) - .map((dir) => [dir, require(`../../packages/${dir}/package.json`)]) - .flatMap(([dir, pak]) => - Object.entries(pak.exports || {}) - .reverse() - .filter(([, value]) => value[condition] != null) - .map(([key, value]) => [ - path.join(pak.name, key), - path.resolve(packages, dir, value[condition]), - ]) - ) -); - -module.exports = { - presets: ['module:@react-native/babel-preset'], - plugins: [ - [ - 'module-resolver', - { - root: ['..'], - extensions: ['.tsx', '.ts', '.js', '.json'], - alias: { - 'react-native': require.resolve('react-native-web'), - ...alias, - }, - }, - ], - ], -}; diff --git a/example/server/env.tsx b/example/server/env.tsx deleted file mode 100644 index b7ef694de0..0000000000 --- a/example/server/env.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import process from 'node:process'; - -process.env.EXPO_OS = 'web'; diff --git a/example/server/index.tsx b/example/server/index.tsx deleted file mode 100644 index c4d98407b6..0000000000 --- a/example/server/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable require-atomic-updates */ - -import './resolve-hooks'; -import './env'; - -import process from 'node:process'; -import { PassThrough } from 'node:stream'; - -import { ServerContainer } from '@react-navigation/native/server'; -import Koa from 'koa'; -import * as React from 'react'; -import { renderToPipeableStream } from 'react-dom/server'; -import { AppRegistry } from 'react-native-web'; - -import { App } from '../src/index'; - -AppRegistry.registerComponent('App', () => App); - -const PORT = process.env.PORT || 3275; - -const app = new Koa(); - -type DocumentProps = { - children: React.ReactNode; - styles: React.ReactNode; -}; - -function Document({ children, styles }: DocumentProps) { - return ( - - - - - - {styles} - - -
- {children} -
- - - ); -} - -const render = (element: React.ReactNode) => { - return new Promise((resolve, reject) => { - const stream = new PassThrough(); - - let isShellReady = false; - let isFinished = false; - - const { pipe, abort } = renderToPipeableStream(element, { - onShellReady() { - isShellReady = true; - - stream.write(''); - - pipe(stream); - resolve(stream); - }, - onError(e) { - if (isShellReady) { - console.error(e); - } else { - abort(); - reject(e); - } - }, - onShellError(e) { - abort(); - reject(e); - }, - }); - - stream.on('error', abort); - stream.on('finish', () => { - isFinished = true; - }); - stream.on('close', () => { - if (!isFinished) { - abort(); - } - }); - }); -}; - -app.use(async (ctx) => { - const { element, getStyleElement } = AppRegistry.getApplication('App'); - - const url = new URL(ctx.href); - - const body = await render( - - {element} - - ); - - ctx.status = 200; - ctx.type = 'text/html'; - ctx.body = body; -}); - -app.listen(PORT, () => { - console.log(`Running at http://localhost:${PORT}`); -}); diff --git a/example/server/resolve-hooks.tsx b/example/server/resolve-hooks.tsx deleted file mode 100644 index cd3aca1ee7..0000000000 --- a/example/server/resolve-hooks.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import 'mock-require-assets'; - -import Module from 'module'; - -// We need to make sure that .web.xx extensions are resolved before .xx -// @ts-expect-error: _extensions doesn't exist in the type definitions -Module._extensions = Object.fromEntries( - // @ts-expect-error _extensions doesn't exist in the type definitions - Object.entries(Module._extensions).sort((a, b) => { - return b[0].split('.').length - a[0].split('.').length; - }) -); - -// Set __DEV__ that expo needs -// @ts-expect-error __DEV__ doesn't exist in the type definitions -globalThis.__DEV__ = process.env.NODE_ENV !== 'production'; diff --git a/example/src/Screens/ActivityModes.tsx b/example/src/Screens/ActivityModes.tsx deleted file mode 100644 index 5259488e9a..0000000000 --- a/example/src/Screens/ActivityModes.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { - PlatformPressable, - Text, - useHeaderHeight, -} from '@react-navigation/elements'; -import { ActivityView } from '@react-navigation/elements/internal'; -import { useTheme } from '@react-navigation/native'; -import { useEffect, useRef, useState } from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; - -import { SegmentedPicker } from '../Shared/SegmentedPicker'; - -function Content() { - const { colors } = useTheme(); - const dotAnim = useRef(new Animated.Value(0)).current; - - useEffect(() => { - const animation = Animated.loop( - Animated.sequence([ - Animated.timing(dotAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }), - Animated.timing(dotAnim, { - toValue: 0, - duration: 1000, - useNativeDriver: true, - }), - ]) - ); - - animation.start(); - - return () => { - animation.stop(); - }; - }, [dotAnim]); - - const [time, setTime] = useState(() => new Date()); - const [offset, setOffset] = useState(0); - - useEffect(() => { - const interval = setInterval(() => { - setTime(new Date()); - }, 1000); - - return () => clearInterval(interval); - }, []); - - const formattedTime = new Date( - time.getTime() + offset * 1000 * 60 * 60 - ).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }); - - return ( - - {formattedTime} - - setOffset((o) => --o)}> - โ†“ - - setOffset((o) => ++o)}> - โ†‘ - - - - - ); -} - -export function ActivityModes() { - const headerHeight = useHeaderHeight(); - - const [mode, setMode] = - useState['mode']>('normal'); - - const [visible, setVisible] = useState(true); - - return ( - - - - - - setMode(value)} - /> - setVisible(value)} - /> - - - ); -} - -ActivityModes.title = 'Activity Modes'; -ActivityModes.linking = {}; -ActivityModes.options = { - headerShown: true, -}; - -const SPACING = 16; -const CLOCK_SIZE = 160; -const CLOCK_FONT_SIZE = 24; -const BUTTON_FONT_SIZE = 18; -const DOT_SIZE = 10; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: SPACING, - }, - buttons: { - position: 'absolute', - flexDirection: 'row', - marginTop: CLOCK_FONT_SIZE + BUTTON_FONT_SIZE + SPACING, - }, - button: { - fontSize: BUTTON_FONT_SIZE, - fontWeight: '600', - opacity: 0.5, - margin: SPACING / 2, - }, - controls: { - alignItems: 'center', - gap: SPACING / 2, - }, - content: { - minHeight: CLOCK_SIZE, - margin: SPACING, - }, - clock: { - width: CLOCK_SIZE, - height: CLOCK_SIZE, - borderRadius: 10, - borderWidth: 1, - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'center', - }, - time: { - fontSize: CLOCK_FONT_SIZE, - fontWeight: '600', - fontVariant: ['tabular-nums'], - }, - dot: { - position: 'absolute', - bottom: SPACING, - left: SPACING, - width: DOT_SIZE, - height: DOT_SIZE, - borderRadius: DOT_SIZE / 2, - opacity: 0.3, - }, -}); diff --git a/example/src/Screens/AuthFlow.tsx b/example/src/Screens/AuthFlow.tsx deleted file mode 100644 index 2d9d9c4ce3..0000000000 --- a/example/src/Screens/AuthFlow.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { useNavigation, useTheme } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { ActivityIndicator, StyleSheet, TextInput, View } from 'react-native'; - -const AUTH_CONTEXT_ERROR = - 'Authentication context not found. Have your wrapped your components with AuthContext.Consumer?'; - -const AuthContext = React.createContext<{ - isSignedIn: boolean; - signIn: () => void; - signOut: () => void; -}>({ - isSignedIn: false, - signIn: () => { - throw new Error(AUTH_CONTEXT_ERROR); - }, - signOut: () => { - throw new Error(AUTH_CONTEXT_ERROR); - }, -}); - -const useIsSignedIn = () => { - const { isSignedIn } = React.use(AuthContext); - return isSignedIn; -}; - -const useIsSignedOut = () => { - const { isSignedIn } = React.use(AuthContext); - return !isSignedIn; -}; - -const SplashScreen = () => { - const { colors } = useTheme(); - - return ( - - - - ); -}; - -const SignInScreen = () => { - const navigation = useNavigation('SignIn'); - const { signIn } = React.use(AuthContext); - const { colors } = useTheme(); - - return ( - - - - - - - ); -}; - -const HomeScreen = () => { - const navigation = useNavigation('Main'); - const { signOut } = React.use(AuthContext); - - return ( - - Signed in successfully ๐ŸŽ‰ - - - - ); -}; - -const ProfileScreen = () => { - const navigation = useNavigation('Profile'); - const { signOut } = React.use(AuthContext); - - return ( - - This is your profile - - - - ); -}; - -const ChatScreen = () => { - const { isSignedIn, signIn, signOut } = React.use(AuthContext); - - return ( - - What's up? - {isSignedIn ? ( - - ) : ( - - )} - - ); -}; - -type State = { - isLoading: boolean; - isSignout: boolean; - userToken: undefined | string; -}; - -type Action = - | { type: 'RESTORE_TOKEN'; token: undefined | string } - | { type: 'SIGN_IN'; token: string } - | { type: 'SIGN_OUT' }; - -const AuthFlowNavigator = createStackNavigator({ - groups: { - SignedIn: { - if: useIsSignedIn, - screens: { - Main: createStackScreen({ - screen: HomeScreen, - options: { - title: 'Home', - }, - linking: '', - }), - Profile: createStackScreen({ - screen: ProfileScreen, - linking: 'profile', - }), - Chat: createStackScreen({ - screen: ChatScreen, - linking: 'chat', - }), - }, - }, - SignedOut: { - if: useIsSignedOut, - screens: { - SignIn: createStackScreen({ - screen: SignInScreen, - options: { - title: 'Welcome', - }, - linking: 'signin', - }), - Chat: createStackScreen({ - screen: ChatScreen, - linking: 'chat', - }), - }, - }, - }, -}).with(({ Navigator }) => { - const [state, dispatch] = React.useReducer( - (prevState: State, action: Action) => { - switch (action.type) { - case 'RESTORE_TOKEN': - return { - ...prevState, - userToken: action.token, - isLoading: false, - }; - 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, - } - ); - - React.useEffect(() => { - const timer = setTimeout(() => { - dispatch({ type: 'RESTORE_TOKEN', token: undefined }); - }, 1000); - - return () => clearTimeout(timer); - }, []); - - const isSignedIn = state.userToken !== undefined; - - const authContext = React.useMemo( - () => ({ - isSignedIn, - signIn: () => dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' }), - signOut: () => dispatch({ type: 'SIGN_OUT' }), - }), - [isSignedIn] - ); - - if (state.isLoading) { - return ; - } - - return ( - - { - if (route.name === 'SignIn') { - return { - animationTypeForReplace: state.isSignout ? 'pop' : 'push', - }; - } - - return {}; - }} - /> - - ); -}); - -export const AuthFlow = { - screen: AuthFlowNavigator, - title: 'Auth Flow', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - padding: 16, - justifyContent: 'center', - }, - input: { - margin: 8, - padding: 10, - borderRadius: 3, - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(0, 0, 0, 0.08)', - }, - button: { - margin: 8, - }, - heading: { - textAlign: 'center', - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - }, -}); diff --git a/example/src/Screens/BottomTabs.tsx b/example/src/Screens/BottomTabs.tsx deleted file mode 100644 index 36081d75e0..0000000000 --- a/example/src/Screens/BottomTabs.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import { useActionSheet } from '@expo/react-native-action-sheet'; -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { - type BottomTabScreenProps, - createBottomTabNavigator, - useBottomTabBarHeight, -} from '@react-navigation/bottom-tabs'; -import { - Button, - HeaderBackButton, - HeaderButton, - type Icon, - PlatformPressable, - useHeaderHeight, -} from '@react-navigation/elements'; -import { - createPathConfigForStaticNavigation, - type NavigatorScreenParams, - type PathConfig, - type StaticScreenProps, - useIsFocused, - useNavigation, -} from '@react-navigation/native'; -import { BlurView } from 'expo-blur'; -import { StatusBar } from 'expo-status-bar'; -import * as React from 'react'; -import { - type ColorValue, - Image, - Platform, - ScrollView, - StyleSheet, - useWindowDimensions, - View, -} from 'react-native'; - -import iconBookUser from '../../assets/icons/book-user.png'; -import iconListMusic from '../../assets/icons/list-music.png'; -import iconMusic from '../../assets/icons/music.png'; -import iconNewspaper from '../../assets/icons/newspaper.png'; -import { Albums } from '../Shared/Albums'; -import { Chat } from '../Shared/Chat'; -import { Contacts } from '../Shared/Contacts'; -import { NativeStack } from './NativeStack'; - -export type BottomTabParamList = { - TabStack: NavigatorScreenParams; - TabAlbums: undefined; - TabContacts: undefined; - TabChat: { count: number } | undefined; -}; - -const linking = { - screens: { - TabStack: { - path: 'stack', - screens: createPathConfigForStaticNavigation(NativeStack.screen), - }, - TabAlbums: 'albums', - TabContacts: 'contacts', - TabChat: 'chat', - }, -} satisfies PathConfig>; - -const AlbumsScreen = () => { - const navigation = useNavigation(); - const headerHeight = useHeaderHeight(); - const tabBarHeight = useBottomTabBarHeight(); - const isFocused = useIsFocused(); - - return ( - <> - {isFocused && Platform.OS === 'android' && } - - - - - - - - - ); -}; - -let i = 1; - -const Tab = createBottomTabNavigator(); - -const animations = ['none', 'fade', 'shift'] as const; -const variants = ['material', 'uikit'] as const; - -export function BottomTabs( - _: StaticScreenProps> -) { - const { showActionSheetWithOptions } = useActionSheet(); - - const dimensions = useWindowDimensions(); - - const [variant, setVariant] = - React.useState<(typeof variants)[number]>('material'); - const [animation, setAnimation] = - React.useState<(typeof animations)[number]>('none'); - - const [isCompact, setIsCompact] = React.useState(false); - - const isLargeScreen = dimensions.width >= 1024; - - return ( - <> - ) => ({ - headerShown: true, - headerLeft: (props) => ( - - ), - headerRight: ({ tintColor }) => ( - - { - showActionSheetWithOptions( - { - options: variants.map((option) => { - if (option === variant) { - return `${option} (current)`; - } - - return option; - }), - }, - (index) => { - if (index != null) { - setVariant(variants[index] || variants[0]); - } - } - ); - }} - > - - - { - showActionSheetWithOptions( - { - options: animations.map((option) => { - if (option === animation) { - return `${option} (current)`; - } - - return option; - }), - }, - (index) => { - if (index != null) { - setAnimation(animations[index] || animations[0]); - } - } - ); - }} - > - - - - ), - tabBarControllerMode: isLargeScreen ? 'tabSidebar' : 'tabBar', - tabBarVariant: isLargeScreen ? variant : 'uikit', - tabBarLabelPosition: - isLargeScreen && isCompact && variant !== 'uikit' - ? 'below-icon' - : undefined, - animation, - })} - > - - ({ - title: 'Chat', - tabBarButtonTestID: 'chat', - tabBarIcon: ({ - color, - size, - }: { - color: ColorValue; - size: number; - }) => ( - - ), - tabBarBadge: route.params?.count, - })} - /> - ({ - ios: { - type: 'sfSymbol', - name: 'person.2', - }, - android: { - type: 'materialSymbol', - name: 'contacts', - }, - default: { - type: 'image', - source: iconBookUser, - }, - }), - }} - /> - ( - - ), - tabBarIcon: ({ focused }) => ({ - type: 'image', - source: focused ? iconListMusic : iconMusic, - }), - tabBarInactiveTintColor: 'rgba(255, 255, 255, 0.5)', - tabBarActiveTintColor: '#fff', - tabBarStyle: [ - { borderRightWidth: 0, borderLeftWidth: 0, borderTopWidth: 0 }, - isLargeScreen ? null : { position: 'absolute' }, - ], - tabBarBackground: () => ( - <> - {isLargeScreen && ( - - )} - - - ), - }} - /> - - {isLargeScreen ? ( - setIsCompact(!isCompact)} - style={{ - position: 'absolute', - bottom: 0, - start: 0, - padding: 16, - }} - > - - - ) : null} - - ); -} - -BottomTabs.title = 'Bottom Tabs - Basic'; -BottomTabs.linking = linking; - -const styles = StyleSheet.create({ - headerRight: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - marginEnd: 16, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, -}); diff --git a/example/src/Screens/BottomTabsDynamic.tsx b/example/src/Screens/BottomTabsDynamic.tsx deleted file mode 100644 index 8d90f200d1..0000000000 --- a/example/src/Screens/BottomTabsDynamic.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import Feather from '@expo/vector-icons/Feather'; -import { - type BottomTabScreenProps, - createBottomTabNavigator, -} from '@react-navigation/bottom-tabs'; -import { Button, HeaderBackButton, Text } from '@react-navigation/elements'; -import type { - NavigatorScreenParams, - PathConfigMap, - StaticScreenProps, -} from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; - -type DynamicBottomTabParamList = { - [key: `tab-${number}`]: undefined; -}; - -const linking = { - screens: { - 'tab-0': 'tab/0', - 'tab-1': 'tab/1', - 'tab-2': 'tab/2', - 'tab-3': 'tab/3', - 'tab-4': 'tab/4', - 'tab-5': 'tab/5', - }, -} satisfies { screens: PathConfigMap }; - -const BottomTabs = createBottomTabNavigator(); - -export function BottomTabsDynamic( - _: StaticScreenProps> -) { - const [tabs, setTabs] = React.useState([0, 1]); - - return ( - ) => ({ - headerShown: true, - headerLeft: (props) => ( - - ), - })} - > - {tabs.map((i) => ( - ( - - ), - }} - > - {() => ( - - Tab {i} - - {tabs.length < 5 && ( - - )} - {tabs.length > 1 && ( - - )} - - - )} - - ))} - - ); -} - -BottomTabsDynamic.title = 'Bottom Tabs - Dynamic'; -BottomTabsDynamic.linking = linking; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - heading: { - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - }, - buttons: { - gap: 8, - }, -}); diff --git a/example/src/Screens/BottomTabsFullHistory.tsx b/example/src/Screens/BottomTabsFullHistory.tsx deleted file mode 100644 index 9d30ddbb08..0000000000 --- a/example/src/Screens/BottomTabsFullHistory.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { Button, Text } from '@react-navigation/elements'; -import { - type StaticScreenProps, - useNavigation, - useRoute, -} from '@react-navigation/native'; -import { useEffect } from 'react'; -import { StyleSheet, View } from 'react-native'; - -let count = 0; - -function TestScreen({ - route: { params }, -}: StaticScreenProps<{ count: number } | undefined>) { - const route = useRoute(); - const navigation = useNavigation(); - - return ( - - - Tab {route.name} ({params?.count ?? '-'}) - - - {(['First', 'Second', 'Third'] as const).map((name) => ( - - ))} - - - - ); -} - -function CounterLayout({ children }: { children: React.ReactNode }) { - useEffect(() => { - count = 0; - }, []); - - return <>{children}; -} - -const Tabs = createBottomTabNavigator({ - backBehavior: 'fullHistory', - layout: (props) => , - implementation: 'custom', - screenOptions: { - headerShown: true, - }, - screens: { - First: { - screen: TestScreen, - linking: { - path: 'first/:count?', - parse: { count: Number }, - }, - options: { - tabBarButtonTestID: 'first-tab', - }, - }, - Second: { - screen: TestScreen, - linking: { - path: 'second/:count?', - parse: { count: Number }, - }, - options: { - tabBarButtonTestID: 'second-tab', - }, - }, - Third: { - screen: TestScreen, - linking: { - path: 'third/:count?', - parse: { count: Number }, - }, - options: { - tabBarButtonTestID: 'third-tab', - }, - }, - }, -}); - -export const BottomTabsFullHistory = { - screen: Tabs, - title: 'Bottom Tabs - Full History', -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - heading: { - fontSize: 18, - fontWeight: '600', - marginBottom: 12, - }, - buttons: { - gap: 8, - }, -}); diff --git a/example/src/Screens/BottomTabsPreloadFlow.tsx b/example/src/Screens/BottomTabsPreloadFlow.tsx deleted file mode 100644 index 67cf98f7f9..0000000000 --- a/example/src/Screens/BottomTabsPreloadFlow.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - createBottomTabNavigator, - createBottomTabScreen, -} from '@react-navigation/bottom-tabs'; -import { Button, HeaderBackButton, Text } from '@react-navigation/elements'; -import { - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import { useEffect, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; - -const DetailsScreen = () => { - const navigation = useNavigation('BottomTabsPreloadFlowDetails'); - const route = useRoute('BottomTabsPreloadFlowDetails'); - - const [isPreloaded] = useState( - useNavigationState('BottomTabsPreloadFlowDetails', (state) => - state.preloadedRouteKeys.includes(route.key) - ) - ); - - const [loadingCountdown, setLoadingCountdown] = useState(3); - - useEffect(() => { - if (loadingCountdown === 0) { - return; - } - - const timer = setTimeout( - () => setLoadingCountdown(loadingCountdown - 1), - 1000 - ); - - return () => clearTimeout(timer); - }, [loadingCountdown]); - - return ( - - - {loadingCountdown > 0 && loadingCountdown} - - - {loadingCountdown === 0 ? 'Loaded!' : 'Loading...'} - - {isPreloaded ? 'Preloaded' : 'Fresh'} - - - ); -}; - -const HomeScreen = () => { - const navigation = useNavigation('BottomTabsPreloadFlowHome'); - - const [isReady, setIsReady] = useState(false); - - return ( - - - {isReady ? 'Details is preloaded!' : 'Details is not preloaded yet.'} - - - - - ); -}; - -const BottomTabsPreloadNavigator = createBottomTabNavigator({ - screenOptions: ({ navigation }) => ({ - headerShown: true, - headerLeft: (props) => ( - - ), - }), - screens: { - BottomTabsPreloadFlowHome: createBottomTabScreen({ - screen: HomeScreen, - linking: '', - options: { - title: 'Bottom Tabs Preload Flow', - }, - }), - BottomTabsPreloadFlowDetails: createBottomTabScreen({ - screen: DetailsScreen, - }), - }, -}); - -export const BottomTabsPreloadFlow = { - screen: BottomTabsPreloadNavigator, - title: 'Bottom Tabs - Preload Flow', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - padding: 16, - justifyContent: 'center', - }, - button: { - margin: 8, - }, - text: { - textAlign: 'center', - margin: 8, - }, - countdown: { - fontSize: 24, - minHeight: 32, - }, -}); diff --git a/example/src/Screens/ComponentsButton.tsx b/example/src/Screens/ComponentsButton.tsx deleted file mode 100644 index ffee5fbb51..0000000000 --- a/example/src/Screens/ComponentsButton.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { Button, type Icon, Text } from '@react-navigation/elements'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import iconHeart from '../../assets/icons/heart.png'; - -const heartIcon = Platform.select({ - ios: { type: 'sfSymbol', name: 'heart' }, - android: { type: 'materialSymbol', name: 'favorite' }, - default: { type: 'image', source: iconHeart }, -}); - -export function ComponentsButton() { - return ( - - Variants - - - - - - - Custom color - - - - - - - Disabled - - - - - - - Icons - - - - - - - - ); -} - -ComponentsButton.title = 'Components - Button'; -ComponentsButton.options = { headerShown: true }; -ComponentsButton.linking = {}; - -const SPACING = 16; - -const styles = StyleSheet.create({ - content: { - padding: SPACING, - gap: SPACING, - }, - heading: { - fontSize: 16, - fontWeight: '600', - }, - row: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING / 2, - }, -}); diff --git a/example/src/Screens/ComponentsHeaders.tsx b/example/src/Screens/ComponentsHeaders.tsx deleted file mode 100644 index d6dc968aab..0000000000 --- a/example/src/Screens/ComponentsHeaders.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { Header, HeaderButton } from '@react-navigation/elements'; -import { type StaticScreenProps, useTheme } from '@react-navigation/native'; -import { - type ColorValue, - ImageBackground, - ScrollView, - StyleSheet, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import balloons from '../../assets/misc/balloons.jpg'; - -export function ComponentsHeaders(_: StaticScreenProps<{}>) { - const { colors } = useTheme(); - const insets = useSafeAreaInsets(); - - const headerRight = ({ - tintColor, - }: { - tintColor?: ColorValue | undefined; - }) => ( - <> - - - - - - - - ); - - return ( - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
( - - )} - headerStatusBarHeight={0} - /> - - - ); -} - -ComponentsHeaders.title = 'Components - Headers'; -ComponentsHeaders.linking = {}; - -const SPACING = 16; - -const styles = StyleSheet.create({ - content: { - gap: SPACING, - }, - imageBackground: { - marginLeft: -SPACING, - marginRight: -SPACING, - marginTop: -SPACING / 2, - paddingTop: SPACING / 2, - paddingHorizontal: SPACING, - gap: SPACING, - }, -}); diff --git a/example/src/Screens/ComponentsLink.tsx b/example/src/Screens/ComponentsLink.tsx deleted file mode 100644 index a45ca3c87c..0000000000 --- a/example/src/Screens/ComponentsLink.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { - CommonActions, - Link, - StackActions, - useNavigation, - useRoute, -} from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - Go to albums - - - Replace with albums - - - - - - - - - - - {Platform.OS === 'web' && ( - - )} - -
- - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - Go to Article - - - - - - - ); -}; - -const ComponentsLinkNavigator = createStackNavigator({ - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { title: 'Albums' }, - }), - }, -}); - -export const ComponentsLink = { - screen: ComponentsLinkNavigator, - title: 'Components - Link', -}; - -const styles = StyleSheet.create({ - buttons: { - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/ComponentsMaterialSymbols.tsx b/example/src/Screens/ComponentsMaterialSymbols.tsx deleted file mode 100644 index f008495508..0000000000 --- a/example/src/Screens/ComponentsMaterialSymbols.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { - MaterialSymbol, - type MaterialSymbolProps, - type StaticScreenProps, - useNavigation, - useTheme, -} from '@react-navigation/native'; -import * as React from 'react'; -import { FlatList, Image, Platform, StyleSheet, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { MATERIAL_SYMBOL_NAMES } from '../material-symbol-names'; -import { SegmentedPicker } from '../Shared/SegmentedPicker'; - -const COLUMN_COUNT = 4; -const ICON_SIZE = 32; -const ICON_PADDING_VERTICAL = 8; -const ICON_NAME_MARGIN_TOP = 4; -const ICON_NAME_FONT_SIZE = 10; -const ROW_HEIGHT = - ICON_SIZE + - (ICON_PADDING_VERTICAL + ICON_NAME_MARGIN_TOP + ICON_NAME_FONT_SIZE) * 2; - -export function ComponentsMaterialSymbols(_: StaticScreenProps<{}>) { - const navigation = useNavigation('ComponentsMaterialSymbols'); - const { colors } = useTheme(); - const insets = useSafeAreaInsets(); - - const [query, setQuery] = React.useState(''); - const [image, setImage] = React.useState(false); - - React.useEffect(() => { - navigation.setOptions({ - headerSearchBarOptions: { - placeholder: 'Search icons...', - onChange: (event) => { - const text = event.nativeEvent.text; - - setQuery(text); - }, - }, - }); - }, [navigation]); - - const rows = React.useMemo(() => { - const icons = query.trim() - ? MATERIAL_SYMBOL_NAMES.filter((name) => - name.toLowerCase().includes(query.toLowerCase()) - ) - : MATERIAL_SYMBOL_NAMES; - - // FlatList has performance issues with `numColumns` - // So we use a single-column list with rows containing multiple icons - const result: MaterialSymbolProps['name'][][] = []; - - for (let i = 0; i < icons.length; i += COLUMN_COUNT) { - result.push(icons.slice(i, i + COLUMN_COUNT)); - } - - return result; - }, [query]); - - if (Platform.OS !== 'android') { - return ( - - - MaterialSymbol is only available on Android - - - ); - } - - return ( - - setImage(value === 'image')} - /> - - } - renderItem={({ item }) => ( - - )} - keyExtractor={(item) => item.join(',')} - contentContainerStyle={{ - backgroundColor: colors.background, - paddingLeft: insets.left, - paddingRight: insets.right, - paddingBottom: insets.bottom, - }} - getItemLayout={(_, index) => ({ - length: ROW_HEIGHT, - offset: ROW_HEIGHT * index, - index, - })} - initialNumToRender={10} - maxToRenderPerBatch={10} - windowSize={5} - /> - ); -} - -const MaterialSymbolRow = React.memo(function MaterialSymbolRow({ - items, - image, -}: { - items: MaterialSymbolProps['name'][]; - image: boolean; -}) { - const { colors } = useTheme(); - - return ( - - {items.map((item) => ( - - {image ? ( - - ) : ( - - )} - - {item} - - - ))} - - ); -}); - -ComponentsMaterialSymbols.title = 'Components - Material Symbols'; -ComponentsMaterialSymbols.options = { headerShown: true }; -ComponentsMaterialSymbols.linking = {}; - -const styles = StyleSheet.create({ - centered: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - padding: 8, - borderBottomWidth: StyleSheet.hairlineWidth, - }, - row: { - flexDirection: 'row', - }, - item: { - flex: 1, - alignItems: 'center', - paddingBottom: ICON_PADDING_VERTICAL, - paddingTop: - ICON_PADDING_VERTICAL + ICON_NAME_FONT_SIZE + ICON_NAME_MARGIN_TOP, - minWidth: 80, - borderWidth: StyleSheet.hairlineWidth, - }, - iconName: { - marginTop: ICON_NAME_MARGIN_TOP, - fontSize: ICON_NAME_FONT_SIZE, - textAlign: 'center', - opacity: 0.8, - maxWidth: 70, - }, -}); diff --git a/example/src/Screens/ComponentsSFSymbols.tsx b/example/src/Screens/ComponentsSFSymbols.tsx deleted file mode 100644 index dbc88029ff..0000000000 --- a/example/src/Screens/ComponentsSFSymbols.tsx +++ /dev/null @@ -1,952 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { Color } from '@react-navigation/elements/internal'; -import { - SFSymbol, - type SFSymbolProps, - type StaticScreenProps, - useNavigation, - useTheme, -} from '@react-navigation/native'; -import * as React from 'react'; -import { - FlatList, - Platform, - Pressable, - ScrollView, - StyleSheet, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { SF_SYMBOL_NAMES } from '../sf-symbol-names'; - -type SFSymbolWeight = NonNullable; -type SFSymbolScale = NonNullable; -type SFSymbolRenderingMode = NonNullable; -type SFSymbolEffectName = Extract, string>; -type SFSymbolEffect = SFSymbolProps['effect']; -type SFSymbolEffectConfig = Exclude, string>; -type SFSymbolEffectRepeat = SFSymbolEffectConfig['repeat']; -type SFSymbolContentTransition = SFSymbolProps['contentTransition']; -type SFSymbolVariableValueMode = NonNullable< - SFSymbolProps['variableValueMode'] ->; -type SFSymbolColorRenderingMode = NonNullable< - SFSymbolProps['colorRenderingMode'] ->; -type SFSymbolVariableValue = 0 | 0.25 | 0.5 | 0.75 | 1; -type SFSymbolEffectRepeatMode = - | 'default' - | 'continuous' - | 'nonRepeating' - | 'periodic'; -type SFSymbolEffectScope = 'byLayer' | 'wholeSymbol' | 'individually'; -type SFSymbolEffectDirection = - | 'none' - | 'up' - | 'down' - | 'left' - | 'right' - | 'forward' - | 'backward' - | 'clockwise' - | 'counterClockwise'; -type SFSymbolEffectSpeed = 0.5 | 1 | 2; -type SFSymbolBreatheVariant = 'plain' | 'pulse'; -type SFSymbolInactiveLayers = 'none' | 'hide' | 'dim'; -type SFSymbolDrawDirection = 'default' | 'reversed' | 'nonReversed'; -type SFSymbolContentTransitionName = Extract< - NonNullable, - string ->; -type SFSymbolContentTransitionVariant = 'downUp' | 'upUp' | 'offUp'; -type SFSymbolContentTransitionScope = 'byLayer' | 'wholeSymbol'; - -type Choice = { - label: string; - value: T; -}; - -const COLUMN_COUNT = 4; -const ICON_SIZE = 32; -const ICON_PADDING_VERTICAL = 8; -const ICON_NAME_MARGIN_TOP = 4; -const ICON_NAME_FONT_SIZE = 10; -const ROW_HEIGHT = - ICON_SIZE + - (ICON_PADDING_VERTICAL + ICON_NAME_MARGIN_TOP + ICON_NAME_FONT_SIZE) * 2; - -const WEIGHTS: Choice[] = [ - { label: 'Thin', value: 'thin' }, - { label: 'Light', value: 'light' }, - { label: 'Regular', value: 'regular' }, - { label: 'Bold', value: 'bold' }, -]; - -const SCALES: Choice[] = [ - { label: 'Small', value: 'small' }, - { label: 'Medium', value: 'medium' }, - { label: 'Large', value: 'large' }, -]; - -const MODES: Choice[] = [ - { label: 'Mono', value: 'monochrome' }, - { label: 'Hierarchy', value: 'hierarchical' }, - { label: 'Palette', value: 'palette' }, - { label: 'Multi', value: 'multicolor' }, -]; - -const VARIABLE_VALUES: Choice[] = [ - { label: '0', value: 0 }, - { label: '.25', value: 0.25 }, - { label: '.5', value: 0.5 }, - { label: '.75', value: 0.75 }, - { label: '1', value: 1 }, -]; - -const VARIABLE_VALUE_MODES: Choice[] = [ - { label: 'Auto', value: 'automatic' }, - { label: 'Color', value: 'color' }, - { label: 'Draw', value: 'draw' }, -]; - -const COLOR_RENDERING_MODES: Choice[] = [ - { label: 'Auto', value: 'automatic' }, - { label: 'Flat', value: 'flat' }, - { label: 'Gradient', value: 'gradient' }, -]; - -const EFFECTS: Choice[] = [ - { label: 'None', value: 'none' }, - { label: 'Bounce', value: 'bounce' }, - { label: 'Pulse', value: 'pulse' }, - { label: 'Appear', value: 'appear' }, - { label: 'Disappear', value: 'disappear' }, - { label: 'Variable', value: 'variableColor' }, - { label: 'Scale', value: 'scale' }, - { label: 'Breathe', value: 'breathe' }, - { label: 'Wiggle', value: 'wiggle' }, - { label: 'Rotate', value: 'rotate' }, - { label: 'Draw on', value: 'drawOn' }, - { label: 'Draw off', value: 'drawOff' }, -]; - -const EFFECT_REPEATS: Choice[] = [ - { label: 'Default', value: 'default' }, - { label: 'Continuous', value: 'continuous' }, - { label: 'Once', value: 'nonRepeating' }, - { label: 'Periodic', value: 'periodic' }, -]; - -const EFFECT_SCOPES: Choice[] = [ - { label: 'Layers', value: 'byLayer' }, - { label: 'Whole', value: 'wholeSymbol' }, - { label: 'Each', value: 'individually' }, -]; - -const EFFECT_DIRECTIONS: Choice[] = [ - { label: 'None', value: 'none' }, - { label: 'Up', value: 'up' }, - { label: 'Down', value: 'down' }, - { label: 'Left', value: 'left' }, - { label: 'Right', value: 'right' }, - { label: 'Forward', value: 'forward' }, - { label: 'Back', value: 'backward' }, - { label: 'CW', value: 'clockwise' }, - { label: 'CCW', value: 'counterClockwise' }, -]; - -const EFFECT_SPEEDS: Choice[] = [ - { label: '.5x', value: 0.5 }, - { label: '1x', value: 1 }, - { label: '2x', value: 2 }, -]; - -const BREATHE_VARIANTS: Choice[] = [ - { label: 'Plain', value: 'plain' }, - { label: 'Pulse', value: 'pulse' }, -]; - -const INACTIVE_LAYERS: Choice[] = [ - { label: 'Default', value: 'none' }, - { label: 'Hide', value: 'hide' }, - { label: 'Dim', value: 'dim' }, -]; - -const DRAW_DIRECTIONS: Choice[] = [ - { label: 'Default', value: 'default' }, - { label: 'Forward', value: 'nonReversed' }, - { label: 'Reverse', value: 'reversed' }, -]; - -const CONTENT_TRANSITIONS: Choice[] = [ - { label: 'None', value: 'none' }, - { label: 'Auto', value: 'automatic' }, - { label: 'Replace', value: 'replace' }, -]; - -const CONTENT_TRANSITION_VARIANTS: Choice[] = - [ - { label: 'Down/up', value: 'downUp' }, - { label: 'Up/up', value: 'upUp' }, - { label: 'Off/up', value: 'offUp' }, - ]; - -const CONTENT_TRANSITION_SCOPES: Choice[] = [ - { label: 'Layers', value: 'byLayer' }, - { label: 'Whole', value: 'wholeSymbol' }, -]; - -const MAGIC_MODES: Choice[] = [ - { label: 'Replace', value: false }, - { label: 'Magic', value: true }, -]; - -const PREVIEW_SYMBOLS: [SFSymbolProps['name'], ...SFSymbolProps['name'][]] = [ - 'bell', - 'bell.badge', - 'wifi', - 'wifi.slash', - 'heart', - 'heart.fill', -]; - -function getEffectRepeat(mode: SFSymbolEffectRepeatMode): SFSymbolEffectRepeat { - switch (mode) { - case 'continuous': - return 'continuous'; - case 'nonRepeating': - return 'nonRepeating'; - case 'periodic': - return { count: 3, delay: 0.5 }; - default: - return undefined; - } -} - -function getVerticalDirection(direction: SFSymbolEffectDirection) { - return direction === 'up' || direction === 'down' ? direction : undefined; -} - -function getRotateDirection(direction: SFSymbolEffectDirection) { - return direction === 'clockwise' || direction === 'counterClockwise' - ? direction - : undefined; -} - -function getWiggleDirection(direction: SFSymbolEffectDirection) { - return direction === 'none' ? undefined : direction; -} - -function getPreviewSymbol(index: number) { - return PREVIEW_SYMBOLS[index % PREVIEW_SYMBOLS.length] ?? PREVIEW_SYMBOLS[0]; -} - -export function ComponentsSFSymbols(_: StaticScreenProps<{}>) { - const navigation = useNavigation('ComponentsSFSymbols'); - const { colors } = useTheme(); - const insets = useSafeAreaInsets(); - - const [query, setQuery] = React.useState(''); - const [weight, setWeight] = React.useState('regular'); - const [scale, setScale] = React.useState('medium'); - const [renderingMode, setRenderingMode] = - React.useState('monochrome'); - const [variableValue, setVariableValue] = - React.useState(1); - const [variableValueMode, setVariableValueMode] = - React.useState('automatic'); - const [colorRenderingMode, setColorRenderingMode] = - React.useState('automatic'); - const [effect, setEffect] = React.useState( - 'none' - ); - const [effectRepeat, setEffectRepeat] = - React.useState('default'); - const [effectScope, setEffectScope] = - React.useState('byLayer'); - const [effectDirection, setEffectDirection] = - React.useState('none'); - const [effectSpeed, setEffectSpeed] = React.useState(1); - const [breatheVariant, setBreatheVariant] = - React.useState('plain'); - const [inactiveLayers, setInactiveLayers] = - React.useState('none'); - const [drawDirection, setDrawDirection] = - React.useState('default'); - const [contentTransition, setContentTransition] = React.useState< - SFSymbolContentTransitionName | 'none' - >('none'); - const [contentTransitionVariant, setContentTransitionVariant] = - React.useState('downUp'); - const [contentTransitionScope, setContentTransitionScope] = - React.useState('byLayer'); - const [contentTransitionMagic, setContentTransitionMagic] = - React.useState(false); - const [previewIndex, setPreviewIndex] = React.useState(0); - const [expanded, setExpanded] = React.useState(false); - - React.useEffect(() => { - navigation.setOptions({ - headerSearchBarOptions: { - placeholder: 'Search icons...', - onChange: (event) => { - const text = event.nativeEvent.text; - - setQuery(text); - }, - }, - }); - }, [navigation]); - - const rows = React.useMemo(() => { - const search = query.trim().toLowerCase(); - const icons = search - ? SF_SYMBOL_NAMES.filter((name) => name.toLowerCase().includes(search)) - : SF_SYMBOL_NAMES; - - // FlatList has performance issues with `numColumns` - // So we use a single-column list with rows containing multiple icons - const result: SFSymbolProps['name'][][] = []; - - for (let i = 0; i < icons.length; i += COLUMN_COUNT) { - result.push(icons.slice(i, i + COLUMN_COUNT)); - } - - return result; - }, [query]); - - const symbolColors = React.useMemo( - () => - renderingMode === 'palette' || renderingMode === 'hierarchical' - ? { - primary: colors.primary, - secondary: colors.notification, - tertiary: colors.text, - } - : undefined, - [renderingMode, colors.primary, colors.notification, colors.text] - ); - - const selectedEffect = React.useMemo(() => { - if (effect === 'none') { - return undefined; - } - - const repeat = getEffectRepeat(effectRepeat); - const scopedScope = effectScope === 'wholeSymbol' ? effectScope : 'byLayer'; - const base = { - speed: effectSpeed, - repeat, - }; - - switch (effect) { - case 'bounce': - case 'appear': - case 'disappear': - case 'scale': - return { - ...base, - type: effect, - scope: scopedScope, - direction: getVerticalDirection(effectDirection), - }; - - case 'pulse': - return { - ...base, - type: 'pulse', - scope: scopedScope, - }; - - case 'breathe': - return { - ...base, - type: 'breathe', - scope: scopedScope, - variant: breatheVariant, - }; - - case 'wiggle': - return { - ...base, - type: 'wiggle', - scope: scopedScope, - direction: getWiggleDirection(effectDirection), - }; - - case 'rotate': - return { - ...base, - type: 'rotate', - scope: scopedScope, - direction: getRotateDirection(effectDirection), - }; - - case 'drawOn': - return { - ...base, - type: 'drawOn', - scope: effectScope, - }; - - case 'drawOff': - return { - ...base, - type: 'drawOff', - scope: effectScope, - drawDirection: - drawDirection === 'default' ? undefined : drawDirection, - }; - - case 'variableColor': - return { - ...base, - type: 'variableColor', - cumulative: true, - inactiveLayers: - inactiveLayers === 'none' ? undefined : inactiveLayers, - }; - } - }, [ - breatheVariant, - drawDirection, - effect, - effectDirection, - effectRepeat, - effectScope, - effectSpeed, - inactiveLayers, - ]); - - const selectedContentTransition = - React.useMemo(() => { - switch (contentTransition) { - case 'automatic': - return { - type: 'automatic', - speed: effectSpeed, - }; - - case 'replace': - return { - type: 'replace', - speed: effectSpeed, - variant: contentTransitionVariant, - scope: contentTransitionScope, - magic: contentTransitionMagic, - }; - - default: - return undefined; - } - }, [ - contentTransition, - contentTransitionMagic, - contentTransitionScope, - contentTransitionVariant, - effectSpeed, - ]); - - if (Platform.OS !== 'ios') { - return ( - - - SFSymbol is only available on iOS - - - ); - } - - return ( - - setExpanded((v) => !v)} - style={styles.headerToggle} - > - Customization - - - {expanded && ( - -
- - - - - - -
-
- - {effect !== 'none' && ( - <> - - - - - {effect === 'breathe' && ( - - )} - {effect === 'variableColor' && ( - - )} - {effect === 'drawOff' && ( - - )} - - )} -
-
- - {contentTransition === 'replace' && ( - <> - - - - - )} -
-
- )} - - setPreviewIndex((index) => (index + 1) % PREVIEW_SYMBOLS.length) - } - /> - - } - renderItem={({ item }) => ( - - )} - keyExtractor={(item) => item.join(',')} - contentContainerStyle={{ - backgroundColor: colors.background, - paddingLeft: insets.left, - paddingRight: insets.right, - paddingBottom: insets.bottom, - }} - getItemLayout={(_, index) => ({ - length: ROW_HEIGHT, - offset: ROW_HEIGHT * index, - index, - })} - initialNumToRender={10} - maxToRenderPerBatch={10} - windowSize={5} - /> - ); -} - -const SFSymbolRow = React.memo(function SFSymbolRow({ - items, - weight, - scale, - renderingMode, - colors: symbolColors, - variableValue, - variableValueMode, - colorRenderingMode, - effect, - contentTransition, -}: { - items: SFSymbolProps['name'][]; - weight: SFSymbolWeight; - scale: SFSymbolScale; - renderingMode: SFSymbolRenderingMode; - colors?: SFSymbolProps['colors']; - variableValue: SFSymbolVariableValue; - variableValueMode: SFSymbolVariableValueMode; - colorRenderingMode: SFSymbolColorRenderingMode; - effect: SFSymbolEffect; - contentTransition: SFSymbolContentTransition; -}) { - const { colors } = useTheme(); - - return ( - - {items.map((item) => ( - - - - {item} - - - ))} - - ); -}); - -function SFSymbolPreview({ - name, - nextName, - weight, - scale, - renderingMode, - colors, - variableValue, - variableValueMode, - colorRenderingMode, - effect, - contentTransition, - onPress, -}: { - name: SFSymbolProps['name']; - nextName: SFSymbolProps['name']; - weight: SFSymbolWeight; - scale: SFSymbolScale; - renderingMode: SFSymbolRenderingMode; - colors?: SFSymbolProps['colors']; - variableValue: SFSymbolVariableValue; - variableValueMode: SFSymbolVariableValueMode; - colorRenderingMode: SFSymbolColorRenderingMode; - effect: SFSymbolEffect; - contentTransition: SFSymbolContentTransition; - onPress: () => void; -}) { - const { colors: themeColors } = useTheme(); - - return ( - - - - - - - {name} - - - {nextName} - - - - - ); -} - -function Section({ - title, - children, -}: { - title: string; - children: React.ReactNode; -}) { - const { colors } = useTheme(); - - return ( - - {title} - {children} - - ); -} - -function ControlGroup({ - label, - choices, - value, - onValueChange, -}: { - label: string; - choices: Choice[]; - value: T; - onValueChange: (value: T) => void; -}) { - const { colors, fonts } = useTheme(); - - return ( - - {label} - - {choices.map((option) => { - const selected = option.value === value; - - return ( - onValueChange(option.value)} - style={[ - styles.chip, - { - borderColor: colors.border, - }, - selected && { backgroundColor: colors.primary }, - ]} - > - - {option.label} - - - ); - })} - - - ); -} - -ComponentsSFSymbols.title = 'Components - SF Symbols'; -ComponentsSFSymbols.options = { headerShown: true }; -ComponentsSFSymbols.linking = {}; - -const styles = StyleSheet.create({ - centered: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - headerToggle: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 8, - paddingHorizontal: 16, - }, - pickerColumn: { - gap: 20, - paddingVertical: 8, - }, - section: { - gap: 12, - }, - sectionTitle: { - fontSize: 11, - fontWeight: '600', - letterSpacing: 0.5, - textTransform: 'uppercase', - opacity: 0.5, - paddingHorizontal: 16, - }, - controlGroup: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - }, - controlLabel: { - width: 84, - fontSize: 12, - opacity: 0.7, - marginLeft: 16, - }, - chipScroll: { - flex: 1, - }, - chipRow: { - flexDirection: 'row', - gap: 6, - }, - chip: { - borderRadius: 16, - borderWidth: StyleSheet.hairlineWidth, - paddingVertical: 6, - paddingHorizontal: 10, - }, - chipText: { - fontSize: 12, - }, - preview: { - paddingVertical: 8, - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - gap: 12, - borderTopWidth: StyleSheet.hairlineWidth, - }, - previewIcon: { - width: 56, - height: 56, - alignItems: 'center', - justifyContent: 'center', - }, - previewText: { - flex: 1, - minWidth: 0, - }, - previewNext: { - fontSize: 12, - opacity: 0.6, - }, - row: { - flexDirection: 'row', - }, - item: { - flex: 1, - alignItems: 'center', - paddingBottom: ICON_PADDING_VERTICAL, - paddingTop: - ICON_PADDING_VERTICAL + ICON_NAME_FONT_SIZE + ICON_NAME_MARGIN_TOP, - minWidth: 80, - borderWidth: StyleSheet.hairlineWidth, - }, - name: { - marginTop: ICON_NAME_MARGIN_TOP, - fontSize: ICON_NAME_FONT_SIZE, - textAlign: 'center', - opacity: 0.8, - maxWidth: 70, - }, -}); diff --git a/example/src/Screens/DrawerMasterDetail.tsx b/example/src/Screens/DrawerMasterDetail.tsx deleted file mode 100644 index a6f942331c..0000000000 --- a/example/src/Screens/DrawerMasterDetail.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { - createDrawerNavigator, - createDrawerScreen, - DrawerContent, - type DrawerContentComponentProps, -} from '@react-navigation/drawer'; -import { - Header as NavigationHeader, - HeaderBackButton, -} from '@react-navigation/elements'; -import { useNavigation } from '@react-navigation/native'; -import { useWindowDimensions } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const useIsLargeScreen = () => { - const dimensions = useWindowDimensions(); - - return dimensions.width > 414; -}; - -const Header = ({ - onGoBack, - title, -}: { - onGoBack: () => void; - title: string; -}) => { - const isLargeScreen = useIsLargeScreen(); - - return ( - - } - /> - ); -}; - -const ArticleScreen = () => { - const navigation = useNavigation('Article'); - - return ( - <> -
navigation.toggleDrawer()} /> -
- - ); -}; - -const NewsFeedScreen = () => { - const navigation = useNavigation('NewsFeed'); - - return ( - <> -
navigation.toggleDrawer()} /> - - - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - <> -
navigation.toggleDrawer()} /> - - - ); -}; - -const CustomDrawerContent = (props: DrawerContentComponentProps) => { - const navigation = useNavigation(); - - return ( - <> - ( - navigation.goBack()} /> - )} - /> - - - ); -}; - -const DrawerMasterDetailNavigator = createDrawerNavigator({ - backBehavior: 'none', - defaultStatus: 'open', - drawerContent: (props) => , - screenOptions: { - headerShown: false, - drawerContentContainerStyle: { paddingTop: 4 }, - overlayStyle: { backgroundColor: 'transparent' }, - }, - screens: { - Article: createDrawerScreen({ - screen: ArticleScreen, - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createDrawerScreen({ - screen: NewsFeedScreen, - options: { title: 'Feed' }, - initialParams: { date: 0 }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Albums: createDrawerScreen({ - screen: AlbumsScreen, - options: { title: 'Albums' }, - linking: 'albums', - }), - }, -}).with(({ Navigator }) => { - const isLargeScreen = useIsLargeScreen(); - - return ( - - ); -}); - -export const DrawerMasterDetail = { - screen: DrawerMasterDetailNavigator, - title: 'Drawer - Master Detail', -}; diff --git a/example/src/Screens/LibrariesDrawerLayout.tsx b/example/src/Screens/LibrariesDrawerLayout.tsx deleted file mode 100644 index 55b7c2beb5..0000000000 --- a/example/src/Screens/LibrariesDrawerLayout.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useActionSheet } from '@expo/react-native-action-sheet'; -import { Button } from '@react-navigation/elements'; -import { - type PathConfig, - type StaticScreenProps, - useLocale, - useNavigation, - useRoute, - useTheme, -} from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { Drawer } from 'react-native-drawer-layout'; - -import { DrawerProgress } from '../Shared/DrawerProgress'; - -const DRAWER_TYPES = ['front', 'back', 'slide', 'permanent'] as const; - -type Params = - | { - type: (typeof DRAWER_TYPES)[number]; - position: 'left' | 'right'; - } - | undefined; - -const linking = { - path: 'drawer-view', - parse: { - type: (type: string) => { - if ( - type === 'front' || - type === 'back' || - type === 'slide' || - type === 'permanent' - ) { - return type; - } - - return 'front'; - }, - position: (position: string) => { - if (position === 'left' || position === 'right') { - return position; - } - - return 'left'; - }, - }, -} satisfies PathConfig; - -export function LibrariesDrawerLayout(_: StaticScreenProps) { - const { showActionSheetWithOptions } = useActionSheet(); - const { colors } = useTheme(); - const { direction } = useLocale(); - - const navigation = useNavigation('LibrariesDrawerLayout'); - const route = useRoute('LibrariesDrawerLayout'); - - const { type: drawerType = 'front', position: drawerPosition = 'left' } = - route.params ?? {}; - - const [open, setOpen] = React.useState(false); - - return ( - setOpen(true)} - onClose={() => setOpen(false)} - direction={direction} - drawerType={drawerType} - drawerPosition={drawerPosition} - renderDrawerContent={() => { - return ( - - - - ); - }} - > - - - - - - - - - - ); -} - -LibrariesDrawerLayout.title = 'Libraries - Drawer Layout'; -LibrariesDrawerLayout.linking = linking; -LibrariesDrawerLayout.options = { - headerShown: true, -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 8, - }, - buttons: { - gap: 8, - }, -}); diff --git a/example/src/Screens/LibrariesTabView.tsx b/example/src/Screens/LibrariesTabView.tsx deleted file mode 100644 index 23ea9ceb73..0000000000 --- a/example/src/Screens/LibrariesTabView.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useNavigation } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { ScrollView } from 'react-native'; - -import { Divider } from '../Shared/Divider'; -import { ListItem } from '../Shared/LIstItem'; -import { fromEntries } from '../utilities'; -import { AutoWidthTabBar } from './TabView/AutoWidthTabBar'; -import { Coverflow } from './TabView/Coverflow'; -import { CustomIndicator } from './TabView/CustomIndicator'; -import { CustomTabBar } from './TabView/CustomTabBar'; -import { ScrollableTabBar } from './TabView/ScrollableTabBar'; -import { ScrollAdapter } from './TabView/ScrollAdapter'; -import { TabBarIcon } from './TabView/TabBarIcon'; - -const EXAMPLE_SCREENS = { - ScrollableTabBar, - AutoWidthTabBar, - TabBarIcon, - CustomIndicator, - CustomTabBar, - Coverflow, - ScrollAdapter, -} as const; - -const EXAMPLE_SCREEN_NAMES = Object.keys( - EXAMPLE_SCREENS -) as (keyof typeof EXAMPLE_SCREENS)[]; - -const ExampleListScreen = () => { - const navigation = useNavigation('ExampleList'); - - return ( - - {EXAMPLE_SCREEN_NAMES.map((name) => ( - - { - navigation.navigate(name); - }} - /> - - - ))} - - ); -}; - -const TabViewStackNavigator = createStackNavigator({ - screenOptions: { headerMode: 'screen', cardStyle: { flex: 1 } }, - screens: { - ExampleList: createStackScreen({ - screen: ExampleListScreen, - options: { title: 'TabView Examples' }, - linking: '', - }), - ...fromEntries( - EXAMPLE_SCREEN_NAMES.map((name) => [ - name, - createStackScreen({ - screen: EXAMPLE_SCREENS[name], - options: EXAMPLE_SCREENS[name].options, - }), - ]) - ), - }, -}); - -export const LibrariesTabView = { - screen: TabViewStackNavigator, - title: 'Libraries - Tab View', -}; diff --git a/example/src/Screens/Loaders.tsx b/example/src/Screens/Loaders.tsx deleted file mode 100644 index 0130291596..0000000000 --- a/example/src/Screens/Loaders.tsx +++ /dev/null @@ -1,357 +0,0 @@ -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { Button, type Icon, Text } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { - QueryClient, - QueryClientProvider, - queryOptions, - useSuspenseQueries, - useSuspenseQuery, -} from '@tanstack/react-query'; -import * as React from 'react'; -import { useEffect } from 'react'; -import { - type ImageSourcePropType, - Platform, - StyleSheet, - View, -} from 'react-native'; - -import iconBookOpen from '../../assets/icons/book-open.png'; -import iconPawPrint from '../../assets/icons/paw-print.png'; -import iconSwords from '../../assets/icons/swords.png'; -import brachiosaurus from '../../assets/showcase/dinos/brachiosaurus.png'; -import spinosaurus from '../../assets/showcase/dinos/spinosaurus.png'; -import stegosaurus from '../../assets/showcase/dinos/stegosaurus.png'; -import triceratops from '../../assets/showcase/dinos/triceratops.png'; -import tyrannosaurusRex from '../../assets/showcase/dinos/tyrannosaurus-rex.png'; -import velociraptor from '../../assets/showcase/dinos/velociraptor.png'; -import { ErrorBoundary } from '../Shared/ErrorBoundary'; - -const queryClient = new QueryClient(); - -let shouldFailNextLoad = false; - -type Dino = { - id: string; - name: string; - description: string; - image: ImageSourcePropType; -}; - -const DINOS: Dino[] = [ - { - id: '1', - name: 'Tyrannosaurus rex', - description: - 'One of the largest land predators ever discovered, with bone-crushing jaws and powerful senses. It lived in western North America during the late Cretaceous.', - image: tyrannosaurusRex, - }, - { - id: '2', - name: 'Triceratops', - description: - 'An iconic three-horned dinosaur with a massive bony frill. One of the last non-avian dinosaurs, it lived alongside Tyrannosaurus in late Cretaceous North America.', - image: triceratops, - }, - { - id: '3', - name: 'Velociraptor', - description: - 'A small dromaeosaur from late Cretaceous Mongolia. It likely had a feathered covering and was much smaller than its popular movie depiction.', - image: velociraptor, - }, - { - id: '4', - name: 'Stegosaurus', - description: - 'Recognizable by its double row of plates and spiked tail. Although it had a small brain for its body size, it was closer to the size of a plum than the old "walnut-sized brain" myth suggests.', - image: stegosaurus, - }, - { - id: '5', - name: 'Brachiosaurus', - description: - 'A towering sauropod with front legs longer than its hind legs, giving it a giraffe-like posture. Full-body reconstructions rely partly on its close relative Giraffatitan because no complete Brachiosaurus skeleton is known.', - image: brachiosaurus, - }, - { - id: '6', - name: 'Spinosaurus', - description: - 'A huge spinosaurid with a distinctive sail on its back and a long, crocodile-like snout. Evidence suggests it spent much of its time in and around water and fed heavily on fish.', - image: spinosaurus, - }, -]; - -const TEAM_DINO_IDS = ['1', '2', '3', '4'] as const; -const BATTLE_DINO_IDS = ['5', '6'] as const; - -const dinoQuery = (id: string) => - queryOptions({ - queryKey: ['dino', id] as const, - queryFn: () => - new Promise((resolve, reject) => { - setTimeout( - () => { - if (shouldFailNextLoad) { - shouldFailNextLoad = false; - reject(new Error('Failed to load dinosaur.')); - } else { - const match = DINOS.find((item) => item.id === id); - - if (match) { - resolve(match); - } else { - reject(new Error('Dinosaur not found.')); - } - } - }, - shouldFailNextLoad ? 50 : id === '1' ? 10 : id === '2' ? 50 : 8000 - ); - }), - }); - -function DinoCatalogScreen() { - const navigation = useNavigation('DinoCatalog'); - - return ( - - - {DINOS.map((item) => ( - - ))} - - - - - ); -} - -function DinoDetailScreen() { - const route = useRoute('DinoDetail'); - - const { data } = useSuspenseQuery(dinoQuery(route.params.id)); - - return ( - - {data.name} - {data.description} - - ); -} - -function DinoTeamScreen() { - const data = useSuspenseQueries({ - queries: TEAM_DINO_IDS.map(dinoQuery), - }); - - return ( - - Current Team - - {data.map((item) => item.data.name).join(', ')} - - - - ); -} - -function DinoBattleScreen() { - const data = useSuspenseQueries({ - queries: BATTLE_DINO_IDS.map(dinoQuery), - }); - - return ( - - Battle - - {data.map((item) => item.data.name).join(' vs ')} - - - - ); -} - -function Layout({ children }: { children: React.ReactNode }) { - return ( - - - Loadingโ€ฆ - - } - > - {children} - - - ); -} - -function Provider({ children }: { children: React.ReactNode }) { - useEffect(() => { - return () => { - queryClient.clear(); - }; - }, []); - - return ( - {children} - ); -} - -const LoaderTabs = createBottomTabNavigator({ - screenOptions: { - lazy: true, - }, - screens: { - DinoList: { - screen: DinoCatalogScreen, - options: { - title: 'Catalog', - tabBarIcon: Platform.select({ - ios: { type: 'sfSymbol', name: 'book' }, - android: { type: 'materialSymbol', name: 'menu_book' }, - default: { type: 'image', source: iconBookOpen }, - }), - }, - }, - DinoTeam: { - screen: DinoTeamScreen, - options: { - title: 'Team', - tabBarIcon: Platform.select({ - ios: { type: 'sfSymbol', name: 'pawprint' }, - android: { type: 'materialSymbol', name: 'pets' }, - default: { type: 'image', source: iconPawPrint }, - }), - }, - UNSTABLE_loader: async () => { - await Promise.all( - TEAM_DINO_IDS.map((id) => queryClient.ensureQueryData(dinoQuery(id))) - ); - }, - }, - DinoBattle: { - screen: DinoBattleScreen, - layout: ({ children }) => {children}, - options: { - title: 'Battle', - tabBarIcon: Platform.select({ - ios: { type: 'sfSymbol', name: 'flame' }, - android: { type: 'materialSymbol', name: 'swords' }, - default: { type: 'image', source: iconSwords }, - }), - }, - UNSTABLE_loader: async () => { - await Promise.all( - BATTLE_DINO_IDS.map((id) => - queryClient.ensureQueryData(dinoQuery(id)) - ) - ); - }, - }, - }, -}); - -const LoaderStack = createNativeStackNavigator({ - layout: ({ children }) => {children}, - screenLayout: ({ children }) => {children}, - screens: { - DinoCatalog: { - screen: LoaderTabs, - options: { - title: 'Dinos', - }, - }, - DinoDetail: createNativeStackScreen({ - screen: DinoDetailScreen, - linking: { - path: 'dino/:id', - }, - options: { - title: '', - headerTransparent: true, - }, - UNSTABLE_loader: async ({ params }) => { - await queryClient.ensureQueryData(dinoQuery(params.id)); - }, - }), - }, -}); - -export const Loaders = { - screen: LoaderStack, - title: 'Misc - Loaders', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - padding: 16, - justifyContent: 'center', - alignItems: 'center', - }, - heading: { - fontSize: 20, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 8, - }, - description: { - textAlign: 'center', - marginBottom: 16, - lineHeight: 22, - }, - buttons: { - gap: 12, - alignItems: 'center', - }, - button: { - minWidth: 220, - }, -}); diff --git a/example/src/Screens/MaterialTopTabs.tsx b/example/src/Screens/MaterialTopTabs.tsx deleted file mode 100644 index 3350719e29..0000000000 --- a/example/src/Screens/MaterialTopTabs.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { - createMaterialTopTabNavigator, - createMaterialTopTabScreen, -} from '@react-navigation/material-top-tabs'; - -import { Albums } from '../Shared/Albums'; -import { Chat } from '../Shared/Chat'; -import { Contacts } from '../Shared/Contacts'; - -const ChatScreen = () => ; - -const MaterialTopTabsNavigator = createMaterialTopTabNavigator({ - screens: { - Chat: createMaterialTopTabScreen({ - screen: ChatScreen, - options: { title: 'Chat' }, - }), - Contacts: createMaterialTopTabScreen({ - screen: Contacts, - options: { title: 'Contacts' }, - }), - Albums: createMaterialTopTabScreen({ - screen: Albums, - options: { title: 'Albums' }, - }), - }, -}); - -export const MaterialTopTabsBasic = { - screen: MaterialTopTabsNavigator, - title: 'Material Top Tabs - Basic', - options: { - headerShown: true, - cardStyle: { flex: 1 }, - }, -}; diff --git a/example/src/Screens/NativeBottomTabs.tsx b/example/src/Screens/NativeBottomTabs.tsx deleted file mode 100644 index d8eba044e7..0000000000 --- a/example/src/Screens/NativeBottomTabs.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { - createBottomTabNavigator, - createBottomTabScreen, -} from '@react-navigation/bottom-tabs'; -import { - Button, - getHeaderTitle, - Header, - HeaderButton, - type Icon, - useHeaderHeight, -} from '@react-navigation/elements'; -import { - type StaticScreenProps, - useIsFocused, - useNavigation, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { BlurView } from 'expo-blur'; -import { StatusBar } from 'expo-status-bar'; -import { Alert, Platform, ScrollView, StyleSheet, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import iconBookUser from '../../assets/icons/book-user.png'; -import iconHeart from '../../assets/icons/heart.png'; -import iconListMusic from '../../assets/icons/list-music.png'; -import iconMusic from '../../assets/icons/music.png'; -import iconNewspaper from '../../assets/icons/newspaper.png'; -import { Albums } from '../Shared/Albums'; -import { Contacts } from '../Shared/Contacts'; -import { MiniPlayer } from '../Shared/MiniPlayer'; -import { NativeStack } from './NativeStack'; - -function ContactsScreen(_: StaticScreenProps<{ count: number }>) { - const insets = useSafeAreaInsets(); - - return ( - - ); -} - -function AlbumsScreen() { - const navigation = useNavigation(); - const headerHeight = useHeaderHeight(); - const insets = useSafeAreaInsets(); - const isFocused = useIsFocused(); - - return ( - <> - {isFocused && } - - - - - - - - - ); -} - -const FavoritesStack = createNativeStackNavigator({ - screens: { - Favorites: createNativeStackScreen({ - screen: () => null, - options: { - title: 'Favorites', - headerSearchBarOptions: { - placeholder: 'Search Favorites', - }, - }, - }), - }, -}); - -let i = 1; - -const NativeBottomTabsNavigator = createBottomTabNavigator({ - screens: { - TabStack: createBottomTabScreen({ - screen: NativeStack.screen, - options: { - popToTopOnBlur: true, - title: 'Article', - headerRight: ({ tintColor }) => ( - Alert.alert('Favorite button pressed')}> - - - ), - tabBarIcon: { - type: 'image', - source: iconNewspaper, - }, - tabBarMinimizeBehavior: 'onScrollDown', - tabBarControllerMode: 'tabSidebar', - }, - linking: 'stack', - }), - TabContacts: createBottomTabScreen({ - screen: ContactsScreen, - initialParams: { count: i }, - options: ({ route }) => ({ - title: 'Contacts', - tabBarIcon: Platform.select({ - ios: { - type: 'sfSymbol', - name: 'person.2', - }, - android: { - type: 'materialSymbol', - name: 'contacts', - }, - default: { - type: 'image', - source: iconBookUser, - }, - }), - tabBarBadge: route.params?.count, - }), - linking: 'contacts', - }), - TabAlbums: createBottomTabScreen({ - screen: AlbumsScreen, - options: () => { - return { - title: 'Albums', - header: ({ options, route }) => ( -
- ), - headerTintColor: '#fff', - headerTransparent: true, - headerBackground: () => ( - - ), - tabBarIcon: ({ focused }) => ({ - type: 'image', - source: focused ? iconListMusic : iconMusic, - }), - tabBarActiveTintColor: - Platform.OS !== 'ios' ? 'rgba(255, 255, 255, 0.9)' : undefined, - tabBarInactiveTintColor: 'rgba(255, 255, 255, 0.7)', - tabBarStyle: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - borderTopColor: 'transparent', - }, - tabBarMinimizeBehavior: 'onScrollDown', - bottomAccessory: ({ placement }) => ( - - ), - }; - }, - linking: 'albums', - }), - TabFavorites: createBottomTabScreen({ - screen: FavoritesStack, - options: { - title: 'Favorites', - tabBarSystemItem: 'search', - tabBarLabel: 'Favorites', - tabBarIcon: { - type: 'image', - source: iconHeart, - }, - }, - }), - }, -}); - -export const NativeBottomTabs = { - screen: NativeBottomTabsNavigator, - title: 'Native Bottom Tabs - Basic', -}; - -const styles = StyleSheet.create({ - headerRight: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - marginEnd: 16, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, -}); diff --git a/example/src/Screens/NativeBottomTabsCustomTabBar.tsx b/example/src/Screens/NativeBottomTabsCustomTabBar.tsx deleted file mode 100644 index b6f247a67d..0000000000 --- a/example/src/Screens/NativeBottomTabsCustomTabBar.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { - BottomTabBar, - createBottomTabNavigator, - createBottomTabScreen, -} from '@react-navigation/bottom-tabs'; -import { - Button, - getHeaderTitle, - Header, - type Icon, - useHeaderHeight, -} from '@react-navigation/elements'; -import { - type StaticScreenProps, - useIsFocused, - useNavigation, -} from '@react-navigation/native'; -import { BlurView } from 'expo-blur'; -import { StatusBar } from 'expo-status-bar'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import iconBookUser from '../../assets/icons/book-user.png'; -import iconListMusic from '../../assets/icons/list-music.png'; -import iconMusic from '../../assets/icons/music.png'; -import iconNewspaper from '../../assets/icons/newspaper.png'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { Contacts } from '../Shared/Contacts'; -import { MiniPlayer } from '../Shared/MiniPlayer'; - -function ArticleScreen() { - const insets = useSafeAreaInsets(); - const navigation = useNavigation('TabArticle'); - - return ( - - - - - -
- - ); -} - -function ContactsScreen(_: StaticScreenProps<{ count: number }>) { - const insets = useSafeAreaInsets(); - - return ; -} - -function AlbumsScreen() { - const navigation = useNavigation(); - const headerHeight = useHeaderHeight(); - const insets = useSafeAreaInsets(); - const isFocused = useIsFocused(); - - return ( - <> - {isFocused && } - - - - - - - - - ); -} - -let i = 1; - -const NativeBottomTabsCustomNavigator = createBottomTabNavigator({ - tabBar: (props) => , - screenOptions: { - tabBarPosition: 'left', - }, - screens: { - TabArticle: createBottomTabScreen({ - screen: ArticleScreen, - options: { - title: 'Article', - tabBarButtonTestID: 'article', - tabBarIcon: { - type: 'image', - source: iconNewspaper, - }, - }, - }), - TabContacts: createBottomTabScreen({ - screen: ContactsScreen, - initialParams: { count: i }, - options: ({ route }) => ({ - title: 'Contacts', - tabBarButtonTestID: 'contacts', - tabBarIcon: Platform.select({ - ios: { - type: 'sfSymbol', - name: 'person.2', - }, - android: { - type: 'materialSymbol', - name: 'contacts', - }, - default: { - type: 'image', - source: iconBookUser, - }, - }), - tabBarBadge: route.params?.count, - }), - linking: 'contacts', - }), - TabAlbums: createBottomTabScreen({ - screen: AlbumsScreen, - options: () => { - return { - title: 'Albums', - tabBarButtonTestID: 'albums', - header: ({ options, route }) => ( -
- ), - headerTintColor: '#fff', - headerTransparent: true, - headerBackground: () => ( - - ), - tabBarIcon: ({ focused }) => ({ - type: 'image', - source: focused ? iconListMusic : iconMusic, - }), - tabBarInactiveTintColor: 'rgba(255, 255, 255, 0.7)', - tabBarStyle: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', - borderTopColor: 'transparent', - }, - tabBarPosition: 'bottom', - bottomAccessory: ({ placement }) => ( - - ), - }; - }, - linking: 'albums', - }), - }, -}); - -export const NativeBottomTabsCustomTabBar = { - screen: NativeBottomTabsCustomNavigator, - title: 'Native Bottom Tabs - Custom Tab Bar', -}; - -const styles = StyleSheet.create({ - headerRight: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - marginEnd: 16, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, -}); diff --git a/example/src/Screens/NativeStack.tsx b/example/src/Screens/NativeStack.tsx deleted file mode 100644 index 061567e014..0000000000 --- a/example/src/Screens/NativeStack.tsx +++ /dev/null @@ -1,470 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { - Button, - HeaderButton, - Text, - useHeaderHeight, -} from '@react-navigation/elements'; -import { useNavigation, useRoute, useTheme } from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, - type NativeStackHeaderItem, - useAnimatedHeaderHeight, -} from '@react-navigation/native-stack'; -import * as React from 'react'; -import { - Alert, - Animated, - Image, - Platform, - ScrollView, - StyleSheet, - View, -} from 'react-native'; - -import messageCircle from '../../assets/icons/message-circle.png'; -import userRoundPlus from '../../assets/icons/user-round-plus.png'; -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { Contacts } from '../Shared/Contacts'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const navigation = useNavigation('Article'); - const route = useRoute('Article'); - - return ( - - - - - - - - -
- - - - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - - - - ); -}; - -const ContactsScreen = () => { - const navigation = useNavigation('Contacts'); - - const [query, setQuery] = React.useState(''); - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerSearchBarOptions: { - placeholder: 'Filter contacts', - placement: 'inline', - onChange: (e) => { - setQuery(e.nativeEvent.text); - }, - }, - }); - }, [navigation]); - - return ( - - - - - } - /> - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - const headerHeight = useHeaderHeight(); - - return ( - - - - - - - - - - - ); -}; - -const HeaderHeightView = ({ hasOffset }: { hasOffset: boolean }) => { - const { colors } = useTheme(); - - const animatedHeaderHeight = useAnimatedHeaderHeight(); - const headerHeight = useHeaderHeight(); - - return ( - - {headerHeight.toFixed(2)} - - ); -}; - -const NativeStackNavigator = createNativeStackNavigator({ - screens: { - Article: createNativeStackScreen({ - screen: ArticleScreen, - options: ({ route, navigation }) => { - const leftItems: NativeStackHeaderItem[] = [ - { - type: 'button', - label: 'Back', - onPress: () => navigation.goBack(), - }, - ]; - - const rightItems: NativeStackHeaderItem[] = [ - { - type: 'button', - label: 'Follow', - icon: { - type: 'image', - source: userRoundPlus, - }, - onPress: () => Alert.alert('Follow button pressed'), - }, - { - type: 'button', - label: 'Favorite', - icon: { - type: 'sfSymbol', - name: 'heart', - }, - onPress: () => Alert.alert('Favorite button pressed'), - }, - { - type: 'menu', - label: 'Options', - icon: { - type: 'sfSymbol', - name: 'ellipsis', - }, - badge: { - value: 3, - }, - menu: { - title: 'Article options', - items: [ - { - type: 'action', - label: 'Share', - icon: { - type: 'sfSymbol', - name: 'square.and.arrow.up', - }, - onPress: () => Alert.alert('Share pressed'), - }, - { - type: 'action', - label: 'Delete', - icon: { - type: 'sfSymbol', - name: 'trash', - }, - destructive: true, - onPress: () => Alert.alert('Delete pressed'), - }, - { - type: 'action', - label: 'Report', - icon: { - type: 'sfSymbol', - name: 'flag', - }, - destructive: true, - onPress: () => Alert.alert('Report pressed'), - }, - { - type: 'action', - label: 'Message', - icon: { - type: 'image', - source: Platform.select({ - // Avoid calling `Image.resolveAssetSource` on Web to prevent crash - get ios() { - return { - ...Image.resolveAssetSource(messageCircle), - scale: - Image.resolveAssetSource(messageCircle).scale * 1.4, - }; - }, - default: messageCircle, - }), - }, - onPress: () => Alert.alert('Message pressed'), - }, - { - type: 'submenu', - label: 'View history', - icon: { - type: 'sfSymbol', - name: 'clock', - }, - items: [ - { - type: 'action', - label: 'Version 1.0', - icon: { - type: 'sfSymbol', - name: 'checkmark', - }, - onPress: () => Alert.alert('View version 1.0'), - }, - { - type: 'action', - label: 'Version 0.9', - onPress: () => Alert.alert('View version 0.9'), - }, - ], - }, - { - label: 'Theme', - inline: true, - destructive: true, - icon: { type: 'sfSymbol', name: 'star' }, - type: 'submenu', - items: [ - { - label: 'Auto', - state: 'mixed', - type: 'action', - description: 'Adapts to system settings', - onPress: () => Alert.alert('Sub Action 1 pressed'), - destructive: true, - keepsMenuPresented: true, - discoverabilityLabel: 'Sub Action 1', - }, - { - label: 'Light', - type: 'action', - onPress: () => Alert.alert('Light theme selected'), - }, - { - label: 'Dark', - type: 'action', - onPress: () => Alert.alert('Dark theme selected'), - }, - ], - }, - { - label: 'Text Size', - inline: true, - layout: 'palette', - destructive: true, - type: 'submenu', - items: [ - { - label: 'Small', - icon: { - type: 'sfSymbol', - name: 'textformat.size.smaller', - }, - type: 'action', - onPress: () => Alert.alert('Small text selected'), - }, - { - label: 'Medium', - state: 'on', - icon: { type: 'sfSymbol', name: 'textformat.size' }, - type: 'action', - onPress: () => Alert.alert('Medium text selected'), - }, - { - label: 'Large', - icon: { - type: 'sfSymbol', - name: 'textformat.size.larger', - }, - type: 'action', - onPress: () => Alert.alert('Large text selected'), - }, - ], - }, - ], - }, - }, - { - type: 'custom', - element: ( - Alert.alert('Info pressed')}> - - - ), - }, - ]; - - return { - title: `Article by ${route.params?.author ?? 'Unknown'}`, - headerLargeTitleEnabled: true, - headerLargeTitleShadowVisible: false, - headerRight: ({ tintColor }) => ( - Alert.alert('Favorite button pressed')} - > - - - ), - unstable_headerLeftItems: () => leftItems, - unstable_headerRightItems: () => rightItems, - }; - }, - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createNativeStackScreen({ - screen: NewsFeedScreen, - options: { - title: 'Feed', - fullScreenGestureEnabled: true, - }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Contacts: createNativeStackScreen({ - screen: ContactsScreen, - options: { - headerSearchBarOptions: { - placeholder: 'Filter contacts', - }, - }, - linking: 'contacts', - }), - Albums: createNativeStackScreen({ - screen: AlbumsScreen, - options: ({ theme }) => ({ - title: 'Albums', - presentation: 'modal', - headerTransparent: true, - headerBlurEffect: 'light', - headerStyle: { - backgroundColor: Platform.select({ - // Add a background color since Android doesn't support blur effect - android: theme.colors.card, - default: 'transparent', - }), - }, - }), - linking: 'albums', - }), - }, -}); - -export const NativeStack = { - screen: NativeStackNavigator, - title: 'Native Stack - Basic', - options: { - gestureEnabled: false, - }, -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, - headerHeight: { - position: 'absolute', - top: 0, - right: 0, - padding: 12, - borderWidth: StyleSheet.hairlineWidth, - borderRightWidth: 0, - borderTopWidth: 0, - borderBottomLeftRadius: 3, - }, -}); diff --git a/example/src/Screens/NativeStackCardModal.tsx b/example/src/Screens/NativeStackCardModal.tsx deleted file mode 100644 index 7c8d5b6d48..0000000000 --- a/example/src/Screens/NativeStackCardModal.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - - -
- - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - - ); -}; - -const NativeStackCardModalNavigator = createNativeStackNavigator({ - screens: { - Article: createNativeStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - Albums: createNativeStackScreen({ - screen: AlbumsScreen, - options: { - title: 'Albums', - presentation: 'modal', - }, - }), - }, -}); - -export const NativeStackCardModal = { - screen: NativeStackCardModalNavigator, - title: 'Native Stack - Card + Modal', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/NativeStackFormSheet.tsx b/example/src/Screens/NativeStackFormSheet.tsx deleted file mode 100644 index 9fd365a4b1..0000000000 --- a/example/src/Screens/NativeStackFormSheet.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import type { StaticScreenProps } from '@react-navigation/native'; -import { useNavigation, useTheme } from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, - type NativeStackNavigationOptions, -} from '@react-navigation/native-stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { entries, fromEntries } from '../utilities'; - -export type FormSheetConfig = { - name: string; - options: NativeStackNavigationOptions; - params?: { - paragraphs?: number; - }; -}; - -const FORM_SHEETS = { - FormSheetViewFitToContents: { - name: 'Fit To Contents', - options: { - sheetAllowedDetents: 'fitToContents', - sheetCornerRadius: 8, - }, - }, - FormSheetViewHeightsArray: { - name: 'Height Steps', - options: { - sheetAllowedDetents: [0.24, 0.41, 0.8], - sheetCornerRadius: 8, - }, - params: { - paragraphs: 4, - }, - }, - FormSheetViewNoRoundedCorners: { - name: 'No Rounded Corners', - options: { - sheetAllowedDetents: 'fitToContents', - sheetCornerRadius: 0, - }, - }, - FormSheetViewWithGrabber: { - name: 'Grabber Visible', - options: { - sheetAllowedDetents: 'fitToContents', - sheetCornerRadius: 8, - sheetGrabberVisible: true, - }, - }, - FormSheetViewDimming: { - name: 'Custom Dimming', - options: { - sheetAllowedDetents: [0.24, 0.41, 0.8], - sheetCornerRadius: 8, - sheetLargestUndimmedDetentIndex: 0, - }, - params: { - paragraphs: 4, - }, - }, - FormSheetViewInitialDetent: { - name: 'Initial Detent', - options: { - sheetAllowedDetents: [0.24, 0.41, 0.8], - sheetCornerRadius: 8, - sheetInitialDetentIndex: 'last', - }, - params: { - paragraphs: 4, - }, - }, - FormSheetViewScrollingExpand: { - name: 'Lock Expanding With Scrolling', - options: { - sheetAllowedDetents: [0.41], - sheetCornerRadius: 8, - sheetExpandsWhenScrolledToEdge: false, - }, - params: { - paragraphs: 10, - }, - }, - FormSheetViewNoSheetElevation: { - name: 'Sheet Elevation: 0', - options: { - sheetAllowedDetents: 'fitToContents', - sheetCornerRadius: 8, - sheetElevation: 0, - sheetLargestUndimmedDetentIndex: 0, - }, - }, - FormSheetViewWithSheetElevation: { - name: 'Sheet Elevation: 48', - options: { - sheetAllowedDetents: 'fitToContents', - sheetCornerRadius: 8, - sheetElevation: 48, - sheetLargestUndimmedDetentIndex: 0, - }, - }, -} satisfies Record; - -function Main() { - const navigation = useNavigation('Main'); - - return ( - - - {entries(FORM_SHEETS).map(([key, value]) => ( - - ))} - - - - - ); -} - -function FormSheetView({ - route, -}: StaticScreenProps<{ paragraphs?: number } | undefined>) { - const navigation = useNavigation(); - const paragraph = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ' + - 'aliquam justo at dolor condimentum tincidunt. Proin velit nibh, ' + - 'efficitur non metus a, egestas semper nisi. Proin egestas neque ' + - 'sollicitudin magna semper, id ultrices urna egestas. Aliquam vitae ' + - 'libero vestibulum, ultrices mauris vel, facilisis turpis. Morbi ac ' + - 'volutpat ipsum.'; - - const paragraphs = - route.params?.paragraphs === undefined ? 1 : route.params.paragraphs; - - const textContent = Array(paragraphs).fill(paragraph).join('\n\n'); - - return ( - - - - - - - - {textContent} - - - - ); -} - -const UnsupportedScreen = () => { - const { colors } = useTheme(); - - return ( - - - Form Sheet presentation is only available on Android and iOS - - - ); -}; - -const isFormSheetSupported = Platform.OS === 'android' || Platform.OS === 'ios'; - -const NativeStackFormSheetNavigator = createNativeStackNavigator({ - groups: { - Supported: { - if: () => isFormSheetSupported, - screens: { - Main: createNativeStackScreen({ - screen: Main, - options: { - title: 'Form Sheet', - }, - }), - ...fromEntries( - entries(FORM_SHEETS).map(([key, config]) => [ - key, - createNativeStackScreen({ - screen: FormSheetView, - options: { - presentation: 'formSheet', - headerShown: false, - ...config.options, - }, - }), - ]) - ), - }, - }, - Unsupported: { - if: () => !isFormSheetSupported, - screens: { - Unsupported: createNativeStackScreen({ - screen: UnsupportedScreen, - options: { - headerShown: false, - }, - }), - }, - }, - }, -}); - -export const NativeStackFormSheet = { - screen: NativeStackFormSheetNavigator, - title: 'Native Stack - Form Sheet', -}; - -const styles = StyleSheet.create({ - centered: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - buttons: { - flexDirection: 'column', - gap: 12, - padding: 12, - }, - closeButtonContainer: { - paddingTop: 20, - justifyContent: 'center', - }, - textContent: { - paddingTop: 10, - paddingBottom: 20, - paddingHorizontal: 20, - flex: 1, - }, -}); diff --git a/example/src/Screens/NativeStackHeaderCustomization.tsx b/example/src/Screens/NativeStackHeaderCustomization.tsx deleted file mode 100644 index 22783bfb8c..0000000000 --- a/example/src/Screens/NativeStackHeaderCustomization.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { - Button, - getHeaderTitle, - Header, - HeaderButton, -} from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { - Alert, - Image, - Platform, - ScrollView, - StyleSheet, - View, -} from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - return ( - - - - - - - - ); -}; - -const onPress = () => { - Alert.alert( - 'Never gonna give you up!', - 'Never gonna let you down! Never gonna run around and desert you!' - ); -}; - -const NativeStackHeaderCustomizationNavigator = createNativeStackNavigator({ - screens: { - Article: createNativeStackScreen({ - screen: ArticleScreen, - options: ({ route, navigation }) => ({ - title: `Article byyyy ${route.params?.author ?? 'Unknown'}`, - headerTintColor: 'white', - headerTitle: ({ tintColor }) => ( - - - - ), - headerLeft: ({ tintColor, canGoBack }) => - canGoBack ? ( - - - - ) : null, - headerRight: ({ tintColor }) => ( - - - - ), - headerBackground: () => ( - - ), - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createNativeStackScreen({ - screen: NewsFeedScreen, - options: { - title: 'Feed', - header: ({ options, route, back }) => ( -
- ), - }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Albums: createNativeStackScreen({ - screen: AlbumsScreen, - options: { - title: 'Albums', - headerTintColor: 'tomato', - headerStyle: { backgroundColor: 'papayawhip' }, - headerBackVisible: true, - headerLeft: ({ tintColor }) => ( - - - - ), - }, - }), - }, -}); - -export const NativeStackHeaderCustomization = { - screen: NativeStackHeaderCustomizationNavigator, - title: 'Native Stack - Header Customization', - options: { - gestureEnabled: false, - }, -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, - headerBackground: { - height: 'auto', - width: 'auto', - flex: 1, - }, -}); diff --git a/example/src/Screens/NativeStackPreloadFlow.tsx b/example/src/Screens/NativeStackPreloadFlow.tsx deleted file mode 100644 index 09fa7e000a..0000000000 --- a/example/src/Screens/NativeStackPreloadFlow.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import { useEffect, useRef, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; - -const DetailsScreen = () => { - const navigation = useNavigation('NativeStackPreloadFlowDetails'); - const route = useRoute('NativeStackPreloadFlowDetails'); - - const [isPreloaded] = useState( - useNavigationState('NativeStackPreloadFlowDetails', (state) => { - const index = state.routes.findIndex((r) => r.key === route.key); - - return ( - index > state.index && !state.retainedRouteKeys.includes(route.key) - ); - }) - ); - - const [loadingCountdown, setLoadingCountdown] = useState(3); - - useEffect(() => { - if (loadingCountdown === 0) { - return; - } - - const timer = setTimeout( - () => setLoadingCountdown(loadingCountdown - 1), - 1000 - ); - - return () => clearTimeout(timer); - }, [loadingCountdown]); - - return ( - - - {loadingCountdown > 0 && loadingCountdown} - - - {loadingCountdown === 0 ? 'Loaded!' : 'Loading...'} - - {isPreloaded ? 'Preloaded' : 'Fresh'} - - - ); -}; - -const HomeScreen = () => { - const navigation = useNavigation('NativeStackPreloadFlowHome'); - - const [isReady, setIsReady] = useState(false); - const timerRef = useRef | undefined>(undefined); - - useEffect(() => { - return navigation.addListener('blur', () => { - clearTimeout(timerRef.current); - setIsReady(false); - }); - }, [navigation]); - - return ( - - - {isReady ? 'Details is preloaded!' : 'Details is not preloaded yet.'} - - - - - ); -}; - -const NativeStackPreloadNavigator = createNativeStackNavigator({ - screens: { - NativeStackPreloadFlowHome: createNativeStackScreen({ - screen: HomeScreen, - linking: '', - options: { - title: 'Native Stack Preload Flow', - }, - }), - NativeStackPreloadFlowDetails: createNativeStackScreen({ - screen: DetailsScreen, - }), - }, -}); - -export const NativeStackPreloadFlow = { - screen: NativeStackPreloadNavigator, - title: 'Native Stack - Preload Flow', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - padding: 16, - justifyContent: 'center', - }, - button: { - margin: 8, - }, - text: { - textAlign: 'center', - margin: 8, - }, - countdown: { - fontSize: 24, - minHeight: 32, - }, -}); diff --git a/example/src/Screens/NativeStackPreventRemove.tsx b/example/src/Screens/NativeStackPreventRemove.tsx deleted file mode 100644 index e050b8c5da..0000000000 --- a/example/src/Screens/NativeStackPreventRemove.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { - CommonActions, - useNavigation, - usePreventRemove, - useRoute, - useTheme, -} from '@react-navigation/native'; -import { - createNativeStackNavigator, - createNativeStackScreen, -} from '@react-navigation/native-stack'; -import * as React from 'react'; -import { - Alert, - Platform, - ScrollView, - StyleSheet, - TextInput, - View, -} from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - -
- - ); -}; - -const InputScreen = () => { - const navigation = useNavigation('Input'); - const [text, setText] = React.useState(''); - const { colors } = useTheme(); - - const hasUnsavedChanges = Boolean(text); - - usePreventRemove(hasUnsavedChanges, ({ data }) => { - if (Platform.OS === 'web') { - const discard = confirm( - 'You have unsaved changes. Discard them and leave the screen?' - ); - if (discard) { - navigation.dispatch(data.action); - } - } else { - Alert.alert( - 'Discard changes?', - 'You have unsaved changes. Discard them and leave the screen?', - [ - { text: "Don't leave", style: 'cancel', onPress: () => {} }, - { - text: 'Discard', - style: 'destructive', - onPress: () => navigation.dispatch(data.action), - }, - ] - ); - } - }); - - return ( - - - - - - ); -}; - -const NativeStackPreventRemoveNavigator = createNativeStackNavigator({ - screens: { - Input: createNativeStackScreen({ - screen: InputScreen, - options: { - presentation: 'modal', - }, - }), - Article: createNativeStackScreen({ - screen: ArticleScreen, - linking: COMMON_LINKING_CONFIG.Article, - }), - }, -}); - -export const NativeStackPreventRemove = { - screen: NativeStackPreventRemoveNavigator, - title: 'Native Stack - Prevent Remove', -}; - -const styles = StyleSheet.create({ - content: { - flexGrow: 1, - gap: 12, - padding: 12, - }, - input: { - padding: 12, - borderRadius: 3, - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(0, 0, 0, 0.08)', - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/NavigatorLayout.tsx b/example/src/Screens/NavigatorLayout.tsx deleted file mode 100644 index 2a55e28b4c..0000000000 --- a/example/src/Screens/NavigatorLayout.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { - Button, - getDefaultHeaderHeight, - getHeaderTitle, - Text, - useFrameSize, -} from '@react-navigation/elements'; -import { - CommonActions, - useNavigation, - useRoute, - useTheme, -} from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, - type StackNavigatorProps, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { - Platform, - Pressable, - ScrollView, - StyleSheet, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const navigation = useNavigation('Article'); - const route = useRoute('Article'); - - return ( - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - ); -}; - -function BreadcrumbLayout({ - children, - state, - descriptors, - navigation, -}: Parameters>[0]) { - const { colors } = useTheme(); - - const insets = useSafeAreaInsets(); - const defaultHeaderHeight = useFrameSize((size) => - getDefaultHeaderHeight({ - landscape: size.width > size.height, - modalPresentation: false, - topInset: insets.top, - }) - ); - - return ( - - - {state.routes.map((route, i, self) => { - const descriptor = descriptors[route.key]; - - if (descriptor == null) { - throw new Error( - `Couldn't find a descriptor for route '${route.key}'.` - ); - } - - return ( - - { - navigation.dispatch((state) => { - return CommonActions.reset({ - ...state, - index: i, - routes: state.routes.slice(0, i + 1), - }); - }); - }} - > - - {getHeaderTitle(descriptor.options, route.name)} - - - {self.length - 1 !== i ? ( - โฏ - ) : null} - - ); - })} - - {children} - - ); -} - -const NavigatorLayoutNavigator = createStackNavigator({ - layout: (props) => , - screenOptions: { - headerShown: false, - }, - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createStackScreen({ - screen: NewsFeedScreen, - options: { title: 'Feed' }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { title: 'Albums' }, - linking: 'albums', - }), - }, -}); - -export const NavigatorLayout = { - screen: NavigatorLayoutNavigator, - title: 'Navigator Layout', -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, - breadcrumbs: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 8, - }, - title: { - fontSize: 16, - lineHeight: 16, - fontWeight: 'bold', - paddingVertical: 16, - paddingHorizontal: 8, - }, - arrow: { - fontSize: 14, - lineHeight: 14, - paddingVertical: 16, - opacity: 0.3, - }, -}); diff --git a/example/src/Screens/NotFound.tsx b/example/src/Screens/NotFound.tsx deleted file mode 100644 index 37735613da..0000000000 --- a/example/src/Screens/NotFound.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { StyleSheet, View } from 'react-native'; - -export const NotFound = () => { - const route = useRoute(); - const navigation = useNavigation(); - - return ( - - 404 Not Found ({route.path}) - - - ); -}; - -const styles = StyleSheet.create({ - title: { - fontSize: 36, - }, - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 8, - }, - button: { - margin: 24, - }, -}); diff --git a/example/src/Screens/ScreenLayout.tsx b/example/src/Screens/ScreenLayout.tsx deleted file mode 100644 index 32fa2c596b..0000000000 --- a/example/src/Screens/ScreenLayout.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { useNavigation } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { ScrollView, StyleSheet, View } from 'react-native'; - -const createPromise = () => - new Promise((resolve) => { - setTimeout(() => resolve(42), 3000); - }); - -type SuspenseContextType = { - promise: Promise; - refresh: () => void; -}; - -const SuspenseContext = React.createContext( - undefined -); - -const useSuspenseContext = () => { - const context = React.useContext(SuspenseContext); - - if (context === undefined) { - throw new Error('SuspenseContext is missing'); - } - - return context; -}; - -const SuspenseDemoScreen = () => { - const navigation = useNavigation('SuspenseDemo'); - - const { promise, refresh } = useSuspenseContext(); - - const [error, setError] = React.useState(null); - - React.use(promise); - - if (error) { - throw error; - } - - return ( - - - - - - - - ); -}; - -class ErrorBoundary extends React.Component< - { children: React.ReactNode }, - { hasError: boolean } -> { - static getDerivedStateFromError() { - return { hasError: true }; - } - - override state = { hasError: false }; - - override render() { - if (this.state.hasError) { - return ( - - Something went wrong - - - ); - } - - return this.props.children; - } -} - -const SuspenseDemoLayout = ({ children }: { children: React.ReactNode }) => { - const [promise, setPromise] = React.useState(createPromise); - - const value = React.useMemo( - () => ({ - promise, - refresh: () => setPromise(createPromise()), - }), - [promise] - ); - - return ( - - - - Loadingโ€ฆ - - } - > - {children} - - - - ); -}; - -const ScreenLayoutNavigator = createStackNavigator({ - screens: { - SuspenseDemo: createStackScreen({ - screen: SuspenseDemoScreen, - options: { title: 'Suspense & ErrorBoundary' }, - linking: 'suspense', - layout: ({ children }) => ( - {children} - ), - }), - }, -}); - -export const ScreenLayout = { - screen: ScreenLayoutNavigator, - title: 'Screen Layout', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, - fallback: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - - text: { - textAlign: 'center', - margin: 12, - }, -}); diff --git a/example/src/Screens/StackBasic.tsx b/example/src/Screens/StackBasic.tsx deleted file mode 100644 index a0daad07d3..0000000000 --- a/example/src/Screens/StackBasic.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { Contacts } from '../Shared/Contacts'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const navigation = useNavigation('Article'); - const route = useRoute('Article'); - - return ( - - - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - - ); -}; - -const ContactsScreen = () => { - const navigation = useNavigation('Contacts'); - const [query, setQuery] = React.useState(''); - - React.useLayoutEffect(() => { - navigation.setOptions({ - headerSearchBarOptions: { - placeholder: 'Filter contacts', - onChange: (e) => { - setQuery(e.nativeEvent.text); - }, - }, - }); - }, [navigation]); - - return ( - - - - - } - /> - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - ); -}; - -const StackBasicNavigator = createStackNavigator({ - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - getId: ({ params }) => params?.author, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createStackScreen({ - screen: NewsFeedScreen, - options: { title: 'Feed' }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Contacts: createStackScreen({ - screen: ContactsScreen, - options: { - headerSearchBarOptions: { - placeholder: 'Filter contacts', - }, - }, - linking: 'contacts', - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { title: 'Albums' }, - linking: 'albums', - }), - }, -}); - -export const StackBasic = { - screen: StackBasicNavigator, - title: 'Stack - Basic', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, -}); diff --git a/example/src/Screens/StackCardModal.tsx b/example/src/Screens/StackCardModal.tsx deleted file mode 100644 index 0932f5b414..0000000000 --- a/example/src/Screens/StackCardModal.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - - -
- - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - - ); -}; - -const StackCardModalNavigator = createStackNavigator({ - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { - title: 'Albums', - presentation: 'modal', - }, - }), - }, -}); - -export const StackCardModal = { - screen: StackCardModalNavigator, - title: 'Stack - Card + Modal', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/StackFloatScreenHeader.tsx b/example/src/Screens/StackFloatScreenHeader.tsx deleted file mode 100644 index fb1dc943ff..0000000000 --- a/example/src/Screens/StackFloatScreenHeader.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - ); -}; - -const StackFloatScreenHeaderNavigator = createStackNavigator({ - initialRouteName: 'Article', - screens: { - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { - animation: 'slide_from_bottom', - headerMode: 'screen', - title: 'Albums', - }, - }), - }, - groups: { - FloatHeader: { - screenOptions: { - animation: 'slide_from_right', - headerMode: 'float', - }, - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createStackScreen({ - screen: NewsFeedScreen, - options: { title: 'Feed' }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - }, - }, - }, -}); - -export const StackFloatScreenHeader = { - screen: StackFloatScreenHeaderNavigator, - title: 'Stack - Float + Screen Header', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/StackHeaderCustomization.tsx b/example/src/Screens/StackHeaderCustomization.tsx deleted file mode 100644 index ac413769c5..0000000000 --- a/example/src/Screens/StackHeaderCustomization.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { - Button, - getHeaderTitle, - Header as ElementsHeader, - HeaderButton, - useHeaderHeight, -} from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, - Header as StackHeader, - type StackHeaderProps, -} from '@react-navigation/stack'; -import { BlurView } from 'expo-blur'; -import * as React from 'react'; -import { - Alert, - Animated, - Platform, - ScrollView, - StyleSheet, - View, -} from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const navigation = useNavigation('Article'); - const route = useRoute('Article'); - - return ( - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - const headerHeight = useHeaderHeight(); - - return ( - - - - - - - - ); -}; - -function CustomHeader(props: StackHeaderProps) { - const { current, next } = props.progress; - - const progress = Animated.add(current, next || 0); - const opacity = progress.interpolate({ - inputRange: [0, 1, 2], - outputRange: [0, 1, 0], - }); - - return ( - <> - - - Why hello there, pardner! - - - ); -} - -const StackHeaderCustomizationNavigator = createStackNavigator({ - screenOptions: { headerMode: 'float' }, - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author}`, - header: (props) => , - headerTintColor: '#fff', - headerStyle: { backgroundColor: '#ff005d' }, - headerBackButtonDisplayMode: 'minimal', - headerBackIcon: ({ tintColor }) => ( - - ), - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createStackScreen({ - screen: NewsFeedScreen, - options: { - title: 'Feed', - headerMode: 'screen', - header: ({ options, route, back }) => ( - - ), - }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: ({ theme }) => ({ - title: 'Albums', - headerBackTitle: 'Back', - headerTransparent: true, - headerBackground: () => ( - - ), - }), - linking: 'albums', - }), - }, -}).with(({ Navigator }) => { - const [headerTitleCentered, setHeaderTitleCentered] = React.useState(true); - - return ( - { - switch (route.name) { - case 'Article': - return { - headerTitleAlign: headerTitleCentered ? 'center' : 'left', - headerRight: ({ tintColor }) => ( - { - setHeaderTitleCentered((centered) => !centered); - Alert.alert( - 'Never gonna give you up!', - 'Never gonna let you down! Never gonna run around and desert you!' - ); - }} - > - - - ), - }; - default: - return {}; - } - }} - /> - ); -}); - -export const StackHeaderCustomization = { - screen: StackHeaderCustomizationNavigator, - title: 'Stack - Header Customization', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, - banner: { - textAlign: 'center', - color: 'tomato', - backgroundColor: 'papayawhip', - padding: 4, - }, -}); diff --git a/example/src/Screens/StackModal.tsx b/example/src/Screens/StackModal.tsx deleted file mode 100644 index 9e97ca5a07..0000000000 --- a/example/src/Screens/StackModal.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { useNavigation, useRoute } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Albums } from '../Shared/Albums'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - -
- - ); -}; - -const AlbumsScreen = () => { - const navigation = useNavigation('Albums'); - - return ( - - - - - - - - ); -}; - -const StackModalNavigator = createStackNavigator({ - screenOptions: { presentation: 'modal' }, - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - options: ({ route }) => ({ - title: `Article by ${route.params?.author ?? 'Unknown'}`, - }), - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - Albums: createStackScreen({ - screen: AlbumsScreen, - options: { title: 'Albums' }, - }), - }, -}); - -export const StackModal = { - screen: StackModalNavigator, - title: 'Stack - Modal', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/StackPreloadFlow.tsx b/example/src/Screens/StackPreloadFlow.tsx deleted file mode 100644 index 3f4d6c475a..0000000000 --- a/example/src/Screens/StackPreloadFlow.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import { useEffect, useRef, useState } from 'react'; -import { StyleSheet, View } from 'react-native'; - -const DetailsScreen = () => { - const navigation = useNavigation('StackPreloadFlowDetails'); - const route = useRoute('StackPreloadFlowDetails'); - - const [isPreloaded] = useState( - useNavigationState('StackPreloadFlowDetails', (state) => { - const index = state.routes.findIndex((r) => r.key === route.key); - - return ( - index > state.index && !state.retainedRouteKeys.includes(route.key) - ); - }) - ); - - const [loadingCountdown, setLoadingCountdown] = useState(3); - - useEffect(() => { - if (loadingCountdown === 0) { - return; - } - - const timer = setTimeout( - () => setLoadingCountdown(loadingCountdown - 1), - 1000 - ); - - return () => clearTimeout(timer); - }, [loadingCountdown]); - - return ( - - - {loadingCountdown > 0 && loadingCountdown} - - - {loadingCountdown === 0 ? 'Loaded!' : 'Loading...'} - - {isPreloaded ? 'Preloaded' : 'Fresh'} - - - ); -}; - -const HomeScreen = () => { - const navigation = useNavigation('StackPreloadFlowHome'); - - const [isReady, setIsReady] = useState(false); - const timerRef = useRef | undefined>(undefined); - - useEffect(() => { - return navigation.addListener('blur', () => { - clearTimeout(timerRef.current); - setIsReady(false); - }); - }, [navigation]); - - return ( - - - {isReady ? 'Details is preloaded!' : 'Details is not preloaded yet.'} - - - - - ); -}; - -const StackPreloadNavigator = createStackNavigator({ - screens: { - StackPreloadFlowHome: createStackScreen({ - screen: HomeScreen, - linking: '', - options: { - title: 'Stack Preload Flow', - }, - }), - StackPreloadFlowDetails: createStackScreen({ - screen: DetailsScreen, - linking: 'details', - }), - }, -}); - -export const StackPreloadFlow = { - screen: StackPreloadNavigator, - title: 'Stack - Preload Flow', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - padding: 16, - justifyContent: 'center', - }, - button: { - margin: 8, - }, - text: { - textAlign: 'center', - margin: 8, - }, - countdown: { - fontSize: 24, - minHeight: 32, - }, -}); diff --git a/example/src/Screens/StackPreventRemove.tsx b/example/src/Screens/StackPreventRemove.tsx deleted file mode 100644 index 77362c374f..0000000000 --- a/example/src/Screens/StackPreventRemove.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { Button } from '@react-navigation/elements'; -import { - CommonActions, - useNavigation, - useRoute, - useTheme, -} from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, -} from '@react-navigation/stack'; -import * as React from 'react'; -import { - Alert, - Platform, - ScrollView, - StyleSheet, - TextInput, - View, -} from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Article } from '../Shared/Article'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - -
- - ); -}; - -const InputScreen = () => { - const navigation = useNavigation('Input'); - const [text, setText] = React.useState(''); - const { colors } = useTheme(); - - const hasUnsavedChanges = Boolean(text); - - React.useEffect( - () => - navigation.addListener('beforeRemove', (e) => { - if (!hasUnsavedChanges) { - return; - } - - e.preventDefault(); - - if (Platform.OS === 'web') { - const discard = confirm( - 'You have unsaved changes. Discard them and leave the screen?' - ); - - if (discard) { - navigation.dispatch(e.data.action); - } - } else { - Alert.alert( - 'Discard changes?', - 'You have unsaved changes. Discard them and leave the screen?', - [ - { text: "Don't leave", style: 'cancel', onPress: () => {} }, - { - text: 'Discard', - style: 'destructive', - onPress: () => navigation.dispatch(e.data.action), - }, - ] - ); - } - }), - [hasUnsavedChanges, navigation] - ); - - return ( - - - - - - ); -}; - -const StackPreventRemoveNavigator = createStackNavigator({ - screens: { - Input: createStackScreen({ - screen: InputScreen, - }), - Article: createStackScreen({ - screen: ArticleScreen, - linking: COMMON_LINKING_CONFIG.Article, - }), - }, -}); - -export const StackPreventRemove = { - screen: StackPreventRemoveNavigator, - title: 'Stack - Prevent Remove', -}; - -const styles = StyleSheet.create({ - content: { - flex: 1, - gap: 12, - padding: 12, - }, - input: { - padding: 12, - borderRadius: 3, - borderWidth: StyleSheet.hairlineWidth, - borderColor: 'rgba(0, 0, 0, 0.08)', - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/StackRetain.tsx b/example/src/Screens/StackRetain.tsx deleted file mode 100644 index fccde47734..0000000000 --- a/example/src/Screens/StackRetain.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { - useNavigation, - useNavigationState, - useRoute, - useTheme, -} from '@react-navigation/native'; -import { createStackNavigator } from '@react-navigation/stack'; -import * as React from 'react'; -import { ScrollView, StyleSheet, View } from 'react-native'; - -const HomeScreen = () => { - const navigation = useNavigation('RetainHome'); - const { colors } = useTheme(); - - return ( - - - - ); -}; - -const CounterScreen = () => { - const [count, setCount] = React.useState(0); - const { colors } = useTheme(); - - const navigation = useNavigation('RetainCounter'); - - const route = useRoute('RetainCounter'); - const isRetained = useNavigationState('RetainCounter', (state) => - state.retainedRouteKeys.includes(route.key) - ); - - return ( - - - - {isRetained ? '๐Ÿ“Œ' : ''} - - - {count} - - - - - ); -}; - -const StackRetainNavigator = createStackNavigator({ - screens: { - RetainHome: { - screen: HomeScreen, - options: { - title: 'Home', - }, - }, - RetainCounter: { - screen: CounterScreen, - options: { - title: 'Counter', - headerBackTestID: 'Go back', - }, - }, - }, -}); - -export const StackRetain = { - screen: StackRetainNavigator, - title: 'Stack - Retain', -}; - -const styles = StyleSheet.create({ - screen: { - flex: 1, - }, - scrollContent: { - flexGrow: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 16, - }, -}); diff --git a/example/src/Screens/StackTransparentModal.tsx b/example/src/Screens/StackTransparentModal.tsx deleted file mode 100644 index 828394e55e..0000000000 --- a/example/src/Screens/StackTransparentModal.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { useNavigation, useRoute, useTheme } from '@react-navigation/native'; -import { - createStackNavigator, - createStackScreen, - useCardAnimation, -} from '@react-navigation/stack'; -import { - Animated, - Platform, - Pressable, - ScrollView, - StyleSheet, - View, -} from 'react-native'; - -import { COMMON_LINKING_CONFIG } from '../constants'; -import { Article } from '../Shared/Article'; -import { NewsFeed } from '../Shared/NewsFeed'; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const ArticleScreen = () => { - const route = useRoute('Article'); - const navigation = useNavigation('Article'); - - return ( - - - - - - -
- - ); -}; - -const NewsFeedScreen = () => { - const route = useRoute('NewsFeed'); - const navigation = useNavigation('NewsFeed'); - - return ( - - - - - - - - ); -}; - -const DialogScreen = () => { - const navigation = useNavigation('Dialog'); - const { colors } = useTheme(); - const { current } = useCardAnimation(); - - return ( - - - - Trivia - - 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. - - - - - ); -}; - -const StackTransparentModalNavigator = createStackNavigator({ - screens: { - Article: createStackScreen({ - screen: ArticleScreen, - initialParams: { author: 'Gandalf' }, - linking: COMMON_LINKING_CONFIG.Article, - }), - NewsFeed: createStackScreen({ - screen: NewsFeedScreen, - options: { presentation: 'modal' }, - linking: COMMON_LINKING_CONFIG.NewsFeed, - }), - Dialog: createStackScreen({ - screen: DialogScreen, - options: { - headerShown: false, - presentation: 'transparentModal', - }, - }), - }, -}); - -export const StackTransparentModal = { - screen: StackTransparentModalNavigator, - title: 'Stack - Transparent Modal', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - dialog: { - padding: 24, - gap: 24, - width: '90%', - maxWidth: 400, - borderRadius: 3, - }, - backdrop: { - backgroundColor: 'rgba(0, 0, 0, 0.6)', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, - close: { - alignSelf: 'flex-end', - }, -}); diff --git a/example/src/Screens/StandardNavigator/MyStackDynamic.tsx b/example/src/Screens/StandardNavigator/MyStackDynamic.tsx deleted file mode 100644 index dac2abbdc9..0000000000 --- a/example/src/Screens/StandardNavigator/MyStackDynamic.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { - type NavigatorScreenParams, - type PathConfig, - useNavigation, - useNavigationState, -} from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; - -import { - createMyStackNavigator, - type MyStackScreenProps, -} from './createMyStackNavigator'; - -type MyStackParamList = { - StandardDynamicHome: undefined; - StandardDynamicProfile: { user: string }; - StandardDynamicDetails: { section: string }; -}; - -const linking = { - screens: { - StandardDynamicHome: 'standard-dynamic-home', - StandardDynamicProfile: 'standard-dynamic-profile/:user', - StandardDynamicDetails: 'standard-dynamic-details/:section', - }, -} satisfies PathConfig>; - -const MyStack = createMyStackNavigator(); - -function HomeScreen() { - const navigation = useNavigation(); - - return ( - - Dynamic home - - - - ); -} - -function ProfileScreen({ - route, -}: MyStackScreenProps) { - const navigation = useNavigation(); - - return ( - - {route.params.user} - - - - - - ); -} - -function DetailsScreen({ - route, -}: MyStackScreenProps) { - const navigation = useNavigation(); - - const isPreloaded = useNavigationState((state) => - state.routes.slice(state.index + 1).some((r) => r.key === route.key) - ); - - const [wasPreloaded, setWasPreloaded] = React.useState(isPreloaded); - - if (isPreloaded && !wasPreloaded) { - setWasPreloaded(true); - } - - return ( - - {route.params.section} - {wasPreloaded ? (preloaded) : null} - - - ); -} - -export function MyStackDynamic() { - return ( - { - alert(`Right button pressed (${event.data.count})`); - }, - }} - > - - - - - ); -} - -MyStackDynamic.title = 'Standard Navigation - Dynamic'; -MyStackDynamic.linking = linking; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - gap: 12, - padding: 16, - }, - title: { - fontSize: 20, - fontWeight: '600', - }, - buttons: { - gap: 12, - }, -}); diff --git a/example/src/Screens/StandardNavigator/MyStackNavigator.tsx b/example/src/Screens/StandardNavigator/MyStackNavigator.tsx deleted file mode 100644 index 0eb914d41b..0000000000 --- a/example/src/Screens/StandardNavigator/MyStackNavigator.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import * as React from 'react'; -import { - Pressable, - type StyleProp, - StyleSheet, - // eslint-disable-next-line no-restricted-imports - Text, - View, - type ViewStyle, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { createStandardNavigator } from 'standard-navigation'; - -export type MyStackOptions = { - title?: string; - rightButtonTitle?: string; -}; - -export type MyStackEventMap = { - rightButtonPress: { - data: { count: number }; - canPreventDefault: true; - }; -}; - -export type MyStackNavigatorProps = { - variant: 'compact' | 'regular'; - style?: StyleProp; -}; - -export type MyStackMapperProps = { - preloadedCount: number; -}; - -export const MyStackNavigator = createStandardNavigator< - MyStackOptions, - MyStackEventMap, - MyStackNavigatorProps & MyStackMapperProps ->( - ({ - state, - descriptors, - actions, - emitter, - preloadedCount, - style, - variant, - }) => { - const insets = useSafeAreaInsets(); - const countRef = React.useRef(0); - - return ( - - {state.routes.map((route) => { - const isFocused = route.key === state.routes[state.index]?.key; - const descriptor = descriptors[route.key]; - - return ( - - - - {state.index > 0 ? ( - actions.back()} - style={styles.button} - > - ๐Ÿ‘ˆ - - ) : null} - - {descriptor?.options.title ?? route.name} - - - {preloadedCount > 0 ? ( - โŒ› ({preloadedCount}) - ) : null} - {descriptor?.options.rightButtonTitle ? ( - { - emitter.emit({ - type: 'rightButtonPress', - data: { count: ++countRef.current }, - canPreventDefault: true, - }); - }} - style={styles.button} - > - - {descriptor.options.rightButtonTitle} - - - ) : null} - - - {descriptor?.render()} - - ); - })} - - ); - } -); - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - screen: { - flex: 1, - }, - button: { - padding: 12, - }, - inset: { - backgroundColor: 'tomato', - }, - header: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-between', - height: 56, - paddingHorizontal: 12, - backgroundColor: 'tomato', - }, - headerCompact: { - height: 44, - }, - title: { - fontSize: 16, - fontWeight: '600', - color: 'white', - marginHorizontal: 12, - }, - label: { - fontSize: 20, - color: 'white', - }, - right: { - flex: 1, - alignItems: 'flex-end', - }, -}); diff --git a/example/src/Screens/StandardNavigator/MyStackStatic.tsx b/example/src/Screens/StandardNavigator/MyStackStatic.tsx deleted file mode 100644 index 68ea21158c..0000000000 --- a/example/src/Screens/StandardNavigator/MyStackStatic.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Button, Text } from '@react-navigation/elements'; -import { - useNavigation, - useNavigationState, - useRoute, -} from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; - -import { - createMyStackNavigator, - createMyStackScreen, -} from './createMyStackNavigator'; - -function HomeScreen() { - const navigation = useNavigation('StandardStaticHome'); - - return ( - - Static home - - - - ); -} - -function ProfileScreen() { - const navigation = useNavigation('StandardStaticProfile'); - const route = useRoute('StandardStaticProfile'); - - return ( - - {route.params.user} - - - - - - ); -} - -function DetailsScreen() { - const navigation = useNavigation('StandardStaticDetails'); - const route = useRoute('StandardStaticDetails'); - - const isPreloaded = useNavigationState('StandardStaticDetails', (state) => - state.routes.slice(state.index + 1).some((r) => r.key === route.key) - ); - - const [wasPreloaded, setWasPreloaded] = React.useState(isPreloaded); - - if (isPreloaded && !wasPreloaded) { - setWasPreloaded(true); - } - - return ( - - {route.params.section} - {wasPreloaded ? (preloaded) : null} - - - ); -} - -export const MyStack = createMyStackNavigator({ - variant: 'regular', - screenListeners: { - rightButtonPress: (event) => { - alert(`Right button pressed (${event.data.count})`); - }, - }, - screens: { - StandardStaticHome: createMyStackScreen({ - screen: HomeScreen, - options: { title: 'Home', rightButtonTitle: '๐ŸŒŸ' }, - linking: 'standard-static-home', - }), - StandardStaticProfile: createMyStackScreen({ - screen: ProfileScreen, - options: { title: 'Profile' }, - linking: 'standard-static-profile/:user', - }), - StandardStaticDetails: createMyStackScreen({ - screen: DetailsScreen, - options: { title: 'Details', rightButtonTitle: '๐ŸŽจ' }, - linking: 'standard-static-details/:section', - }), - }, -}); - -export const MyStackStatic = { - screen: MyStack, - title: 'Standard Navigation - Static', -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - gap: 12, - padding: 16, - }, - title: { - fontSize: 20, - fontWeight: '600', - }, - buttons: { - gap: 12, - }, -}); diff --git a/example/src/Screens/StandardNavigator/createMyStackNavigator.tsx b/example/src/Screens/StandardNavigator/createMyStackNavigator.tsx deleted file mode 100644 index b506135924..0000000000 --- a/example/src/Screens/StandardNavigator/createMyStackNavigator.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - createStandardNavigationFactories, - type NavigationProp, - type ParamListBase, - type RouteProp, - type StackActionHelpers, - type StackNavigationState, - StackRouter, - type StackRouterOptions, - type StandardNavigationTypeBagBase, -} from '@react-navigation/native'; - -import { - type MyStackEventMap, - type MyStackMapperProps, - MyStackNavigator, - type MyStackNavigatorProps, - type MyStackOptions, -} from './MyStackNavigator'; - -export type MyStackNavigationProp< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = keyof ParamList, -> = NavigationProp< - ParamList, - RouteName, - StackNavigationState, - MyStackOptions, - MyStackEventMap, - StackActionHelpers ->; - -export type MyStackScreenProps< - ParamList extends ParamListBase, - RouteName extends keyof ParamList = keyof ParamList, -> = { - navigation: MyStackNavigationProp; - route: RouteProp; -}; - -export interface MyStackTypeBag extends StandardNavigationTypeBagBase { - State: StackNavigationState; - ActionHelpers: StackActionHelpers; - ScreenOptions: MyStackOptions; - EventMap: MyStackEventMap; - RouterOptions: StackRouterOptions; -} - -export const { - createNavigator: createMyStackNavigator, - createScreen: createMyStackScreen, -} = createStandardNavigationFactories< - MyStackTypeBag, - MyStackNavigatorProps, - MyStackMapperProps ->(MyStackNavigator, StackRouter, ({ state }) => ({ - preloadedCount: state.routes.slice(state.index + 1).length, -})); diff --git a/example/src/Screens/StandardNavigator/typechecks.tsx b/example/src/Screens/StandardNavigator/typechecks.tsx deleted file mode 100644 index b4b13f3eee..0000000000 --- a/example/src/Screens/StandardNavigator/typechecks.tsx +++ /dev/null @@ -1,434 +0,0 @@ -import { - createStandardNavigationFactories, - type EventArg, - type NavigationProp, - type StackActionHelpers, - type StackNavigationState, - StackRouter, - type StaticParamList, -} from '@react-navigation/native'; -import { expectTypeOf } from 'expect-type'; -import type * as React from 'react'; - -import { - createMyStackNavigator, - createMyStackScreen, - type MyStackTypeBag, -} from './createMyStackNavigator'; -import { - type MyStackMapperProps, - MyStackNavigator, - type MyStackNavigatorProps, - type MyStackOptions, -} from './MyStackNavigator'; -import { MyStack } from './MyStackStatic'; - -{ - type MyStackParamList = { - StandardStaticHome: undefined; - StandardStaticProfile: { user: string }; - StandardStaticDetails: { section: string }; - }; - - type MyStackNavigation< - RouteName extends keyof MyStackParamList = keyof MyStackParamList, - > = NavigationProp< - MyStackParamList, - RouteName, - StackNavigationState, - MyStackOptions, - MyStackTypeBag['EventMap'], - StackActionHelpers - >; - - function TypeCheckProfileScreen() { - return null; - } - - /** - * Infer param list from the static config - */ - type InferredMyStackParamList = StaticParamList; - - expectTypeOf().toEqualTypeOf(); - - /** - * Custom screen options are exposed through `setOptions` - */ - expectTypeOf() - .parameter(0) - .toEqualTypeOf>(); - - /** - * Custom options & events accepted on the navigation prop in real use - */ - const checkMyStackHomeNavigation = ( - navigation: MyStackNavigation<'StandardStaticHome'> - ) => { - navigation.replace('StandardStaticProfile', { user: 'Satya' }); - navigation.setOptions({ title: 'Home', rightButtonTitle: 'Press me' }); - - // @ts-expect-error `subtitle` isn't a valid option for MyStack. - navigation.setOptions({ subtitle: 'Invalid option' }); - - navigation.addListener('rightButtonPress', (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }); - }; - - /** - * `createMyStackScreen` accepts custom `options` (object form) - */ - createMyStackScreen({ - screen: TypeCheckProfileScreen, - options: { title: 'Profile', rightButtonTitle: 'Press me' }, - }); - - createMyStackScreen({ - screen: TypeCheckProfileScreen, - options: { - title: 'Profile', - // @ts-expect-error `subtitle` isn't a valid static screen option. - subtitle: 'Invalid option', - }, - }); - - /** - * `createMyStackScreen` accepts custom `options` (function form) - */ - createMyStackScreen({ - screen: TypeCheckProfileScreen, - options: ({ route, navigation }) => { - expectTypeOf(route.name).toEqualTypeOf(); - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { title: 'Profile', rightButtonTitle: 'Press me' }; - }, - }); - - createMyStackScreen({ - screen: TypeCheckProfileScreen, - // @ts-expect-error `subtitle` isn't a valid static screen option. - options: () => ({ subtitle: 'Invalid option' }), - }); - - /** - * `createMyStackScreen` accepts custom event `listeners` (object form) - */ - createMyStackScreen({ - screen: TypeCheckProfileScreen, - listeners: { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }, - }); - - /** - * `createMyStackScreen` accepts custom event `listeners` (function form) - */ - createMyStackScreen({ - screen: TypeCheckProfileScreen, - listeners: ({ navigation }) => { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }; - }, - }); - - /** - * Static navigator accepts `screenOptions` typed as `MyStackOptions` (object form) - */ - createMyStackNavigator({ - variant: 'regular', - screens: { StandardStaticHome: TypeCheckProfileScreen }, - screenOptions: { title: 'Default title' }, - }); - - createMyStackNavigator({ - variant: 'regular', - screens: { StandardStaticHome: TypeCheckProfileScreen }, - screenOptions: { - // @ts-expect-error `subtitle` isn't a valid option for MyStack. - subtitle: 'Invalid option', - }, - }); - - /** - * Static navigator accepts `screenOptions` (function form) - */ - createMyStackNavigator({ - variant: 'regular', - screens: { StandardStaticHome: TypeCheckProfileScreen }, - screenOptions: ({ navigation }) => { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { title: 'Default title' }; - }, - }); - - /** - * Static navigator accepts `screenListeners` for custom events (object form) - */ - createMyStackNavigator({ - variant: 'regular', - screens: { StandardStaticHome: TypeCheckProfileScreen }, - screenListeners: { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }, - }); - - /** - * Static navigator accepts `screenListeners` for custom events (function form) - */ - createMyStackNavigator({ - variant: 'regular', - screens: { StandardStaticHome: TypeCheckProfileScreen }, - screenListeners: ({ navigation }) => { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }; - }, - }); - - /** - * Required navigator props must be provided in the static config - */ - createMyStackNavigator( - // @ts-expect-error `variant` is a required navigator prop - { - screens: { StandardStaticHome: TypeCheckProfileScreen }, - } - ); - - /** - * Dynamic factory: `createMyStackNavigator()` - */ - type DynamicMyStackParamList = { - DynamicHome: undefined; - DynamicProfile: { user: string }; - DynamicDetails: { section: string }; - }; - - const DynamicMyStack = createMyStackNavigator(); - - type DynamicMyStackNavigatorProps = React.ComponentProps< - typeof DynamicMyStack.Navigator - >; - - expectTypeOf< - DynamicMyStackNavigatorProps['initialRouteName'] - >().toEqualTypeOf(); - - expectTypeOf(DynamicMyStack.Screen).parameter(0).toExtend<{ - name?: keyof DynamicMyStackParamList; - }>(); - - /** - * Navigator component exposes the consumer props: required `variant`, optional `style` - */ - expectTypeOf< - Pick - >().toEqualTypeOf(); - - /** - * Mapper-supplied props are not exposed on the navigator component - */ - expectTypeOf().not.toHaveProperty( - 'preloadedCount' - ); - - /** - * Dynamic `` `screenOptions` accepts `MyStackOptions` (object + function form) - */ - const dynamicScreenOptions: DynamicMyStackNavigatorProps['screenOptions'] = { - title: 'Default title', - // @ts-expect-error `subtitle` isn't a valid option for MyStack. - subtitle: 'Invalid option', - }; - - const dynamicScreenOptionsFn: DynamicMyStackNavigatorProps['screenOptions'] = - ({ navigation }) => { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { title: 'Default title' }; - }; - - /** - * Dynamic `` `screenListeners` accepts custom events (object + function form) - */ - const dynamicScreenListeners: DynamicMyStackNavigatorProps['screenListeners'] = - { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - - // @ts-expect-error rightButtonPress doesn't include routeKey. - void event.data.routeKey; - }, - }; - - const dynamicScreenListenersFn: DynamicMyStackNavigatorProps['screenListeners'] = - ({ navigation }) => { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }; - }; - - /** - * Dynamic `` accepts custom `options` (function form) and `listeners` (object form) - */ - const dynamicScreen = ( - { - expectTypeOf(route.params).toEqualTypeOf>(); - - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { title: route.params.user, rightButtonTitle: 'Press me' }; - }} - listeners={{ - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }} - /> - ); - - /** - * Dynamic `` accepts custom `options` (object form) and `listeners` (function form) - */ - const dynamicScreenObjectForm = ( - { - expectTypeOf(navigation.setOptions) - .parameter(0) - .toEqualTypeOf>(); - - return { - rightButtonPress: (event) => { - expectTypeOf(event).toEqualTypeOf< - EventArg<'rightButtonPress', true, { count: number }> - >(); - }, - }; - }} - /> - ); - - const invalidDynamicScreenName = ( - - ); - - const invalidDynamicScreenOptions = ( - - ); - - const dynamicNavigator = ( - - {dynamicScreen} - - ); - - const invalidDynamicNavigator = ( - - {dynamicScreen} - - ); - - /** - * `createStandardNavigationFactories` validates mapper return against `MapperProps` - */ - createStandardNavigationFactories< - MyStackTypeBag, - MyStackNavigatorProps, - MyStackMapperProps - >(MyStackNavigator, StackRouter, ({ state }) => ({ - preloadedCount: state.routes.slice(state.index + 1).length, - })); - - createStandardNavigationFactories< - MyStackTypeBag, - MyStackNavigatorProps, - MyStackMapperProps - >( - MyStackNavigator, - StackRouter, - // @ts-expect-error Mapped props must be keys from MyStackMapperProps. - () => ({ label: 'Invalid mapped prop' }) - ); - - void checkMyStackHomeNavigation; - void dynamicScreenOptions; - void dynamicScreenListenersFn; - void dynamicScreenObjectForm; - void invalidDynamicScreenName; - void invalidDynamicScreenOptions; - void dynamicNavigator; - void invalidDynamicNavigator; -} diff --git a/example/src/Screens/StaticConfig.tsx b/example/src/Screens/StaticConfig.tsx deleted file mode 100644 index fc3ed23284..0000000000 --- a/example/src/Screens/StaticConfig.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { Button, HeaderBackButton } from '@react-navigation/elements'; -import type { StaticParamList } from '@react-navigation/native'; -import { createStackNavigator } from '@react-navigation/stack'; -import * as React from 'react'; -import { Platform, ScrollView, StyleSheet, View } from 'react-native'; - -import iconBookUser from '../../assets/icons/book-user.png'; -import iconListMusic from '../../assets/icons/list-music.png'; -import iconMessage from '../../assets/icons/message-circle.png'; -import iconMusic from '../../assets/icons/music.png'; -import { Albums } from '../Shared/Albums'; -import { Chat } from '../Shared/Chat'; -import { Contacts } from '../Shared/Contacts'; - -export type StaticScreenParamList = StaticParamList; - -const ChatShownContext = React.createContext({ - isChatShown: false, - setIsChatShown: (_isChatShown: boolean) => {}, -}); - -const useIsChatShown = () => { - const { isChatShown } = React.use(ChatShownContext); - - return isChatShown; -}; - -const ChatShownLayout = ({ children }: { children: React.ReactNode }) => { - const [isChatShown, setIsChatShown] = React.useState(false); - - const context = React.useMemo( - () => ({ isChatShown, setIsChatShown }), - [isChatShown] - ); - - return ( - - {children} - - ); -}; - -const scrollEnabled = Platform.select({ web: true, default: false }); - -const AlbumsScreen = () => { - const { isChatShown, setIsChatShown } = React.use(ChatShownContext); - - return ( - - - - - - - ); -}; - -const HomeTabs = createBottomTabNavigator({ - implementation: 'custom', - screenOptions: ({ theme, navigation }) => ({ - headerShown: true, - headerLeft: (props) => ( - - ), - tabBarActiveTintColor: theme.colors.notification, - }), - screens: { - Albums: { - screen: AlbumsScreen, - options: { - tabBarButtonTestID: 'albums', - tabBarIcon: ({ focused }) => ({ - type: 'image', - source: focused ? iconListMusic : iconMusic, - }), - }, - }, - Contacts: { - screen: Contacts, - options: { - tabBarButtonTestID: 'contacts', - tabBarIcon: { - type: 'image', - source: iconBookUser, - }, - }, - }, - Chat: { - screen: Chat, - options: { - tabBarButtonTestID: 'chat', - tabBarIcon: { - type: 'image', - source: iconMessage, - }, - }, - if: useIsChatShown, - }, - }, -}); - -const StaticStack = createStackNavigator({ - layout: (props) => , - screenOptions: { - headerShown: false, - }, - screens: { - Home: { - screen: HomeTabs, - linking: '', - }, - }, -}); - -export const StaticConfig = { - screen: StaticStack, - title: 'Static config', -}; - -const styles = StyleSheet.create({ - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - padding: 12, - }, -}); diff --git a/example/src/Screens/TabView/AutoWidthTabBar.tsx b/example/src/Screens/TabView/AutoWidthTabBar.tsx deleted file mode 100644 index 402f60dee9..0000000000 --- a/example/src/Screens/TabView/AutoWidthTabBar.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet } from 'react-native'; -import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; - -import { Albums } from '../../Shared/Albums'; -import { Article } from '../../Shared/Article'; -import { Chat } from '../../Shared/Chat'; -import { Contacts } from '../../Shared/Contacts'; - -type Route = { - key: string; - title: string; -}; - -const renderScene = SceneMap({ - albums: () => , - contacts: () => , - article: () =>
, - chat: () => , - long: () =>
, - medium: () =>
, -}); - -export const AutoWidthTabBar = () => { - const { direction } = useLocale(); - const [index, onIndexChange] = React.useState(1); - const [routes] = React.useState([ - { key: 'article', title: 'Article' }, - { key: 'contacts', title: 'Contacts' }, - { key: 'albums', title: 'Albums' }, - { key: 'chat', title: 'Chat' }, - { key: 'long', title: 'long long long title' }, - { key: 'medium', title: 'medium title' }, - ]); - - const renderTabBar: React.ComponentProps< - typeof TabView - >['renderTabBar'] = (props) => ( - - ); - - return ( - - ); -}; - -AutoWidthTabBar.options = { - title: 'Scrollable tab bar (auto width)', - headerShadowVisible: false, -}; - -const styles = StyleSheet.create({ - tabbarContentContainer: { - paddingHorizontal: 10, - }, - tabStyle: { - width: 'auto', - }, -}); diff --git a/example/src/Screens/TabView/Coverflow.tsx b/example/src/Screens/TabView/Coverflow.tsx deleted file mode 100644 index a7b43fc528..0000000000 --- a/example/src/Screens/TabView/Coverflow.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* eslint-disable import-x/no-commonjs */ -import { Text, useFrameSize } from '@react-navigation/elements'; -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { - Animated, - Image, - type ImageRequireSource, - Platform, - StyleSheet, - View, -} from 'react-native'; -import { - type SceneRendererProps, - ScrollViewAdapter, - TabView, -} from 'react-native-tab-view'; - -type Route = { - key: string; -}; - -type Props = SceneRendererProps & { - index: number; - length: number; - route: Route; -}; - -const ALBUMS: { [key: string]: ImageRequireSource } = { - 'Abbey Road': require('../../../assets/album-art/01.jpg'), - 'Bat Out of Hell': require('../../../assets/album-art/02.jpg'), - Homogenic: require('../../../assets/album-art/03.jpg'), - 'Number of the Beast': require('../../../assets/album-art/04.jpg'), - "It's Blitz": require('../../../assets/album-art/05.jpg'), - 'The Man-Machine': require('../../../assets/album-art/06.jpg'), - 'The Score': require('../../../assets/album-art/07.jpg'), - 'Lost Horizons': require('../../../assets/album-art/08.jpg'), -}; - -const Scene = ({ route, position, index, length }: Props) => { - const width = useFrameSize((frame) => frame.width); - - const coverflowStyle: any = React.useMemo(() => { - const inputRange = Array.from({ length }, (_, i) => i); - const translateOutputRange = inputRange.map((i) => { - return (width / 2) * (index - i) * -1; - }); - const scaleOutputRange = inputRange.map((i) => { - if (index === i) { - return 1; - } else { - return 0.7; - } - }); - const opacityOutputRange = inputRange.map((i) => { - if (index === i) { - return 1; - } else { - return 0.3; - } - }); - - const translateX = position.interpolate({ - inputRange, - outputRange: translateOutputRange, - extrapolate: 'clamp', - }); - const scale = position.interpolate({ - inputRange, - outputRange: scaleOutputRange, - extrapolate: 'clamp', - }); - const opacity = position.interpolate({ - inputRange, - outputRange: opacityOutputRange, - extrapolate: 'clamp', - }); - - return { - transform: [{ translateX }, { scale }], - opacity, - }; - }, [index, length, position, width]); - - return ( - - - - - {route.key} - - ); -}; - -export function Coverflow() { - const { direction } = useLocale(); - - const [index, onIndexChange] = React.useState(2); - const [routes] = React.useState(Object.keys(ALBUMS).map((key) => ({ key }))); - - return ( - null} - renderScene={(props: SceneRendererProps & { route: Route }) => ( - - )} - renderAdapter={(props) => } - /> - ); -} - -Coverflow.options = { - title: 'Coverflow', - headerShadowVisible: false, - headerTintColor: '#fff', - headerStyle: { - backgroundColor: '#000', - }, -}; - -const styles = StyleSheet.create({ - container: { - backgroundColor: '#000', - }, - scene: { - overflow: 'visible', - }, - page: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - album: { - backgroundColor: '#000', - width: 200, - height: 200, - elevation: 12, - ...Platform.select({ - default: { - shadowColor: '#000', - shadowOpacity: 0.5, - shadowRadius: 8, - shadowOffset: { - height: 8, - width: 0, - }, - }, - web: { - boxShadow: '0 8px 8px rgba(0, 0, 0, 0.5)', - }, - }), - }, - cover: { - width: 200, - height: 200, - }, - label: { - margin: 16, - color: '#fff', - }, -}); diff --git a/example/src/Screens/TabView/CustomIndicator.tsx b/example/src/Screens/TabView/CustomIndicator.tsx deleted file mode 100644 index 3e88022cc1..0000000000 --- a/example/src/Screens/TabView/CustomIndicator.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import Ionicons from '@expo/vector-icons/Ionicons'; -import { Text } from '@react-navigation/elements'; -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { Animated, StyleSheet, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { - SceneMap, - TabBar, - type TabBarIndicatorProps, - TabView, -} from 'react-native-tab-view'; - -import { Albums } from '../../Shared/Albums'; -import { Article } from '../../Shared/Article'; -import { Contacts } from '../../Shared/Contacts'; - -type Route = { - key: string; - icon: React.ComponentProps['name']; -}; - -const renderScene = SceneMap({ - article: () =>
, - contacts: () => , - albums: () => , -}); - -export const CustomIndicator = () => { - const { direction } = useLocale(); - const insets = useSafeAreaInsets(); - const [index, onIndexChange] = React.useState(0); - const [routes] = React.useState([ - { - key: 'article', - icon: 'document', - }, - { - key: 'contacts', - icon: 'people', - }, - { - key: 'albums', - icon: 'albums', - }, - ]); - - const renderIndicator = (props: TabBarIndicatorProps) => { - const { position, widths, offsets, style } = props; - const inputRange = [ - 0, 0.48, 0.49, 0.51, 0.52, 1, 1.48, 1.49, 1.51, 1.52, 2, - ]; - - const scale = position.interpolate({ - inputRange, - outputRange: inputRange.map((x) => (Math.trunc(x) === x ? 2 : 0.1)), - }); - - const opacity = position.interpolate({ - inputRange, - outputRange: inputRange.map((x) => { - const d = x - Math.trunc(x); - return d === 0.49 || d === 0.51 ? 0 : 1; - }), - }); - - const translateX = position.interpolate({ - inputRange: inputRange, - outputRange: inputRange.map((x) => { - const i = Math.round(x); - const offset = offsets[i]; - - if (offset == null) { - throw new Error(`Couldn't find an offset at index ${i}.`); - } - - return offset * (direction === 'rtl' ? -1 : 1); - }), - }); - - return ( - - - - ); - }; - - const renderBadge = React.useCallback( - () => ( - - 42 - - ), - [] - ); - - const renderIcon = React.useCallback((props: { route: Route }) => { - return ; - }, []); - - const renderTabBar: React.ComponentProps< - typeof TabView - >['renderTabBar'] = (props) => ( - - - - ); - - return ( - - ); -}; - -CustomIndicator.options = { - title: 'Custom indicator', - headerShadowVisible: false, - headerTintColor: '#fff', - headerStyle: { - backgroundColor: '#263238', - }, -}; - -const styles = StyleSheet.create({ - tabbar: { - backgroundColor: '#263238', - borderBottomWidth: 0, - overflow: 'hidden', - }, - tabbarContentContainer: { - paddingHorizontal: 10, - }, - icon: { - backgroundColor: 'transparent', - color: 'white', - }, - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - }, - indicator: { - backgroundColor: 'rgb(0, 132, 255)', - width: 48, - height: 48, - borderRadius: 24, - margin: 6, - }, - badge: { - marginTop: 4, - marginEnd: 32, - backgroundColor: '#f44336', - height: 24, - width: 24, - borderRadius: 12, - alignItems: 'center', - justifyContent: 'center', - }, - count: { - color: '#fff', - fontSize: 12, - fontWeight: 'bold', - marginTop: -2, - }, -}); diff --git a/example/src/Screens/TabView/CustomTabBar.tsx b/example/src/Screens/TabView/CustomTabBar.tsx deleted file mode 100644 index a5da56ffe9..0000000000 --- a/example/src/Screens/TabView/CustomTabBar.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import Ionicons from '@expo/vector-icons/Ionicons'; -import { Text } from '@react-navigation/elements'; -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { Animated, Pressable, StyleSheet, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { type NavigationState, SceneMap, TabView } from 'react-native-tab-view'; - -import { Albums } from '../../Shared/Albums'; -import { Article } from '../../Shared/Article'; -import { Chat } from '../../Shared/Chat'; -import { Contacts } from '../../Shared/Contacts'; - -type Route = { - key: string; - title: string; - icon: React.ComponentProps['name']; -}; - -type State = NavigationState; - -const renderScene = SceneMap({ - contacts: () => , - albums: () => , - article: () =>
, - chat: () => , -}); - -export const CustomTabBar = () => { - const { direction } = useLocale(); - const insets = useSafeAreaInsets(); - const [index, onIndexChange] = React.useState(0); - const [routes] = React.useState([ - { key: 'contacts', title: 'Contacts', icon: 'people' }, - { key: 'albums', title: 'Albums', icon: 'albums' }, - { key: 'article', title: 'Article', icon: 'document' }, - { key: 'chat', title: 'Chat', icon: 'chatbubble' }, - ]); - - const renderItem = - ({ - navigationState, - position, - }: { - navigationState: State; - position: Animated.AnimatedInterpolation; - }) => - ({ route, index }: { route: Route; index: number }) => { - const inputRange = navigationState.routes.map((_, i) => i); - - const activeOpacity = position.interpolate({ - inputRange, - outputRange: inputRange.map((i: number) => (i === index ? 1 : 0)), - }); - const inactiveOpacity = position.interpolate({ - inputRange, - outputRange: inputRange.map((i: number) => (i === index ? 0 : 1)), - }); - - return ( - - - - {route.title} - - - - {route.title} - - - ); - }; - - const renderTabBar: React.ComponentProps< - typeof TabView - >['renderTabBar'] = (props) => ( - - - {props.navigationState.routes.map((route: Route, index: number) => { - return ( - props.jumpTo(route.key)} - style={[styles.tab]} - > - {renderItem(props)({ route, index })} - - ); - })} - - - ); - - return ( - - ); -}; - -CustomTabBar.options = { - title: 'Custom tab bar', -}; - -const styles = StyleSheet.create({ - tabbar: { - backgroundColor: '#fafafa', - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: 'rgba(0, 0, 0, .2)', - }, - inner: { - flexDirection: 'row', - width: '100%', - maxWidth: 600, - marginHorizontal: 'auto', - }, - tab: { - flex: 1, - alignItems: 'center', - }, - item: { - alignItems: 'center', - justifyContent: 'center', - paddingTop: 4.5, - }, - activeItem: { - position: 'absolute', - top: 0, - start: 0, - end: 0, - bottom: 0, - }, - active: { - color: '#0084ff', - }, - inactive: { - color: '#939393', - }, - icon: { - height: 26, - width: 26, - }, - label: { - fontSize: 10, - marginTop: 3, - marginBottom: 1.5, - backgroundColor: 'transparent', - }, -}); diff --git a/example/src/Screens/TabView/ScrollAdapter.tsx b/example/src/Screens/TabView/ScrollAdapter.tsx deleted file mode 100644 index e87a71a7f7..0000000000 --- a/example/src/Screens/TabView/ScrollAdapter.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { SceneMap, ScrollViewAdapter, TabView } from 'react-native-tab-view'; - -import { Albums } from '../../Shared/Albums'; -import { Article } from '../../Shared/Article'; -import { Contacts } from '../../Shared/Contacts'; - -const renderScene = SceneMap({ - albums: () => , - contacts: () => , - article: () =>
, -}); - -export function ScrollAdapter() { - const { direction } = useLocale(); - const [index, onIndexChange] = React.useState(1); - const [routes] = React.useState([ - { key: 'article', title: 'Article' }, - { key: 'contacts', title: 'Contacts' }, - { key: 'albums', title: 'Albums' }, - ]); - - return ( - } - onIndexChange={onIndexChange} - /> - ); -} - -ScrollAdapter.options = { - title: 'ScrollView Adapter', - headerShadowVisible: false, -}; diff --git a/example/src/Screens/TabView/ScrollableTabBar.tsx b/example/src/Screens/TabView/ScrollableTabBar.tsx deleted file mode 100755 index 56cd731b4d..0000000000 --- a/example/src/Screens/TabView/ScrollableTabBar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet } from 'react-native'; -import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; - -import { Albums } from '../../Shared/Albums'; -import { Article } from '../../Shared/Article'; -import { Chat } from '../../Shared/Chat'; -import { Contacts } from '../../Shared/Contacts'; - -type Route = { - key: string; - title: string; -}; - -const renderScene = SceneMap({ - albums: () => , - contacts: () => , - article: () =>
, - chat: () => , -}); - -export const ScrollableTabBar = () => { - const [index, onIndexChange] = React.useState(1); - const { direction } = useLocale(); - const [routes] = React.useState([ - { key: 'article', title: 'Article' }, - { key: 'contacts', title: 'Contacts' }, - { key: 'albums', title: 'Albums' }, - { key: 'chat', title: 'Chat' }, - ]); - - const renderTabBar: React.ComponentProps< - typeof TabView - >['renderTabBar'] = (props) => ( - - ); - - return ( - - ); -}; - -ScrollableTabBar.options = { - title: 'Scrollable tab bar', - headerShadowVisible: false, -}; - -const styles = StyleSheet.create({ - tabbarContentContainer: { - paddingHorizontal: 10, - }, - tab: { - width: 120, - }, - indicator: { - marginHorizontal: 10, - }, -}); diff --git a/example/src/Screens/TabView/TabBarIcon.tsx b/example/src/Screens/TabView/TabBarIcon.tsx deleted file mode 100644 index 82709028b5..0000000000 --- a/example/src/Screens/TabView/TabBarIcon.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import Ionicons from '@expo/vector-icons/Ionicons'; -import { useLocale } from '@react-navigation/native'; -import * as React from 'react'; -import { StyleSheet } from 'react-native'; -import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; - -import { Article } from '../../Shared/Article'; -import { Chat } from '../../Shared/Chat'; -import { Contacts } from '../../Shared/Contacts'; - -type Route = { - key: string; - icon: React.ComponentProps['name']; -}; - -const renderScene = SceneMap({ - chat: () => , - contacts: () => , - article: () =>
, -}); - -export const TabBarIcon = () => { - const { direction } = useLocale(); - const [index, onIndexChange] = React.useState(0); - const [routes] = React.useState([ - { key: 'chat', icon: 'chatbubbles' }, - { key: 'contacts', icon: 'people' }, - { key: 'article', icon: 'list' }, - ]); - - const renderIcon = React.useCallback((props: { route: Route }) => { - return ; - }, []); - - const renderTabBar: React.ComponentProps< - typeof TabView - >['renderTabBar'] = (props) => ( - - ); - - return ( - - ); -}; - -TabBarIcon.options = { - title: 'Top tab bar with icons', - headerShadowVisible: false, - headerTintColor: '#fff', - headerStyle: { - backgroundColor: '#e91e63', - }, -}; - -const styles = StyleSheet.create({ - tabbar: { - backgroundColor: '#e91e63', - }, - tabbarContentContainer: { - paddingHorizontal: '10%', - }, - indicator: { - backgroundColor: '#ffeb3b', - }, -}); diff --git a/example/src/Shared/Albums.tsx b/example/src/Shared/Albums.tsx deleted file mode 100644 index b4fcf3af4c..0000000000 --- a/example/src/Shared/Albums.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useScrollToTop } from '@react-navigation/native'; -import * as React from 'react'; -import { - Image, - Platform, - ScrollView, - type ScrollViewProps, - StyleSheet, - useWindowDimensions, - View, -} from 'react-native'; - -/* eslint-disable import-x/no-commonjs */ -const COVERS = [ - 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'), -]; -/* eslint-enable import-x/no-commonjs */ - -export function Albums(props: Partial) { - const dimensions = useWindowDimensions(); - - const ref = React.useRef(null); - - useScrollToTop(ref); - - const itemSize = dimensions.width / Math.floor(dimensions.width / 150); - - return ( - - {Array.from({ length: 5 }, () => COVERS) - .flat() - .map((source, i) => ( - - - - ))} - - ); -} - -const styles = StyleSheet.create({ - ...Platform.select({ - web: { - content: { - // FIXME: React Native's types for `display` don't include `grid`. - display: 'grid' as any, - gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', - }, - item: { - width: '100%', - }, - photo: { - flex: 1, - paddingTop: '100%', - height: 'auto', - width: 'auto', - }, - }, - default: { - content: { - flexDirection: 'row', - flexWrap: 'wrap', - }, - item: {}, - photo: { - height: '100%', - width: '100%', - }, - }, - }), -}); diff --git a/example/src/Shared/Article.tsx b/example/src/Shared/Article.tsx deleted file mode 100644 index 87a49d7c55..0000000000 --- a/example/src/Shared/Article.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { useScrollToTop, useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { - Image, - ScrollView, - type ScrollViewProps, - StyleSheet, - type TextProps, - View, -} from 'react-native'; - -type Props = Partial & { - date?: string; - author?: { - name: string; - }; -}; - -const Heading = ({ - style, - ...rest -}: TextProps & { children: React.ReactNode }) => { - const { colors } = useTheme(); - - return ( - - ); -}; - -const Paragraph = ({ - style, - ...rest -}: TextProps & { children: React.ReactNode }) => { - const { colors } = useTheme(); - - return ( - - ); -}; - -const DEFAULT_AUTHOR = { - name: 'Knowledge Bot', -}; - -export function Article({ - date = '1st Jan 2025', - author = DEFAULT_AUTHOR, - ...rest -}: Props) { - const ref = React.useRef(null); - - useScrollToTop(ref); - - const { colors } = useTheme(); - - return ( - - - - - - {author.name} - - {date} - - - What is Lorem Ipsum? - - Lorem Ipsum is simply dummy text of the printing and typesetting - industry. Lorem Ipsum has been the industry's standard dummy text - ever since the 1500s, when an unknown printer took a galley of type and - scrambled it to make a type specimen book. It has survived not only five - centuries, but also the leap into electronic typesetting, remaining - essentially unchanged. It was popularised in the 1960s with the release - of Letraset sheets containing Lorem Ipsum passages, and more recently - with desktop publishing software like Aldus PageMaker including versions - of Lorem Ipsum. - - - Where does it come from? - - Contrary to popular belief, Lorem Ipsum is not simply random text. It - has roots in a piece of classical Latin literature from 45 BC, making it - over 2000 years old. Richard McClintock, a Latin professor at - Hampden-Sydney College in Virginia, looked up one of the more obscure - Latin words, consectetur, from a Lorem Ipsum passage, and going through - the cites of the word in classical literature, discovered the - undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 - of "de Finibus Bonorum et Malorum" (The Extremes of Good and - Evil) by Cicero, written in 45 BC. This book is a treatise on the theory - of ethics, very popular during the Renaissance. The first line of Lorem - Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in - section 1.10.32. - - - The standard chunk of Lorem Ipsum used since the 1500s is reproduced - below for those interested. Sections 1.10.32 and 1.10.33 from "de - Finibus Bonorum et Malorum" by Cicero are also reproduced in their - exact original form, accompanied by English versions from the 1914 - translation by H. Rackham. - - Why do we use it? - - It is a long established fact that a reader will be distracted by the - readable content of a page when looking at its layout. The point of - using Lorem Ipsum is that it has a more-or-less normal distribution of - letters, as opposed to using "Content here, content here", - making it look like readable English. Many desktop publishing packages - and web page editors now use Lorem Ipsum as their default model text, - and a search for "lorem ipsum" will uncover many web sites - still in their infancy. Various versions have evolved over the years, - sometimes by accident, sometimes on purpose (injected humour and the - like). - - Where can I get some? - - There are many variations of passages of Lorem Ipsum available, but the - majority have suffered alteration in some form, by injected humour, or - randomised words which don't look even slightly believable. If you - are going to use a passage of Lorem Ipsum, you need to be sure there - isn't anything embarrassing hidden in the middle of text. All the - Lorem Ipsum generators on the Internet tend to repeat predefined chunks - as necessary, making this the first true generator on the Internet. It - uses a dictionary of over 200 Latin words, combined with a handful of - model sentence structures, to generate Lorem Ipsum which looks - reasonable. The generated Lorem Ipsum is therefore always free from - repetition, injected humour, or non-characteristic words etc. - - - ); -} - -const styles = StyleSheet.create({ - content: { - paddingVertical: 16, - }, - author: { - flexDirection: 'row', - marginVertical: 8, - marginHorizontal: 16, - }, - meta: { - marginHorizontal: 8, - justifyContent: 'center', - }, - name: { - fontWeight: 'bold', - fontSize: 16, - lineHeight: 24, - }, - timestamp: { - opacity: 0.5, - fontSize: 14, - lineHeight: 21, - }, - avatar: { - height: 48, - width: 48, - borderRadius: 24, - }, - heading: { - fontWeight: 'bold', - fontSize: 24, - marginVertical: 8, - marginHorizontal: 16, - }, - paragraph: { - fontSize: 16, - lineHeight: 24, - marginVertical: 8, - marginHorizontal: 16, - }, - image: { - width: '100%', - height: 200, - marginVertical: 8, - }, -}); diff --git a/example/src/Shared/Chat.tsx b/example/src/Shared/Chat.tsx deleted file mode 100644 index c4feb86129..0000000000 --- a/example/src/Shared/Chat.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { Color } from '@react-navigation/elements/internal'; -import { useScrollToTop, useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { - Image, - ScrollView, - type ScrollViewProps, - StyleSheet, - TextInput, - View, -} from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -const MESSAGES = [ - 'okay', - 'sudo make me a sandwich', - 'what? make it yourself', - 'make me a sandwich', -]; - -export function Chat({ - bottom, - ...rest -}: Partial) { - const insets = useSafeAreaInsets(); - const ref = React.useRef(null); - - useScrollToTop(ref); - - const { colors } = useTheme(); - - return ( - - - {MESSAGES.map((text, i) => { - const odd = i % 2; - - return ( - - - - - {text} - - - - ); - })} - - - {bottom ? ( - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - inverted: { - transform: [{ scaleY: -1 }], - }, - content: { - padding: 16, - }, - even: { - flexDirection: 'row', - }, - odd: { - flexDirection: 'row-reverse', - }, - avatar: { - marginVertical: 8, - marginHorizontal: 6, - height: 40, - width: 40, - borderRadius: 20, - borderColor: 'rgba(0, 0, 0, .16)', - borderWidth: StyleSheet.hairlineWidth, - }, - bubble: { - marginVertical: 8, - marginHorizontal: 6, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 20, - }, - input: { - height: 48, - paddingVertical: 12, - paddingHorizontal: 24, - }, - spacer: { - backgroundColor: '#fff', - }, -}); diff --git a/example/src/Shared/Contacts.tsx b/example/src/Shared/Contacts.tsx deleted file mode 100644 index 4c8911fa8a..0000000000 --- a/example/src/Shared/Contacts.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { useScrollToTop, useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { FlatList, type FlatListProps, StyleSheet, View } from 'react-native'; - -type Item = { name: string; number: number }; - -type Props = Partial> & { - query?: string; -}; - -const CONTACTS: Item[] = [ - { name: 'Marissa Castillo', number: 7766398169 }, - { name: 'Denzel Curry', number: 9394378449 }, - { name: 'Miles Ferguson', number: 8966872888 }, - { name: 'Desiree Webster', number: 6818656371 }, - { name: 'Samantha Young', number: 6538288534 }, - { name: 'Irene Hunter', number: 2932176249 }, - { name: 'Annie Ryan', number: 4718456627 }, - { name: 'Sasha Oliver', number: 9743195919 }, - { name: 'Jarrod Avila', number: 8339212305 }, - { name: 'Griffin Weaver', number: 6059349721 }, - { name: 'Emilee Moss', number: 7382905180 }, - { name: 'Angelique Oliver', number: 9689298436 }, - { name: 'Emanuel Little', number: 6673376805 }, - { name: 'Wayne Day', number: 6918839582 }, - { name: 'Lauren Reese', number: 4652613201 }, - { name: 'Kailey Ward', number: 2232609512 }, - { name: 'Gabrielle Newman', number: 2837997127 }, - { name: 'Luke Strickland', number: 8404732322 }, - { name: 'Payton Garza', number: 7916140875 }, - { name: 'Anna Moss', number: 3504954657 }, - { name: 'Kailey Vazquez', number: 3002136330 }, - { name: 'Jennifer Coleman', number: 5469629753 }, - { name: 'Cindy Casey', number: 8446175026 }, - { name: 'Dillon Doyle', number: 5614510703 }, - { name: 'Savannah Garcia', number: 5634775094 }, - { name: 'Kailey Hudson', number: 3289239675 }, - { name: 'Ariel Green', number: 2103492196 }, - { name: 'Weston Perez', number: 2984221823 }, - { name: 'Kari Juarez', number: 9502125065 }, - { name: 'Sara Sanders', number: 7696668206 }, - { name: 'Griffin Le', number: 3396937040 }, - { name: 'Fernando Valdez', number: 9124257306 }, - { name: 'Taylor Marshall', number: 9656072372 }, - { name: 'Elias Dunn', number: 9738536473 }, - { name: 'Diane Barrett', number: 6886824829 }, - { name: 'Samuel Freeman', number: 5523948094 }, - { name: 'Irene Garza', number: 2077694008 }, - { name: 'Devante Alvarez', number: 9897002645 }, - { name: 'Sydney Floyd', number: 6462897254 }, - { name: 'Toni Dixon', number: 3775448213 }, - { name: 'Anastasia Spencer', number: 4548212752 }, - { name: 'Reid Cortez', number: 6668056507 }, - { name: 'Ramon Duncan', number: 8889157751 }, - { name: 'Kenny Moreno', number: 5748219540 }, - { name: 'Shelby Craig', number: 9473708675 }, - { name: 'Jordyn Brewer', number: 7552277991 }, - { name: 'Tanya Walker', number: 4308189657 }, - { name: 'Nolan Figueroa', number: 9173443776 }, - { name: 'Sophia Gibbs', number: 6435942770 }, - { name: 'Vincent Sandoval', number: 2606111495 }, -]; - -const ContactItem = React.memo( - ({ item }: { item: { name: string; number: number } }) => { - const { colors } = useTheme(); - - return ( - - - - {item.name.slice(0, 1).toUpperCase()} - - - - {item.name} - {item.number} - - - ); - } -); - -ContactItem.displayName = 'ContactItem'; - -const ItemSeparator = () => { - const { colors } = useTheme(); - - return ( - - ); -}; - -export function Contacts({ query, ...rest }: Props) { - const ref = React.useRef>(null); - - useScrollToTop(ref); - - const renderItem = ({ item }: { item: Item }) => ; - - return ( - - c.name.toLowerCase().includes(query.toLowerCase()) - ) - : CONTACTS - } - keyExtractor={(_, i) => String(i)} - renderItem={renderItem} - ItemSeparatorComponent={ItemSeparator} - /> - ); -} - -const styles = StyleSheet.create({ - item: { - flexDirection: 'row', - alignItems: 'center', - padding: 8, - }, - avatar: { - height: 36, - width: 36, - borderRadius: 18, - backgroundColor: '#e91e63', - alignItems: 'center', - justifyContent: 'center', - }, - letter: { - color: 'white', - fontWeight: 'bold', - }, - details: { - margin: 8, - }, - name: { - fontWeight: 'bold', - fontSize: 14, - }, - number: { - fontSize: 12, - }, - separator: { - height: StyleSheet.hairlineWidth, - }, -}); diff --git a/example/src/Shared/Divider.tsx b/example/src/Shared/Divider.tsx deleted file mode 100644 index 0fca755518..0000000000 --- a/example/src/Shared/Divider.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useTheme } from '@react-navigation/native'; -import { StyleSheet, View } from 'react-native'; - -export function Divider() { - const { colors } = useTheme(); - - return ( - - ); -} - -const styles = StyleSheet.create({ - divider: { - borderBottomWidth: StyleSheet.hairlineWidth, - }, -}); diff --git a/example/src/Shared/DrawerProgress.native.tsx b/example/src/Shared/DrawerProgress.native.tsx deleted file mode 100644 index 5b6dbf6603..0000000000 --- a/example/src/Shared/DrawerProgress.native.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { StyleSheet, View } from 'react-native'; -import { useDrawerProgress } from 'react-native-drawer-layout'; -import Animated, { - interpolate, - useAnimatedStyle, -} from 'react-native-reanimated'; - -export function DrawerProgress() { - const progress = useDrawerProgress(); - - const animatedStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateY: interpolate(progress.value, [0, 1], [56, 0]), - }, - ], - }; - }, [progress]); - - return ( - - - - ); -} - -const styles = StyleSheet.create({ - container: { - width: 64, - height: 72, - marginBottom: 8, - overflow: 'hidden', - }, - progress: { - position: 'absolute', - top: 0, - start: 0, - end: 8, - bottom: 0, - backgroundColor: '#ebdec1', - borderColor: '#3e3a3a', - borderWidth: 4, - borderBottomWidth: 0, - borderRadius: 2, - }, -}); diff --git a/example/src/Shared/DrawerProgress.tsx b/example/src/Shared/DrawerProgress.tsx deleted file mode 100644 index a896249478..0000000000 --- a/example/src/Shared/DrawerProgress.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { View } from 'react-native'; - -export function DrawerProgress() { - return ; -} diff --git a/example/src/Shared/ErrorBoundary.tsx b/example/src/Shared/ErrorBoundary.tsx deleted file mode 100644 index 9c9a0f9c74..0000000000 --- a/example/src/Shared/ErrorBoundary.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { DefaultTheme } from '@react-navigation/native'; -import * as React from 'react'; -// eslint-disable-next-line no-restricted-imports -import { Pressable, StyleSheet, Text, View } from 'react-native'; - -export class ErrorBoundary extends React.Component< - { - children: React.ReactNode; - onError?: (error: Error, info: React.ErrorInfo) => void; - onReset?: () => void; - }, - { error: Error | null } -> { - static getDerivedStateFromError(error: Error) { - return { error }; - } - - override state: { error: Error | null } = { error: null }; - - override componentDidCatch(error: Error, info: React.ErrorInfo) { - this.props.onError?.(error, info); - } - - override render() { - const { error } = this.state; - - if (error) { - return ( - - {error.message} - { - this.props.onReset?.(); - this.setState({ error: null }); - }} - > - Try Again - - - ); - } - - return this.props.children; - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 16, - }, - errorText: { - ...DefaultTheme.fonts.heavy, - color: DefaultTheme.colors.notification, - fontSize: 16, - }, - button: { - marginTop: 8, - padding: 16, - }, - buttonText: { - ...DefaultTheme.fonts.medium, - color: DefaultTheme.colors.primary, - }, -}); diff --git a/example/src/Shared/HeavyComponent.tsx b/example/src/Shared/HeavyComponent.tsx deleted file mode 100644 index 492cb68a73..0000000000 --- a/example/src/Shared/HeavyComponent.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { StyleSheet, View } from 'react-native'; - -export function HeavyComponent() { - const items = []; - - for (let index = 0; index < 500; index++) { - let value = 0; - - for (let iteration = 0; iteration < 1000; iteration++) { - value += Math.sqrt(index * iteration + 1); - } - - items.push( - - {Math.round(value)} - - ); - } - - return ( - - {items} - - ); -} - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - width: 0, - height: 0, - overflow: 'hidden', - }, - item: { - height: 1, - opacity: 0, - }, -}); diff --git a/example/src/Shared/LIstItem.tsx b/example/src/Shared/LIstItem.tsx deleted file mode 100644 index 7b583c6fae..0000000000 --- a/example/src/Shared/LIstItem.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { PlatformPressable, Text } from '@react-navigation/elements'; -import { StyleSheet, View } from 'react-native'; - -type Props = { - title: string; - onPress?: () => void; - testID?: string; - children?: React.ReactNode; -}; - -export function ListItem({ title, onPress, testID, children }: Props) { - if (onPress) { - return ( - - {title} - {children} - - ); - } - - return ( - - - {title} - - {children} - - ); -} - -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 16, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - title: { - fontSize: 16, - marginVertical: 16, - }, -}); diff --git a/example/src/Shared/ListGroupItem.tsx b/example/src/Shared/ListGroupItem.tsx deleted file mode 100644 index 90e505479f..0000000000 --- a/example/src/Shared/ListGroupItem.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Text } from '@react-navigation/elements'; -import { useTheme } from '@react-navigation/native'; -import { StyleSheet, View } from 'react-native'; - -type Props = { - title?: string; - children: React.ReactNode; -}; - -export function ListGroupItem({ title, children }: Props) { - const { colors, fonts } = useTheme(); - - return ( - - {title != null ? ( - {title} - ) : null} - - {children} - - - ); -} - -const styles = StyleSheet.create({ - container: { - padding: 16, - }, - group: { - borderRadius: 20, - borderCurve: 'continuous', - borderWidth: StyleSheet.hairlineWidth, - }, - title: { - marginHorizontal: 16, - fontSize: 13, - marginBottom: 8, - }, -}); diff --git a/example/src/Shared/MiniPlayer.tsx b/example/src/Shared/MiniPlayer.tsx deleted file mode 100644 index 1a01f79ddf..0000000000 --- a/example/src/Shared/MiniPlayer.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { Text } from '@react-navigation/elements'; -import { Image, StyleSheet, View } from 'react-native'; - -import album10 from '../../assets/album-art/10.jpg'; - -export function MiniPlayer({ placement }: { placement: 'inline' | 'regular' }) { - return ( - - - - - {placement === 'inline' ? ( - - - - - - ) : ( - <> - - Sgt Pepper's Lonely Hearts Club Band - - - - - - - )} - - ); -} - -const styles = StyleSheet.create({ - headerRight: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - marginEnd: 16, - }, - buttons: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - margin: 12, - }, - miniPlayer: { - flexDirection: 'row', - gap: 10, - alignItems: 'center', - }, - miniPlayerCoverContainer: { - margin: 5, - }, - miniPlayerCover: { - height: '100%', - aspectRatio: 1, - borderRadius: '50%', - }, - miniPlayerAlbumTitle: { - flexShrink: 1, - alignSelf: 'center', - marginStart: 10, - fontWeight: 'bold', - }, - miniPlayerButtons: { - flexDirection: 'row', - gap: 15, - justifyContent: 'center', - marginHorizontal: 15, - }, -}); diff --git a/example/src/Shared/NewsFeed.tsx b/example/src/Shared/NewsFeed.tsx deleted file mode 100644 index 4f6bb3bb53..0000000000 --- a/example/src/Shared/NewsFeed.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; -import { PlatformPressable, Text } from '@react-navigation/elements'; -import { Color } from '@react-navigation/elements/internal'; -import { useScrollToTop, useTheme } from '@react-navigation/native'; -import * as React from 'react'; -import { - Image, - ScrollView, - type ScrollViewProps, - StyleSheet, - TextInput, - View, -} from 'react-native'; - -import { Divider } from './Divider'; - -type Props = Partial & { - date?: number; -}; - -const Card = ({ children }: { children: React.ReactNode }) => { - const { colors } = useTheme(); - - return ( - - {children} - - ); -}; - -const FooterIcon = ({ - icon, - size = 16, -}: { - icon: React.ComponentProps['name']; - size?: number; -}) => { - const { colors } = useTheme(); - - return ( - - - - ); -}; - -const Author = () => { - return ( - - - Joke bot - - ); -}; - -const Footer = () => { - return ( - - - - - - ); -}; - -export function NewsFeed(props: Props) { - const ref = React.useRef(null); - - useScrollToTop(ref); - - const { colors } = useTheme(); - - return ( - - - - - - - - - If you aren't impressed with the picture of the first Black - Hole, you clearly don't understand the gravity of the - situation. - - - -